Task — engineering-spec@1

The index moves without the truth, in both directions

doneTASK-IMP-120
module improvement · class product · priority p1 · created · shipped null
depends on none · blocks none

TASK-IMP-120 - the index moves without the truth, in both directions

Summary

Make backlog-mutate flip REFUSE unless the task's frontmatter already carries the target status. The frontmatter is the record of truth and BACKLOG.md is its index; nothing binds the two writes, so they diverge silently. Twice on 2026-07-17, in opposite directions, from two different actors.

Problem

STATUS-REFERENCE.md §1 is unambiguous: frontmatter IS the record of truth, BACKLOG.md is only its index. backlog-mutate executes the index write with optimistic concurrency, a pre-image check, a 3-line footprint ceiling and a full retally. The frontmatter write is a separate hand edit with no mechanism at all.

Two instances, one day, opposite directions:

whenactorwhat movedwhat did not
TASK-IMP-116methe index, twice (reviewing -> ready_to_test -> testing)the frontmatter, which still said reviewing
TASK-IMP-028a swarm sub-agentthe frontmatter (draft -> duplicate, per 110 §1.7)the index, which still said draft

Neither is carelessness. 116's own coverage gate caught the first by comparing Totals against an independent frontmatter count - 544 index rows, 543 specs. The second was caught the same way, at the batch close. Both times the reconcile was a manual step someone happened to run.

The asymmetry is the tell: the index write is a hardened tool and the truth write is a sed. We built the mechanism on the derived artefact and left the authoritative one bare.

TASK-IMP-116's goal file already names this gap and the candidate fix. This task is that fix.

Proposed Solution

flip <id> <from> <to> reads the task's frontmatter FIRST. If it does not already say <to>, the flip REFUSES with exit 6 (the existing pre-image refusal code) and names both values. The truth becomes the precondition rather than the afterthought, and the index can only ever catch up to it - never lead it.

This makes the order structural instead of advisory: write the truth, then run the tool. A caller who does it backwards gets a refusal, not a divergence.

Alternatives Considered

Success Metrics

AI Authorship Disclosure

Dependencies

None. backlog-mutate already has exit 6 for pre-image refusals, and flip already resolves and rewrites a single BACKLOG row. It did NOT previously parse spec frontmatter anywhere: insert reads only BACKLOG rows and never opens a spec, so before this task no command read a spec's status:. THIS task adds that frontmatter read to flip (resolveSpecPaths + frontmatterStatus, mirroring task-reconcile's id->spec resolver) and gates the existing index write on it, reusing exit 6. A precondition on an existing refusal path, plus the small reader it needs - not a new writer, and not a new refusal code.

Scope

In scope: the flip precondition in backlog-mutate.mjs, its suite arms, and the ordering contract in backlog-state-update-author/SKILL.md.

Out of scope / Non-Goals

1. Clauses

1.1 flip <id> <from> <to> MUST read <id>'s spec frontmatter and REFUSE with exit 6 unless status already equals <to>, naming both the frontmatter value and the requested target. Test: t14_flip_refuses_when_truth_disagrees

1.2 When the frontmatter DOES equal <to>, flip proceeds exactly as today - same pre-image checks, same 3-line footprint, same retally. This adds a precondition and changes nothing else. Test: t15_flip_proceeds_when_truth_agrees

1.3 A spec that cannot be found or read MUST refuse, not proceed. An unreadable truth is not a matching truth. Test: t16_unreadable_spec_refuses

1.4 insert MUST be unaffected. Test: t17_insert_unchanged

1.5 backlog-state-update-author/SKILL.md MUST state the ordering as a contract: write the truth, then the index; the tool enforces it. Both vendored copies. Test: t18_skill_states_the_order

1.6 The seam test tools/install/tests/test_e2e_skeleton.sh - the end-to-end spine that drives flip -> coverage-scope -> reconcile -> uninstall - MUST exercise its lifecycle flips in the truth-first order this task mandates (the spec frontmatter status is written to <to> BEFORE the index flip), and MUST positively assert that an index-first flip (the truth still lagging) REFUSES with exit 6 and does NOT move the row. The guard's behaviour change broke this seam - it had encoded index-first, truth-lagging flips as correct, the exact shape 1.1 forbids - so the cone is grown to correct the seam to the new contract, never to weaken the guard. Test: t05_index_first_flip_refuses (index-first flip refuses exit 6, row unmoved; the truth-first flip then proceeds) and t01_spine_green (the full spine drives every lifecycle flip truth-first).

3. Edge case matrix

