Task — engineering-spec@1

Mechanical HITL lock - verdict-gated flips + memory.status_overridden

doneTASK-CUO-303
module cuo · class product · priority p0 · created 2026-07-23 · shipped null
depends on none · blocks none

TASK-CUO-303: Mechanical HITL lock - verdict-gated flips + memory.status_overridden

Summary

The two human-acceptance gates (reviewing -> ready_to_test, testing -> done) are doctrine in STATUS-REFERENCE §1.4 and EXECUTION-DISCIPLINE §2a, but nothing mechanical refuses them: backlog-mutate.mjs flips any enum-legal transition, the promised memory.status_overridden audit row is unwritable by the doc-driven appender (closed 4-kind list), and HITL_REQUIRED="true" in gates.env is read by nothing. An agent that ignores the prompt can self-approve its own work end to end. This task adds a verdict gate to the single backlog write path, adds the missing audit-row kind, and removes the dead flag.

Problem

Audit finding C2, verified first-hand on 2026-07-23:

  1. No transition lock. cmdFlip (backlog-mutate.mjs:277) enforces the status enum, a byte pre-image, and truth-precedes-index (TASK-IMP-120) - all integrity checks, none of them authority checks. The two transitions that doctrine reserves for a recorded human verdict flip exactly like any other.
  2. The promised audit row cannot exist. STATUS-REFERENCE §1.4 says every human verdict or override emits one memory.status_overridden aux row. memory-append.mjs - the only writer available to doc-driven (non-Python) workflows - refuses every kind outside its closed four (memory-append.mjs:102). The doctrine promises an audit trail the tooling cannot produce.
  3. A dead flag implies enforcement that does not exist. install.sh:319 writes HITL_REQUIRED="true" into every gates.env; nothing reads it. A reader auditing the machine sees a flag named like a lock and reasonably concludes one exists.

Proposed Solution

Gate the two human-acceptance transitions in cmdFlip behind two new required flags: --verdict-by <actor> (non-empty identity string) and --verdict-evidence <path> (an existing, non-empty file - the review note, test-acceptance note, or transcript the human produced). A flip of reviewing -> ready_to_test or testing -> done without both flags refuses with a new distinct exit code 8 and a message quoting STATUS-REFERENCE §1.4; all other transitions are untouched. When the flags are present, the flip proceeds and additionally appends one memory.status_overridden row (payload {actor, task_id, prior_status, new_status, reason: evidence-path}) via memory-append.mjs when a BRAIN store is resolvable; when no store exists, the evidence file itself is the record and the flip still succeeds - append-failure on a present store, however, fails the flip (audit-before-action). Extend memory-append.mjs's closed kind list with status_overridden and its payload validation. Remove the dead HITL_REQUIRED variable from the gates.env generator, keeping the prose comment about the two human gates. Update ship-tasks.md's HITL step descriptions to pass the new flags, and add a CHANGELOG entry marking the new refusal as breaking for any tooling that flips the two gate transitions bare.

Alternatives Considered

Success Metrics

Scope

In scope: cmdFlip verdict gate + exit code 8, memory-append.mjs kind extension + payload validation, install.sh gates.env generation (drop the dead variable, keep the prose), ship-tasks.md HITL step invocation updates, CHANGELOG entry, and the new test suite.

Out of scope / Non-Goals

Dependencies

