Skip to content

I3131 — A refusal answered by no change to the tree is not counted as a caught false done

Record on task.refused and task.finished an answer digest over every input a refusing doctrine reads (the tree id from mj_worktree_tree_id, already written as verify.tree, the completion note's content, the verify command and the obligation evidence), and have the aggregate classify a refusal as unanswered only when none of the inputs its own doctrine reads changed before acceptance, and as downgraded when the accepted outcome is lower than the refused one.

BLOCKED wave 2 · p2 · implementation profile · parallel safe

Part of the-product-shows-what-it-caught — What finish caught is counted from the ledger and compared on the benchmark, never asserted.

Blocked. This issue cannot start until I3130 is done. The status is derived from that, not declared.

Objective

Record on task.refused and task.finished an answer digest over every input a refusing doctrine reads (the tree id from mj_worktree_tree_id, already written as verify.tree, the completion note's content, the verify command and the obligation evidence), and have the aggregate classify a refusal as unanswered only when none of the inputs its own doctrine reads changed before acceptance, and as downgraded when the accepted outcome is lower than the refused one.

Why

The 2026-10-14 assessment named false refusal a critical bug class, because a governance tool that refuses correct work gets bypassed; counting such refusals as value would inflate the very metric meant to be honest. On 2026-10-18 the ledgers of every checkout `git worktree list` reported held 129 task.refused lines, 95 of them refused by majordomus.note-integrity alone (recount: `git worktree list --porcelain | awk '/^worktree /{print $2}' | while read w; do cat \"$w/.ai/local/state/ledger.jsonl\" 2>/dev/null; done | grep -c '\"event\":\"task.refused\"'`); a note-integrity refusal is answered in the gitignored completion note, which a tree-only digest never sees. The tree id is reused at the cost of one temporary-index `git add -A` per refusal. Proposed in ADR 0208 (The product's value is stated as the false dones its finish caught, beside the friction it cost) (proposed; it carries no authority until the owner accepts it). Sources: chatgpt-majordomus:c262#10.

Current state

task.refused carries task_id, outcome, unmet, refused and contract, and no input state (share/events.yaml:56); task.finished already records verify.tree from mj_worktree_tree_id (lib/common.sh); the completion note lives under the gitignored $MJ_STATE_DIR.

Desired state

Both events carry the answer digest; the aggregate separates caught, unanswered, downgraded and digest unknown by the doctrine-to-input map derived from the doctrine declarations; a case holds it.

Scope

  • share/events.yaml
  • lib/finish.sh
  • apps/majordomus-cli/src/capability/builtin/gates.rs
  • lib/common.sh
  • docs/SCHEMAS.md
  • docs/DYNAMICITY.md
  • .ai/repo/project/intents
  • share/standard/majordomus/rules/note-integrity.v1.md
  • share/standard/majordomus/rules/obligation-closure.v1.md
  • share/standard/majordomus/rules/principle-08-verify-outcomes.v1.md
  • .ai/repo/rules/vendor/majordomus/rules/note-integrity.v1.md
  • .ai/repo/rules/vendor/majordomus/rules/obligation-closure.v1.md
  • .ai/repo/rules/vendor/majordomus/rules/principle-08-verify-outcomes.v1.md
  • .ai/repo/adrs/0208-the-product-s-value-is-stated-as-the-false-dones-its-finish-caug.md
  • test/cases/32_refusal_lifecycle.sh

Out of scope

  • Deciding why a refusal was wrong

Dependencies

Acceptance criteria

  • A task refused and then accepted with no input its refusing doctrine reads changed is reported unanswered, not caught
  • A task refused, edited and accepted is reported caught
  • Ledgers written before the digest existed read 'digest unknown', never caught
  • task.refused and task.finished carry an answer digest over the inputs the doctrines read: the tree id from mj_worktree_tree_id (the same value as verify.tree), the completion note's content, the verify command and the recorded obligation evidence
  • A refusal is unanswered only when none of the inputs its own refusing doctrine reads changed before acceptance; the doctrine-to-input map is derived from the doctrine declarations, not typed by hand
  • A note-integrity refusal answered by rewriting the gitignored completion note, and an obligation-closure refusal answered by recording evidence at the same HEAD, are both reported caught
  • A task refused as completed and then accepted at a lower outcome is reported downgraded, apart from caught and unanswered
  • The intent's invariant and the criterion unanswered-refusals-are-separated read 'no change to any input the refusing doctrine reads' instead of 'same tree digest'
  • The case is numbered by `scripts/case-numbers next` and announced on the board at start, and the intent criterion carries evidence: test with that case as its ref
  • docs/SCHEMAS.md lists the new fields, and docs/DYNAMICITY.md no longer calls a refused finish an absent event

Validation

  • bash test/run.sh 32_refusal_lifecycle
  • majordomus plan validate

Evidence required

  • case_unanswered_refusal

Evidence

None recorded. Every token above needs a command or an artifact behind it before this issue can be completed; narrative is refused.

Timeline

started
—
verified
—
completed
—

Those three fields, the evidence above and the state of the dependencies are all the status is made of. There is no status field to disagree with them.

Canonical record: .ai/repo/project/issues/I3131.yaml. Read it back with majordomus plan show I3131.

Searches the titles and descriptions of every page this site publishes.