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:
2026-08-09 16:45:29 -04:00
parent 288506e84d
commit d0e8ad5571
17 changed files with 183 additions and 82 deletions

View File

@@ -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"));
});
});