feat(run): resume-aware per-check runs, verify hooks, run-state hardening
/pygienium-<check> is now resume-aware (terminal checks skipped unless --fresh) and shares run-state with all/resume; every check gets a verify hook that fails loudly when a sub-agent returns ok with no artifact; run-state clears stale errors on retry success and reconciles a run as failed only when every check failed. Drops the superseded hygiene-state.ts model.
This commit is contained in:
@@ -16,10 +16,11 @@ import {
|
||||
getCheck,
|
||||
type CheckDefinition,
|
||||
} from "./checks/registry.js";
|
||||
import { runCheck, parseCheckArgs } from "./modes/check-runner.js";
|
||||
import {
|
||||
runCheck,
|
||||
parseCheckArgs,
|
||||
type CheckRunOutcome,
|
||||
} from "./modes/check-runner.js";
|
||||
import { parseAllArgs, runAllChecks, allSummaryPath } from "./modes/all.js";
|
||||
import { buildPygieniumHelpLines } from "./help.js";
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
markRunStatus,
|
||||
reconcileRunStatus,
|
||||
resetCheckEntry,
|
||||
isCheckTerminal,
|
||||
shouldRunOnResume,
|
||||
} from "./run-state.js";
|
||||
import { formatRunStatus } from "./status.js";
|
||||
@@ -116,21 +118,58 @@ export async function handleHelpCommand(
|
||||
* Exported so `index.ts` can bind one per registered `CheckDefinition` and so
|
||||
* tests can invoke it directly with a stub context.
|
||||
*/
|
||||
/**
|
||||
* `/pygienium-<check> [path] [--fix] [--fresh] [--no-gitignore]` — the
|
||||
* per-check command handler. Exported so `index.ts` binds one per registered
|
||||
* `CheckDefinition` and tests invoke it directly with a stub context.
|
||||
*
|
||||
* Resume-aware (parity with `/pygienium-all` and `/pygienium-resume`): a check
|
||||
* already terminal (`complete`/`skipped`) is NOT re-dispatched unless `--fresh`
|
||||
* resets its run-state entry. A failed/pending/in-progress check is re-run from
|
||||
* analysis — recovering the exact failure mode the MagniFluo run exposed
|
||||
* (sub-agent returns ok with no output → verify now fails loudly → resume
|
||||
* re-runs the analysis and the artifact lands).
|
||||
*/
|
||||
export async function handleCheckCommand(
|
||||
check: CheckDefinition,
|
||||
args: string,
|
||||
ctx: PygieniumCtx,
|
||||
): Promise<void> {
|
||||
const { fix, rest } = splitFlags(args);
|
||||
const { fix, fresh, rest, noGitignore } = splitFlags(args);
|
||||
const target = resolveCwd(rest, ctx.cwd);
|
||||
const scope = parseCheckArgs(fix ? `--fix ${rest}` : rest, ctx.cwd);
|
||||
|
||||
// Resume semantics: skip an already-terminal check unless --fresh forces a
|
||||
// reset. This mirrors the all/resume skip predicate so running the same
|
||||
// per-check command again after a success is a no-op (use --fresh to
|
||||
// re-scan deliberately).
|
||||
const existing = await loadRunState(ctx.cwd);
|
||||
const entry = existing?.checks[check.name];
|
||||
if (entry && isCheckTerminal(entry) && !fresh) {
|
||||
print(
|
||||
ctx,
|
||||
`pygienium ${check.label}: already ${entry.status} (use --fresh to re-run)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Reset the entry when --fresh, or when the fix flag changed since the prior
|
||||
// run: the phase skeleton (fix phase present only with --fix) must match
|
||||
// the requested mode, otherwise re-running analysis wouldn't record a fix
|
||||
// phase entry on a scan-only→--fix transition (and vice versa).
|
||||
if (existing && entry && (fresh || entry.fix !== fix)) {
|
||||
resetCheckEntry(existing, check.name, fix);
|
||||
await saveRunState(existing);
|
||||
}
|
||||
|
||||
const outcome = await runCheck({
|
||||
check,
|
||||
cwd: ctx.cwd,
|
||||
scope: { ...scope, cwd: ctx.cwd, target },
|
||||
scope: { ...scope, cwd: ctx.cwd, target, fix },
|
||||
existingState: existing,
|
||||
ui: ctx.ui,
|
||||
hasUI: ctx.hasUI,
|
||||
sendChatMessage: ctx.sendChatMessage,
|
||||
gitignore: !noGitignore,
|
||||
});
|
||||
|
||||
const giNote = outcome.gitignoreAppended
|
||||
@@ -145,7 +184,7 @@ export async function handleCheckCommand(
|
||||
/**
|
||||
* `/pygienium-all [path] [--fix] [--fresh] [--only=a,b]` — run every
|
||||
* registered check in sequence under a unified status strip, writing
|
||||
* `pygienium/all-summary.md`. Delegates to {@link runAllChecks}.
|
||||
* `.pygienium/all-summary.md`. Delegates to {@link runAllChecks}.
|
||||
*/
|
||||
export async function handleAllCommand(
|
||||
args: string,
|
||||
@@ -246,32 +285,36 @@ export async function handleResumeCommand(
|
||||
resetCheckEntry(state, entry.name);
|
||||
}
|
||||
print(ctx, `pygienium: resuming ${def.label}…`);
|
||||
const outcome = await runCheck({
|
||||
const outcome: CheckRunOutcome = await runCheck({
|
||||
check: def,
|
||||
cwd,
|
||||
scope: { cwd, target: cwd, fix: entry.fix, rest: [] },
|
||||
ui: ctx.ui,
|
||||
hasUI: ctx.hasUI,
|
||||
existingState: state,
|
||||
sendChatMessage: ctx.sendChatMessage,
|
||||
gitignore,
|
||||
});
|
||||
state = outcome.state;
|
||||
giAppended = giAppended || outcome.gitignoreAppended === true;
|
||||
ran++;
|
||||
print(ctx, `pygienium ${def.label}: ${outcome.status}`);
|
||||
}
|
||||
|
||||
markRunStatus(state, reconcileRunStatus(state));
|
||||
await saveRunState(state);
|
||||
const giNote = giAppended ? " · .pygienium/ added to .gitignore" : "";
|
||||
print(
|
||||
ctx,
|
||||
`pygienium: resume done — re-dispatched ${ran}, skipped ${skipped}; run ${state.status} (${runStatePath(cwd)})`,
|
||||
`pygienium: resume done — re-dispatched ${ran}, skipped ${skipped}; run ${state.status} (${runStatePath(cwd)})${giNote}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `/pygienium-export [path] [--check=<n>[,<n>]] [--status=<s>[,<s>]] [--out=md|json]`
|
||||
* — collect every check's `findings.md`/`changes.md` artifacts from
|
||||
* `pygienium/checks/<name>/` (and the legacy `.pygienium/checks/` root), apply
|
||||
* filters, and write a single bundle to `pygienium/export.{md|json}`.
|
||||
* `.pygienium/checks/<name>/`, apply filters, and write a single bundle to
|
||||
* `.pygienium/export.{md|json}`.
|
||||
*/
|
||||
export async function handleExportCommand(
|
||||
args: string,
|
||||
|
||||
Reference in New Issue
Block a user