Initial commit: pygenium as git submodule

This commit is contained in:
2026-08-07 14:54:45 -04:00
commit 581436ed23
61 changed files with 9331 additions and 0 deletions

310
src/modes/check-runner.ts Normal file
View File

@@ -0,0 +1,310 @@
/**
* modes/check-runner.ts — orchestrates a single check run.
*
* Pipeline:
* init/resolve run-state → Q0 recon (shared, once) →
* analysis sub-agent (buildScanTask) → fix sub-agent (buildFixTask, only
* with --fix) → verify gate → cleanup transient artifacts.
*
* Every phase is recorded on the persisted run-state via `run-state.ts`, so
* `/pygienium-status`, `/pygienium-resume`, and `/pygienium-export` reflect
* real progress. The check-runner is check-agnostic: a `CheckDefinition`
* supplies the task builders and gate; this module only wires the phases
* together.
*
* @module pygienium/modes/check-runner
*/
import { rm } from "node:fs/promises";
import { resolve, join } from "node:path";
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import type { CheckDefinition, CheckScope } from "../checks/registry.js";
import { runAgentTask } from "../agent-runner.js";
import { runRecon } from "../recon.js";
import { createPhaseStrip } from "../phases.js";
import {
applyPhaseStatus,
initRunState,
loadRunState,
markCheckStatus,
markRunStatus,
PHASE_ANALYSIS,
PHASE_CLEANUP,
PHASE_FIX,
PHASE_RECON,
PHASE_VERIFY,
phasesForCheck,
recordCheckOutput,
reconcileRunStatus,
saveRunState,
stateDir,
type RunState,
} from "../run-state.js";
/** Parsed command args handed to a `/pygienium-<check>` handler. */
export interface CheckRunnerArgs {
/** Raw arg string from the slash command. */
raw: string;
}
/** Resolve a raw arg string into a check scope (target path + flags). */
export function parseCheckArgs(raw: string, cwd: string): CheckScope {
const tokens = raw.trim().length > 0 ? raw.trim().split(/\s+/) : [];
let fix = false;
let target = cwd;
const rest: string[] = [];
for (const tok of tokens) {
if (tok === "--fix") {
fix = true;
} else if (tok.startsWith("--")) {
rest.push(tok);
} else {
target = tok;
}
}
// Absolute-ize target against cwd.
target = resolve(cwd, target);
return { cwd, target, fix, rest };
}
export interface RunCheckOptions {
/** The check definition to run. */
check: CheckDefinition;
/** Working directory (from `ctx.cwd`). */
cwd: string;
/** Parsed scope (target + flags). When omitted, derived from `rawArgs`. */
scope?: CheckScope;
/** Raw command args, used when `scope` is omitted. */
rawArgs?: string;
/** UI context (optional; null in print mode). */
ui?: ExtensionUIContext;
/** Whether dialog-capable UI is available. */
hasUI?: boolean;
/** Pre-existing run state to update (for `/pygienium-all` and resume). */
existingState?: RunState;
}
/** Outcome of a single check run. */
export interface CheckRunOutcome {
/** Final check status. */
status: "complete" | "failed" | "skipped";
/** Findings text from the analysis phase. */
findings?: string;
/** Changes text from the fix phase (when run with --fix). */
changes?: string;
/** Error message on failure. */
error?: string;
/** The updated run state. */
state: RunState;
}
/**
* Run a single check end-to-end, persisting progress to run-state.
*
* Resumable: if `existingState` already has terminal-ish progress for this
* check, the runner resumes the last in-progress phase rather than restarting.
*/
export async function runCheck(
opts: RunCheckOptions,
): Promise<CheckRunOutcome> {
const { check, cwd } = opts;
const scope = opts.scope ?? parseCheckArgs(opts.rawArgs ?? "", cwd);
// Resolve or init the run state, recording this check on first sight.
const state: RunState =
opts.existingState ?? (await loadRunState(cwd)) ?? initRunState(cwd, []);
if (!state.checks[check.name]) {
state.checks[check.name] = {
name: check.name,
label: check.label,
status: "pending",
fix: scope.fix,
phases: phasesForCheck(scope.fix),
};
}
await saveRunState(state);
const strip = createPhaseStrip({
ui: opts.ui,
hasUI: opts.hasUI ?? false,
checkLabel: check.label,
});
let findings = "";
let changes = "";
let error: string | undefined;
try {
// --- Phase: gate -----------------------------------------------------
const gateResult = await Promise.resolve(check.gate(cwd));
if (gateResult) {
// Skip this check entirely (no agent work).
for (const phase of state.checks[check.name]?.phases ?? []) {
if (phase.status === "pending") phase.status = "skipped";
}
markCheckStatus(state, check.name, "skipped", gateResult);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
strip.setPhase(PHASE_CLEANUP);
strip.done();
return { status: "skipped", error: gateResult, state };
}
// --- Phase: recon (shared, run once per run) -------------------------
if (!state.recon.complete) {
strip.setPhase(PHASE_RECON);
applyPhaseStatus(state, check.name, PHASE_RECON, "in_progress");
await saveRunState(state);
const snapshot = await runRecon(cwd);
state.recon = {
complete: true,
path: join(stateDir(cwd), "recon.json"),
finishedAt: snapshot.createdAt,
};
applyPhaseStatus(state, check.name, PHASE_RECON, "complete");
await saveRunState(state);
} else {
// Recon already done this run — mark this check's recon complete.
applyPhaseStatus(state, check.name, PHASE_RECON, "complete");
}
// --- Phase: analysis -------------------------------------------------
strip.setPhase(PHASE_ANALYSIS);
applyPhaseStatus(state, check.name, PHASE_ANALYSIS, "in_progress");
await saveRunState(state);
const scanTask = await check.buildScanTask(cwd, scope);
const scanResult = await runAgentTask({
cwd: scope.target,
agentName: check.agentName,
task: scanTask,
});
findings = scanResult.text;
recordCheckOutput(state, check.name, { findings });
if (!scanResult.ok) {
applyPhaseStatus(
state,
check.name,
PHASE_ANALYSIS,
"failed",
scanResult.error,
);
markCheckStatus(state, check.name, "failed", scanResult.error);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
return { status: "failed", error: scanResult.error, findings, state };
}
applyPhaseStatus(state, check.name, PHASE_ANALYSIS, "complete");
await saveRunState(state);
// --- Phase: fix (only with --fix) -----------------------------------
if (scope.fix) {
strip.setPhase(PHASE_FIX);
applyPhaseStatus(state, check.name, PHASE_FIX, "in_progress");
await saveRunState(state);
const fixTask = await check.buildFixTask(cwd, scope, findings);
const fixResult = await runAgentTask({
cwd: scope.target,
agentName: check.fixAgentName ?? "fixer",
task: fixTask,
});
changes = fixResult.text;
recordCheckOutput(state, check.name, { changes });
if (!fixResult.ok) {
applyPhaseStatus(
state,
check.name,
PHASE_FIX,
"failed",
fixResult.error,
);
markCheckStatus(state, check.name, "failed", fixResult.error);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
return {
status: "failed",
error: fixResult.error,
findings,
changes,
state,
};
}
applyPhaseStatus(state, check.name, PHASE_FIX, "complete");
await saveRunState(state);
}
// --- Phase: verify ---------------------------------------------------
strip.setPhase(PHASE_VERIFY);
applyPhaseStatus(state, check.name, PHASE_VERIFY, "in_progress");
await saveRunState(state);
// Verify is a lightweight self-check. A check may supply a dedicated
// `verify` hook to confirm its artifacts were produced (e.g.
// findings.md / changes.md exist). When absent, fall back to re-running
// the gate — unchanged from the historical behaviour.
const verifyResult = await Promise.resolve(
check.verify ? check.verify(scope) : check.gate(cwd),
);
if (verifyResult) {
applyPhaseStatus(state, check.name, PHASE_VERIFY, "failed", verifyResult);
markCheckStatus(state, check.name, "failed", verifyResult);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
return {
status: "failed",
error: verifyResult,
findings,
changes,
state,
};
}
applyPhaseStatus(state, check.name, PHASE_VERIFY, "complete");
await saveRunState(state);
// --- Phase: cleanup --------------------------------------------------
strip.setPhase(PHASE_CLEANUP);
applyPhaseStatus(state, check.name, PHASE_CLEANUP, "in_progress");
await saveRunState(state);
await cleanupTransientArtifacts(cwd, check.name);
applyPhaseStatus(state, check.name, PHASE_CLEANUP, "complete");
markCheckStatus(state, check.name, "complete");
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
return { status: "complete", findings, changes, state };
} catch (err) {
error = err instanceof Error ? err.message : String(err);
markCheckStatus(state, check.name, "failed", error);
markRunStatus(state, reconcileRunStatus(state));
await saveRunState(state);
return { status: "failed", error, findings, changes, state };
} finally {
strip.done();
}
}
/**
* Remove transient per-check scratch artifacts (e.g. agent-extracted
* manifests) written under `<cwd>/.pygienium/<check>-tmp-*`. Findings and
* changes are kept in run-state, not these scratch files, so removing them is
* safe.
*/
async function cleanupTransientArtifacts(
cwd: string,
checkName: string,
): Promise<void> {
const dir = stateDir(cwd);
// Best-effort: remove any `*-tmp-<check>` entries created by agents.
const { readdir } = await import("node:fs/promises");
let entries: string[];
try {
entries = await readdir(dir);
} catch {
return;
}
for (const entry of entries) {
if (entry.includes(`-tmp-${checkName}`)) {
await rm(join(dir, entry), { recursive: true, force: true }).catch(
() => {},
);
}
}
}