Mechanical HITL lock - verdict-gated flips + memory.status_overridden
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:
- 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. - The promised audit row cannot exist. STATUS-REFERENCE §1.4 says every human verdict or override emits one
memory.status_overriddenaux 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. - A dead flag implies enforcement that does not exist.
install.sh:319writesHITL_REQUIRED="true"into everygates.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
- Consume
HITL_REQUIRED(make the lock conditional on it). Rejected: the lock is doctrine, not configuration - EXECUTION-DISCIPLINE §2a governs platform-wide and offers no opt-out; a flag that looks like it can disable the lock by editing a gitignored file would replace a dead lie with a live one. - Verify the verdict actor is human (signature, identity attestation). Rejected for this task: signed/attributed verdict artifacts are the 1.4.x roadmap item the audit already names. This task makes the verdict recorded and refusable, not cryptographically attributable - the honest increment that closes the self-approval path for compliant tooling.
- Lock the transitions in the spec frontmatter instead (a pre-commit hook rejecting status edits). Rejected: frontmatter is written by humans and agents alike in editors; a commit-time reject fires after the work is staged, refuses legitimate operator overrides, and cannot capture WHO decided. The flip executor is where authority is asserted (TASK-CUO-205 made it the single write path); guarding there is both sufficient for tooling and non-intrusive for operators.
- Auto-generate the evidence file when absent. Rejected: an auto-generated verdict is a self-approval with paperwork; the entire point is that the file preexists the flip because a human produced it.
Success Metrics
- Primary: by the next CyberOS release, a bare
backlog-mutate flip <id> reviewing ready_to_test(ortesting done) exits 8 with no file written, and the same flip with--verdict-by+--verdict-evidencesucceeds and (when a store is present) lands exactly onememory.status_overriddenrow on the chain. Baseline today: the bare flip succeeds silently. - Guardrail: zero behavior change for every other transition - the existing backlog-mutate coverage in
tools/install/tests/test_workflow_helpers.shand the mechanical spine intools/install/tests/test_e2e_skeleton.shpass unmodified (except where the e2e drives the two gate transitions, which gains the flags), and rework/off-ramp flips (-> ready_to_implement,-> on_hold,-> closed) require no verdict flags.
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
- Cryptographic signing or identity verification of verdict actors (1.4.x roadmap; this task records, it does not attest).
- Locking direct frontmatter edits or the
regenpath - an agent editingspec.mdby hand bypasses any tool gate; that residual is explicitly accepted and documented in ship-tasks.md, with the transition-locked state engine (1.5.0 roadmap on the 1.x line) as the full closure. The G2 benchmark checker (TASK-IMP-140) asserts the tool-path refusal this task ships. - The route-back ceiling constant (TASK-CUO-304) and gate-floor behavior (TASK-CUO-302) - separate tasks in this batch.
- Retroactively generating
status_overriddenrows for historical transitions.
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
- Tools used: Claude (Fable 5) running the CyberOS
task-authorskill in Cursor, as the task-authoring wave of the 2026-07-23 hardening plan. - Scope: every
source_pagesline was read at HEAD in this checkout during authoring; the absence of anyHITL_REQUIREDconsumer and the closed kind list were verified by repo-wide grep, not carried from the audit report. - Human review: the hardening plan (including this task's scope bullet) was operator-approved on 2026-07-23; the remove-not-consume decision for
HITL_REQUIREDis recorded insource_decisionsfor the reviewer to revisit at the review acceptance gate.
1. Description (normative)
- 1.1
backlog-mutate.mjs flipMUST refuse the transitionsreviewing -> ready_to_testandtesting -> doneunless BOTH--verdict-by <actor>(non-empty string) and--verdict-evidence <path>(a path that exists and is a non-empty regular file at flip time) are supplied. The refusal MUST use the new distinct exit code 8 (verdict required), MUST name STATUS-REFERENCE §1.4 in its message, and MUST NOT write any file. Every other transition MUST behave exactly as today, verdict flags ignored if supplied. - 1.2 The verdict gate MUST evaluate AFTER the existing refusals (missing/duplicate row exit 6, pre-image drift exit 6, truth-precedes-index exit 6), so existing failure modes keep their codes and the new code 8 means exactly one thing: the transition was otherwise legal but no verdict was recorded.
- 1.3
memory-append.mjsMUST accept the kindstatus_overridden(emitted on-chain as opstatus_overridden, consistent with the existing four kinds), validating a payload object with required non-empty string fieldsactor,task_id,prior_status,new_status,reason. Unknown kinds MUST keep today's refusal behavior. - 1.4 On a verdict-gated flip where a BRAIN store is resolvable (the same store-resolution the appender already implements), the flip MUST append exactly one
status_overriddenrow whose payload carries{actor: <--verdict-by>, task_id, prior_status, new_status, reason: <--verdict-evidence path>}, and a failed append on a present store MUST fail the flip (audit-before-action: no index move without its audit row). When no store is resolvable, the flip MUST succeed without a row - the evidence file is the record - and MUST say so on stderr. - 1.5
install.shMUST stop emitting theHITL_REQUIREDvariable into generatedgates.envfiles, keeping the prose comment that states the two human-acceptance gates are never automated. No script consumes the variable today (verified 2026-07-23), so removal changes no behavior. - 1.6
ship-tasks.md's two HITL steps MUST document the flag-carrying flip invocation as the way the recorded human verdict advances the cell, andCHANGELOG.mdMUST gain an entry marking the bare-flip refusal as a breaking change for tooling that automates the two gate transitions.
2. Acceptance criteria
- [ ] AC 1 (traces_to: #1.1) - bare
flip <id> reviewing ready_to_testandflip <id> testing doneexit 8, mention "STATUS-REFERENCE" and "verdict", and leave BACKLOG.md byte-identical; the same flips with both flags succeed;flip <id> testing ready_to_implement(route-back) needs no flags - test:tools/install/tests/test_hitl_lock.sh::t01_bare_gate_flip_refused - [ ] AC 2 (traces_to: #1.1) -
--verdict-evidencepointing at a missing path or an empty file refuses with exit 8 and no write;--verdict-by ""refuses with exit 8 - test:tools/install/tests/test_hitl_lock.sh::t02_evidence_must_exist_nonempty - [ ] AC 3 (traces_to: #1.2) - on a row whose cell drifted from the recorded pre-image AND with verdict flags absent, the exit is 6 (pre-image) not 8, proving refusal precedence - test:
tools/install/tests/test_hitl_lock.sh::t03_refusal_precedence_six_before_eight - [ ] AC 4 (traces_to: #1.3) -
memory-append.mjs append <store> status_overriddenwith a complete payload appends a chained row; each missing/empty required field refuses with exit 2 and writes nothing; an unknown kind still refuses - test:tools/install/tests/test_hitl_lock.sh::t04_status_overridden_kind_validated - [ ] AC 5 (traces_to: #1.4) - a verdict-gated flip against a scratch repo WITH a seeded store lands exactly one
status_overriddenrow (payload fields match the flags); with the store made unwritable the flip fails and BACKLOG.md is unchanged; with NO store the flip succeeds, no row, stderr notes the evidence file is the record - test:tools/install/tests/test_hitl_lock.sh::t05_audit_before_action - [ ] AC 6 (traces_to: #1.5) - a scratch install's generated
gates.envcontains noHITL_REQUIREDsubstring and retains the human-gates prose comment - test:tools/install/tests/test_hitl_lock.sh::t06_dead_flag_removed - [ ] AC 7 (traces_to: #1.6) -
ship-tasks.mddocuments--verdict-byand--verdict-evidenceat both HITL steps, and CHANGELOG's top entry mentions the refusal, the word "breaking", and exit code 8 - test:tools/install/tests/test_hitl_lock.sh::t07_docs_and_changelog
3. Edge cases
- Operator superset overrides (STATUS-REFERENCE §1.4) - e.g.
done -> ready_to_reviewre-audit,ready_to_review -> ready_to_testskip-review - are NOT the two forward gate transitions and stay flag-free in this task. Widening verdict recording to all overrides is deliberate future scope; this task locks exactly the two transitions doctrine names as mandatory-human. - Direct frontmatter edit + regen bypass: an agent can write
status: doneinto spec.md and regenerate the backlog without ever callingflip. Accepted residual, stated in ship-tasks.md: the lock closes the documented tool path (the only path compliant workflows use); the 1.5.0 state engine closes the rest. The G2 checker tests the tool path. - Evidence file is a directory or unreadable: treated as "does not exist" - refusal 8. The gate checks regular-file-ness and non-zero size, nothing else; content quality is the reviewer's judgment, not the tool's.
--jsonoutput mode: the refusal and the success MUST both carry the verdict fields in the JSON envelope so ship-manifest consumers can record them; the exit code is authoritative either way.- Two flips racing on the same row: unchanged from today - the pre-image/optimistic-concurrency refusal (exit 6) fires before the verdict gate (AC 3's precedence), so the race loser cannot consume a verdict.
- Security-class: the verdict flags introduce no new execution surface (no eval, no shell-out); the evidence path is read for existence/size only, never executed or parsed. The appended row goes through the existing appender's §4.2 lock + two-phase write discipline.
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)
| Clause | Verb demand | Cited test asserts | Verdict |
|---|---|---|---|
| 1.1 MUST refuse bare gate flips (exit 8, no write); others unchanged | refusal code + byte-identical file + flagged success + route-back flag-free | AC 1: all four asserted; AC 2: evidence validation refusals | sufficient |
| 1.2 MUST evaluate after existing refusals | constructed drift case yields 6 not 8 | AC 3: asserts the precedence directly | sufficient |
| 1.3 MUST accept status_overridden with validated payload | append succeeds complete, refuses per-missing-field, unknown kind still refused | AC 4: asserts all three behaviors | sufficient |
| 1.4 MUST append exactly one row store-present; MUST fail flip on append failure; MUST succeed store-less with stderr note | row count + payload match + unwritable-store failure + storeless success | AC 5: asserts all four halves | sufficient after revision (ISS-003) |
| 1.5 MUST stop emitting HITL_REQUIRED, keep prose | negative substring + positive prose in generated file | AC 6: asserts both halves against scratch install | sufficient |
| 1.6 MUST document flags in ship-tasks + breaking CHANGELOG | positive substrings in both files | AC 7: asserts both | sufficient |
§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.