feat: keep run artifacts under hidden .pygienium/ and out of git
All per-check artifacts, all-summary, and export move from pygienium/ to .pygienium/; ensureRunStateIgnored appends .pygienium/ to the target repo's .gitignore on first run (opt out with --no-gitignore), so a run never stages its own output. Export now reads a single canonical root.
This commit is contained in:
@@ -35,9 +35,9 @@ function fakeCheck(name: string): CheckDefinition {
|
||||
fixAgentName: "fixer",
|
||||
phaseId: "scan",
|
||||
buildScanTask: (_cwd, scope) =>
|
||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
buildFixTask: (_cwd, _scope, findings) =>
|
||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
gate: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Mirrors the spec scenario: run `/pygienium-all` on a small repo and assert:
|
||||
* - every registered check runs exactly once in registry order;
|
||||
* - run-state shows all checks complete and the overall run complete;
|
||||
* - `pygienium/all-summary.md` is present and lists per-check outcomes;
|
||||
* - `.pygienium/all-summary.md` is present and lists per-check outcomes;
|
||||
* - `--only=alpha,gamma` narrows the candidate set preserving order;
|
||||
* - interrupted/resumed runs re-dispatch non-terminal checks while skipping
|
||||
* terminal ones, unless `--fresh` resets everything.
|
||||
@@ -57,9 +57,9 @@ function fakeCheck(name: string): CheckDefinition {
|
||||
fixAgentName: "fixer",
|
||||
phaseId: "scan",
|
||||
buildScanTask: (_cwd, scope) =>
|
||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
buildFixTask: (_cwd, _scope, findings) =>
|
||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
gate: () => undefined,
|
||||
};
|
||||
}
|
||||
@@ -132,12 +132,16 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
await rm(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("parseAllArgs parses path, --fix, --fresh, and --only", () => {
|
||||
it("parseAllArgs parses path, --fix, --fresh, --no-gitignore, and --only", () => {
|
||||
const p = parseAllArgs("subdir --fix --only=alpha,beta --fresh", cwd);
|
||||
expect(p.target).toBe(join(cwd, "subdir"));
|
||||
expect(p.fix).toBe(true);
|
||||
expect(p.fresh).toBe(true);
|
||||
expect(p.gitignore).toBe(true); // default: keep the .gitignore guard on
|
||||
expect(p.only).toEqual(["alpha", "beta"]);
|
||||
const noGi = parseAllArgs("--no-gitignore", cwd);
|
||||
expect(noGi.gitignore).toBe(false);
|
||||
expect(noGi.target).toBe(cwd);
|
||||
});
|
||||
|
||||
it("selectChecks preserves registry order for the --only subset", () => {
|
||||
@@ -176,7 +180,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
expect(state?.recon.complete).toBe(true);
|
||||
});
|
||||
|
||||
it("writes pygienium/all-summary.md listing per-check outcomes", async () => {
|
||||
it("writes .pygienium/all-summary.md listing per-check outcomes", async () => {
|
||||
registerCheck(fakeCheck("alpha"));
|
||||
registerCheck(fakeCheck("beta"));
|
||||
|
||||
@@ -193,7 +197,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
expect(summary).toContain("changes:");
|
||||
// Artifacts actually exist on disk.
|
||||
const alphaFindings = await readFile(
|
||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
||||
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||
"utf8",
|
||||
);
|
||||
expect(alphaFindings).toContain("alpha findings");
|
||||
@@ -233,7 +237,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
|
||||
expect(track.dispatched.length).toBe(4); // 2 checks × 2 phases
|
||||
const firstAlphaFix = await readFile(
|
||||
join(cwd, "pygienium", "checks", "alpha", "changes.md"),
|
||||
join(cwd, ".pygienium", "checks", "alpha", "changes.md"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
@@ -255,7 +259,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
expect(state3?.status).toBe("complete");
|
||||
// The fresh re-run overwrote alpha's changes.md (still valid content).
|
||||
const alphaFix2 = await readFile(
|
||||
join(cwd, "pygienium", "checks", "alpha", "changes.md"),
|
||||
join(cwd, ".pygienium", "checks", "alpha", "changes.md"),
|
||||
"utf8",
|
||||
);
|
||||
expect(alphaFix2).toContain("alpha changes");
|
||||
@@ -278,7 +282,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
||||
finishedAt: Date.now(),
|
||||
};
|
||||
// Pre-create alpha's on-disk artifacts so its completed entry has artifacts.
|
||||
const alphaDir = join(cwd, "pygienium", "checks", "alpha");
|
||||
const alphaDir = join(cwd, ".pygienium", "checks", "alpha");
|
||||
await mkdir(alphaDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(alphaDir, "findings.md"),
|
||||
|
||||
@@ -145,15 +145,15 @@ describe("deep-modules check", () => {
|
||||
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("findings.md and changes.md live under pygienium/checks/deep-modules/", async () => {
|
||||
it("findings.md and changes.md live under .pygienium/checks/deep-modules/", async () => {
|
||||
await seedPassThrough(cwd);
|
||||
const check = getCheck("deep-modules")!;
|
||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||
expect(findingsPath(cwd)).toBe(
|
||||
join(cwd, "pygienium", "checks", "deep-modules", "findings.md"),
|
||||
join(cwd, ".pygienium", "checks", "deep-modules", "findings.md"),
|
||||
);
|
||||
expect(changesPath(cwd)).toBe(
|
||||
join(cwd, "pygienium", "checks", "deep-modules", "changes.md"),
|
||||
join(cwd, ".pygienium", "checks", "deep-modules", "changes.md"),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -208,16 +208,16 @@ describe("defensive-guards check", () => {
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("findings.md and changes.md live under pygienium/checks/defensive-guards/", async () => {
|
||||
it("findings.md and changes.md live under .pygienium/checks/defensive-guards/", async () => {
|
||||
await seedNoise(cwd);
|
||||
await seedBoundary(cwd);
|
||||
const check = getCheck("defensive-guards")!;
|
||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||
expect(findingsPath(cwd)).toBe(
|
||||
join(cwd, "pygienium", "checks", "defensive-guards", "findings.md"),
|
||||
join(cwd, ".pygienium", "checks", "defensive-guards", "findings.md"),
|
||||
);
|
||||
expect(changesPath(cwd)).toBe(
|
||||
join(cwd, "pygienium", "checks", "defensive-guards", "changes.md"),
|
||||
join(cwd, ".pygienium", "checks", "defensive-guards", "changes.md"),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ function fakeCheck(name: string): CheckDefinition {
|
||||
fixAgentName: "fixer",
|
||||
phaseId: "scan",
|
||||
buildScanTask: (_cwd, scope) =>
|
||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||
buildFixTask: (_cwd, _scope, findings) =>
|
||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||
gate: () => undefined,
|
||||
};
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function stubCtx(cwd: string): PygieniumCtx {
|
||||
function trackingRunner(): { runner: AgentRunner; dispatched: string[] } {
|
||||
const dispatched: string[] = [];
|
||||
const runner: AgentRunner = async (opts) => {
|
||||
// Tag by check name from the task text (`!write pygienium/checks/<name>/`).
|
||||
// Tag by check name from the task text (`!write .pygienium/checks/<name>/`).
|
||||
const m = /pygienium\/checks\/([^/]+)\//.exec(opts.task);
|
||||
if (m) dispatched.push(m[1] as string);
|
||||
return fakeAgentRunner(opts);
|
||||
@@ -297,7 +297,7 @@ describe("status / resume / export (task 13)", () => {
|
||||
void state;
|
||||
|
||||
await captureStdout(() => handleExportCommand("", stubCtx(cwd)));
|
||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
||||
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||
expect(bundle).toContain("# Pygienium export");
|
||||
expect(bundle).toContain("## alpha (complete)");
|
||||
expect(bundle).toContain("## beta (complete)");
|
||||
@@ -314,7 +314,7 @@ describe("status / resume / export (task 13)", () => {
|
||||
await captureStdout(() =>
|
||||
handleExportCommand("--check=beta", stubCtx(cwd)),
|
||||
);
|
||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
||||
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||
expect(bundle).toContain("## beta (complete)");
|
||||
expect(bundle).not.toContain("## alpha");
|
||||
});
|
||||
@@ -330,7 +330,7 @@ describe("status / resume / export (task 13)", () => {
|
||||
await captureStdout(() =>
|
||||
handleExportCommand("--status=failed", stubCtx(cwd)),
|
||||
);
|
||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
||||
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||
expect(bundle).toContain("## alpha (failed)");
|
||||
expect(bundle).not.toContain("## beta");
|
||||
});
|
||||
@@ -345,7 +345,7 @@ describe("status / resume / export (task 13)", () => {
|
||||
handleExportCommand("--out=json", stubCtx(cwd)),
|
||||
);
|
||||
expect(out.join("\n")).toContain("export.json");
|
||||
const raw = await readFile(join(cwd, "pygienium", "export.json"), "utf8");
|
||||
const raw = await readFile(join(cwd, ".pygienium", "export.json"), "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
checks: Array<{ name: string; status: string; findings: string }>;
|
||||
};
|
||||
@@ -370,20 +370,22 @@ describe("status / resume / export (task 13)", () => {
|
||||
expect(f.out).toBe("json");
|
||||
});
|
||||
|
||||
it("gatherExportEntries reads only the canonical pygienium/checks/ root", async () => {
|
||||
await mkdir(join(cwd, "pygienium", "checks", "alpha"), { recursive: true });
|
||||
await writeFile(
|
||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
||||
"# alpha findings\n",
|
||||
"utf8",
|
||||
);
|
||||
// A stray .pygienium/checks/ dir (the removed legacy root) is ignored now
|
||||
// that all checks write to the single canonical `pygienium/checks/` root.
|
||||
await mkdir(join(cwd, ".pygienium", "checks", "ghost"), {
|
||||
it("gatherExportEntries reads only the canonical .pygienium/checks/ root", async () => {
|
||||
await mkdir(join(cwd, ".pygienium", "checks", "alpha"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(
|
||||
join(cwd, ".pygienium", "checks", "ghost", "findings.md"),
|
||||
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||
"# alpha findings\n",
|
||||
"utf8",
|
||||
);
|
||||
// A stray pygienium/checks/ dir (the old non-hidden root) is ignored now
|
||||
// that all checks write to the single canonical `.pygienium/checks/` root.
|
||||
await mkdir(join(cwd, "pygienium", "checks", "ghost"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(
|
||||
join(cwd, "pygienium", "checks", "ghost", "findings.md"),
|
||||
"# ghost findings\n",
|
||||
"utf8",
|
||||
);
|
||||
@@ -393,7 +395,7 @@ describe("status / resume / export (task 13)", () => {
|
||||
expect(alpha?.findings).toContain("alpha findings");
|
||||
expect(alpha?.status).toBe("unknown");
|
||||
expect(alpha?.findingsPath).toBe(
|
||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
||||
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||
);
|
||||
expect(entries.find((e) => e.name === "ghost")).toBeUndefined();
|
||||
});
|
||||
@@ -415,6 +417,6 @@ describe("status / resume / export (task 13)", () => {
|
||||
it("exportRun writes nothing useful and reports zero entries cleanly", async () => {
|
||||
const result = await exportRun(cwd, undefined, {});
|
||||
expect(result.entries).toHaveLength(0);
|
||||
expect(relative(cwd, result.path)).toBe(join("pygienium", "export.md"));
|
||||
expect(relative(cwd, result.path)).toBe(join(".pygienium", "export.md"));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user