None blocking. Builds on TASK-CUO-205 (done - made backlog-mutate.mjs the single documented backlog write path, which is what gives a lock in this tool its force) and TASK-IMP-120 (done - truth-precedes-index; this task's gate runs AFTER those refusals so the refusal precedence is: missing row / drift / truth-mismatch first, verdict gate last). TASK-IMP-140's benchmark gate G2 verifies this task's refusal in CI - soft forward reference via related_tasks, no cycle.

AI Authorship Disclosure

1. Description (normative)

2. Acceptance criteria

3. Edge cases

Audit

§1 — Verdict summary

Six §1 clauses, seven ACs, six edge cases including a security-class row. All clauses trace to ACs via traces_to. The audit's pressure points were authority-vs-integrity separation (refusal precedence), the audit-before-action ordering on the store-present path, and an honest statement of the frontmatter-edit bypass this task deliberately does not close. One fabricated test-suite reference was caught and corrected against the real corpus.

§2 — Findings (all resolved)

ISS-001 — guardrail metric cited a test suite that does not exist

The Success Metrics guardrail named test_backlog_mutate.sh; the backlog-mutate coverage actually lives in tools/install/tests/test_workflow_helpers.sh and the lifecycle spine in tools/install/tests/test_e2e_skeleton.sh (verified by listing tools/install/tests/ and grepping for backlog-mutate). A metric anchored to a nonexistent file is unfalsifiable — the anti-fabrication class. Resolved: guardrail now names the two real suites, and both were added to modified_files since the e2e drives the gate transitions and must gain the flags.

ISS-002 — refusal precedence was implied, not contractual (exit 6 vs exit 8)

If the verdict gate evaluated before the pre-image checks, a racing flip would report "verdict required" when the truth is "your pre-image drifted" — misdiagnosis that sends the operator hunting the wrong fix, and a verdict could be consumed by a doomed flip. Resolved: clause 1.2 pins the evaluation order; AC 3 constructs the drift+no-flags case and asserts exit 6, not 8.

ISS-003 — store-present append failure was originally silent-tolerable

The first draft let the flip succeed when the row append failed, which inverts audit-before-action (§3.8 of the authoring discipline): the index would move with no audit row on a store that exists. Resolved: clause 1.4 makes append failure on a present store fail the whole flip; AC 5 asserts it with an unwritable store fixture, and distinguishes the legitimately store-less path (flip succeeds, stderr says the evidence file is the record).

ISS-004 — row kind naming had to match the appender's grammar, not the doctrine's prose

STATUS-REFERENCE §1.4 says memory.status_overridden; the appender's existing kinds are bare (task_routed_back, not memory.task_routed_back) and become the row's op field. A spec that demanded the dotted form would have shipped an inconsistency with the appender's own closed-set grammar at memory-append.mjs:102. Resolved: clause 1.3 specifies kind status_overridden emitted as op status_overridden, "consistent with the existing four kinds" — the doctrine's dotted name is the audit-row taxonomy name, the appender's bare kind is the wire form, and the spec now says which is which.

ISS-005 — the frontmatter-edit bypass was unstated in the first draft

The lock guards the tool path; an agent editing spec.md directly and regenerating the backlog bypasses it. Omitting that would oversell the control — exactly the doctrine-vs-enforcement gap the parent audit exists to close. Resolved: Non-Goals + edge case state the residual explicitly, cite TASK-CUO-205 (single write path) as why the tool gate is still load-bearing, and name the 1.5.0 state engine as the full closure.

ISS-006 — evidence-file validation semantics were unpinned

"An evidence file" left directories, empty files, and unreadable paths undefined. Resolved: clause 1.1 pins exists + regular file + non-empty at flip time; AC 2 asserts missing and empty both refuse; the edge case pins directory/unreadable as "does not exist" and scopes content quality to the human reviewer, not the tool.

ISS-007 — operator superset overrides needed an explicit boundary

STATUS-REFERENCE §1.4 grants operators any-to-any override power; a careless reading of this task could extend the verdict gate to all of them (breaking re-audit and skip-review flows) or none (breaking nothing but recording nothing). Resolved: edge case pins the boundary — exactly the two forward gate transitions are locked in this task; widening verdict recording to all overrides is named future scope.

§3 — TRACE-006 semantic sufficiency (per clause)

ClauseVerb demandCited test assertsVerdict
1.1 MUST refuse bare gate flips (exit 8, no write); others unchangedrefusal code + byte-identical file + flagged success + route-back flag-freeAC 1: all four asserted; AC 2: evidence validation refusalssufficient
1.2 MUST evaluate after existing refusalsconstructed drift case yields 6 not 8AC 3: asserts the precedence directlysufficient
1.3 MUST accept status_overridden with validated payloadappend succeeds complete, refuses per-missing-field, unknown kind still refusedAC 4: asserts all three behaviorssufficient
1.4 MUST append exactly one row store-present; MUST fail flip on append failure; MUST succeed store-less with stderr noterow count + payload match + unwritable-store failure + storeless successAC 5: asserts all four halvessufficient after revision (ISS-003)
1.5 MUST stop emitting HITL_REQUIRED, keep prosenegative substring + positive prose in generated fileAC 6: asserts both halves against scratch installsufficient
1.6 MUST document flags in ship-tasks + breaking CHANGELOGpositive substrings in both filesAC 7: asserts bothsufficient

§4 — Resolution

Seven findings — one anti-fabrication, six material contract gaps — all resolved in the audited revision. Score = 10/10.

Status transition draft -> ready_to_implement is authorised by this verdict per STATUS-REFERENCE.md §1.1. The two human-acceptance gates in /ship-tasks are unchanged and remain recorded human verdicts — this audit clears the spec-correctness gate only. (This task is itself the one that makes those two gates mechanically refusable.)


End of TASK-CUO-303 audit.