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:
2026-06-30 18:01:50 +02:00
co-authored by Claude Opus 4.8
parent b056ff334f
commit cef9ae4368
5 changed files with 159 additions and 52 deletions
@@ -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"],
);
});