#CategoryTriggerExpectedTest
1NULL/EMPTYspec has no status fieldrefuse - absent is not agreementt16
2NULL/EMPTYspec file missing entirelyrefuse, name the patht16
3BOUNDSfrontmatter equals <to> exactlyproceedt15
4BOUNDSfrontmatter equals <from> (caller ran it backwards)refuse - this is the whole defectt14
5MALFORMEDstatus: done # comment (FM-001, live in 501 specs)trailing comment stripped before comparet14
6MALFORMEDtwo status: linesrefuse - ambiguous truth is not trutht16
7MALFORMEDstatus value with trailing whitespacetrimmed, compares equalt15
8CONCURRENTfrontmatter changes between read and writeexisting row pre-image still catches itt15
9SECURITYid resolves to a spec outside docs/tasksrefuse - relUnderRoot, as everywhere elset16
10DEGRADATIONcorpus has duplicate ids for <id>refuse - existing duplicate-row refusal patht16
11DEGRADATIONa status not in STATUS_ORDERrefuse before touching the filet14
12CONCURRENTseam driver moves the index before writing the truth (index-first)refuse exit 6, row unmoved - truth precedes index, end to endt05_index_first_flip_refuses (test_e2e_skeleton.sh)

4. Out of scope / non-goals

See "## Scope -> ### Out of scope / Non-Goals" above.

Acceptance criteria

Audit

Audit - TASK-IMP-120

