feat(harness): single-select answers auto-confirm (reviewer_select persists)
F — reviewer_select options may now carry a `decision` payload {type, subject,
detail?, rationale?} plus an optional `advance`. Picking such an option IS the
confirmation: the gate persists it directly (tht decision add) and optionally
advances, with no redundant reviewer_decide/reviewer_confirm follow-up gate.
Options without a payload stay ask-only; back/exit/Other never persist.
Pure logic extracted + exported for unit tests: resolveSelectOutcome (classifies
the response) and decisionAddArgs (shared with reviewer_decide, DRY). Gate JS
suite 33/33 (gate_select_decision.test.js, +5); harness pytest 269 unchanged.
Contract docs updated together: reviewer_select tool description, SKILL.md
(widget summary, disciplines 2-3, Phase-1 single-pick), and the CLAUDE.md gate
note. Live verification (model truly emits reviewer_select+decision, decision in
review_decisions.jsonl, no follow-up gate) deferred to workstream G — it is
model-behavior-dependent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { resolveSelectOutcome, decisionAddArgs } = require("../../tht-gate.js");
|
||||
|
||||
// Workstream F — single-select answers auto-confirm. A concrete reviewer_select choice
|
||||
// that carries a `decision` payload IS the confirmation: the gate persists it directly
|
||||
// (tht decision add), with no redundant reviewer_decide gate. Control responses
|
||||
// (back / exit / Other) and bare ask-only choices stay non-persisting.
|
||||
|
||||
const OPTS = [
|
||||
{ id: "last24", label: "Ultimi 24 mesi", decision: { type: "time_window", subject: "periodo", detail: "24m" } },
|
||||
{ id: "ask_only", label: "Interpretazione A" }, // no decision payload -> ask-only
|
||||
];
|
||||
|
||||
test("a concrete choice carrying a decision resolves to a persisting outcome", () => {
|
||||
const out = resolveSelectOutcome(OPTS, { id: "u1", choices: ["last24"] });
|
||||
assert.equal(out.kind, "decision");
|
||||
assert.equal(out.option.label, "Ultimi 24 mesi");
|
||||
assert.deepEqual(out.decision, { type: "time_window", subject: "periodo", detail: "24m" });
|
||||
});
|
||||
|
||||
test("a concrete choice without a decision stays ask-only (no persistence)", () => {
|
||||
const out = resolveSelectOutcome(OPTS, { id: "u1", choices: ["ask_only"] });
|
||||
assert.equal(out.kind, "choice");
|
||||
assert.equal(out.option.label, "Interpretazione A");
|
||||
});
|
||||
|
||||
test("control responses never persist", () => {
|
||||
assert.equal(resolveSelectOutcome(OPTS, { control: "back" }).kind, "back");
|
||||
assert.equal(resolveSelectOutcome(OPTS, { control: "exit" }).kind, "exit");
|
||||
const ft = resolveSelectOutcome(OPTS, { control: "freetext", text: "qualcos'altro" });
|
||||
assert.equal(ft.kind, "freetext");
|
||||
assert.equal(ft.text, "qualcos'altro");
|
||||
});
|
||||
|
||||
test("decisionAddArgs builds the full tht decision add argv", () => {
|
||||
assert.deepEqual(
|
||||
decisionAddArgs("s1", { type: "time_window", subject: "periodo", detail: "24m", rationale: "scelto dal reviewer" }),
|
||||
["decision", "add", "--session", "s1", "--type", "time_window", "--subject", "periodo", "--detail", "24m", "--rationale", "scelto dal reviewer"],
|
||||
);
|
||||
});
|
||||
|
||||
test("decisionAddArgs omits optional detail/rationale when absent", () => {
|
||||
assert.deepEqual(
|
||||
decisionAddArgs("s1", { type: "t", subject: "sub" }),
|
||||
["decision", "add", "--session", "s1", "--type", "t", "--subject", "sub"],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user