feat(harness): F6/F7 close on their last approval — no echo phase gate
Where completeness is machine-detectable, the reviewer's last substantive approval now closes the phase itself (same pattern as F3/F4/F8): - F6: approving the LAST CTE of the plan (kind:"cte_result" with next_cte now empty) advances the phase; a non-final CTE keeps the phase open and names the next one. - F7: kind:"sql" records sql_approved — which IS F7's only advance prerequisite — and advances immediately. Two reviewer interactions per session removed, both pure echoes. The summary phase gate remains only where completeness is a human judgment (F1, F2 with recorded memories, F5). SKILL.md states the rule and the five self-closing gates; L1 tests cover last/non-last CTE and sql close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
// F6/F7 auto-close: where completeness is machine-detectable the LAST substantive
|
||||
// approval closes the phase itself — approving the final CTE of the plan (F6) and
|
||||
// approving the SQL (F7) must run `tht phase advance` without a separate
|
||||
// reviewer_confirm kind:"phase"; a non-final CTE must NOT advance.
|
||||
//
|
||||
// HARNESS NOTE: the gate's ESM `import { execFileSync }` is snapshotted from the CJS
|
||||
// namespace at first require — later reassignments of cp.execFileSync are NOT seen.
|
||||
// Install ONE dispatcher before requiring the gate; tests swap its `current` handler.
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const cp = require("node:child_process");
|
||||
const { createRequire } = require("node:module");
|
||||
const path = require("node:path");
|
||||
|
||||
const GATE = path.join(__dirname, "..", "..", "tht-gate.js");
|
||||
if (typeof globalThis.require === "undefined") {
|
||||
globalThis.require = createRequire(GATE);
|
||||
}
|
||||
|
||||
const shell = { current: () => "" };
|
||||
cp.execFileSync = (file, args, opts) => shell.current(file, args, opts);
|
||||
|
||||
const META = JSON.stringify({
|
||||
max_phase: 8,
|
||||
phases: [
|
||||
{ num: 6, id: "F6" },
|
||||
{ num: 7, id: "F7" },
|
||||
{ num: 8, id: "F8" },
|
||||
],
|
||||
});
|
||||
|
||||
// `cte next` walks the queue one entry per `decision add cte_approved`.
|
||||
function useShell({ phase, cteQueue = [] }) {
|
||||
const calls = [];
|
||||
const queue = [...cteQueue];
|
||||
shell.current = (file, args) => {
|
||||
calls.push(args.join(" "));
|
||||
if (args[0] === "phase" && args[1] === "meta") return META;
|
||||
if (args[0] === "phase" && args[1] === "show") return `Fase corrente: ${phase}\n`;
|
||||
if (args[0] === "cte" && args[1] === "next") return queue.length ? `${queue[0]}\n` : "";
|
||||
if (args[0] === "decision" && args[1] === "add" && args.includes("cte_approved")) {
|
||||
queue.shift();
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
async function runConfirm(kind, artifactKind) {
|
||||
const gate = require(GATE);
|
||||
const { createFakePi } = require("./fake_pi_runtime.js");
|
||||
const { pi, ctx, tools } = createFakePi();
|
||||
ctx.cwd = "/nonexistent-thothii-test-cwd";
|
||||
gate.default(pi);
|
||||
// Reset the module-level phase-meta cache captured by a previous test's handler.
|
||||
await pi.emit("session_start", {});
|
||||
ctx.ui.input = async (title) => {
|
||||
const d = JSON.parse(title);
|
||||
return JSON.stringify({ id: d.id, kind: "artifact-gate", choices: ["approve"] });
|
||||
};
|
||||
return tools.get("reviewer_confirm").def.execute(
|
||||
"call-1",
|
||||
{ session: "s1", kind, title: "t", artifact: { kind: artifactKind, data: "# md" } },
|
||||
null,
|
||||
null,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
test("approving a NON-final CTE records it and does NOT advance the phase", async () => {
|
||||
const calls = useShell({ phase: 6, cteQueue: ["cte_a", "cte_b"] });
|
||||
const result = await runConfirm("cte_result", "cte_result");
|
||||
assert.match(result.content[0].text, /cte_a.*approvato/i);
|
||||
assert.match(result.content[0].text, /cte_b/);
|
||||
assert.ok(!calls.some((c) => c.startsWith("phase advance")), `no advance in: ${calls}`);
|
||||
});
|
||||
|
||||
test("approving the LAST CTE of the plan closes F6 automatically", async () => {
|
||||
const calls = useShell({ phase: 6, cteQueue: ["cte_finale"] });
|
||||
const result = await runConfirm("cte_result", "cte_result");
|
||||
assert.match(result.content[0].text, /cte_finale.*approvato/i);
|
||||
assert.match(result.content[0].text, /chiusa automaticamente/);
|
||||
assert.match(result.content[0].text, /NON presentare reviewer_confirm/);
|
||||
assert.ok(
|
||||
calls.some((c) => c === "phase advance --session s1"),
|
||||
`expected phase advance in: ${calls}`,
|
||||
);
|
||||
assert.ok(!calls.some((c) => c.startsWith("session finalize")), "F6 must not finalize");
|
||||
});
|
||||
|
||||
test("approving the SQL closes F7 automatically (no separate phase gate)", async () => {
|
||||
const calls = useShell({ phase: 7 });
|
||||
const result = await runConfirm("sql", "sql");
|
||||
assert.match(result.content[0].text, /SQL approvato/);
|
||||
assert.match(result.content[0].text, /chiusa automaticamente/);
|
||||
assert.ok(
|
||||
calls.some((c) => c.includes("decision add --session s1 --type sql_approved")),
|
||||
`expected sql_approved in: ${calls}`,
|
||||
);
|
||||
assert.ok(
|
||||
calls.some((c) => c === "phase advance --session s1"),
|
||||
`expected phase advance in: ${calls}`,
|
||||
);
|
||||
assert.ok(!calls.some((c) => c.startsWith("session finalize")), "F7 must not finalize");
|
||||
});
|
||||
Reference in New Issue
Block a user