Machine floor first per TASK-IMP-084: re-ran task-lint on the AMENDED spec - 0 errors, 1 info (TRACE-001, the ## 1. Clauses heading shape, as with 117/118/119). VERIFIED by command (node tools/install/docs-tools/task-lint.mjs docs/tasks/improvement/TASK-IMP-120-truth-precedes-index/spec.md; exit 0).

Amendment (2026-07-18 re-audit) - operator-approved cone growth

The guard shipped and committed at 38199b27 (clauses 1.1-1.5). Its behaviour change broke one out-of-cone seam - tools/install/tests/test_e2e_skeleton.sh::t01_spine_green (and the mini-spine in t04_scratch_isolation), which deliberately flipped the INDEX first and asserted the frontmatter still read the OLD status: the exact index-first, truth-lagging shape 1.1 forbids. The prior ship-manifest's integration_finding had already SIZED this and left it for a follow-up per cone discipline (IMP-119). The operator approved GROWING this task's cone to correct the seam rather than opening a separate task. This re-audit blesses the amendment:

  • modified_files gains tools/install/tests/test_e2e_skeleton.sh (the cone entry).
  • Clause 1.6 + AC7 + edge row 12: the seam MUST drive its flips truth-first and MUST positively assert an index-first flip REFUSES (exit 6) without moving the row. Cited tests: t05_index_first_flip_refuses and t01_spine_green (truth-first spine).
  • Dependencies corrected - see ISS-008.

The amendment WEAKENS no existing clause or AC. 1.1-1.5 and AC1-AC6 are byte-unchanged; the edit touched only the frontmatter cone list, the Dependencies paragraph, and appended 1.6 / edge-12 / AC7 (VERIFIED: git diff of the spec shows only those four regions).

Findings

ISS-001 (info, accepted): TRACE-001 heading shape. 1.1-1.6 cite tests; AC1-AC7 cite back.

ISS-002 (accepted, and the reason this is p1): two instances in one day, opposite directions, two different actors - the agent on TASK-IMP-116 and a swarm sub-agent on TASK-IMP-028. Both were caught by a manual reconcile someone chose to run. The fix was already named in 116's goal file BEFORE the second instance happened, which is why this is a task and not a discovery.

ISS-003 (accepted): the Alternatives section rejects having flip WRITE the frontmatter. One tool owning both the truth and its index means a bug in it corrupts the record with no second opinion. The tool's value is that it is narrow and refuses; widening it to write specs makes it the thing it guards against.

ISS-004 (accepted): edge case 5 - status: done # comment must be stripped before comparing. Live in 501 specs today, so a naive compare would refuse every flip on an FM-001-carrying spec and wedge the workflow on contact. Caught in the matrix rather than in production.

ISS-005 (accepted): edge case 6 - two status: lines refuse. Ambiguous truth is not truth, and picking the first would be the machine choosing which record is authoritative.

ISS-006 (accepted): AC6 replays 116's exact sequence and requires a refusal at the FIRST flip. A fix that cannot stop the case that motivated it is decoration.

ISS-007 (NEW, accepted - the amendment): cone grown to test_e2e_skeleton.sh with clause 1.6 / AC7 / edge row 12. The seam encoded "flip is index-only, truth may lag" as CORRECT (its header comment and t01's inline assertion) - exactly the behaviour 1.1 forbids - so the guard's landing made it 3/3 RED. Growing the cone to fix the seam to the new contract (truth-first drive + a positive index-first-refuses assertion) is the operator's approved disposition. 1.6's verbs and the tests that discharge them are recorded under TRACE-006 below. No existing clause/AC weakened; the seam's other coverage (spine green, reconcile recommendation, corpus survival, scratch isolation) is preserved, not deleted.

ISS-008 (NEW, accepted - stale originated claim corrected): the Dependencies section claimed "backlog-mutate already parses frontmatter for the insert path." FALSE. cmdInsert reads only BACKLOG rows and never opens a spec; before this task NO command parsed a spec's frontmatter - the resolveSpecPaths + frontmatterStatus reader is what THIS task ADDED, inside cmdFlip.

  • BEFORE: "None. backlog-mutate already parses frontmatter for the insert path and already has exit 6 for pre-image refusals. This adds a precondition, not a mechanism."
  • AFTER: "None. backlog-mutate already has exit 6 for pre-image refusals, and flip already resolves and rewrites a single BACKLOG row. It did NOT previously parse spec frontmatter anywhere: insert reads only BACKLOG rows and never opens a spec ... THIS task adds that frontmatter read to flip (resolveSpecPaths + frontmatterStatus ...) and gates the existing index write on it, reusing exit 6. A precondition on an existing refusal path, plus the small reader it needs - not a new writer, and not a new refusal code." This is the same class of defect as the session's governing finding (authors do not check what they originate) and the AC5-on-IMP-117 correction the operator called out. VERIFIED against source: cmdInsert and its helpers (parseRow/parseCountsHeader/retallyTotals/retallyHeader/sectionBlocks/ blockOf) contain no spec read; resolveSpecPaths and frontmatterStatus are referenced only from cmdFlip.

TRACE-006 (semantic sufficiency - per clause, judgment family)

For each §1 clause: [verb it demands] vs [what its cited test asserts]. 1.1-1.5 carry over from the prior audit (their clauses are byte-unchanged); 1.6 is added here.

  • 1.1 refuse (exit 6, naming both) -> t14 asserts exit 6 + names the frontmatter value and the target, and the index row is unmoved. Discharges "refuse" (absent side effect + signalled refusal). PASS.
  • 1.2 proceed unchanged -> t15 asserts the flip succeeds and the row/header/Totals retally exactly as before. PASS.
  • 1.3 refuse on unreadable/ambiguous -> t16 asserts refusal on missing / no-status / two-status. PASS.
  • 1.4 insert unaffected -> t17 asserts insert runs identically with the guard present. PASS.
  • 1.5 SKILL states the order (both copies) -> t18 asserts the ordering contract in source + vendored payload. PASS.
  • 1.6 (two verbs) - "exercise flips truth-first" AND "assert index-first REFUSES (exit 6), row unmoved."
  • Verb "refuse": demands the guarded index write did NOT happen AND a refusal was signalled. t05_index_first_flip_refuses asserts BOTH: the flip exits 6, the refusal names the truth-precedes-index contract, AND the BACKLOG row is still [draft] (unmoved); then the truth-first flip proceeds and moves the row. Discharges "refuse".
  • Verb "exercise truth-first": t01_spine_green writes the frontmatter to <next> BEFORE each flip and asserts the index catches up (row == <next> after the flip) through the whole lifecycle to done. Discharges the truth-first drive.
  • PASS (verb-demand vs test-assertion recorded above). SCOPE NOTE: this is the spec-correctness gate (draft -> ready_to_implement), so it blesses the clause and its declared tests; the tests themselves are this cone's Phase B seam-fix and are run GREEN in the Phase B commit (the coverage concern, a different gate - RUBRIC.md §9). This audit does NOT claim the tests are green; it verifies the spec's contract for them.

Rubric families

  • FM: clean (task-lint re-run, 0 errors). SEC: seven required sections present and non-empty. COND: three-bullet AI-authorship disclosure. Dependencies now truthful (ISS-008).
  • QA: 12 edge rows across all six categories (row 12 added: the index-first seam case, proven end-to-end by t05). 1 SECURITY, 2 DEGRADATION. QA-008 clean (Dependencies leads with "None.").
  • SAFE: adds a precondition to an existing refusal path; reuses exit 6 and relUnderRoot. The cone growth adds a test file to the cone (coverage), no new writer, no widened runtime surface.
  • TRACE: 1.1-1.6 -> tests; AC1-AC7 -> clauses. AC6 (the motivating 116 replay) and AC7 (the seam the guard broke) are load-bearing.

Verdict

pass - 10/10. The amendment closes the loop the guard opened: the behaviour change that broke the seam is now covered BY that seam (truth-first spine + a positive index-first-refuses assertion), inside this task's cone, and the one false originated claim in Dependencies is corrected to the truth. The binding is rebound to the amended normative half (audited_body_sha256_prefix: 8aacd63f9e6fc0bb).

Verified vs reconstructed:

  • VERIFIED by command: task-lint on the amended spec (0 errors, 1 info); the sha bindings (recomputed via task-reconcile's normativeHalf over the amended file); cmdInsert has no spec read (source inspection of backlog-mutate.mjs).
  • SPEC-GATE SCOPE (not yet green at this commit): clause 1.6's cited tests t05_index_first_flip_refuses and the truth-first t01_spine_green are blessed here as the clause's contract; they are implemented and verified GREEN in this cone's Phase B commit (coverage gate), which follows this Phase A spec+audit commit.