From 5c0dfc6022827c26e5f36140686221291e1cd7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:14:10 +0200 Subject: [PATCH 1/6] Spec the hardening-ledger supersession convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every staged path is explanatory documentation (docs/**.md) — Gate B N/A. Story profiled at its design checkpoint under acceptance criterion 1: Risk standard, Security none, Validation battery+check, human-confirmed. The guard-scope story gains one inherited open question: the supersession convention leaves a prose-only fingerprint correction that nothing consumes, and the story that redesigns the recurrence reader owns whether it should. --- ...05-hardening-ledger-supersession-design.md | 225 ++++++++++++++++++ ...rden-finding-guard-scope-precheck-story.md | 9 +- ...-04-hardening-ledger-supersession-story.md | 9 +- 3 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md diff --git a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md new file mode 100644 index 0000000..2a4f017 --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -0,0 +1,225 @@ +# A supersession convention for the hardening ledger — Design + +**Story:** `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` +— the profile lives in that story's header and is read fresh at every pass. No value from it +is copied here. + +**Scope:** the ledger header convention, its mirror in `/workflow-init`'s inline template, and +the one phrase in `harden-finding` that the convention falsifies. Nothing else. + +**How to read this document.** It states decisions and their reasons. It does not record how +those decisions were revised — that history lives in +`.context/codex-reviews/gate-a-spec-pass-*-dispositions.md`, one file per review pass. + +## 1. The gap + +`docs/hardening-log.md`'s header carries two standing rules: never edit a row, and one row per +hardening. When a landed row's text stops describing reality, neither move is sanctioned — +editing breaks the first rule, and appending a row that records no hardening breaks the second. + +The gap is live. The 2026-07-20 `truncated-tool-output-read-as-complete` row narrates the gate +hook counting every incomplete pass, which is pre-0.8.0 behaviour, and a reader who trusts it +is misled about how the hook counts today. + +## 2. The convention + +Two paragraphs are added to the ledger header, after the existing `Never edit a row…` sentence. + +### 2.1 The sanctioned move and its boundary + +```markdown +A row states what was true at its date. When a **landed** row's text no longer describes +reality — falsified by a later change, or wrong when written — append a `Superseded rows` +entry rather than editing it. *Landed* = present in this file at the merge-base with `main`; +a row authored in the cycle now open is not landed and is amended in place until that cycle +closes. Supersession marks a row's **text**, never its hardening: the row keeps its +fingerprint, keeps matching the column-2 grep, and keeps counting. A hardening later removed +is out of scope. +``` + +Each clause closes a state class, which is why the paragraph is this long: + +| Clause | The state it settles | +|---|---| +| `A row states what was true at its date` | what a row is a claim about | +| `falsified by a later change, or wrong when written` | the two covered causes | +| `*Landed* = …merge-base with main` | when append-only starts binding | +| `a row authored in the cycle now open… amended in place` | the in-flight row | +| `marks a row's text, never its hardening` | what supersession does not undo | +| `keeps its fingerprint… keeps counting` | the effect on recurrence: none | +| `A hardening later removed is out of scope` | the uncovered case, named rather than implied | + +**Why append-only binds at the merge-base and not at the first commit.** A Gate-B cycle takes +`WIP:` snapshot commits by construction (`CLAUDE.md` §5 Mechanics), so "committed" would make +every mid-cycle snapshot final and force a supersession entry for a row nobody outside the +cycle has ever read. The merge-base line puts the boundary where the record actually becomes +shared. It is also the one part of this convention a machine could decide — `git show +:docs/hardening-log.md` settles it. No checker is proposed; the decidability is +recorded so a future reader knows the option exists. + +**Why supersession leaves recurrence alone.** A falsified row's *hardening* still stands — the +rule, test or lint it landed is untouched by the change that falsified its narration. Ejecting +it from the lineage would misreport the class's history to `harden-finding`'s recurrence step +and to any later reader of the ledger. + +### 2.2 The block + +```markdown +**Superseded rows** — omitted until the first entry, then one appended line per row: + +`- · supersedes · what is now false · where the current answer is` + +Locator is date + fingerprint; where that pair matches more than one row, add a +distinguishing fragment of the row's `finding`. Superseded twice → two entries, latest +wins. Nothing +mechanical reads this block: to every grep and skill scanning the table a superseded row is +unchanged, including one whose entry says its fingerprint is wrong — prose readers get the +correction, mechanical readers do not, and nothing checks the difference. +``` + +**Placement:** between the header prose and the `Columns:` paragraph, above the table. + +- Rows append at the end of the file and this block sits at the top, so the two append points + are maximally separated and `merge=union` cannot interleave an entry into the table. +- A reader meets it before the rows rather than below twenty-two rows that each run to roughly + a thousand characters. +- It is a bold label, not a `##` heading, so the file keeps its single-heading structure and + the table does not fall under a section named for something else. This matches the story + template's `**Profile log:**`, including the omit-until-first-entry rule. + +**Locator.** Date plus fingerprint is the locator the header's existing `ref` convention +already uses ("the prior row's date + anchor"). **The pair is not unique, today:** of the +twenty-two current rows, `2026-07-18` + `docs-drift` matches two — the manifest-declares-hooks +row and the spec-not-updated-with-the-fix row. The distinguishing-fragment fallback is +therefore a live requirement, not a contingency, and superseding either of those two rows +needs one. + +The fragment is free text and does not *guarantee* uniqueness; it is a reader's disambiguator. +Requiring one on every entry was rejected — it is redundant on the twenty other pairs, and a +uniform three-field locator would still not guarantee more than this one does. + +**Format.** Prose only — no machine-readable fields. A supersession entry that a tool parses +becomes wire format in every scaffolded ledger, and the reader that would consume it does not +exist yet and is being redesigned elsewhere (§6). + +## 3. The two cases the convention must resolve + +### 3.1 The 2026-07-20 row — the first entry + +``` +- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}` — true when written, and as of 0.8.0 the hook withholds the count for three recognized shapes, that envelope among them, while every other shape still counts · CLAUDE.md §5, "What this does not do" +``` + +The entry is falsification-scoped, not row-scoped: it names the sentence that stopped being +true and says what replaced it, and it does not imply the rest of the row is stale. The +`mcp-codex-dev@1.0.1` pin the same row cites is still what `.mcp.json` carries, and the entry +says nothing about it. + +### 3.2 Row D — resolved by the boundary, not by an entry + +The 2026-08-04 `mechanical-check-skipped-before-review` row was amended during its own +authoring cycle in PR #22. Under §2.1 that was correct and leaves no record: the row was not +landed. The precedent becomes the rule rather than an exception to it. + +## 4. Change surface + +| File | Change | +|---|---| +| `docs/hardening-log.md` | §2.1 and §2.2 header paragraphs, the block, and §3.1's entry | +| `plugins/dev-workflow/commands/workflow-init.md` | §2.1 and §2.2 mirrored into the inline ledger-header template — prose only, no block, no entry | +| `plugins/dev-workflow/skills/harden-finding/SKILL.md` | §5 | +| `plugins/dev-workflow/.claude-plugin/plugin.json` | `0.8.1` → `0.8.2` | +| `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2` | +| `todos.md` | the source row resolved; the uncovered case parked with its trigger | + +**Parity, stated so a check knows what it compares:** the *header prose paragraphs* are +byte-identical between `docs/hardening-log.md` and the inline template, modulo hard-wrap +position. The `Superseded rows` block and the table rows are project content and exist only in +the repo copy — the same asymmetry the two files already carry, since the template ships an +empty table. + +**Invariant 9 needs nothing.** An existing scaffolded project re-running `/workflow-init` meets +the header as "present and different", which already routes to show-the-diff-and-ask. + +**Version bump.** `0.8.2`, patch: the change adds a convention to a scaffolded template and +corrects one phrase in a skill. No component is added, removed, or renamed. + +## 5. The `harden-finding` phrase + +`plugins/dev-workflow/skills/harden-finding/SKILL.md`, § Log format, currently ends: + +> …`ref` naming the prior row's date + anchor); never edit an existing row. + +Under §2.1 that sentence is wrong in the row-D direction: a run that appends a row and then +finds a mistake in it before the cycle closes would read "never edit an existing row" and +supersede a row that never landed. The replacement defers to the header rather than restating +it, so the two cannot drift: + +> …`ref` naming the prior row's date + anchor); never edit a landed row (see the ledger +> header). + +Nothing else in the skill changes. Its recurrence step (step 3) is untouched by design — +supersession is invisible to it, per §2.1. + +## 6. What this design hands to another story + +`docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` owns the +redesign of how a recurrence decision reads the ledger. §2.2 leaves a prose-only correction +that nothing consumes, including a note that a row's fingerprint is wrong. Whether the +redesigned reader should consult the `Superseded rows` block, and whether a prose-corrected +class changes the lineage it escalates from, is recorded there as an inherited open question. +It arrives as a handoff, not as a failure: no instance has occurred. + +## 7. Validation + +**Mode:** `battery+check`, from the story header. + +**Battery:** the full command in `AGENTS.md` § Commands. + +**The check — a named verification, with its counterfactual.** No harness reads ledger prose, +so the `+check` obligation is met by a named verification rather than an automated test. + +- *Claim:* a reader of the 2026-07-20 row can reach the correction from the ledger alone. +- *Verification:* `grep -n 'supersedes 2026-07-20' docs/hardening-log.md` returns §3.1's entry, + and its locator (`2026-07-20` + `truncated-tool-output-read-as-complete`) resolves to exactly + one row in the table. +- *The falsifying observation:* at the merge-base with `main`, that grep returns nothing and + the header offers no sanctioned move for either case in §3. That is the pre-change state, so + the check fails without the change rather than being wired to pass. + +**Prompt conformance:** `docs/prompt-standards.md`, all twelve items, on +`plugins/dev-workflow/commands/workflow-init.md` and +`plugins/dev-workflow/skills/harden-finding/SKILL.md` — both invariant-11 surfaces. + +## 8. Gate-A riders, verbatim in every pass prompt + +1. **Sweep before reading.** Mechanically settle whatever this spec asserts that a machine can + decide without side effects — cited paths, quoted passages, line numbers, stated counts, the + uniqueness claim in §2.2, the parity claim in §4, and the syntax of standalone fenced blocks. + Both source files are hard-wrapped, so compare quoted markdown with whitespace normalized and + report a wrap artifact as a wrap artifact. Inspect quoted commands rather than running them. + Report what the sweep found separately from what the read found. +2. **Self-test.** Apply §2.1's convention to both cases in §3 — case 3.1 must route to an entry + and case 3.2 must route to an in-place amend. Report both explicitly either way. +3. **Old-conditions audit.** §5 replaces a decision procedure's phrase. List every condition the + current sentence carries and mark each kept, moved, or deliberately dropped + (`AGENTS.md` Don'ts). + +## 9. What this does not do + +- **It adds no mechanical check.** Nothing validates that a superseded row has an entry, that an + entry's locator resolves, or that a row was landed when superseded. A reader is the detection. +- **A superseded row is unchanged to every tool.** The column-2 recurrence grep, any count over + the table, and `harden-finding`'s escalation all see the row exactly as before — including one + whose entry says its fingerprint is wrong. +- **The convention does not reach the reader that would consume it.** §6 hands that to the + guard-scope story; until it lands, the correction is prose a human reads. +- **It does not cover a hardening that is later removed.** Named as out of scope in §2.1 and + parked in `todos.md` with its trigger — the first rung actually removed. No instance exists. +- **A generic anchored grep over the block would also match the format example** in §2.2's + fenced line, in both the repo header and the template. POSIX grep has no fence awareness — + the same limitation `scripts/check-invariants.sh`'s check 4a already records. §7's + verification greps the specific locator instead, and this is stated for whoever later builds + a reader. +- **It changes nothing about how the gate hook counts.** §3.1 records that 0.8.0 already + changed it; this design only marks the row that still describes the old behaviour. diff --git a/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md b/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md index 496a741..ff85f40 100644 --- a/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md +++ b/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md @@ -61,7 +61,7 @@ ledger. Escalation follows from a guard that failed, never from a count. ## 5. Open questions -Six, each paired with the failure that raised it: +Seven — six paired with the failure that raised each, and one inherited from another story: - A verdict was reached by reading one prior row's guard and stopping, when an older row's guard covered the case. **Which prior rows must a recurrence be judged against before "outside" is @@ -81,6 +81,13 @@ Six, each paired with the failure that raised it: - The old step-3 text required proposing one rung stronger on recurrence, and a replacement choosing "the rung that fits the repair" does not carry it. **Is automatic escalation kept, narrowed, or deliberately dropped?** +- **Inherited** from `2026-08-04-hardening-ledger-supersession-story.md`: that story's convention + lets a superseded row record, in prose, that it was filed under the wrong fingerprint — and + deliberately leaves nothing mechanical reading it, so the row keeps its class, keeps matching + the column-2 grep, and keeps counting. The redesign here owns the reader, so it owns the + choice. **Should a recurrence read consult the supersession record, and if so, does a + prose-corrected class change the lineage it escalates from?** Arrives as a handoff, not as a + failure: no instance has occurred. ## 6. Suggested size diff --git a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md index e9ac5ca..cf8843c 100644 --- a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md +++ b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md @@ -1,11 +1,10 @@ # The hardening ledger has no supersession convention — Story **Date:** 2026-08-04 · **Size:** story +**Risk:** standard · **Security:** none · **Validation:** battery+check -**Unprofiled, deliberately** — a split from a designed round, so it bypassed -`dev-workflow:intake`, which excludes work already in solution design. A profile written now -would look confirmed without being confirmed, and nothing would reveal that; acceptance -criterion 1 carries the debt instead. +**Profile log:** +- 2026-08-05 · adoption · unprofiled split from a designed round, profiled at its design checkpoint under acceptance criterion 1 · gates now read this header ## 1. Problem statement @@ -37,7 +36,7 @@ row falsified by a later change can be marked as such without breaking either st ## 3. Acceptance criteria -- [ ] Before design resumes on this story, whoever picks it up proposes both axes and the mode +- [x] Before design resumes on this story, whoever picks it up proposes both axes and the mode derived from them, pauses for Daniel's confirmation, and writes the confirmed profile into this header. Design continues only after that. - [ ] The 2026-07-20 row no longer reads as current behaviour, and it was not edited. From 5e295f04ecf5b36c9a7b1175963875a33b7012d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:20:57 +0200 Subject: [PATCH 2/6] Revise the supersession spec through Gate-A spec passes 1-3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every staged path is explanatory documentation (docs/**.md) — Gate B N/A. Gate A is INCOMPLETE: three valid passes, floor met, final pass not clean. Pass 3 returned seven Majors; pass 4 failed twice and its recovery budget is spent. State and resume instructions in .context/codex-reviews/gate-a-spec-resume.md. Two human-confirmed story amendments, each with kept/narrowed/dropped accounting: acceptance criterion 4, and the append-only rule narrowed in text to "never edit a landed row". --- ...05-hardening-ledger-supersession-design.md | 327 +++++++++++++----- ...-04-hardening-ledger-supersession-story.md | 26 +- 2 files changed, 266 insertions(+), 87 deletions(-) diff --git a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md index 2a4f017..5b874cf 100644 --- a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -23,63 +23,115 @@ is misled about how the hook counts today. ## 2. The convention -Two paragraphs are added to the ledger header, after the existing `Never edit a row…` sentence. +The header's existing first paragraph is **amended in one phrase** (§2.0), and §2.1 and §2.2 +are added after it. All three are **identical in the repo ledger and in the inline template** — +they are the convention. The `Superseded rows:` block described by §2.2 is content, and exists +only where an entry exists (§4). + +### 2.0 The amended phrase + +The existing sentence reads `Never edit a row; resolve a pending row by appending…`. Left +absolute, it contradicts §2.1's in-place amend of an open-cycle row — two incompatible +instructions on one surface, which is prompt-standards item 7. One word changes: + +> `Never edit a row;` → `Never edit a landed row;` + +The rest of the sentence is untouched, and §2.1 supplies the definition. ### 2.1 The sanctioned move and its boundary ```markdown -A row states what was true at its date. When a **landed** row's text no longer describes -reality — falsified by a later change, or wrong when written — append a `Superseded rows` -entry rather than editing it. *Landed* = present in this file at the merge-base with `main`; -a row authored in the cycle now open is not landed and is amended in place until that cycle -closes. Supersession marks a row's **text**, never its hardening: the row keeps its -fingerprint, keeps matching the column-2 grep, and keeps counting. A hardening later removed -is out of scope. +A row records a hardening as of its date, and its narration may be found wrong or made stale +later. When a **landed** row's text no longer describes reality — falsified by a later change, +or wrong when it was written — append a `Superseded rows` entry rather than editing it. +*Landed* = appended by any cycle other than the one you have open: a row you appended in your +open cycle is amended in place until that cycle closes and is landed from then on; with no +cycle open every row is landed; a row another worktree appended in its own open cycle is landed +to you; and where you cannot establish that your open cycle appended a row, treat it as landed. +A *cycle* is the Gate-B cycle of `CLAUDE.md` §5 Mechanics, closing at the commit that replaces +its `WIP:` snapshot. Resolving a `pending` row always appends, in any cycle — that is a new +hardening, not a correction to a row's text. Supersession marks a row's **text**, never its +hardening: the row keeps its fingerprint, keeps matching the column-2 grep, and keeps counting. +A hardening later removed is out of scope. ``` Each clause closes a state class, which is why the paragraph is this long: | Clause | The state it settles | |---|---| -| `A row states what was true at its date` | what a row is a claim about | -| `falsified by a later change, or wrong when written` | the two covered causes | -| `*Landed* = …merge-base with main` | when append-only starts binding | -| `a row authored in the cycle now open… amended in place` | the in-flight row | +| `records a hardening as of its date` | what a row is a claim about | +| `may be found wrong or made stale later` | that a row's narration is not permanently true | +| `falsified by a later change, or wrong when it was written` | the two covered causes | +| `appended by any cycle other than the one you have open` | when append-only starts binding | +| `amended in place until that cycle closes… landed from then on` | the in-flight row, and its exit | +| `with no cycle open every row is landed` | the no-open-cycle state | +| `another worktree… is landed to you` | concurrent cycles, which own only their own rows | +| `where you cannot establish… treat it as landed` | unknown provenance, resolved conservatively | +| `A cycle is the Gate-B cycle… closing at the commit that replaces its WIP: snapshot` | cycle identity and its close event | +| `Resolving a pending row always appends, in any cycle` | the pending-row overlap | | `marks a row's text, never its hardening` | what supersession does not undo | | `keeps its fingerprint… keeps counting` | the effect on recurrence: none | | `A hardening later removed is out of scope` | the uncovered case, named rather than implied | -**Why append-only binds at the merge-base and not at the first commit.** A Gate-B cycle takes -`WIP:` snapshot commits by construction (`CLAUDE.md` §5 Mechanics), so "committed" would make -every mid-cycle snapshot final and force a supersession entry for a row nobody outside the -cycle has ever read. The merge-base line puts the boundary where the record actually becomes -shared. It is also the one part of this convention a machine could decide — `git show -:docs/hardening-log.md` settles it. No checker is proposed; the decidability is -recorded so a future reader knows the option exists. +**Why the boundary is authorship-by-cycle, and not a commit or a merge-base.** A Gate-B cycle +takes `WIP:` snapshot commits by construction, so a commit-based boundary would make every +mid-cycle snapshot final and force a supersession entry for a row nobody outside the cycle has +read. A merge-base boundary fails in the other direction: a row appended by an **earlier, +completed** cycle on a branch not yet merged is absent from the merge-base with `main` and +would wrongly read as amendable, which is the append-only breach the convention exists to +prevent. Authorship puts the boundary exactly where the record stops being the author's own. + +**The base check is one-way, and uncertainty resolves to landed.** Presence in the ledger at +the open cycle's base commit proves a row is landed. Absence does *not* prove the converse — a +row can arrive from another cycle by merge after that base — so the rule is authorship, and the +base check only ever confirms landed. Interruption, handoff and context loss can all leave +authorship unestablished; the convention's answer is to treat the row as landed, which costs a +supersession entry nobody needed and never costs an edit to someone else's record. No checker +is proposed; this is recorded so a future reader knows which direction is decidable. + +**Why the `pending` overlap resolves toward appending.** Resolving a `pending` row is a *new +hardening* — it earns its own row under the one-row-per-hardening rule — while amendment +corrects the *text* of a row still being authored. A cycle that appended a `pending` row and +then lands its prerequisite therefore appends the resolving row, exactly as before this change. +Amendment reaches that `pending` row's own wording, and nothing else. **Why supersession leaves recurrence alone.** A falsified row's *hardening* still stands — the rule, test or lint it landed is untouched by the change that falsified its narration. Ejecting it from the lineage would misreport the class's history to `harden-finding`'s recurrence step and to any later reader of the ledger. -### 2.2 The block +### 2.2 The convention prose, and the block it describes -```markdown -**Superseded rows** — omitted until the first entry, then one appended line per row: - -`- · supersedes · what is now false · where the current answer is` +The prose below is shared by both surfaces. It carries **no** `Superseded rows:` label of its +own — that label belongs to the block, and appears only where entries do, so an empty +scaffolded ledger does not ship a label with nothing under it. -Locator is date + fingerprint; where that pair matches more than one row, add a -distinguishing fragment of the row's `finding`. Superseded twice → two entries, latest -wins. Nothing -mechanical reads this block: to every grep and skill scanning the table a superseded row is -unchanged, including one whose entry says its fingerprint is wrong — prose readers get the -correction, mechanical readers do not, and nothing checks the difference. +```markdown +**Correcting a landed row.** Corrections live in a `Superseded rows` block above the `Columns:` +paragraph — a `**Superseded rows:**` label carrying one appended line per supersession, present +only once at least one entry exists: + + - · supersedes `` · what is false · where the current answer is + +Locator is date + fingerprint; where that pair matches more than one row, add a distinguishing +fragment of the row's `finding`, and where it still singles out no one row, resolve that before +writing the entry rather than filing an entry that points at two. Name the claim that does not +hold — saying whether it stopped holding or was never true — and cite where the current answer +lives; restating that answer here only makes the entry the next stale narration. Entries are +cumulative and themselves append-only: two clauses of one row falsified separately get two +entries and both stand. An entry is located by its own date plus the row it supersedes, under +the same fallback and the same stop; a correcting entry retires exactly the entries it names +and states what now holds, so nothing is restored implicitly. If a union merge leaves two +`Superseded rows:` labels, keep one and keep every entry under it. Nothing mechanical reads +this block — to every grep and skill scanning the table a superseded row is unchanged, +including one whose entry says its fingerprint is wrong; prose readers get the correction, +mechanical readers do not, and nothing checks the difference. ``` -**Placement:** between the header prose and the `Columns:` paragraph, above the table. +**Layout.** Amended first paragraph, then §2.1, then §2.2's prose, then the block, then the +existing `Columns:` paragraph, then the table. -- Rows append at the end of the file and this block sits at the top, so the two append points +- Rows append at the end of the file and the block sits at the top, so the two append points are maximally separated and `merge=union` cannot interleave an entry into the table. - A reader meets it before the rows rather than below twenty-two rows that each run to roughly a thousand characters. @@ -87,16 +139,40 @@ correction, mechanical readers do not, and nothing checks the difference. the table does not fall under a section named for something else. This matches the story template's `**Profile log:**`, including the omit-until-first-entry rule. -**Locator.** Date plus fingerprint is the locator the header's existing `ref` convention -already uses ("the prior row's date + anchor"). **The pair is not unique, today:** of the -twenty-two current rows, `2026-07-18` + `docs-drift` matches two — the manifest-declares-hooks -row and the spec-not-updated-with-the-fix row. The distinguishing-fragment fallback is -therefore a live requirement, not a contingency, and superseding either of those two rows -needs one. - -The fragment is free text and does not *guarantee* uniqueness; it is a reader's disambiguator. -Requiring one on every entry was rejected — it is redundant on the twenty other pairs, and a -uniform three-field locator would still not guarantee more than this one does. +**Locator.** Date plus fingerprint is a **new, explicit locator choice**, not an inheritance: +the header's existing `ref` convention says "the prior row's date + anchor" and never defines +*anchor*, so treating it as precedent for *fingerprint* would be reading a decision procedure +more narrowly than its source says. + +**The pair is not unique, today:** of the twenty-two current rows, `2026-07-18` + `docs-drift` +matches two — the manifest-declares-hooks row and the spec-not-updated-with-the-fix row. The +distinguishing-fragment fallback is therefore a live requirement, not a contingency, and +superseding either of those two rows needs one. + +**Entry locators collide the same way**, and take the same rule: two entries against one row, +written the same day — which §2.2 explicitly permits, since one row can carry two +falsification-scoped entries — share date plus superseded row. That is why the fallback and the +stop apply to entry locators too; without them a correcting entry could not retire exactly one +of a same-day pair, which is what the precedence rule requires of it. + +The fragment is free text and does not *guarantee* uniqueness, which is why an unresolvable +locator is a stop rather than a best effort. Requiring a fragment on every entry was rejected — +it is redundant on the twenty other pairs, and a uniform three-field locator would still not +guarantee more than this one does. + +**Cumulative, not latest-wins.** Entries are falsification-scoped (§3.1), so one row can carry +two entries for two independently falsified clauses. A latest-wins rule would let a correction +to clause B silently retire a still-current correction to clause A. An entry retires an earlier +entry only by naming it. + +**Cite, do not restate.** An entry says *which claim does not hold* and *where the answer now +lives*. It does not carry the new answer, because a copy of a mechanism is exactly the artifact +that goes stale next — which is how the row being superseded here got into this state. + +**Both covered causes get accurate wording.** The scope covers a claim falsified later *and* a +claim that was never true, so the entry field is "what is false", and the entry says which of +the two it is. An instruction to name "what stopped being true" would have no accurate value +for the never-true case, and an author would either omit the field or misdescribe it. **Format.** Prose only — no machine-readable fields. A supersession entry that a tool parses becomes wire format in every scaffolded ledger, and the reader that would consume it does not @@ -107,36 +183,47 @@ exist yet and is being redesigned elsewhere (§6). ### 3.1 The 2026-07-20 row — the first entry ``` -- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}` — true when written, and as of 0.8.0 the hook withholds the count for three recognized shapes, that envelope among them, while every other shape still counts · CLAUDE.md §5, "What this does not do" +- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}`; true when written, and it no longer holds for every pass · CLAUDE.md §5, the paragraph opening `**What this does not do.** The hook counts on \`PostToolUse\`` ``` -The entry is falsification-scoped, not row-scoped: it names the sentence that stopped being -true and says what replaced it, and it does not imply the rest of the row is stale. The -`mcp-codex-dev@1.0.1` pin the same row cites is still what `.mcp.json` carries, and the entry -says nothing about it. +The entry is falsification-scoped, not row-scoped: it names the claim that no longer holds, +says it *was* true when written rather than never true, and cites where the answer now lives — +without implying the rest of the row is stale. Two properties are deliberate. It **does not +restate** 0.8.0's counting rules, per §2.2's cite-don't-restate clause. And its citation quotes +the source markdown **including the emphasis markers**, because `CLAUDE.md` §5 contains **two** +paragraphs titled "What this does not do" — the section reference alone does not resolve, and a +fragment stripped of its `**` is not byte-findable in the file it points at. + +The `mcp-codex-dev@1.0.1` pin the same row cites is still what `.mcp.json` carries, and the +entry says nothing about it. ### 3.2 Row D — resolved by the boundary, not by an entry The 2026-08-04 `mechanical-check-skipped-before-review` row was amended during its own -authoring cycle in PR #22. Under §2.1 that was correct and leaves no record: the row was not -landed. The precedent becomes the rule rather than an exception to it. +authoring cycle in PR #22. Under §2.1 that was correct and leaves no record: the cycle that +appended it was the cycle then open. The precedent becomes the rule rather than an exception +to it. ## 4. Change surface | File | Change | |---|---| -| `docs/hardening-log.md` | §2.1 and §2.2 header paragraphs, the block, and §3.1's entry | -| `plugins/dev-workflow/commands/workflow-init.md` | §2.1 and §2.2 mirrored into the inline ledger-header template — prose only, no block, no entry | +| `docs/hardening-log.md` | §2.0's phrase, §2.1 and §2.2 prose, then the `Superseded rows:` block holding §3.1's entry | +| `plugins/dev-workflow/commands/workflow-init.md` | §2.0's phrase, §2.1 and §2.2 prose, into the inline ledger-header template — no block, since a scaffolded ledger has no entries | | `plugins/dev-workflow/skills/harden-finding/SKILL.md` | §5 | | `plugins/dev-workflow/.claude-plugin/plugin.json` | `0.8.1` → `0.8.2` | | `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2` | | `todos.md` | the source row resolved; the uncovered case parked with its trigger | +| `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` | already amended: acceptance criteria 3 and 4, the desired outcome, and the inherited-conditions table, each with kept/narrowed/dropped accounting — §2.0 changes a rule the story pinned, so the story is part of this change surface rather than a passive source | -**Parity, stated so a check knows what it compares:** the *header prose paragraphs* are -byte-identical between `docs/hardening-log.md` and the inline template, modulo hard-wrap -position. The `Superseded rows` block and the table rows are project content and exist only in -the repo copy — the same asymmetry the two files already carry, since the template ships an -empty table. +**Parity, with sentinels a check can locate.** The shared region runs from the line +`# Hardening log` **inclusive** to the line ending `…and nothing checks the difference.` +**inclusive** — the last sentence of §2.2's prose. The region **includes** the four-space +indented format example, and **excludes** the `Superseded rows:` block, the `Columns:` +paragraph and the table. Inside that region the two files are byte-identical modulo hard-wrap +position. Both sentinels are unique in both files once the change lands; before it, the end +sentinel exists in neither, which §7's check 2 treats as a failure rather than as an empty +region. **Invariant 9 needs nothing.** An existing scaffolded project re-running `/workflow-init` meets the header as "present and different", which already routes to show-the-diff-and-ask. @@ -152,12 +239,33 @@ corrects one phrase in a skill. No component is added, removed, or renamed. Under §2.1 that sentence is wrong in the row-D direction: a run that appends a row and then finds a mistake in it before the cycle closes would read "never edit an existing row" and -supersede a row that never landed. The replacement defers to the header rather than restating -it, so the two cannot drift: +supersede a row that never landed. The replacement defers the *definition* to the header rather +than restating it, so the two cannot drift: > …`ref` naming the prior row's date + anchor); never edit a landed row (see the ledger > header). +**Old-conditions audit**, per the `AGENTS.md` Don't, covering the sentence and the paragraph +around it: + +| Condition the current § Log format carries | Disposition | +|---|---| +| Append rather than edit, to resolve a `pending` row | **kept, and now explicit**: §2.1 states that resolving a `pending` row always appends, in any cycle, so open-cycle amendment never reaches it | +| The resolving row carries the same fingerprint | kept | +| `ref` names the prior row's date + anchor | kept | +| Existing rows are not editable | **kept for landed rows**, whose prohibition the sentence still states; only the *definition* of landed moves to the ledger header | +| Existing rows are not editable — for a row the open cycle appended | **deliberately dropped**, which is §2.1's boundary and row D's precedent | +| Append target is `docs/hardening-log.md` | kept | +| The header owns the columns, enums and escaping | kept | +| `fingerprint` is canonical | kept | +| `finding` is one line, with `\|` escaped | kept | +| `rung` is one of the short names | kept | +| `ref` is concrete | kept | + +The prohibition is therefore stated on **two** surfaces — this sentence and the ledger header — +and the definition on one. That duplication is deliberate: a `harden-finding` run reads the +skill, and a sentence that named no prohibition at all would leave it with nothing. + Nothing else in the skill changes. Its recurrence step (step 3) is untouched by design — supersession is invisible to it, per §2.1. @@ -176,16 +284,47 @@ It arrives as a handoff, not as a failure: no instance has occurred. **Battery:** the full command in `AGENTS.md` § Commands. -**The check — a named verification, with its counterfactual.** No harness reads ledger prose, -so the `+check` obligation is met by a named verification rather than an automated test. - -- *Claim:* a reader of the 2026-07-20 row can reach the correction from the ledger alone. -- *Verification:* `grep -n 'supersedes 2026-07-20' docs/hardening-log.md` returns §3.1's entry, - and its locator (`2026-07-20` + `truncated-tool-output-read-as-complete`) resolves to exactly - one row in the table. -- *The falsifying observation:* at the merge-base with `main`, that grep returns nothing and - the header offers no sanctioned move for either case in §3. That is the pre-change state, so - the check fails without the change rather than being wired to pass. +### Check 1 — the correction is reachable from the ledger alone + +Two halves, because the cardinality is decidable and the content is not. Both must hold. + +- *Mechanical:* both commands must report `1`. They are given in a fenced block rather than + inline because the first pattern contains backticks, which an inline code span cannot carry. + + ```sh + grep -c '^- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` ' docs/hardening-log.md + grep -cE '^\| 2026-07-20 \| truncated-tool-output-read-as-complete \|' docs/hardening-log.md + ``` + + The fingerprint is inside **both** patterns on purpose: matching the + entry on its dates alone and the row on date-plus-fingerprint would leave an entry carrying a + wrong or missing fingerprint passing both, which is exactly the state the pair is supposed to + rule out. +- *Named read:* a reader confirms the entry names the claim that does not hold, and that its + citation resolves to exactly one paragraph in `CLAUDE.md`. This half is a read, **not** a + parse — nothing validates it, and the greps above would pass an entry whose prose said + nothing useful. Stated rather than implied, because the greps alone are what a green + `battery+check` would otherwise rest on. +- *The falsifying observation:* at the base of this cycle the first grep reports `0` and the + header offers no sanctioned move for either case in §3 — the pre-change state, so the check + fails without the change rather than being wired to pass. + +### Check 2 — the convention actually reached both surfaces + +The parity claim in §4 is what the both-surfaces decision rests on, and check 1 would pass with +the template untouched. + +- *Operation:* locate §4's two sentinels in each file. **A sentinel that is missing, or that + matches more than once, is a failure — never an empty region.** Extract the delimited region + from each file and compare, normalizing hard-wrap joins within a paragraph only. Leading + indentation and blank-line structure are preserved and compared exactly: the four-space prefix + on the format example is the only thing distinguishing it from a live entry, and a + normalization that flattened it would let one surface convert the example into entry-shaped + content and still report equal. +- *Three states, each distinguished:* **pre-change**, the end sentinel is convention text and + exists in neither file, so the check fails — it does not read two absent regions as equal, and + that failure is the falsifying observation. **One surface edited**, the sentinel is present in + one file and absent in the other: fails. **Both edited**, the regions compare equal: passes. **Prompt conformance:** `docs/prompt-standards.md`, all twelve items, on `plugins/dev-workflow/commands/workflow-init.md` and @@ -194,16 +333,23 @@ so the `+check` obligation is met by a named verification rather than an automat ## 8. Gate-A riders, verbatim in every pass prompt 1. **Sweep before reading.** Mechanically settle whatever this spec asserts that a machine can - decide without side effects — cited paths, quoted passages, line numbers, stated counts, the - uniqueness claim in §2.2, the parity claim in §4, and the syntax of standalone fenced blocks. - Both source files are hard-wrapped, so compare quoted markdown with whitespace normalized and - report a wrap artifact as a wrap artifact. Inspect quoted commands rather than running them. - Report what the sweep found separately from what the read found. -2. **Self-test.** Apply §2.1's convention to both cases in §3 — case 3.1 must route to an entry - and case 3.2 must route to an in-place amend. Report both explicitly either way. -3. **Old-conditions audit.** §5 replaces a decision procedure's phrase. List every condition the - current sentence carries and mark each kept, moved, or deliberately dropped - (`AGENTS.md` Don'ts). + decide without side effects — cited paths, quoted passages **byte-for-byte including + markdown emphasis markers**, line numbers, stated counts, the non-uniqueness claim in §2.2, + §4's parity sentinels (unique and locatable in both files, and does the region contain what + §4 says?), §3.1's citation, the §5 disposition table against the current skill text, and the + syntax and balance of standalone fenced and indented blocks. Both source files are + hard-wrapped, so compare quoted markdown with whitespace normalized and report a wrap + artifact as a wrap artifact. Inspect quoted commands rather than running them, but do check + by reading that §7's greps would match what the spec says they match — including what they + would *fail* to rule out. Report what the sweep found separately from what the read found. +2. **Self-test, six cases.** Apply §2.1 to: case §3.1; case §3.2; a row appended by an earlier + completed cycle on a branch not yet merged; a row appended by the open cycle after a `WIP:` + snapshot; a `pending` row the open cycle appended and now wants to resolve; and a row whose + authoring cycle cannot be established. Report all six verdicts explicitly, including where + they come out as the spec says. +3. **Old-conditions audit.** §5 carries a disposition table for the decision-procedure phrase it + replaces. Check it against the current skill text — every condition present, every + disposition right (`AGENTS.md` Don'ts). ## 9. What this does not do @@ -212,14 +358,29 @@ so the `+check` obligation is met by a named verification rather than an automat - **A superseded row is unchanged to every tool.** The column-2 recurrence grep, any count over the table, and `harden-finding`'s escalation all see the row exactly as before — including one whose entry says its fingerprint is wrong. +- **Check 1's content half is a human read.** The greps establish that an entry exists carrying + the expected fingerprint and that its locator selects one row; nothing establishes that the + entry says anything true or useful. +- **The landed test is decidable in one direction only**, and the convention closes the other by + defaulting to landed. That default is a bias, not a determination: a row the open cycle really + did append can be treated as landed and get an entry it did not need. That cost was chosen + over the alternative, which is editing a record someone else has read. - **The convention does not reach the reader that would consume it.** §6 hands that to the guard-scope story; until it lands, the correction is prose a human reads. - **It does not cover a hardening that is later removed.** Named as out of scope in §2.1 and parked in `todos.md` with its trigger — the first rung actually removed. No instance exists. -- **A generic anchored grep over the block would also match the format example** in §2.2's - fenced line, in both the repo header and the template. POSIX grep has no fence awareness — - the same limitation `scripts/check-invariants.sh`'s check 4a already records. §7's - verification greps the specific locator instead, and this is stated for whoever later builds - a reader. +- **The union-merge repair rule is a manual instruction.** `merge=union` will duplicate a + `Superseded rows:` label when two branches each create the block, and nothing detects or + repairs that; §2.2 tells a human what to do when they see it. +- **The prohibition on editing a landed row is duplicated across two prompt surfaces** — the + ledger header and `harden-finding`'s § Log format (§5). Only the definition is single-sourced. + Nothing keeps the two prohibitions in step. +- **The format example is not an entry, and nothing enforces that distinction beyond its + shape.** It is a four-space indented code block using `` and `` placeholders, + so no line-anchored grep for a dated entry reaches it. That is the whole guard: POSIX grep has + no code-block awareness — the limitation recorded in `docs/hardening-log.md`'s 2026-07-26 + `unverified-enforcement-claim` row, not in `scripts/check-invariants.sh` itself, which carries + no such comment — so an example later rewritten flush-left with a real date would become + indistinguishable from an entry to any reader built over this block. - **It changes nothing about how the gate hook counts.** §3.1 records that 0.8.0 already changed it; this design only marks the row that still describes the old behaviour. diff --git a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md index cf8843c..1a6b6a0 100644 --- a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md +++ b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md @@ -22,17 +22,26 @@ From `todos.md`, "**The hardening ledger has no supersession convention.**": | Condition | Disposition | |---|---| -| Never edit a row | **kept** — any solution must preserve it | +| Never edit a row | **kept in substance, narrowed in text** — see the amendment note below | | One row per hardening | **kept** — any solution must preserve it | | The 2026-07-20 row now describes pre-0.8.0 behaviour as current | **kept** as the motivating instance | | The 2026-07-20 *spec* took a version-qualified supersession note and it worked | **kept** as prior art the design should evaluate first | | Alternative: an explicit "rows are historical, read the newest row for current behaviour" header statement | **kept** as a candidate | | Trigger: the next row falsified by a later change — this is the second | **moved** — fired, and recorded here | +**Amended 2026-08-05** from Gate-A spec pass 3, finding 1, and human-confirmed. Accounting per +the AGENTS.md Don't — **kept:** the committed record's protection, which is what every consumer +of the rule actually relied on. **Narrowed:** the textual rule, from "never edit a row" to +"never edit a landed row", making textual what #22's Gate B already accepted in practice (row D, +amend-during-authoring, reasoning recorded in its evidence). **Dropped:** nothing. The amendment +records both facts — the text changed here, and the boundary it moved to was already the +practiced one. §2 and acceptance criterion 3 carry the same change. + ## 2. Desired outcome A reader who opens any ledger row can tell whether it still describes current behaviour, and a -row falsified by a later change can be marked as such without breaking either standing rule. +row falsified by a later change can be marked as such without breaking either standing rule — +as amended: never edit a **landed** row, and one row per hardening. ## 3. Acceptance criteria @@ -40,8 +49,17 @@ row falsified by a later change can be marked as such without breaking either st derived from them, pauses for Daniel's confirmation, and writes the confirmed profile into this header. Design continues only after that. - [ ] The 2026-07-20 row no longer reads as current behaviour, and it was not edited. -- [ ] The append-only rule and the one-row-per-hardening rule both still hold after the change. -- [ ] A reader can determine, from the ledger alone, which row to trust for current behaviour. +- [ ] The append-only rule — as amended, protecting **landed** rows — and the + one-row-per-hardening rule both still hold after the change. + *(Amended 2026-08-05 from Gate-A spec pass 3, finding 1, and human-confirmed; the + kept/narrowed/dropped accounting is in §1.)* +- [ ] A reader can determine, from the ledger alone, which of a row's claims no longer hold and + where the current answer lives. + *(Amended 2026-08-05 from Gate-A spec pass 1, finding 4, and human-confirmed. Accounting + per the AGENTS.md Don't — **kept:** determination from the ledger alone. **Moved:** current + behaviour lives at the cited artifact, deliberately, per cite-don't-restate. **Dropped:** + "a row to trust", because no ledger row ever provided current behaviour and the criterion + demanded what the genre cannot supply.)* ## 4. Affected AGENTS.md invariants From c3fc742524dde891e0734c5f665724b382f739cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:55:24 +0200 Subject: [PATCH 3/6] Revise the supersession spec through Gate-A spec passes 4-18 Gate A is NOT closed. Pass 18 returned six findings (4 Major, no Blocker); none of them are applied here. Pass 19 is pending. What the design became over these passes: - The landed/amendable boundary is deleted entirely. Every row is protected; a correction is always an append. Three boundaries were designed and removed in turn (authorship-by-cycle, reachability from origin/main, content presence in the published ledger), each found unsound by the pass after it. The resolution floor is bound to rows, not commits. - Entries are row-markers with latest-wins. The correcting-entry path is cut. - Match semantics replace locator uniqueness: an entry applies to every row it matches dated on or before its own date; zero matches is inert and stands as history; backdating is forbidden. - The harden-finding skill left the change surface: the convention narrows nothing, so its "never edit an existing row" sentence stays true. - Section 6 carries no executable shell. It states properties, falsifying observations and oracles; the executable form is written at execution time, labelled in the plan, and reviewed by Gate B against the real diff. Over six passes the shell had produced most findings while the design drew none. The governing story carries six amendments, each with old-condition accounting. The append-only rule was narrowed at pass 3 and restored at pass 7; acceptance criterion 3 holds literally again. Docs-only, so Gate B is N/A per CLAUDE.md section 5's prose exemption. Full pass history, dispositions and the resume note are in .context/codex-reviews/ (gitignored, not backed up by this push). --- docs/coding-workflow.md | 7 +- ...6-08-04-hardening-round-0-8-0-and-pr-21.md | 12 + ...05-hardening-ledger-supersession-design.md | 781 ++++++++++++------ ...rden-finding-guard-scope-precheck-story.md | 16 +- ...-04-hardening-ledger-supersession-story.md | 91 +- 5 files changed, 643 insertions(+), 264 deletions(-) diff --git a/docs/coding-workflow.md b/docs/coding-workflow.md index b145332..c69fcab 100644 --- a/docs/coding-workflow.md +++ b/docs/coding-workflow.md @@ -201,8 +201,11 @@ reappears in a new location, that is the signal to stop fixing instances one at time. Instead, audit the whole surface for that class in a single pass, fix every instance together, and — if the pattern is mechanically detectable — add a linter or static-analysis rule so it can never silently return. The ledger is -strictly append-only: history is never rewritten, and a resolution is a *new* -entry that references the one it closes. +strictly append-only: a **row** is never edited, a resolution is a *new row* +that references the row it closes, and a row whose text is later found wrong +is corrected by appending a supersession **entry** rather than by editing it. +(*Row* and *entry* are distinct: rows are the ledger's records, entries are +the supersession markers that correct them.) ### Cross-cutting disciplines diff --git a/docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md b/docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md index 0a0b658..7a2dd11 100644 --- a/docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md +++ b/docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md @@ -1,5 +1,17 @@ # Hardening round — the 0.8.0 cycle and PR #21 — Implementation Plan +> **Historical snapshot — executed and merged.** This plan records the state of things as they +> stood when it ran. It is **not** maintained against later changes and must not be read as +> current guidance. Its `never edit an existing row` / `Never edit a row` statements are **still +> accurate**: `2026-08-05-hardening-ledger-supersession-design.md` briefly narrowed that rule and +> then restored it, so the absolute wording stands and nothing here contradicts it. What *is* +> historical is narrower — the plan's desired-outcome wording predates that story's amended +> acceptance criterion 4, and its treatment of the row-D amend-during-authoring precedent is +> superseded: under the convention that row's correct move was an entry. Executed plans are +> deliberately left as written — a plan is a record of what was done, and editing one to match +> later rules would destroy that record — so this note is how the standing falsification sweep +> resolves them, rather than by rewriting. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Land four hardenings from the 0.8.0 cycle and PR #21 as text, record them in four ledger rows, park what has no repair, and open five split stories — without changing any skill file. diff --git a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md index 5b874cf..988d199 100644 --- a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -4,8 +4,15 @@ — the profile lives in that story's header and is read fresh at every pass. No value from it is copied here. -**Scope:** the ledger header convention, its mirror in `/workflow-init`'s inline template, and -the one phrase in `harden-finding` that the convention falsifies. Nothing else. +**Scope — behavioural.** The ledger header convention and its mirror in `/workflow-init`'s inline +template. No other *behaviour* changes. `harden-finding` is **not** in scope: §2.1 narrows +nothing, so that skill's `never edit an existing row` sentence stays true and is untouched. + +That is not the same as the file list. §4's change surface is authoritative and is larger: it +carries the supporting artifacts this change obliges — a version bump and changelog entry +(invariant 12), the backlog row, two stories, and one prose site the change falsified. An +implementer follows §4, not this paragraph; an earlier draft ended this sentence with "Nothing +else", which read as permission to skip them. **How to read this document.** It states decisions and their reasons. It does not record how those decisions were revised — that history lives in @@ -14,7 +21,7 @@ those decisions were revised — that history lives in ## 1. The gap `docs/hardening-log.md`'s header carries two standing rules: never edit a row, and one row per -hardening. When a landed row's text stops describing reality, neither move is sanctioned — +hardening. When a row's text stops describing reality, neither move is sanctioned — editing breaks the first rule, and appending a row that records no hardening breaks the second. The gap is live. The 2026-07-20 `truncated-tool-output-read-as-complete` row narrates the gate @@ -23,83 +30,98 @@ is misled about how the hook counts today. ## 2. The convention -The header's existing first paragraph is **amended in one phrase** (§2.0), and §2.1 and §2.2 -are added after it. All three are **identical in the repo ledger and in the inline template** — -they are the convention. The `Superseded rows:` block described by §2.2 is content, and exists -only where an entry exists (§4). - -### 2.0 The amended phrase - -The existing sentence reads `Never edit a row; resolve a pending row by appending…`. Left -absolute, it contradicts §2.1's in-place amend of an open-cycle row — two incompatible -instructions on one surface, which is prompt-standards item 7. One word changes: - -> `Never edit a row;` → `Never edit a landed row;` - -The rest of the sentence is untouched, and §2.1 supplies the definition. +The header's existing first paragraph is **untouched**. §2.1 and §2.2 are added after it, and +are **identical in the repo ledger and in the inline template** — they are the convention. The +`Superseded rows:` block described by §2.2 is content, and exists only where an entry exists +(§4). -### 2.1 The sanctioned move and its boundary +### 2.1 The sanctioned move ```markdown -A row records a hardening as of its date, and its narration may be found wrong or made stale -later. When a **landed** row's text no longer describes reality — falsified by a later change, -or wrong when it was written — append a `Superseded rows` entry rather than editing it. -*Landed* = appended by any cycle other than the one you have open: a row you appended in your -open cycle is amended in place until that cycle closes and is landed from then on; with no -cycle open every row is landed; a row another worktree appended in its own open cycle is landed -to you; and where you cannot establish that your open cycle appended a row, treat it as landed. -A *cycle* is the Gate-B cycle of `CLAUDE.md` §5 Mechanics, closing at the commit that replaces -its `WIP:` snapshot. Resolving a `pending` row always appends, in any cycle — that is a new -hardening, not a correction to a row's text. Supersession marks a row's **text**, never its -hardening: the row keeps its fingerprint, keeps matching the column-2 grep, and keeps counting. -A hardening later removed is out of scope. +A row records a hardening claim as of its date, and its narration may be found wrong or made +stale later. When a row's text no longer describes reality — falsified by a later change, or +wrong when it was written — append a `Superseded rows` entry rather than editing it. One later +change is **excluded**: a hardening that is itself removed, for which this convention supplies no +move at all (see the end of this paragraph). This holds +for every row without exception: the existing `Never edit a row` rule is absolute, and correcting +a row is always an append. The rule is bound to rows, not to commits: once text exists as a row +it is never edited, committed or not. Drafting before a row exists — an editor buffer, a line not +yet written — is below the rule's resolution, and nothing checks one. Resolving a `pending` row also appends — that +is a new hardening, not a correction to a row's text. Supersession marks a row's **text** and +never alters mechanical behaviour, including when the entry records that the row's hardening +claim was itself false: the row keeps its fingerprint, keeps matching the column-2 grep, and +keeps counting. A hardening later removed is out of scope. ``` -Each clause closes a state class, which is why the paragraph is this long: - | Clause | The state it settles | |---|---| -| `records a hardening as of its date` | what a row is a claim about | +| `records a hardening claim as of its date` | what a row is a claim about — a *claim*, since the covered scope includes a hardening that never existed | | `may be found wrong or made stale later` | that a row's narration is not permanently true | | `falsified by a later change, or wrong when it was written` | the two covered causes | -| `appended by any cycle other than the one you have open` | when append-only starts binding | -| `amended in place until that cycle closes… landed from then on` | the in-flight row, and its exit | -| `with no cycle open every row is landed` | the no-open-cycle state | -| `another worktree… is landed to you` | concurrent cycles, which own only their own rows | -| `where you cannot establish… treat it as landed` | unknown provenance, resolved conservatively | -| `A cycle is the Gate-B cycle… closing at the commit that replaces its WIP: snapshot` | cycle identity and its close event | -| `Resolving a pending row always appends, in any cycle` | the pending-row overlap | -| `marks a row's text, never its hardening` | what supersession does not undo | +| `This holds for every row without exception` | that there is no amendable class — the whole boundary question | +| `the existing Never edit a row rule is absolute` | that this convention adds to that rule rather than narrowing it | +| `The rule is bound to rows, not to commits` | the resolution floor: what the rule attaches to | +| `Drafting before a row exists… is below the rule's resolution` | the level the rule deliberately does not reach | +| `Resolving a pending row also appends` | the pending-row overlap | +| `marks a row's text and never alters mechanical behaviour` | what supersession does not undo | +| `including when the entry records that the row's hardening claim was itself false` | the phantom-hardening row, which is still not ejected | | `keeps its fingerprint… keeps counting` | the effect on recurrence: none | | `A hardening later removed is out of scope` | the uncovered case, named rather than implied | -**Why the boundary is authorship-by-cycle, and not a commit or a merge-base.** A Gate-B cycle -takes `WIP:` snapshot commits by construction, so a commit-based boundary would make every -mid-cycle snapshot final and force a supersession entry for a row nobody outside the cycle has -read. A merge-base boundary fails in the other direction: a row appended by an **earlier, -completed** cycle on a branch not yet merged is absent from the merge-base with `main` and -would wrongly read as amendable, which is the append-only breach the convention exists to -prevent. Authorship puts the boundary exactly where the record stops being the author's own. - -**The base check is one-way, and uncertainty resolves to landed.** Presence in the ledger at -the open cycle's base commit proves a row is landed. Absence does *not* prove the converse — a -row can arrive from another cycle by merge after that base — so the rule is authorship, and the -base check only ever confirms landed. Interruption, handoff and context loss can all leave -authorship unestablished; the convention's answer is to treat the row as landed, which costs a -supersession entry nobody needed and never costs an edit to someone else's record. No checker -is proposed; this is recorded so a future reader knows which direction is decidable. +**Why there is no amendable class.** Three boundaries were designed and deleted before this one: +authorship-by-cycle, reachability from `origin/main`, and content presence in the published +ledger. Each tried to name a set of rows a reader may still edit, and each failed the same way — +Gate-A passes 4 through 7 each found the *replacement* for the previous boundary unsound, three +times in the very sentence written to fix its predecessor: "the repository always answers" +became "confident *present* and confident *absent*", which was itself unsound because a +stale-but-readable ref returns a confident *absent* for a row already published. Every version +also needed its own old-conditions accounting, its own six-case self-test, its own concurrency +assumption and its own residual list, and each of those grew defects of its own. + +The boundary is deleted rather than fixed again. There is no test to get wrong, no set to +enumerate, no verdict that can flip, and nothing to keep in step across two prompt surfaces. A +correction is always an append. The whole cost is stated below and is one entry. + +**The resolution floor, and why it is bound to rows.** A rule that says "never edit a row" +invites the same debate one level down: *may I fix a typo before I commit?* Left unanswered, that +question rebuilds the amendable class from scratch — first as a buffer, then as a working tree, +then as a branch, and the boundary is back. The convention therefore states its own resolution. + +It is bound to **rows**, not to commits. An earlier draft said the convention "governs committed +content", which read against "every row without exception" left a row *already appended but not +yet committed* matching both descriptions with no verdict — the amendable class reappearing +through the sentence written to prevent it. Attaching the rule to the row removes that state: +the moment text exists as a row it is protected, committed or not, and what sits below the rule +is only the drafting that precedes a row existing at all. That distinction needs no test, because +"is there a row here" is not a question anyone has to compute. + +It is deliberately *not* a licence with conditions. Nothing distinguishes a buffer from a working +tree, because a rule that started distinguishing would be the boundary again. + +**What this costs: one entry, once.** Under the deleted boundaries, the 2026-08-04 +`mechanical-check-skipped-before-review` row (row D) was a *correct* in-place amend. Under this +convention it is not: the correct move was an entry. That is the entire historical cost — one +row, one entry, in the whole ledger — and §3.2 records it rather than carving an exception for +it. **Why the `pending` overlap resolves toward appending.** Resolving a `pending` row is a *new -hardening* — it earns its own row under the one-row-per-hardening rule — while amendment -corrects the *text* of a row still being authored. A cycle that appended a `pending` row and -then lands its prerequisite therefore appends the resolving row, exactly as before this change. -Amendment reaches that `pending` row's own wording, and nothing else. +hardening* — it earns its own row under the one-row-per-hardening rule — while an entry corrects +a row's *text*. The two never compete now that both are appends: a `pending` row's resolution +appends a row, and a correction to any row's wording appends an entry. **Why supersession leaves recurrence alone.** A falsified row's *hardening* still stands — the rule, test or lint it landed is untouched by the change that falsified its narration. Ejecting it from the lineage would misreport the class's history to `harden-finding`'s recurrence step and to any later reader of the ledger. +This holds even in the case that looks like an exception. Because the scope covers a claim that +was *never true*, an entry may record that the row's **hardening itself** never existed or was +misclassified — and such a row still keeps counting, so a lineage can escalate from a rung that +was never landed. The boundary is therefore stated on mechanical behaviour rather than on the +hardening's truth: supersession changes text and changes nothing a tool reads, in every case, +which is a rule a reader can apply without first adjudicating whether a past hardening was real. +The consequence is owned by the reader being redesigned in §5, not by this convention. + ### 2.2 The convention prose, and the block it describes The prose below is shared by both surfaces. It carries **no** `Superseded rows:` label of its @@ -107,29 +129,41 @@ own — that label belongs to the block, and appears only where entries do, so a scaffolded ledger does not ship a label with nothing under it. ```markdown -**Correcting a landed row.** Corrections live in a `Superseded rows` block above the `Columns:` +**Correcting a row.** Corrections live in a `Superseded rows` block above the `Columns:` paragraph — a `**Superseded rows:**` label carrying one appended line per supersession, present only once at least one entry exists: - - · supersedes `` · what is false · where the current answer is - -Locator is date + fingerprint; where that pair matches more than one row, add a distinguishing -fragment of the row's `finding`, and where it still singles out no one row, resolve that before -writing the entry rather than filing an entry that points at two. Name the claim that does not -hold — saying whether it stopped holding or was never true — and cite where the current answer -lives; restating that answer here only makes the entry the next stale narration. Entries are -cumulative and themselves append-only: two clauses of one row falsified separately get two -entries and both stand. An entry is located by its own date plus the row it supersedes, under -the same fallback and the same stop; a correcting entry retires exactly the entries it names -and states what now holds, so nothing is restored implicitly. If a union merge leaves two -`Superseded rows:` labels, keep one and keep every entry under it. Nothing mechanical reads + - · supersedes `` "" · what is false · where the current answer is + +`` is the day the entry is written, in `YYYY-MM-DD`. A row is located by date + +fingerprint. **An entry applies to every row its locator matches** — uniqueness is not a +requirement, and an entry that matches two rows says the same thing about both. To narrow it to +one, add `""`, a quoted distinguishing fragment of that row's `finding`, in the +position shown immediately after the fingerprint; pick one containing no double quote. Omit it, +quotes included, when you mean every row the pair matches — including when the pair matches only +one. Name the claim that does +not hold — saying whether it stopped holding or was never true — and cite where the current +answer lives; restating that answer here only makes the entry the next stale narration. **An entry applies only to matching rows dated on or before the entry's own date** — supersession +marks the past, so a row dated later never comes under an entry written before it. **A row's date +is the day it is appended**, and the table is chronological: backdating a row is forbidden, which +is what makes the date bound mean what it says. Nothing can verify the append day itself; the +rule is stated and a check enforces the weaker property that dates never decrease. An entry +whose locator matches no such row is **inert**: it governs nothing and is not an error to repair +in place — append a new entry with a locator that matches, and leave the inert one standing as +history, like every other entry. **An +entry marks a row, and the last entry for a row is the one that governs** — where a row carries +more than one, later in the file wins and the earlier ones are history. **A later entry must +therefore describe the row as it now stands, not only the newly found fault**, or it retires a +still-accurate earlier entry from a reader's view. Entries are never edited, never removed, and +never reference one another. If a union merge +leaves two `Superseded rows:` labels, keep one and keep every entry under it. No standing tool reads this block — to every grep and skill scanning the table a superseded row is unchanged, including one whose entry says its fingerprint is wrong; prose readers get the correction, mechanical readers do not, and nothing checks the difference. ``` -**Layout.** Amended first paragraph, then §2.1, then §2.2's prose, then the block, then the -existing `Columns:` paragraph, then the table. +**Layout.** The existing first paragraph — unchanged — then §2.1, then §2.2's prose, then the +block, then the existing `Columns:` paragraph, then the table. - Rows append at the end of the file and the block sits at the top, so the two append points are maximally separated and `merge=union` cannot interleave an entry into the table. @@ -145,25 +179,78 @@ the header's existing `ref` convention says "the prior row's date + anchor" and more narrowly than its source says. **The pair is not unique, today:** of the twenty-two current rows, `2026-07-18` + `docs-drift` -matches two — the manifest-declares-hooks row and the spec-not-updated-with-the-fix row. The -distinguishing-fragment fallback is therefore a live requirement, not a contingency, and -superseding either of those two rows needs one. - -**Entry locators collide the same way**, and take the same rule: two entries against one row, -written the same day — which §2.2 explicitly permits, since one row can carry two -falsification-scoped entries — share date plus superseded row. That is why the fallback and the -stop apply to entry locators too; without them a correcting entry could not retire exactly one -of a same-day pair, which is what the precedence rule requires of it. - -The fragment is free text and does not *guarantee* uniqueness, which is why an unresolvable -locator is a stop rather than a best effort. Requiring a fragment on every entry was rejected — -it is redundant on the twenty other pairs, and a uniform three-field locator would still not -guarantee more than this one does. - -**Cumulative, not latest-wins.** Entries are falsification-scoped (§3.1), so one row can carry -two entries for two independently falsified clauses. A latest-wins rule would let a correction -to clause B silently retire a still-current correction to clause A. An entry retires an earlier -entry only by naming it. +matches two — the manifest-declares-hooks row and the spec-not-updated-with-the-fix row. Under +match semantics that is not a problem to solve: an entry with that locator and no fragment says +its thing about both rows, which is often exactly right. The fragment exists to narrow when it +is not. + +**Why uniqueness is not required.** An earlier draft demanded a locator resolving to exactly one +row, and stopped the author when no fragment could achieve it. That rule could not survive its +own immutability guarantee: an entry unique when written is made ambiguous by a *later* row +sharing its date and fingerprint, and entries are never edited or removed, so there was no legal +repair — the convention would have contained a state it forbade fixing. Two rows identical in +`finding` admit no distinguishing fragment at all, so the stop could also be unsatisfiable on the +day it was written. Match semantics has neither failure: an entry means what it matches, a later +row simply joins the set, and an author who wants one row narrows the locator. + +The fragment is free text and guarantees nothing about uniqueness — under match semantics it does +not need to. It narrows the match set; it is not a resolution procedure, and it is drawn from +`finding`, so **two rows identical in date, fingerprint and `finding` cannot be separated at all** +— an entry against such a pair marks every one of them, accurate siblings included. Short of that +case there is no state the locator stops on: many matches are valid and zero is inert. Requiring a +fragment on every entry was rejected — it is redundant on the twenty other pairs, and a uniform +three-field locator would still not guarantee more than this one does. + +**Entries are row-markers, and the latest one for a row governs — positionally.** An earlier +draft made entries *falsification-scoped* and *cumulative*, so one row could carry two standing +entries for two independently falsified clauses, retired only by a third "correcting entry" that +named them. That machinery is **cut**. It needed a second line format, a locator that could +address an individual entry, a rule about which entries one correction may retire, and an escape +convention for quoting inside a locator — four mechanisms, none of which any tool reads, to serve +a case that has never occurred. Cutting it removes all four. + +What replaces it is position: entries append, and where a row has more than one, the last in file +order is the current one. A reader needs no rule beyond "read down". The cost is real and named: +a later entry about clause B supersedes the reader's view of an earlier still-accurate entry +about clause A, so an author correcting one clause of a twice-falsified row should write an entry +that covers the row as it now stands, not only the new clause. That is a heavier ask on the +author than the cut design made, and it is the trade — author effort against four mechanisms and +their failure modes. + +**Concurrency takes the same answer, and there is no assumption behind it.** Two branches can +each append an entry for one row; `merge=union` keeps both, and latest-in-file governs. No +single-writer assumption is needed **across independently committed branches** — nothing is ever +edited, so a merge has nothing to overwrite, which is the one concurrency property this design +gets for free by having no amend path at all. It says nothing about two writers in **one +worktree**: inserting an entry above `Columns:` is a read-modify-write on one file, so +simultaneous writers there can lose an entry before git's merge driver is ever involved. That +case is unsupported, not solved (§8). What remains is ordering: position after a union merge need not match either +branch's intent, so the governing entry may not be the one either author expected. §8 records +that, and its repair is the ordinary one — append another entry. + +**A locator matching nothing is inert, and the match set is bounded backwards in time.** Match +semantics says an entry applies to what it matches; two cases it did not cover are matching +*nothing* — a mistyped fingerprint, a row that never existed — and matching something that did +not exist yet. Both are settled together. + +*Inert.* Making a zero-match entry an error would need a repair, and the only repair under +immutability is an edit — the amendable class again. So it is inert: it governs nothing, and the +remedy is the move every other correction uses, append. + +*Bounded backwards.* Matching is evaluated on read, so without a bound a mistyped entry sitting in +history would **activate** the day someone appends a row carrying its locator, and silently govern +a row nobody wrote it for. The bound is the entry's own date: an entry applies only to rows dated +on or before it. No eligibility record is needed and nothing has to be stored — both dates are +already in the lines, and supersession marks the past by definition, so the bound is what the +convention meant all along rather than a guard bolted onto it. + +**What the bound does not close.** Row dates are days, not instants, so a row appended *the same +day* as the entry, after it, still satisfies "on or before" and does come under that entry. +Backdating is forbidden and mechanically checked, so that day is the whole exposure: activation is +bounded to the entry's own date rather than made impossible. Closing it would need a finer +timestamp in the row format, which §2.2 rejects as wire format. The residual is one day wide, +requires the same date *and* the same fingerprint, and is the only way an entry's match set can +grow after it is written. **Cite, do not restate.** An entry says *which claim does not hold* and *where the answer now lives*. It does not carry the new answer, because a copy of a mechanism is exactly the artifact @@ -176,205 +263,429 @@ for the never-true case, and an author would either omit the field or misdescrib **Format.** Prose only — no machine-readable fields. A supersession entry that a tool parses becomes wire format in every scaffolded ledger, and the reader that would consume it does not -exist yet and is being redesigned elsewhere (§6). +exist yet and is being redesigned elsewhere (§5). ## 3. The two cases the convention must resolve ### 3.1 The 2026-07-20 row — the first entry ``` -- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}`; true when written, and it no longer holds for every pass · CLAUDE.md §5, the paragraph opening `**What this does not do.** The hook counts on \`PostToolUse\`` +- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}`; true when written, and it no longer holds for every pass · CLAUDE.md §5, the paragraph opening `` **What this does not do.** The hook counts on `PostToolUse` `` ``` -The entry is falsification-scoped, not row-scoped: it names the claim that no longer holds, -says it *was* true when written rather than never true, and cites where the answer now lives — -without implying the rest of the row is stale. Two properties are deliberate. It **does not +The entry is a **row marker**: it marks the 2026-07-20 row, and its text names the claim that no +longer holds, says it *was* true when written rather than never true, and cites where the answer +now lives — without implying the rest of the row is stale. Being a marker rather than a +clause-scoped record is what makes latest-wins safe: should this row later be found wrong in a +second way, the next entry must describe the row's supersession state as a whole, because it +becomes the one that governs. Two properties are deliberate. It **does not restate** 0.8.0's counting rules, per §2.2's cite-don't-restate clause. And its citation quotes the source markdown **including the emphasis markers**, because `CLAUDE.md` §5 contains **two** paragraphs titled "What this does not do" — the section reference alone does not resolve, and a -fragment stripped of its `**` is not byte-findable in the file it points at. +fragment stripped of its `**` is not byte-findable in the file it points at. (The two are +distinguishable on their punctuation alone, `do.` against `do:` at `CLAUDE.md:168` and `:390`, +but only once the fragment is quoted as source bytes rather than as rendered text.) + +The citation is delimited with **double** backticks and one space inside each delimiter, +because the fragment itself contains a `` `PostToolUse` `` span. A single-backtick span cannot +carry backticks, and a backslash does not escape them — markdown has no escape inside a code +span, so the backslashes an earlier draft used would have been copied into the ledger as +literal bytes that are not in `CLAUDE.md`, breaking the byte-findability the emphasis markers +were added to secure. + +The entry carries **no** `""` because it is meant to apply to **every** row the pair +matches — which is what omitting the fragment says under match semantics. That the pair happens to +match exactly one row today is an observation, not the reason: another row carrying the same date +and fingerprint would join the set, and that is intended rather than a defect. Since rows carry +their append date and backdating is forbidden (§2.2), "another row" means one appended **the same +day** — a row appended later carries a later date and falls outside the entry's bound. The two +`2026-07-18` + `docs-drift` rows are where an entry meaning only one of them would carry a +fragment. The `mcp-codex-dev@1.0.1` pin the same row cites is still what `.mcp.json` carries, and the entry says nothing about it. -### 3.2 Row D — resolved by the boundary, not by an entry +### 3.2 Row D — a precedent the convention supersedes -The 2026-08-04 `mechanical-check-skipped-before-review` row was amended during its own -authoring cycle in PR #22. Under §2.1 that was correct and leaves no record: the cycle that -appended it was the cycle then open. The precedent becomes the rule rather than an exception -to it. +The 2026-08-04 `mechanical-check-skipped-before-review` row was amended in place while PR #22 was +still open, and #22's Gate B accepted that. Earlier drafts of this design read that as a rule to +generalise, and built a boundary to make it correct — the boundary §2.1 has now deleted. + +Under this convention it is simply **not** correct: the row is a row, and the correct move was an +entry. The precedent is superseded rather than promoted. No exception is carved for it, and none +is needed — the ledger is not retro-corrected, so what this costs is one entry that was never +written, once, in the whole history. That is the price of having no amendable class, and it is +the entire price. ## 4. Change surface | File | Change | |---|---| -| `docs/hardening-log.md` | §2.0's phrase, §2.1 and §2.2 prose, then the `Superseded rows:` block holding §3.1's entry | -| `plugins/dev-workflow/commands/workflow-init.md` | §2.0's phrase, §2.1 and §2.2 prose, into the inline ledger-header template — no block, since a scaffolded ledger has no entries | -| `plugins/dev-workflow/skills/harden-finding/SKILL.md` | §5 | +| `docs/hardening-log.md` | §2.1 and §2.2 prose appended after the existing first paragraph — which is **not** edited — then the `Superseded rows:` block holding §3.1's entry | +| `plugins/dev-workflow/commands/workflow-init.md` | §2.1 and §2.2 prose, into the inline ledger-header template — no block, since a scaffolded ledger has no entries | | `plugins/dev-workflow/.claude-plugin/plugin.json` | `0.8.1` → `0.8.2` | | `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2` | -| `todos.md` | the source row resolved; the uncovered case parked with its trigger | -| `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` | already amended: acceptance criteria 3 and 4, the desired outcome, and the inherited-conditions table, each with kept/narrowed/dropped accounting — §2.0 changes a rule the story pinned, so the story is part of this change surface rather than a passive source | +| `todos.md` | the source row **The hardening ledger has no supersession convention** marked done and rewritten in the past tense, so it no longer reads as an open gap; **plus** a new parked row for the uncovered case — a hardening later *removed* — carrying its trigger, the first rung actually removed. **plus** a third row: wire §6's inert-entry assertion into `AGENTS.md`'s quality battery, triggered by the first inert entry found after this change lands — **with the rider that any standing check must be diff-scoped**, for the reason §6 gives: a whole-block scan is unsatisfiable once an immutable inert entry exists. A named read confirms all three: no present-tense claim that the ledger lacks a convention, and both parked rows present with their triggers | +| `docs/coding-workflow.md` | line 204's append-only sentence rewritten to state the rule **absolutely** — "strictly append-only: a **row** is never edited" — with the supersession move appended. It previously said "history is never rewritten"; an intermediate draft of this spec qualified it to *merged* history, which would have reinstated a pre-merge amendable class. No merged/unmerged qualifier appears in the landed wording. Found by the standing falsification lens, outside every path this change otherwise touches | +| `docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` | its inherited open question widened from wrong-fingerprint to include phantom-hardening rows (that story's §5 Open questions) | +| `docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md` | a header note marking it a historical snapshot — it carries the un-narrowed rule three times. Executed plans are records of what was done and are **not** rewritten to match later rules; the note is how the falsification sweep resolves them | +| `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` | **six** amendments, each with explicit old-condition accounting; the disposition labels differ per amendment, which is expected. **Pass 1** — AC 4 (kept/moved/dropped). **Pass 3** — the append-only rule narrowed (kept/narrowed/dropped). **Pass 4** — §4's invariants 11 and 12 made unconditional and §5's open question resolved to both surfaces, with §6's size following. **Pass 5** — the desired outcome brought into line with AC 4, which still promised a reader could tell whether *any* row describes current behaviour. **Pass 7** — the absolute rule **restored** (restored/dropped/kept), reversing pass 3. **Pass 9** — AC 2 reworded, because "the row no longer reads as current behaviour" was satisfiable only by the edit the same criterion forbids. Attribution names the pass whose findings prompted each change. §1 is also in the past tense, which is framing rather than an amendment | **Parity, with sentinels a check can locate.** The shared region runs from the line `# Hardening log` **inclusive** to the line ending `…and nothing checks the difference.` **inclusive** — the last sentence of §2.2's prose. The region **includes** the four-space -indented format example, and **excludes** the `Superseded rows:` block, the `Columns:` -paragraph and the table. Inside that region the two files are byte-identical modulo hard-wrap -position. Both sentinels are unique in both files once the change lands; before it, the end -sentinel exists in neither, which §7's check 2 treats as a failure rather than as an empty -region. +indented format example, now a single line, and **excludes** the `Superseded rows:` block, the +`Columns:` paragraph and the table. +Inside that region the two files are byte-identical modulo hard-wrap position. Both sentinels are +unique in both files once the change lands; before it, the end sentinel exists in neither, which +§6's check 2 treats as a failure rather than as an empty region — and, since check 2 now asserts +presence per surface before parity, one that its anchor greps fail on first. **Invariant 9 needs nothing.** An existing scaffolded project re-running `/workflow-init` meets the header as "present and different", which already routes to show-the-diff-and-ask. -**Version bump.** `0.8.2`, patch: the change adds a convention to a scaffolded template and -corrects one phrase in a skill. No component is added, removed, or renamed. - -## 5. The `harden-finding` phrase +**Version bump.** `0.8.2`, patch: the change adds a convention to a scaffolded template. +No component is added, removed, or renamed. `harden-finding` is untouched (§2.1 narrows nothing, +so its `never edit an existing row` sentence stays true), which is why the plugin change is +confined to `workflow-init.md` — invariant 12 still binds, since that path is under `plugins/`. -`plugins/dev-workflow/skills/harden-finding/SKILL.md`, § Log format, currently ends: - -> …`ref` naming the prior row's date + anchor); never edit an existing row. - -Under §2.1 that sentence is wrong in the row-D direction: a run that appends a row and then -finds a mistake in it before the cycle closes would read "never edit an existing row" and -supersede a row that never landed. The replacement defers the *definition* to the header rather -than restating it, so the two cannot drift: - -> …`ref` naming the prior row's date + anchor); never edit a landed row (see the ledger -> header). - -**Old-conditions audit**, per the `AGENTS.md` Don't, covering the sentence and the paragraph -around it: - -| Condition the current § Log format carries | Disposition | -|---|---| -| Append rather than edit, to resolve a `pending` row | **kept, and now explicit**: §2.1 states that resolving a `pending` row always appends, in any cycle, so open-cycle amendment never reaches it | -| The resolving row carries the same fingerprint | kept | -| `ref` names the prior row's date + anchor | kept | -| Existing rows are not editable | **kept for landed rows**, whose prohibition the sentence still states; only the *definition* of landed moves to the ledger header | -| Existing rows are not editable — for a row the open cycle appended | **deliberately dropped**, which is §2.1's boundary and row D's precedent | -| Append target is `docs/hardening-log.md` | kept | -| The header owns the columns, enums and escaping | kept | -| `fingerprint` is canonical | kept | -| `finding` is one line, with `\|` escaped | kept | -| `rung` is one of the short names | kept | -| `ref` is concrete | kept | - -The prohibition is therefore stated on **two** surfaces — this sentence and the ledger header — -and the definition on one. That duplication is deliberate: a `harden-finding` run reads the -skill, and a sentence that named no prohibition at all would leave it with nothing. - -Nothing else in the skill changes. Its recurrence step (step 3) is untouched by design — -supersession is invisible to it, per §2.1. - -## 6. What this design hands to another story +## 5. What this design hands to another story `docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` owns the redesign of how a recurrence decision reads the ledger. §2.2 leaves a prose-only correction -that nothing consumes, including a note that a row's fingerprint is wrong. Whether the -redesigned reader should consult the `Superseded rows` block, and whether a prose-corrected -class changes the lineage it escalates from, is recorded there as an inherited open question. -It arrives as a handoff, not as a failure: no instance has occurred. +that nothing consumes — including a note that a row's fingerprint is wrong, and, since the +scope covers a claim that was never true, a note that the row's **hardening itself** never +existed or was misclassified. §2.1 keeps such a row counting on purpose, so a lineage can +escalate from a rung that was never landed. Both are one family — a row whose mechanical +identity is corrected only in prose — and that story's inherited open question was widened to +carry both. Whether the redesigned reader should consult the `Superseded rows` block, and +whether a prose-corrected class or a prose-retracted hardening changes the lineage it escalates +from, is recorded there. It arrives as a handoff, not as a failure: no instance has occurred. -## 7. Validation +## 6. Validation -**Mode:** `battery+check`, from the story header. +**Mode:** whatever the story header carries, **read fresh at execution**. No value is copied here +— the opening of this document says none is, and a copied mode goes stale the moment a confirmed +profile change moves it. The story is `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md`. **Battery:** the full command in `AGENTS.md` § Commands. +**This section states properties, not commands.** Each check below gives what must be true, the +observation that fails without the change, and the **oracle** — what a correct implementation must +be able to distinguish. The executable form is written at execution time, carried in the plan with +one label per check, and reviewed by **Gate B against the real diff**, where a check has something +to be checked against. Earlier drafts carried the shell here; over six Gate-A passes that shell +produced the large majority of findings while §1–§3 and §5 drew none, and a spec is the wrong place +to review an implementation that has no diff to run against. + +**The entry date is `$D`, the day the ledger change is actually made.** §2.2 defines an entry's +`` as the day it is written, so no literal is pinned here. §3.1 shows `2026-08-05` because +that is when it was drafted; the implementer substitutes `$D` in the entry and in every dated +check. Only the *entry's* date is `$D` — the superseded row's date is `2026-07-20`, fixed, being a +property of the row. + +**Every check reads an explicit base ref.** No check may default it to `HEAD`: once the change is +committed, an edited row or paragraph would become its own baseline and the check would pass on +the mutation it exists to catch. + ### Check 1 — the correction is reachable from the ledger alone -Two halves, because the cardinality is decidable and the content is not. Both must hold. - -- *Mechanical:* both commands must report `1`. They are given in a fenced block rather than - inline because the first pattern contains backticks, which an inline code span cannot carry. - - ```sh - grep -c '^- 2026-08-05 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` ' docs/hardening-log.md - grep -cE '^\| 2026-07-20 \| truncated-tool-output-read-as-complete \|' docs/hardening-log.md - ``` - - The fingerprint is inside **both** patterns on purpose: matching the - entry on its dates alone and the row on date-plus-fingerprint would leave an entry carrying a - wrong or missing fingerprint passing both, which is exactly the state the pair is supposed to - rule out. -- *Named read:* a reader confirms the entry names the claim that does not hold, and that its - citation resolves to exactly one paragraph in `CLAUDE.md`. This half is a read, **not** a - parse — nothing validates it, and the greps above would pass an entry whose prose said - nothing useful. Stated rather than implied, because the greps alone are what a green - `battery+check` would otherwise rest on. -- *The falsifying observation:* at the base of this cycle the first grep reports `0` and the - header offers no sanctioned move for either case in §3 — the pre-change state, so the check - fails without the change rather than being wired to pass. +**Six** properties, 1a through 1f. All must hold, the named read included. + +**1a — the entry exists and names the right row.** The `Superseded rows` block carries an entry +dated `$D` superseding `2026-07-20` with fingerprint `truncated-tool-output-read-as-complete`, and +the table carries a row with that date and fingerprint. +*Falsifying observation:* before the change the entry is absent, and the header offers no +sanctioned move for either case in §3. +*Oracles:* the count of each must be **asserted, not printed** — a block that reports `0` and exits +0 reports its own falsifying observation as success. And the row must be matched **as a complete +row**, not by a date-and-fingerprint prefix: a truncated `| | |` line satisfies +a prefix test, so this property's own cardinality result would read as satisfied while 1d fails, +which misleads whoever is diagnosing which guarantee broke. + +**1b — the superseded row is untouched, byte for byte.** Supersession corrects a row without +editing it, so the row's whole line must be identical to the same line at the base ref. +*Falsifying observation:* mutate any character of that row and this must fail. +*Oracle:* a full-line comparison against the line **taken from the base**, not against a literal +copied into the spec — a literal drifts from the row it protects. This is story AC 2's every-byte +requirement, mechanised. + +**1c — the pre-existing first header paragraph is untouched, in both surfaces.** §4 says that +paragraph is not edited and that both surfaces carry it identically; both halves must be checked. +*Falsifying observation:* change one word of it in either surface and this must fail. Verified +today: both surfaces already carry it byte-identically. +*Oracle:* the delimiters bounding the paragraph must be asserted **present and unique in all three +inputs before any comparison is taken** — a checksum-style comparison returns a value for empty +input, so a guard on the checksum alone can never fire and two unreadable inputs compare equal. + +**1d — no entry this change appends is inert.** Every entry **added by the change under review** +must have a locator matching at least one table row dated on or before the entry's own date. +Matching two is fine — that is match semantics — so the bound is *at least one*, never *exactly +one*. Pre-existing entries are out of scope: §2.2 says an inert entry is never removed or edited, +so a whole-block scan would be unsatisfiable the first time the typo case fires — appending the +correction leaves the inert one in the scan, and deleting it breaks the absolute rule. +*Falsifying observation:* an entry whose fingerprint is mistyped must fail; one whose date precedes +the row it names must fail. +*Oracles* — **five** things a correct implementation must distinguish, each of which a draft got wrong: +- **Which entries are in scope.** Entries added by this change, computed against the base ref — + not every entry in the file. +- **A row from a non-row.** A candidate must be a complete seven-column row: **exactly** the + expected number of unescaped delimiters, terminal delimiter included. Rejecting only short lines + lets a longer malformed line supply a `finding`; rejecting neither lets a truncated + `| | |` satisfy the gate. +- **A delimiter from an escaped pipe.** A pipe delimits a column only when the run of backslashes + immediately before it is **even**. Testing for a single preceding backslash makes a `finding` + ending in a literal `\\` swallow the next column. Fixtures must include a `finding` containing + both `\|` and the digits `001` — the strings that broke two earlier extractions, and that earlier + fixtures happened not to contain. +- **Parse failure from an empty field.** "This line is not a row" and "this row's `finding` is + empty" must be separable results. Conflated, a fragmentless entry — which locates rows by date + and fingerprint alone — is falsely reported inert because some unrelated row has an empty + `finding`. +- **A read failure from a clean pass.** Every input must be guarded, current ledger as well as + base. An unreadable working-tree ledger must fail, never yield an empty candidate set and exit 0 + having examined nothing. + +**1e — recorded dates never decrease down the table.** Backdating a row would let it fall under an +entry written before it, which §2.2 forbids. +*Falsifying observation:* a row dated earlier than the row above it must fail. +*Oracle:* this checks **recorded order, not append-day truth**. A row appended today and labelled +with a date after the current tail passes. Nothing can verify the append day; the prohibition is +instruction-backed and this is a floor under it. The extraction must also distinguish "no dated +rows found" from "ledger unreadable". + +**1f — a named read.** A reader confirms the entry names the claim that does not hold, and that its +citation resolves to exactly one paragraph in `CLAUDE.md`. This half is a read, **not** a parse: +nothing validates it, and every mechanical property above would pass an entry whose prose said +nothing useful. Stated rather than implied, because the mechanical half is what a green +`battery+check` would otherwise rest on. ### Check 2 — the convention actually reached both surfaces -The parity claim in §4 is what the both-surfaces decision rests on, and check 1 would pass with -the template untouched. - -- *Operation:* locate §4's two sentinels in each file. **A sentinel that is missing, or that - matches more than once, is a failure — never an empty region.** Extract the delimited region - from each file and compare, normalizing hard-wrap joins within a paragraph only. Leading - indentation and blank-line structure are preserved and compared exactly: the four-space prefix - on the format example is the only thing distinguishing it from a live entry, and a - normalization that flattened it would let one surface convert the example into entry-shaped - content and still report equal. -- *Three states, each distinguished:* **pre-change**, the end sentinel is convention text and - exists in neither file, so the check fails — it does not read two absent regions as equal, and - that failure is the falsifying observation. **One surface edited**, the sentinel is present in - one file and absent in the other: fails. **Both edited**, the regions compare equal: passes. +The parity claim in §4 is what the both-surfaces decision rests on, and check 1 would pass with the +template untouched. + +**2a — presence, per surface, independently.** Each of the thirty-three anchors below must occur +**exactly once** in each of `docs/hardening-log.md` and +`plugins/dev-workflow/commands/workflow-init.md`. + +**2b — parity.** §4's two sentinels must each occur exactly once per surface — a missing or +repeated sentinel is a failure, never an empty region — and the delimited regions must compare +equal. + +**Presence first, then parity, and both must hold.** Comparing the two regions only to each other +establishes that they agree, not that either says anything: two identically truncated regions end +at the sentinel and compare equal, so parity alone reports green on a change that reached neither +surface, under a heading claiming it reached both. + +*Falsifying observation:* before the change every anchor is absent and the end sentinel exists in +neither file, so 2a fails first. + +*Oracles:* +- **Wrap-insensitive presence, indentation-exact parity.** Both surfaces are hard-wrapped at + different columns and most anchors straddle a line break in at least one, so presence must be + tested against a paragraph-joined view. Parity must compare leading indentation and blank-line + structure **exactly**: the four-space prefix is the only thing distinguishing the format example + from a live entry, and a normalisation that flattened it would let one surface turn the example + into entry-shaped content and still report equal. The join must therefore leave indented blocks + alone. +- **Occurrences, not matching lines.** After a paragraph join, two copies of an anchor in one + paragraph are on one line; a line-counting test reports `1` and the exactly-once claim measures + nothing. +- **Anchors are data, not code.** Two begin with `-` and many contain backticks; whatever runs them + must pass them as literal patterns and must not let a shell interpret them. An unescaped backtick + silently degraded an anchor to a prefix and still exited 0. + +**Four states, each distinguished:** *pre-change* — every anchor absent, end sentinel in neither: +2a fails. *One surface edited* — anchors resolve in one file, not the other: 2a fails. *Both edited +but incompletely*, an **anchored** clause dropped from both: 2a fails, which is the state parity +alone could not see. *Both edited fully*: passes. Dropping an **unanchored** decision from both is +**not** distinguished by either property; §8 lists the ones known today and does not claim that +list is complete. + +#### The anchors + +Each is the distinguishing text of one decision §2 settles. + +1. `records a hardening claim as of its date` +2. `falsified by a later change, or wrong when it was written` +3. ``append a `Superseded rows` entry rather than editing it`` +4. `This holds for every row without exception` +5. ``the existing `Never edit a row` rule is absolute`` +6. `The rule is bound to rows, not to commits` +7. `is below the rule's resolution, and nothing checks one` +8. ``Resolving a `pending` row also appends`` +9. `never alters mechanical behaviour` +10. `including when the entry records that the row's hardening claim was itself false` +11. `the row keeps its fingerprint, keeps matching the column-2 grep, and keeps counting` +12. `A hardening later removed is out of scope` +13. `**Correcting a row.**` +14. `present only once at least one entry exists` +15. ``block above the `Columns:` `` +16. `one appended line per supersession` +17. `- · supersedes ` +18. `` `` is the day the entry is written, in `YYYY-MM-DD` `` +19. `A row is located by date + fingerprint` +20. `An entry applies to every row its locator matches` +21. `applies only to matching rows dated on or before the entry's own date` +22. `A row's date is the day it is appended` +23. `the table is chronological: backdating a row is forbidden` +24. `matches no such row is **inert**` +25. `append a new entry with a locator that matches` +26. `Name the claim that does not hold` +27. `cite where the current answer lives` +28. `saying whether it stopped holding or was never true` +29. `restating that answer here only makes the entry the next stale narration` +30. `the last entry for a row is the one that governs` +31. `must therefore describe the row as it now stands` +32. `Entries are never edited, never removed, and never reference one another` +33. `keep one and keep every entry under it` +### Check 3 — the block is where §2.2 says, and only there + +Checks 1 and 2 leave §2.2's layout decisions unvalidated: check 1 matches the entry anywhere in the +file, and check 2's region deliberately *excludes* the block, the `Columns:` paragraph and the +table. + +**Property.** In the ledger, in this order: the convention's end sentinel, then exactly one +`**Superseded rows:**` label, then the change's entry, then the `Columns:` paragraph. Every +entry-shaped line in the file lies inside the label-to-`Columns:` interval. In the template the +label does not appear at all. + +*Falsifying observation:* before the change there is no label and no entry, so the ordering cannot +be established. + +*Oracles:* +- **Cardinality before ordering.** Each of the label, the `Columns:` paragraph and the entry must + resolve to exactly one position; two labels — which a union merge can produce — must fail rather + than silently taking the first. +- **The full ordering, not a prefix of it.** `label < entry < Columns:` alone permits the block + above `# Hardening log`, or a second entry below the table. The end sentinel must be pinned as + the lower bound and every entry-shaped line checked against the interval. +- **A desired zero must not be the failing status.** The template assertion is "no label here"; + expressed as a bare search, its success is a nonzero exit. +- *What it does not do:* it validates position and cardinality, not content, and it confirms only + that the template carries no *label* — a template wrongly carrying a live entry without one is + caught by nothing (§8). **Prompt conformance:** `docs/prompt-standards.md`, all twelve items, on -`plugins/dev-workflow/commands/workflow-init.md` and -`plugins/dev-workflow/skills/harden-finding/SKILL.md` — both invariant-11 surfaces. +`plugins/dev-workflow/commands/workflow-init.md` — the only invariant-11 surface this change +touches, now that `harden-finding` is out of the change surface. -## 8. Gate-A riders, verbatim in every pass prompt +## 7. Gate-A riders, verbatim in every pass prompt 1. **Sweep before reading.** Mechanically settle whatever this spec asserts that a machine can - decide without side effects — cited paths, quoted passages **byte-for-byte including - markdown emphasis markers**, line numbers, stated counts, the non-uniqueness claim in §2.2, - §4's parity sentinels (unique and locatable in both files, and does the region contain what - §4 says?), §3.1's citation, the §5 disposition table against the current skill text, and the - syntax and balance of standalone fenced and indented blocks. Both source files are - hard-wrapped, so compare quoted markdown with whitespace normalized and report a wrap - artifact as a wrap artifact. Inspect quoted commands rather than running them, but do check - by reading that §7's greps would match what the spec says they match — including what they - would *fail* to rule out. Report what the sweep found separately from what the read found. -2. **Self-test, six cases.** Apply §2.1 to: case §3.1; case §3.2; a row appended by an earlier - completed cycle on a branch not yet merged; a row appended by the open cycle after a `WIP:` - snapshot; a `pending` row the open cycle appended and now wants to resolve; and a row whose - authoring cycle cannot be established. Report all six verdicts explicitly, including where - they come out as the spec says. -3. **Old-conditions audit.** §5 carries a disposition table for the decision-procedure phrase it - replaces. Check it against the current skill text — every condition present, every - disposition right (`AGENTS.md` Don'ts). - -## 9. What this does not do - -- **It adds no mechanical check.** Nothing validates that a superseded row has an entry, that an - entry's locator resolves, or that a row was landed when superseded. A reader is the detection. + decide without side effects — cited paths, quoted passages **byte-for-byte including markdown + emphasis markers**, stated counts, the non-uniqueness claim in §2.2, §4's parity sentinels + (unique and locatable in both files; does the region contain what §4 says and exclude what it + says?), §6's thirty-three anchors (does each occur exactly once per surface; is any a substring + of another?), §3.1's citation, and the syntax and balance of standalone fenced and indented + blocks. Both source files are hard-wrapped, so compare quoted markdown with whitespace + normalised and report a wrap artifact as a wrap artifact. Report what the sweep found separately + from what the read found. +2. **Self-test the append-only claim and the floor — seven cases.** §2.1 removes the amendable + class rather than defining one, so the question is not "which rows may be edited" but "does + anything still imply that some may be". Walk: a row appended one minute ago and not committed; a + row appended and committed on your own branch; a row on `main`; a `pending` row you now resolve; + a row whose fingerprint you believe is wrong; row D (§3.2); and a half-typed line in your editor + that is not yet a row. Report a verdict for each, and flag **any** place in the spec, the shared + convention prose, the story or `harden-finding` that still offers, implies or presupposes an + in-place edit. +3. **Match-semantics and inert-entry consequences.** Locator uniqueness was withdrawn: an entry + applies to every row it matches dated on or before its own date, and one matching nothing is + inert and stands as history. Walk what the withdrawn guarantee held up — §6 check 1's + properties, §3.1's fragmentless entry, the `2026-07-18` + `docs-drift` pair, and AC 4's "where + the current answer lives" when one entry matches two rows. Report anything still resting on a + guarantee that no longer exists, or assuming an entry always governs something. +4. **Oracle coverage.** §6 states properties, falsifying observations and oracles; it deliberately + carries **no executable commands** — those are written at execution time, labelled in the plan, + and reviewed by Gate B against the real diff. Judge §6 on whether each property is *decidable*, + whether its falsifying observation would really fail before the change, and whether its oracles + name every distinction an implementation has to make. Report a property whose oracle is missing + a distinction; do **not** report the absence of shell. + +**On §8's list of unanchored decisions:** it is deliberately **partial** and makes no completeness +claim — that claim was deleted after being wrong at seven consecutive passes. Report a *wrong* +statement in it; do not report its incompleteness. + +## 8. What this does not do + +- **It adds no *standing* mechanical check.** §6's three checks run once, on this change, and + then stop; nothing runs on a future supersession. Nothing validates that a superseded row has + an entry, that an entry's locator matches any row at all beyond check 1's one-time assertion, or that a later entry describes + the row's supersession state as a whole rather than only its newest fault. For every entry + after §3.1's, a reader + is the whole detection. Check 3 constrains where the block sits and check 2's presence property + constrains which clauses are present — neither looks at an entry's content, and both are + one-time. +- **A row can be corrected in prose while its mechanical identity stays wrong.** §2.1 keeps a + superseded row counting even when the entry records that its fingerprint was wrong, or that + the hardening it claims never existed at all. A lineage can therefore escalate from a rung + that was never landed. This is deliberate — ejecting the row would misreport the class's + history — and the consequence is handed to the guard-scope story (§5), whose inherited open + question now covers both cases. +- **What check 2's anchor list does not cover — a partial list, and it is not exhaustive.** The + rule is exact: an anchor is the distinguishing text of one decision, so **a decision with no + anchor is not detected if it is deleted identically from both surfaces**. What is *not* exact is + any enumeration of which decisions those are. Known unanchored today: the + distinguishing-row-fragment narrowing and its no-double-quote condition, and §2.2's calibration + that append-day truth is unverifiable while the check enforces only non-decreasing recorded + dates. **Others almost certainly exist and are not listed here.** The layout rationale, the + prose-only decision and §2's reasoning generally sit outside the shared region and were never in + scope. Nothing anchors the prose *between* anchors on either surface. + + **Why there is no completeness claim.** There was one, and it was wrong at seven consecutive + review passes — each time naming a decision that turned out to be anchorable, or omitting one + that was not. Stating the rule and a partial list is what is actually true; a claim of + completeness kept regenerating the same defect, which is `prompt-standards` item 11's fourth + correction rule reached for the seventh time. Deriving the list mechanically was considered and + rejected: it would add a checker to protect a claim this design does not need to make. +- **Concurrent entries for one row resolve by file position, which a union merge does not fix.** + `merge=union` keeps both sides' entries and latest-in-file governs, so after a merge the + governing entry may not be the one either author intended. Nothing detects it. The repair is the + ordinary one — append another entry — because nothing is ever edited. Having no amend path + removes the lost-update case **between independently committed branches**; it does not remove + the same-worktree one §2.2 names, where two writers doing read-modify-write on the file can lose + an entry before git is involved at all. That case is unsupported. +- **Latest-wins loses a still-accurate earlier entry from view.** A row falsified twice in + different clauses carries two entries and only the last governs. §2.2 asks the author of the + second to describe the row as it now stands rather than only the new clause; nothing enforces + that, and an author who writes a narrow second entry leaves the first one's still-true content + below the line a reader stops at. +- **A live entry in the scaffolded template is not detected.** Check 3 tests for a + stray `**Superseded rows:**` *label*; a template carrying an entry-shaped line without one + passes it, and check 1's named read never opens the template. No check covers it. +- **Rows identical in date, fingerprint and `finding` cannot be told apart, and an entry marks all + of them.** The locator is date + fingerprint, narrowed only by a fragment of `finding`; where two + rows share all three there is no discriminator, so an entry naming one names every one — its + accurate siblings included. If only one such row's narration were false, the convention could not + mark it alone, which cuts against what the design is for. **No instance exists**: no such pair is + in the ledger today, and the case is hypothetical. A discriminator was considered and rejected — + an ordinal or a second column fragment would add wire format to the one thing §2.2 keeps + deliberately prose-only, to serve a case that has never occurred. + **Reopen trigger:** the first real identical pair whose truth diverges — two rows sharing date, + fingerprint and `finding` where one narration is falsified and the other still holds. Until then + this is a recorded limitation, not an open question. - **A superseded row is unchanged to every tool.** The column-2 recurrence grep, any count over the table, and `harden-finding`'s escalation all see the row exactly as before — including one whose entry says its fingerprint is wrong. - **Check 1's content half is a human read.** The greps establish that an entry exists carrying - the expected fingerprint and that its locator selects one row; nothing establishes that the + the expected fingerprint and that its locator matches the row it names; nothing establishes that the entry says anything true or useful. -- **The landed test is decidable in one direction only**, and the convention closes the other by - defaulting to landed. That default is a bias, not a determination: a row the open cycle really - did append can be treated as landed and get an entry it did not need. That cost was chosen - over the alternative, which is editing a record someone else has read. -- **The convention does not reach the reader that would consume it.** §6 hands that to the +- **The convention does not reach the reader that would consume it.** §5 hands that to the guard-scope story; until it lands, the correction is prose a human reads. - **It does not cover a hardening that is later removed.** Named as out of scope in §2.1 and parked in `todos.md` with its trigger — the first rung actually removed. No instance exists. -- **The union-merge repair rule is a manual instruction.** `merge=union` will duplicate a - `Superseded rows:` label when two branches each create the block, and nothing detects or - repairs that; §2.2 tells a human what to do when they see it. -- **The prohibition on editing a landed row is duplicated across two prompt surfaces** — the - ledger header and `harden-finding`'s § Log format (§5). Only the definition is single-sourced. - Nothing keeps the two prohibitions in step. +- **`merge=union` duplicates rather than overwrites — sometimes — and nothing repairs it.** When + two branches each create the block, union *may* leave two `**Superseded rows:**` labels; §2.2's + wording is conditional for that reason. Check 3's cardinality property rejects a duplicate label, but once only, during this change; **no standing check** looks again, and the merge that produces one will normally happen long after. Reproduced on a scratch repo + with the configured driver, the identical label lines **coalesced** and both distinct entries + were kept — one label, not two. An earlier draft here asserted duplication categorically, which + overstated what the merge mechanism guarantees in the direction that matters least: the repair + instruction has to exist for the case where it does happen, not because it always does. The same mechanic applies to the table: union + keeps both sides' lines, so two branches appending rows can leave duplicates. That is a + pre-existing property of the ledger's merge driver, not something this change introduces — + named here because an earlier draft described the failure as one writer "overwriting" another, + which union never does. - **The format example is not an entry, and nothing enforces that distinction beyond its shape.** It is a four-space indented code block using `` and `` placeholders, so no line-anchored grep for a dated entry reaches it. That is the whole guard: POSIX grep has diff --git a/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md b/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md index ff85f40..0fbc5d6 100644 --- a/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md +++ b/docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md @@ -84,13 +84,19 @@ Seven — six paired with the failure that raised each, and one inherited from a - **Inherited** from `2026-08-04-hardening-ledger-supersession-story.md`: that story's convention lets a superseded row record, in prose, that it was filed under the wrong fingerprint — and deliberately leaves nothing mechanical reading it, so the row keeps its class, keeps matching - the column-2 grep, and keeps counting. The redesign here owns the reader, so it owns the - choice. **Should a recurrence read consult the supersession record, and if so, does a - prose-corrected class change the lineage it escalates from?** Arrives as a handoff, not as a - failure: no instance has occurred. + the column-2 grep, and keeps counting. **Widened 2026-08-10** to the same family's harder + member: that convention covers a claim that was *never true*, so an entry may equally record + that the row's **hardening itself** never existed or was misclassified — a phantom-hardening + row — and such a row also keeps counting, so a lineage can escalate from a rung that was never + landed. Wrong-fingerprint and phantom-hardening are one question, not two: both are a row whose + *mechanical identity* is corrected only in prose, and both land here because the reader is what + would have to act on the correction. The redesign here owns the reader, so it owns the choice. + **Should a recurrence read consult the supersession record, and if so, does a prose-corrected + class or a prose-retracted hardening change the lineage it escalates from?** Arrives as a + handoff, not as a failure: no instance of either has occurred. ## 6. Suggested size `story` — one skill file, one decision procedure, one spec → plan → PR. Above a chore because -the six questions above are real design; below an epic because they all concern one procedure +the seven questions above are real design; below an epic because they all concern one procedure in one file. diff --git a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md index 1a6b6a0..100ad9d 100644 --- a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md +++ b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md @@ -8,9 +8,12 @@ ## 1. Problem statement -`docs/hardening-log.md`'s header says never edit a row, and one row per hardening. When a row's -"what this does NOT do" narration is later falsified by a feature change, neither move is -sanctioned: editing breaks the first rule, appending breaks the second. +**As of this story's writing — the pre-change state this problem describes:** +`docs/hardening-log.md`'s header said never edit a row, and one row per hardening. When a row's +"what this does NOT do" narration was later falsified by a feature change, neither move was +sanctioned: editing broke the first rule, appending broke the second. (Past tense records that +this is the state *before* the change, not that the rule moved: the amendment notes below +narrowed it and then restored it, so "never edit a row" reads today exactly as it did here.) This is live. The 2026-07-20 row describes pre-0.8.0 counting behaviour as current, and a reader who trusts it is misled about how the gate hook counts today. That is the second falsified row, @@ -22,7 +25,7 @@ From `todos.md`, "**The hardening ledger has no supersession convention.**": | Condition | Disposition | |---|---| -| Never edit a row | **kept in substance, narrowed in text** — see the amendment note below | +| Never edit a row | **kept, literally and absolutely** — narrowed at pass 3, restored at pass 7; see both amendment notes below | | One row per hardening | **kept** — any solution must preserve it | | The 2026-07-20 row now describes pre-0.8.0 behaviour as current | **kept** as the motivating instance | | The 2026-07-20 *spec* took a version-qualified supersession note and it worked | **kept** as prior art the design should evaluate first | @@ -33,26 +36,59 @@ From `todos.md`, "**The hardening ledger has no supersession convention.**": the AGENTS.md Don't — **kept:** the committed record's protection, which is what every consumer of the rule actually relied on. **Narrowed:** the textual rule, from "never edit a row" to "never edit a landed row", making textual what #22's Gate B already accepted in practice (row D, -amend-during-authoring, reasoning recorded in its evidence). **Dropped:** nothing. The amendment -records both facts — the text changed here, and the boundary it moved to was already the -practiced one. §2 and acceptance criterion 3 carry the same change. +amend-during-authoring, reasoning recorded in its evidence). **Dropped:** nothing. + +**Amended again 2026-08-10** from Gate-A spec pass 7, and human-confirmed — **this reverses the +2026-08-05 narrowing.** Accounting per the AGENTS.md Don't — **restored:** the original absolute +wording, "never edit a row", with no landed/unlanded distinction anywhere. **Dropped:** the +narrowing itself, and with it row D's status as a precedent — under the convention that row's +correct move was an entry, which costs one entry in the whole ledger's history. **Kept:** the +committed record's protection, unchanged throughout, and now stated without a boundary to +compute. §2 and acceptance criterion 3 carry the same change, and AC 3 now holds literally. + +*Why the narrowing was reversed — recorded so it is not re-explored.* The narrowing required a +definition of *landed*, and three were designed and deleted in turn: authorship-by-cycle, +reachability from `origin/main`, and content presence in the published ledger. Gate-A passes 4 +through 7 each found the **replacement** for the previous definition unsound, three times inside +the very sentence written to fix its predecessor — the last being pass 7's blocker, where a +stale-but-readable ref returns a confident "absent" for a row already published, licensing an +edit to it. Each definition also carried its own old-conditions accounting, six-case self-test, +concurrency assumption and residual list, each of which grew defects of its own. The detour is +**explored and deleted**, not merely unfinished: the design now has no amendable class, so there +is no definition to get wrong. ## 2. Desired outcome -A reader who opens any ledger row can tell whether it still describes current behaviour, and a -row falsified by a later change can be marked as such without breaking either standing rule — -as amended: never edit a **landed** row, and one row per hardening. +A reader who opens any ledger row can tell which of its claims have been recorded as no longer +holding and where the current answer lives, and a row falsified by a later change can be marked +as such without breaking either standing rule, both of which hold in their original absolute +form: never edit a row, and one row per hardening. + +*(Amended 2026-08-10 from Gate-A spec pass 5, finding 11. Accounting per the AGENTS.md Don't — +**kept:** determination from the ledger alone, and both standing rules. **Narrowed:** "can tell +whether it still describes current behaviour" → "can tell which of its claims have been +recorded as no longer holding", matching AC 4. The absence of a supersession entry never proved +a row current — nothing validates completeness — so the outcome was promising what the design +deliberately does not deliver. **Dropped:** nothing. AC 4 was amended this way at pass 1; this +site was missed then and is brought into line now.)* ## 3. Acceptance criteria - [x] Before design resumes on this story, whoever picks it up proposes both axes and the mode derived from them, pauses for Daniel's confirmation, and writes the confirmed profile into this header. Design continues only after that. -- [ ] The 2026-07-20 row no longer reads as current behaviour, and it was not edited. -- [ ] The append-only rule — as amended, protecting **landed** rows — and the - one-row-per-hardening rule both still hold after the change. - *(Amended 2026-08-05 from Gate-A spec pass 3, finding 1, and human-confirmed; the - kept/narrowed/dropped accounting is in §1.)* +- [ ] The ledger no longer presents the 2026-07-20 row as unqualified current behaviour — and + every byte of the row itself is unchanged, the correction living in a marker above it. + *(Amended 2026-08-10 from Gate-A spec pass 9, finding 10. Accounting per the AGENTS.md + Don't — **kept:** both halves, that the row stops reading as current and that it was not + edited. **Clarified:** which artifact changes — the ledger, not the row — because "the row + no longer reads as X" is satisfiable only by editing the row, which the same criterion + forbids. **Dropped:** nothing.)* +- [ ] The append-only rule and the one-row-per-hardening rule both still hold after the change, + **literally and without amendment** — no row is edited, ever, and a correction is an append. + *(Narrowed 2026-08-05 from Gate-A spec pass 3; **restored 2026-08-10** from pass 7, both + human-confirmed. The criterion now reads as it originally did; the accounting for the + narrowing and its reversal is in §1.)* - [ ] A reader can determine, from the ledger alone, which of a row's claims no longer hold and where the current answer lives. *(Amended 2026-08-05 from Gate-A spec pass 1, finding 4, and human-confirmed. Accounting @@ -68,17 +104,28 @@ as amended: never edit a **landed** row, and one row per hardening. - `## Key invariants` → `### Prompts and scaffolding` — "9. **`/workflow-init` never overwrites silently.** Idempotent: missing → write; identical → report unchanged; present and different → show the diff and ask; additive files (`.gitattributes`, `.mcp.json`, …) → merge." -- **Conditional, if the design reaches the scaffolded template** — "11. **Prompt changes pass - `docs/prompt-standards.md`**" and "12. **A plugin change requires a version bump.**" Both bind - only if the convention must reach `/workflow-init`'s inline ledger header, which is §5's open - question. +- "11. **Prompt changes pass `docs/prompt-standards.md`**" and "12. **A plugin change requires a + version bump.**" **Both bind unconditionally**, as of 2026-08-10. They were recorded as + conditional on §5's open question; the design resolved that question toward reaching the + template, so both now bind. *(Amended 2026-08-10 from Gate-A spec pass 4, finding 11. + Accounting per the AGENTS.md Don't — **kept:** both invariants, and the reason each was + listed. **Dropped:** the condition, because the thing it was conditional on is settled. + **Dropped:** nothing else.)* ## 5. Open questions -- Which artifacts must the convention reach before a reader can trust any row — this repo's +- ~~Which artifacts must the convention reach before a reader can trust any row — this repo's ledger alone, or every ledger `/workflow-init` scaffolds? A repo-only fix ships a rule this - kit's ledger obeys and every scaffolded one does not. + kit's ledger obeys and every scaffolded one does not.~~ + **Resolved 2026-08-10: both surfaces.** The design carries the convention into + `/workflow-init`'s inline ledger header as well as this repo's ledger, and rests the + both-surfaces decision on a parity claim its §6 check 2 validates. Recorded rather than + deleted, because §4's invariants and §6's size were both written against the unresolved form. + *(Amended 2026-08-10 from Gate-A spec pass 4, finding 11.)* ## 6. Suggested size -`story` — one file's header convention plus possibly one inline template, one spec → plan → PR. +`story` — one file's header convention **and** one inline template (resolved, §5), plus the +version bump and changelog entry those pull in, one spec → plan → PR. +*(Amended 2026-08-10 from Gate-A spec pass 4, finding 11 — "possibly one inline template" was +written before §5 was resolved.)* From f9ed886347c180198db3630fb2b0fd9c63b9670f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:31:53 +0200 Subject: [PATCH 4/6] Close Gate A on the supersession spec and land its plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec now carries the convention's design: §2.1's sanctioned move — a row whose narration is falsified later or was wrong when written is corrected by appending a `Superseded rows` entry, never by editing the row — and §2.2's entry format, locator semantics, inert-entry rule and date bound, as the two markdown blocks both surfaces will carry byte-identically. The story carries the governing criteria and its eight Gate-A amendments, one per prompting pass, each with explicit old-condition accounting. The plan carries the eight check labels with their properties, falsifying observations and oracles, the fixture matrix, and the eight execution tasks. The executable form of the checks is written at execution time and is never committed. Gate B: N/A. Every path in this commit is under `docs/**.md` — explanatory documentation, none under a `.claude/`, `plugins/`, `skills/` or `commands/` directory at any depth — so CLAUDE.md §5's prose exemption applies. Classified by reading the three paths, not by assumption. --- ...026-08-12-hardening-ledger-supersession.md | 658 ++++++++++++++++++ ...05-hardening-ledger-supersession-design.md | 347 +++++++-- ...-04-hardening-ledger-supersession-story.md | 33 +- 3 files changed, 961 insertions(+), 77 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md diff --git a/docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md b/docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md new file mode 100644 index 0000000..fb32342 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md @@ -0,0 +1,658 @@ +# Hardening-ledger supersession convention — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task, sequentially. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a supersession convention to `docs/hardening-log.md`'s header and to +`/workflow-init`'s inline ledger template, and use it once — to mark the 2026-07-20 +`truncated-tool-output-read-as-complete` row, whose narration 0.8.0 falsified. + +**Architecture:** Two convention blocks — spec §2.1's single paragraph, and §2.2's intro paragraph, +four-space indented format example, and closing paragraph — byte-identical across both surfaces +modulo hard-wrap position, appended after the ledger header's existing first paragraph. Corrections are appended +`Superseded rows` entries that mark rows; no row is ever edited. No standing tool reads the block. + +**Tech Stack:** Markdown; POSIX `sh` (checks must also pass under `dash`); `git`; `shellcheck`; +the `claude` CLI for `plugin validate`. + +**Spec:** `docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md` +**Story:** `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` +— read the profile fresh from that header at every gate; no value from it is copied here. + +**This plan carries labels, properties and oracles. It does not carry the executable form.** +Each check has a stable label, the property it must establish, its falsifying observation, and the +distinctions a correct implementation must make. The shell is written at execution time by the +executor. **Gate B reviews the implementation diff — the two surfaces, the manifest, the changelog, +`todos.md` — and does *not* review the checks:** the harness and fixtures are scratch, never +committed, and so never enter a reviewable git range. Their only guards are the self-test of Task 2, +`shellcheck`, and the per-label counter-checks below — plus the reviewer reading the check source, +which Task 8 requires in `additionalContext`. That is a reviewer reading a paste, not a gate +comparing a diff; spec §8 records the residual and §6 states the same division. + +**No counter-check may mutate a row or a complete entry in the real ledger.** §2.1 protects a row +the moment it exists, committed or not, and §2.2 protects a complete entry the same way — so a +mutate-then-restore against `docs/hardening-log.md` executes exactly the operation this change +exists to forbid, in the file it is being added to. Every such mutation runs against a **scratch +copy** of the ledger with the check pointed at the copy. Mutations to *prose* — a header paragraph, +an anchored clause of the convention — are not covered by either rule and may be done in place. + +**Where this plan and spec §6 disagree, §6 governs — except on the four items spec §8 records as +held-not-fixed**, where the remedies below supplement or correct §6 and are authoritative. Those +four exist *because* §6 is wrong or incomplete there; a precedence rule without this carve-out would +instruct the executor to discard them. + +## Global Constraints + +- **Invariant 12 — a plugin change requires a version bump.** `plugins/dev-workflow/.claude-plugin/plugin.json` moves `0.8.1` → `0.8.2`, with a `plugins/dev-workflow/CHANGELOG.md` entry. `plugins/dev-workflow/commands/workflow-init.md` is under `plugins/`, so the rule binds. +- **Invariant 11 — prompt changes pass `docs/prompt-standards.md`, all twelve items**, on `plugins/dev-workflow/commands/workflow-init.md`. It is the only invariant-11 surface this change touches. +- **Invariant 9 — `/workflow-init` never overwrites silently.** Spec §4's verdict: **nothing is needed**. An existing scaffolded project re-running `/workflow-init` meets the changed header as "present and different", which already routes to show-the-diff-and-ask. The constraint is that the executor must not *break* that path — the template edit stays inside the existing fenced block and adds no write behaviour. Task 8 records the verdict as part of the prompt-conformance read. +- **Invariant 8 — `/workflow-init`'s templates stay inline.** The template edit happens inside the command body; never read a template from disk. +- **Invariant 5 — every version pinned exactly.** This change adds no dependency; do not introduce one. +- **`harden-finding` is out of scope.** §2.1 narrows nothing, so that skill's `never edit an existing row` sentence stays true. Do not edit it. +- **No `Co-Authored-By: Claude` or `Generated with` trailers** on any commit. +- **The entry's date is `$D`, the day the ledger change is made**, in `YYYY-MM-DD`. Only the *entry's* date is `$D`; the superseded row's date is `2026-07-20`, fixed, being a property of the row. The spec's §3.1 shows a different date and is not a literal to copy. +- **Every check reads an explicit base ref and never defaults it to `HEAD`.** `BASE` is captured in Task 1. Once the change is committed, an edited row would otherwise become its own baseline and the check would pass on the mutation it exists to catch. +- **Assert exit status, never printed output**, and treat any stderr shell error as failure. This governs every property and every fixture outcome. It does **not** govern the harness self-test of Task 2, whose whole job is to prove the harness can report a failure at all — that one asserts on its diagnostic output by necessity, and is the single carve-out. +- **Where §5 or §8's "prose-only / nothing consumes" wording disagrees with §2.2's "Format" paragraph, §2.2 governs.** The accurate claim is *no standing machine consumer*; the entry syntax **is** a standing authoring convention every future author honours. *(Closes spec §8 held-not-fixed item 2.)* +- **No standing check is added.** The harness is scratch and is never committed; `todos.md` parks the follow-up for wiring one. This is a statement about *checks* and does not touch the authoring convention above. +- **Mirror edits land in the same commit.** The two surfaces are byte-identical in the shared region; an edit to one that does not reach the other is the defect `C2a` exists to catch, and splitting them across commits makes the intermediate commit wrong on purpose. +- **Never `git add -A`, `git add .`, or any pathspec broader than the files a step names.** The working tree carries `docs/research/`, untracked and not ours, and the executor will create fixtures; either can be swept into a commit or an amend, which changes the Gate-B range and can publish an unrelated tree. **Before every commit and every amend, assert the staged set equals the intended paths** — `git diff --cached --name-only` compared against the step's file list, exit non-zero on any extra. Use deletion-aware explicit pathspecs so a removed fixture cannot force a broad add. +- **The harness lives outside the repository working tree** — the session scratchpad, not a directory under the repo. Nothing untracked that the executor creates should ever be inside a path `git status` reports. + +--- + +## File Structure + +| File | Responsibility | Action | +|---|---|---| +| `docs/hardening-log.md` | this repo's ledger — the convention prose **and** the `Superseded rows:` block holding the one entry | Modify | +| `plugins/dev-workflow/commands/workflow-init.md` | the inline empty-ledger template at §2.2 of the command body — the same prose, **no block** | Modify | +| `plugins/dev-workflow/.claude-plugin/plugin.json` | `version` `0.8.1` → `0.8.2` | Modify | +| `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2`, newest first | Modify | +| `todos.md` | source row closed and rewritten in past tense; **two** new parked rows | Modify | +| `docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md` | this plan — untracked until Task 1 commits it | Add | +| scratch `checks/` | the one-time validation harness — fixtures, checks, runner. **Never committed** | Create | + +**Outside this execution's scope**, though spec §4's change surface names them — they already carry +the required content, and no task touches them: `docs/coding-workflow.md`'s append-only sentence, +the historical-snapshot note on +`docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md`, and the guard-scope story's +inherited open question covering phantom-hardening rows. Verify each reads as expected before +concluding the change surface is met; do not rewrite them. + +--- + +## The checks — labels, properties, oracles + +Eight labels. `C1a`–`C1d` and `C1f` are spec check 1, `C2a`–`C2b` check 2, `C3` check 3. +**Spec check 1e is not implemented** — it validates the ledger's pre-existing chronology, passes +before and after, and its falsifying observation requires a mutation of the table that this diff +cannot produce. (An unreadable ledger would fail it too; that is not evidence about the change +either.) Recorded in spec §8. + +| Label | Property | Falsifying observation | +|---|---|---| +| `C1a` | The `Superseded rows` block carries an entry dated `$D` superseding `2026-07-20` with fingerprint `truncated-tool-output-read-as-complete`, and the table carries that row as a **complete** row | **fails on the clean pre-change tree** — the entry is absent | +| `C1b` | The superseded row's whole line is identical to the same line at `BASE` | passes pre-change; **fails only under mutation** — change one character of that row | +| `C1c` | The pre-existing first header paragraph is untouched in **both** surfaces, each against its own base | passes pre-change; **fails only under mutation** — change one word in either surface | +| `C1d` | The mandated entry is not inert — its locator matches at least one row dated on or before its own date | **fails on the clean pre-change tree** — there is no entry to match anything | +| `C1f` | A named read of the entry's content — four confirmations | not mechanical; nothing establishes that the prose says anything useful | +| `C2a` | Each of the thirty-five anchors occurs **exactly once** in each surface | **fails on the clean pre-change tree** — every anchor is absent | +| `C2b` | Both sentinels occur exactly once per surface, and the delimited regions compare equal | **fails on the clean pre-change tree** — the end sentinel exists in neither file | +| `C3` | Block position, cardinality and blank-line structure in the ledger; no label in the template | **fails on the clean pre-change tree** — no label, no entry, so the ordering cannot be established | + +Five labels fail on the untouched tree and are the change's real counterfactual: `C1a`, `C1d`, +`C2a`, `C2b`, `C3`. `C1b` and `C1c` are protective — they assert something the change must *not* +do — so their counter-checks are mutations, not the baseline. + +### Oracles the executor must satisfy + +**`C1a`** — Assert the counts, never print them: a block that reports `0` and exits 0 reports its +own falsifying observation as success. Match the row **as a complete row**, not by a +date-and-fingerprint prefix: a truncated `| | |` line satisfies a prefix test, +so this label would read as satisfied while `C1d` fails. + +**`C1b`** — Compare the full line against the line **taken from `BASE`**, never against a literal +copied into this plan; a literal drifts from the row it protects. This is story AC 2's every-byte +requirement, mechanised. + +**`C1c`** — **Four inputs, not three.** Each current surface against **its own** base version, with +current parity asserted separately; one base paragraph used as the reference for both establishes +"untouched" only by leaning on the separate fact that the surfaces are byte-identical today. The +delimiters bounding the paragraph must be asserted present and unique in **every** input **before +any comparison is taken** — a checksum-style comparison returns a value for empty input, so a guard +on the checksum alone can never fire and two unreadable inputs compare equal. + +**`C1d`** — Seven distinctions: +1. **Which entry is in scope.** *The mandated entry only*, proven added against `BASE`. Every other + added entry is ignored; a check quantified over all of them is unsatisfiable against §2.2's own + repair. *(Closes spec §8 held-not-fixed item 3.)* +2. **An entry from a line that is not one.** Candidates are the **non-blank** lines of the interval + from the `**Superseded rows:**` label to the `Columns:` paragraph, both exclusive. The blank line + separating the list from that paragraph is required by CommonMark — without it `Columns:` renders + inside the list item — so blank lines are not candidates and every non-blank one is. A candidate + that does not parse must **fail the check**, never be dropped from the candidate set. + *(This bound does **not** by itself close spec §8 item 4 — it makes `C1d` ignore an entry-shaped + line outside the interval, which is the opposite of detecting one. `C3`'s whole-file rule and + Task 5's below-the-table counter-check are what supply the executable guard; this bound is the + half that stops the format example being read as a candidate.)* +3. **Entry shape.** `- ` marker, `YYYY-MM-DD` date, the literal `· supersedes `, a row date, a + backticked fingerprint, an **optional** quoted fragment, and two ` · `-separated prose fields, + **both non-empty** and neither containing ` · `. +4. **Locator equality before eligibility.** Exact **row-date *and* fingerprint** equality is + established first; the on-or-before bound is applied after. A checker comparing only the + fingerprint passes every other fixture while reporting non-inert an entry that matches no row. + *(Closes spec §8 held-not-fixed item 1.)* +5. **Calendar-valid dates**, not merely `YYYY-MM-DD`-shaped: `2026-02-30` parses, sorts, and is not + a day anything was appended on. Applies to the entry date and to the locator's row date. +6. **Zero from at-least-one.** Zero is inert and must fail; one *or many* must pass. A checker + demanding *exactly one* implements the guarantee §2.2 withdrew and would still pass this change. +7. **A row from a non-row, a delimiter from an escaped pipe, an empty field from a parse failure, + and a read failure from a clean pass** — §6's four row-side oracles, verbatim in force. A pipe + delimits a column only when the run of backslashes immediately before it is **even**; a row is + complete only at exactly the expected delimiter count, so an overlong line is rejected too. + +**`C2a`** — **Occurrences, not matching lines**: after a paragraph join, two copies of an anchor in +one paragraph sit on one line, and a line-counting test reports `1` while the exactly-once claim +measures nothing. Presence is tested against a **paragraph-joined** view, because both surfaces are +hard-wrapped at different columns and most anchors straddle a line break in at least one. **Anchors +are data, not code** — one begins with `-` and five contain backticks; pass them as literal patterns +and let no shell interpret them. + +**`C2b`** — Presence first, then parity, **and both must hold**: two identically truncated regions +end at the sentinel and compare equal, so parity alone reports green on a change that reached +neither surface. Parity compares leading indentation and blank-line structure **exactly** — the +four-space prefix is the only thing distinguishing the format example from a live entry — so the +paragraph join must leave indented blocks alone. + +**`C3`** — **Cardinality before ordering**, evaluated once, on this change, before any merge +involving it: each of the label, the `Columns:` paragraph and the entry resolves to exactly one +position, and two labels must fail rather than silently taking the first. **The full ordering, not +a prefix of it**: end sentinel `<` label `<` entry `<` `Columns:`, with every entry-shaped line +checked against the interval. **Blank-line structure is asserted, not assumed**: exactly one blank +line immediately above the entry list and exactly one immediately below it, since without the lower +one CommonMark renders `Columns:` inside the list item and every other check still passes. **A +desired zero must not be the failing status** — "no label in the template" expressed as a bare +search succeeds with a nonzero exit. + +### The thirty-five anchors — data for `C2a` + +Byte-identical to the spec's §6 list. One begins with `-`; five contain backticks. + +```text +records a hardening claim as of its date +falsified by a later change, or wrong when it was written +append a `Superseded rows` entry rather than editing it +This holds for every row without exception +the existing `Never edit a row` rule is absolute +The rule is bound to rows, not to commits +is below the rule's resolution, and nothing checks one +Resolving a `pending` row also appends +never alters mechanical behaviour +including when the entry records that the row's hardening claim was itself false +the row keeps its fingerprint, keeps matching the column-2 grep, and keeps counting +A hardening later removed is out of scope +**Correcting a row.** +present only once at least one entry exists +block above the `Columns:` +one appended line per supersession +- · supersedes +`` is the day the entry is written, in `YYYY-MM-DD` +A row is located by date + fingerprint +An entry applies to every row its locator matches +applies only to matching rows dated on or before the entry's own date +A row's date is the day it is appended +the table is chronological: backdating a row is forbidden +matches no such row is **inert** +append a new entry with a locator that matches +Name the claim that does not hold +cite where the current answer lives +saying whether it stopped holding or was never true +restating that answer here only makes the entry the next stale narration +the last entry for a row is the one that governs +must therefore describe the row as it now stands +Entries are never edited, never removed, and never reference one another +keep one and keep every entry under it +singles out one row only where that row has one no sibling shares +once a line exists as a complete entry it is protected +``` + +### Fixture matrix — expected outcome per label + +Every fixture has a stated expected result under **both** `sh` and `dash`. Fixtures are not all +negative: a checker that rejects valid adversarial content is as wrong as one that accepts invalid +content, and only the pairing distinguishes them. + +**One baseline, one advertised mutation each.** Every fixture derives from a single **valid +post-change baseline** — the ledger as Task 4 Step 5 leaves it, with the mandated entry in place — +and differs from it in exactly the one dimension its row advertises. Without that rule a negative +fixture can take its expected failure from an unrelated locator or parse defect and still look like +evidence for the distinction it names. + +| Fixture | What it is | Expected | Distinction it kills | +|---|---|---|---| +| `rows-escapes` | a valid row whose `finding` holds `\|`, the digits `001`, and a trailing `\\` | **PASS** `C1a`, `C1d` | a naive splitter that treats `\|` as a delimiter, or `001` as a number | +| `rows-truncated` | `\| \| \|` and nothing more | **FAIL** `C1a`, `C1d` | prefix matching instead of complete-row matching — `C1d`.7 | +| `rows-overlong` | eight delimited columns instead of seven | **FAIL** `C1a`, `C1d` | rejecting only short lines — `C1d`.7 | +| `rows-empty-finding` | a valid row whose `finding` is empty | **PASS** `C1d` for a fragmentless locator | conflating "empty field" with "parse failure" — `C1d`.7 | +| `ledger-unreadable` | a directory at the ledger path, or mode 000 | **FAIL** `C1a`, `C1b`, `C1c`, `C1d`, `C2a`, `C2b`, `C3` — named, not "every label that reads it" | an empty candidate set read as a clean pass — `C1d`.7 | +| `entry-good` | the mandated entry with a correct locator, **plus a second, inert entry** below it | **PASS** `C1d` | a checker still quantifying over every added entry — `C1d`.1, and spec §8 item 3 | +| `entry-wrong-fingerprint` | right row date, one character off in the fingerprint | **FAIL** `C1d` | — `C1d`.4 | +| `entry-wrong-rowdate` | **right fingerprint, wrong row date** | **FAIL** `C1d` | a fingerprint-only comparison — `C1d`.4, and spec §8 item 1 | +| `entry-before-row` | entry dated before the row it names | **FAIL** `C1d` | a missing on-or-before bound — `C1d`.4 (manifests as zero matches) | +| `entry-on-row-date` | entry dated **exactly** the row's date | **PASS** `C1d` | a strict-before bound — `C1d`.4 | +| `entry-bad-date-entry` | entry dated `2026-02-30` | **FAIL** `C1d` | lexical-only date validation — `C1d`.5 | +| `entry-bad-date-locator` | valid entry date; locator row date **and** a matching table row both dated `2026-02-30` | **FAIL** `C1d` | validating the entry date only — `C1d`.5 | +| `entry-empty-first-field` | valid locator, **what-is-false** field empty | **FAIL** `C1d` | validating one field and not the other — `C1d`.3 | +| `entry-empty-second-field` | valid locator, **citation** field empty | **FAIL** `C1d` | the same, from the other side — `C1d`.3 | +| `entry-separator-in-field` | a prose field containing ` · ` | **FAIL** `C1d` | an ambiguous split — `C1d`.3 | +| `entry-outside-block` | the **mandated entry moved** below the table, none left inside the block | **FAIL** `C1d`, `C3` | shape-only candidate selection — `C1d`.2. Constructed as a *move*, not an addition: an extra line below the table while the mandated entry stays put must **pass** `C1d` under `.1` and `.2`, and Task 5's counter-check covers that construction | +| `rows-two-matching` | **two** complete rows, both dated `2026-07-20` with fingerprint `truncated-tool-output-read-as-complete` — the *locator's* row date, not the entry's; the entry keeps `$D` | **PASS** `C1d` | a checker demanding *exactly one* — `C1d`.6. Two rows dated `$D` would match nothing and would test the wrong thing | + +**Expected outcome on the untouched tree, per label** — stated here because "run it against the +pre-edit tree" is ambiguous where five labels are *supposed* to fail there and 0 means success: + +| On `BASE`, unmodified | Labels | +|---|---| +| must **fail** — this is the change's counterfactual | `C1a`, `C1d`, `C2a`, `C2b`, `C3` | +| must **pass** — protective, exercised only by the counter-check mutations | `C1b`, `C1c` | + +A label in the first row that passes on the untouched tree is a broken check, not a satisfied one. +A label in the second row that fails there means the harness or the base extraction is wrong, and +nothing below it is evidence. + +--- + +### Task 1: Commit the closed Gate-A artifacts and this plan + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md` (already edited) +- Modify: `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` (already edited) +- Add: `docs/superpowers/plans/2026-08-12-hardening-ledger-supersession.md` — this plan, untracked + +**Interfaces:** +- Consumes: nothing. +- Produces: **`BASE`** — the commit these land on. Every check compares against it. + +- [ ] **Step 1: Confirm the change set is prose-only, path by path** + +`git status --short`. Expected: the spec and the story modified, this plan untracked, and the +untracked `docs/research/`, which is not ours and stays untracked. All three of ours are under +`docs/**.md`, none under `.claude/`, `plugins/`, `skills/` or `commands/` at any depth, so **Gate B +is N/A by CLAUDE.md §5's prose exemption**. Read the paths to establish that; do not assume it. + +- [ ] **Step 2: Re-run the spec sweep** + +Assert, by exit status: 35 anchors, each exactly once in the spec's two `markdown` fenced blocks, +none a substring of another; fences balanced; both anchor-count claims reading "thirty-five". +Any failure stops the task. + +- [ ] **Step 3: Commit all three**, staged by explicit pathspec, with the staged set asserted equal +to those three. The message describes what the artifacts now contain — the convention's design, the +governing story's criteria, and this plan — and records the Step-1 classification. + +- [ ] **Step 4: Pin `BASE` to the resulting commit** and record the value in the task notes. + +The hook may fire a Gate-B STOP on this commit; that is invariant 2's loose-in-the-firing-direction +behaviour, and the N/A classification was checked by hand in Step 1. + +--- + +### Task 2: A harness that has been shown able to fail + +**Files:** create the scratch harness — assertion helpers, a paragraph-join helper, a runner. + +**Interfaces:** +- Consumes: `BASE`. +- Produces: the assertion vocabulary every check uses, and a paragraph-joined view in which blank + lines are preserved, lines indented four or more spaces are emitted verbatim on their own line, + and every other run of non-blank lines collapses to one line with single spaces. + +- [ ] **Step 1: Write the harness.** Required properties: + - Exit status is captured **before** any command substitution runs; a substitution resets `$?`. + - No multi-line pattern is ever passed to `grep -F`. Fixed-string grep treats each line of the + pattern as a **separate** pattern, so it never establishes the contiguous multi-line sequence + you meant and can exit 0 on either component line alone — a false *positive*, not a clean + absence. + - Any `sed` or `awk` delimiter is chosen so no fixture's `\|` can collide with it. + - **Anything on stderr fails the run**, per the global oracle. + - Failure is counted, not merely printed; the runner's exit status is the failure count. + +- [ ] **Step 2: Write a self-test containing a deliberate failing assertion.** It must show: + `true` → 0 and `false` → 1 read correctly; a wrong assertion increments the failure count; **a + command writing to stderr fails the run**; the paragraph join folds wrapped prose and leaves a + four-space-indented line untouched; a literal `\|` and the digits `001` survive unchanged. + +- [ ] **Step 3: Run the self-test under `sh` and under `dash`.** Both must exit 0 **and** both must +print the deliberate failure. If the deliberate failure does not appear, stop — nothing below is +evidence. + +- [ ] **Step 4: `shellcheck --shell=sh` the harness.** Expected: exit 0. + +- [ ] **Step 5: No commit.** The harness is scratch, so it is never inside Gate B's compared range. +Its review is: Steps 3 and 4 here, the per-label counter-checks, and the **Gate-B reviewer's read of +the source pasted into `additionalContext`** at Task 8 Step 5. "Outside the reviewed range" is the +true claim; "unreviewed" is not. + +--- + +### Task 3: `C2a`, `C2b` — land the convention in both surfaces + +**Files:** +- Modify: `docs/hardening-log.md` — insert after the existing first paragraph, before `Columns:` +- Modify: `plugins/dev-workflow/commands/workflow-init.md` — the same insert, inside the fenced + template under `### 2.2 \`docs/hardening-log.md\` — the empty ledger` + +**Interfaces:** +- Consumes: the harness, `BASE`, the anchor list. +- Produces: both surfaces carrying §2.1 and §2.2's prose, byte-identical modulo hard-wrap position. + +- [ ] **Step 1: Implement `C2a` and `C2b`** to the oracles above. + +- [ ] **Step 2: Run them against the pre-change tree and confirm they fail.** Expected: `C2a` fails +first, on the first anchor, in both surfaces; `C2b`'s end-sentinel assertions fail in both. If +either passes here, the check is wrong — stop. + +- [ ] **Step 3: Land §2.1 and §2.2 in the ledger.** The text is the spec's two `markdown` fenced +blocks, **copied, not retyped**. Insert after the existing first paragraph — which is **not** +edited — and before the `Columns:` paragraph. Rewrap to the ledger's own column width; wrap position +is the only permitted difference between surfaces. The four-space indented format example stays a +single indented line. + +- [ ] **Step 4: Land the identical text in the inline template**, in the identical position. +**No `Superseded rows:` block and no entry** — a scaffolded ledger has no entries, and the label +appears only where entries do. + +- [ ] **Step 5: Re-run `C2a` and `C2b` under `sh` and `dash`.** All must pass under both. + +- [ ] **Step 6: Run the matrix rows naming `C2a` or `C2b`** — `ledger-unreadable` — **under both +shells**, and confirm both labels fail on it. A label that yields an empty candidate set and exits 0 +on an unreadable input has examined nothing and reported success. + +- [ ] **Step 7: Counter-check both labels.** + - `C2a`: delete one anchored clause from **both** surfaces, re-run, confirm `C2a` fails — this is + the state parity alone cannot see — then restore and re-run. + - `C2b`: change the region in **one** surface only, re-run, confirm the parity comparison fails, + then restore and re-run. Without this, `C2b` can be a presence-only no-op and still pass every + prescribed run. + +- [ ] **Step 8: Commit both surfaces together**, staged by explicit pathspec with the staged set +asserted equal to those two, message prefixed `WIP: ` (see Task 8 Step 3 for how the prefix is used). + +--- + +### Task 4: `C1a`–`C1d`, `C1f` — land the block and the entry + +**Files:** +- Modify: `docs/hardening-log.md` — the `Superseded rows:` block, between §2.2's last line and the + `Columns:` paragraph +- Create: the fixtures in the matrix above + +**Interfaces:** +- Consumes: the harness, `BASE`, both surfaces as Task 3 left them. +- Produces: the ledger's block holding exactly one entry. + +- [ ] **Step 1: Build every fixture in the matrix**, each with its stated expected outcome. + +- [ ] **Step 2: Implement `C1a`–`C1d`** to the seven `C1d` distinctions and the other oracles above. + +- [ ] **Step 3: Run every matrix row whose expected column names `C1a`, `C1b`, `C1c` or `C1d`, +**under both `sh` and `dash`**, and compare by exit status.** Every one must match — the PASS rows +as well as the FAIL rows. Both shells, because the matrix states its outcomes for both and a +shell-specific parser defect otherwise survives behind a happy path that passes twice. Rows naming +`C3` are deferred to Task 5, where `C3` exists; running them here is not possible. `entry-wrong-rowdate` is +the row a fingerprint-only checker passes, `rows-escapes` the one a naive splitter rejects, and +`entry-good` the one a checker still quantifying over every added entry fails; if any disagrees with +its expected outcome, `C1d` does not decide its property and the run is not evidence. + +- [ ] **Step 4: Run against the real ledger and confirm `C1a` and `C1d` fail** — the entry is absent. + +- [ ] **Step 5: Land the block.** Between the last line of §2.2's prose — the line ending +`and nothing checks the difference.` — and the `Columns:` paragraph, with **one blank line on each +side of the list**; CommonMark requires the lower one or `Columns:` renders inside the list item. + +```markdown +**Superseded rows:** + +- $D · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}`; true when written, and it no longer holds for every pass · CLAUDE.md §5, the paragraph opening `` **What this does not do.** The hook counts on `PostToolUse` `` +``` + +The `**Superseded rows:**` label is §2.2's, not §3.1's. Substitute `$D` in the entry line; +everything else **in that line** is verbatim from the spec's §3.1 — including the **double** +backticks with one space inside each delimiter (the fragment contains a `` `PostToolUse` `` span, a +single-backtick span cannot carry backticks, and markdown has no escape inside a code span), and the +`**` emphasis markers inside the quoted fragment (`CLAUDE.md` §5 holds **two** paragraphs titled +"What this does not do"; a fragment stripped of its `**` is not byte-findable in the file it points +at). + +- [ ] **Step 6: Re-run `C1a`–`C1d` under `sh` and `dash`.** All must pass under both. + +- [ ] **Step 7: Counter-check the protective labels.** + - `C1b`: **on a scratch copy of the ledger, never the real one** — a row is protected the moment + it exists — change one character of the 2026-07-20 row, point `C1b` at the copy with `BASE` + unchanged, confirm failure. The real `docs/hardening-log.md` is not touched at any point. + - `C1c`: change one word of the first header paragraph in the **ledger**, confirm failure, restore; + repeat independently in the **template**, confirm failure, restore. Then corrupt the **base + version of the template** specifically — substitute a stand-in for that extraction in which a + bounding delimiter is missing — and confirm the presence-and-uniqueness guard fires *before* any + comparison, then restore. That input is the one a three-input implementation never reads: it + passes both current-surface mutations and the ledger-base mutation, so mutating "one input" + chosen at random does not distinguish it. + - **Finish with a green run on the restored real surfaces**, `C1c` under both shells, immediately + before Step 9. The last green result otherwise predates the mutations, so an incomplete restore + would be committed under a verdict that never saw it. + +- [ ] **Step 8: `C1f` — the named read.** A human confirms **four** things, one per requirement +§2.2 puts on entry text: the entry names the claim that does not hold; it says **which cause** +applies (here: it stopped holding — true when written); it **cites without restating**, pointing at +where the current answer lives rather than copying 0.8.0's counting rules into the marker; and its +citation resolves to **exactly one** paragraph in `CLAUDE.md`. Verify the last mechanically as an +aid, not a substitute. Record the outcome — it is the evidence half of the story's mode, and a green +mechanical run would otherwise rest on nothing. + +- [ ] **Step 9: Commit**, `WIP: ` prefixed. + +--- + +### Task 5: `C3` — the block is where §2.2 says, and only there + +**Files:** none modified — a validation-only task. + +**Interfaces:** +- Consumes: the harness, both surfaces as Task 4 left them. +- Produces: nothing. + +- [ ] **Step 1: Implement `C3`** to its oracles above, blank-line structure included. + +- [ ] **Step 2: Run under `sh` and `dash`.** All must pass under both. + +- [ ] **Step 3: Run the matrix rows deferred from Task 4** — every row whose expected column names +`C3`, which is `entry-outside-block` and `ledger-unreadable` — **under both `sh` and `dash`**. Both +must fail `C3` under both. + +- [ ] **Step 4: Counter-check, every mutation on a scratch copy of the ledger.** These move, +duplicate and remove **complete entries**, which §2.2 forbids in the real file even before a commit; +the real `docs/hardening-log.md` is not touched. Point `C3` at the copy for each, confirm failure, +then discard the copy: + a second `**Superseded rows:**` label (cardinality); the block moved below the table (ordering); + **a second entry-shaped line appended below the table while the valid block stays in place** + (the interval oracle — moving the whole block does not test it, because a moved block still has + every entry inside its own interval); the blank line **above** the list removed; the blank line + **below** the list removed — this last one is the mutation every other label survives. + +- [ ] **Step 5: No commit.** Record every deferred-matrix and counter-check outcome. + +--- + +### Task 6: Version bump and changelog + +**Files:** +- Modify: `plugins/dev-workflow/.claude-plugin/plugin.json:4` — `"version": "0.8.1"` → `"0.8.2"` +- Modify: `plugins/dev-workflow/CHANGELOG.md` — newest first, below the preamble + +**Interfaces:** +- Consumes: nothing. +- Produces: `0.8.2` — named in the changelog entry and in the closing commit body (Task 8 Step 7). + +- [ ] **Step 1: Bump the manifest.** Patch: the change adds a convention to a scaffolded template; +no component is added, removed or renamed. Add no manifest keys — invariant 6 stands. + +- [ ] **Step 2: Write the changelog entry** in the file's existing format: + +```markdown +## 0.8.2 + +- `workflow-init`: the scaffolded ledger header now carries a supersession convention — + a row whose narration is later found wrong or made stale is corrected by appending a + `Superseded rows` entry, never by editing the row. Both standing rules hold unchanged: + never edit a row, one row per hardening. +``` + +- [ ] **Step 3: Verify** — `sh scripts/check-invariants.sh` and `claude plugin validate . --strict`, +both exit 0. + +- [ ] **Step 4: Commit**, `WIP: ` prefixed. + +--- + +### Task 7: `todos.md` — close the source row, park two + +**Files:** Modify `todos.md`, at the row **The hardening ledger has no supersession convention.** + +**Interfaces:** Consumes nothing; produces nothing. + +- [ ] **Step 1: Close the source row.** `- [ ]` → `- [x]`, body rewritten in the past tense so it +no longer reads as an open gap, pointing at the spec. The fired-trigger record stays. + +- [ ] **Step 2: Park the uncovered case:** + +```markdown +- [ ] **A hardening that is later *removed* has no sanctioned supersession move.** The convention + in `docs/hardening-log.md`'s header covers a row whose narration was falsified later or was + wrong when written, and names a removed hardening as explicitly out of scope. No instance + exists. *Trigger: the first rung actually removed.* +``` + +- [ ] **Step 3: Park the standing check:** + +```markdown +- [ ] **Nothing standing validates a supersession entry, and no chronology check exists.** The + checks in `docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md` §6 ran + once, on the change that introduced the convention, and then stopped; §6's 1e was not + implemented at all, since it validates the ledger's pre-existing chronology rather than that + change. Wiring both into `AGENTS.md`'s quality battery is the follow-up — **with the rider + that any standing check must be diff-scoped *and* must pass on §2.2's sanctioned repair**: a + whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding + that no appended entry be inert fails on the very move the convention prescribes for a + mistyped locator. *Trigger: the first inert entry found after this change lands.* +``` + +- [ ] **Step 4: Named read.** Confirm no present-tense claim that the ledger lacks a convention +survives, and both parked rows are present with their triggers. + +- [ ] **Step 5: Commit**, `WIP: ` prefixed. + +--- + +### Task 8: Conformance, battery, Gate B, close + +**Files:** none modified unless a check fails. + +**Interfaces:** Consumes everything above; produces one commit replacing every `WIP:` snapshot. + +- [ ] **Step 1: Prompt conformance**, all twelve `docs/prompt-standards.md` items, on +`plugins/dev-workflow/commands/workflow-init.md`. Record each item's verdict, and alongside them the +**invariant-9 verdict**: the changed header reaches an existing scaffolded project as "present and +different", which routes to show-the-diff-and-ask, and the edit added no write behaviour. + +- [ ] **Step 2: Run the quality battery** — the full chained command in `AGENTS.md` § Commands. +Expected: exit 0. `check-version-bump.sh main` compares *commits*, so it needs Task 6's snapshot +committed; run with the plugin edits still in the working tree it reports clean, correctly and +uselessly. + +- [ ] **Step 3: Consolidate to exactly one `WIP:` commit.** + +``` +git reset --mixed $BASE +git add -- +git diff --cached --name-only # must equal exactly those five +git commit -m "WIP: supersession convention" +``` + +`BASE` — not "the parent of the first WIP". After the reset HEAD **is** `BASE`, so this step must +create the WIP commit before anything reviews or amends it. + +**Use `--mixed`, not `--soft`.** A soft reset leaves whatever accumulated since `BASE` staged, and +explicitly adding the intended paths afterwards does not *unstage* an extra one — the equality +assertion would then halt execution with no sanctioned route forward. `git reset --mixed $BASE` +moves HEAD and clears the index while leaving the worktree intact; stage from these five and no +others — `docs/hardening-log.md`, `plugins/dev-workflow/commands/workflow-init.md`, +`plugins/dev-workflow/.claude-plugin/plugin.json`, `plugins/dev-workflow/CHANGELOG.md`, `todos.md` — +then assert `git diff --cached --name-only` equals exactly that list. Task 8 modifies no files of +its own, so this is the list the global staging constraint compares against. The message prefix is +`WIP: `; what that prefix does is CLAUDE.md §5 Mechanics, cited. + +- [ ] **Step 4: Compose and validate the evidence entry, before any Gate-B call** — §5 defines what +an entry is and when it is owed; this step supplies this change's values into that procedure. It is +**one entry**, not a list of strings that happen to share a commit body: the story path +`docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` belongs **inside** it, +per §5, so the named evidence is bound to the story whose mode obliges it. The values: the battery +result; the **named check that fails without this change** — one of the five labels failing on the +untouched tree, `C1a` or `C1d` being the narrowest; and the **counterfactual**, the observation that +would exist if the claim were false, with confirmation that the wiring could have produced it. Hold +this as the current entry; later steps quote it whole. + +- [ ] **Step 5: Gate B — follow CLAUDE.md §5 Mechanics as written.** It is cited, not restated. The +values this change resolves: + - `baseSha` = `$BASE`; the range is `$BASE..HEAD`, HEAD being the Step-3 WIP commit. + - `reviewType: full`. Target-file lifecycle and the recovery rules are §5's, cited not copied — + including which files a resume deletes, which is **not** "both" and which §5 states exactly. + - `additionalContext` carries the story path + `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md`, so the reviewer + reads the profile itself; **this plan's path**, since it lands in the Task-1 commit and so sits + *before* `$BASE`, outside the reviewed range; the Step-4 evidence entry quoted verbatim; **and + the complete current source of the harness and every check, with the fixture definitions**. The + checks are never committed, so this paste is the only review they get — the opening contract and + spec §8 both say so, and omitting it would leave that claim false. + - **The falsification lens, aimed:** name what this diff changes the size, value or position of — + the version string, the ledger's line count and byte offsets, the anchor count, the template's + fenced block — and grep for where each is described elsewhere. Check `docs/architecture.md`, + `MANIFEST.md`, `README.md` and `docs/getting-started.md` by name. + +- [ ] **Step 6: After every fix, amend it into the WIP commit before re-reviewing.** Stage the fixed +paths explicitly, assert the staged set equals them, then +`git commit --amend -m "WIP: supersession convention"` — the prefix stays until Step 7. Revalidate +the Step-4 evidence entry at each amend. §5's re-review rule governs; this step exists only because +`mcp__codex__review` reads the **committed** range, so a worktree-only fix is re-reviewed as the +stale diff. + +- [ ] **Step 7: Close by amend, after the final clean pass.** `git commit --amend`, replacing the +WIP message with the real one. The body names `0.8.2` and carries the Step-4 evidence entry whole, +as the final clean pass validated it — the story path is inside that entry and is not listed +separately. §5 governs everything else about closing, revalidation included. + +--- + +## Self-Review + +**Spec coverage.** §2.1, §2.2 → Task 3. §3.1's entry → Task 4. §3.2 (row D) → no action by design; +the ledger is not retro-corrected. §4's change surface → Tasks 3, 4, 6, 7 for the two surfaces, the +manifest, the changelog and `todos.md`; **Task 1** for the governing story's amendments; and three +rows already carrying their content and named as outside this execution — +`docs/coding-workflow.md`, the 2026-08-04 plan's snapshot note, and the guard-scope story's +inherited open question. §5 → handed to another story. §6 check 1 → `C1a`–`C1d`, `C1f`, with **1e not +implemented and recorded in §8**; check 2 → `C2a`–`C2b`; check 3 → `C3`; prompt conformance → +Task 8. §7 → riders governing the spec's own review; no implementation task. §8's four +held-not-fixed items → traced: item 1 to `C1d`.4 +and the `entry-wrong-rowdate` fixture; item 2 to the Global Constraint; item 3 to `C1d`.1 and +`entry-good`'s second inert entry; item 4 to **`C3` plus Task 5's below-the-table counter-check**, +with `C1d`.2's interval as the half that keeps the format example out of the candidate set — the +interval alone would not detect a stray entry, since ignoring one is not finding one. The precedence +paragraph makes all four authoritative over §6. + +**Placeholder scan.** One deliberate blank: the evidence entry, which must be revalidated at the +moment of the amend rather than written now. `$D` and `BASE` are defined in Global Constraints and +Task 1. + +**Label consistency.** `C1a`, `C1b`, `C1c`, `C1d`, `C1f`, `C2a`, `C2b`, `C3` — eight, defined once +in the check table and used under those spellings throughout. `C1e` appears only where its absence +is recorded. Fixture names in the matrix match those in Task 4's steps. diff --git a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md index 988d199..fdc288f 100644 --- a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -98,6 +98,20 @@ is only the drafting that precedes a row existing at all. That distinction needs It is deliberately *not* a licence with conditions. Nothing distinguishes a buffer from a working tree, because a rule that started distinguishing would be the boundary again. +**Entries take the same floor, stated rather than inferred.** §2.2's "never edited, never removed" +would otherwise leave the same gap one level over: an author with a mistyped locator in the working +tree asks the question the row floor already answers, and answering it by analogy is how the +amendable class comes back. So the floor is written into the shared prose — an entry is protected +once the line is a **complete** entry, committed or not — and §6's check 1d is built on that, not +on a convention that a mistyped entry may be quietly deleted before anyone sees it. + +The boundary is completeness rather than existence, and that is load-bearing in both directions. +A line that does not yet carry the entry shape is still drafting, so an author can fix a half-typed +or malformed one — which is what makes 1d's recognition oracle actionable, since it *fails* on an +unparseable candidate and an author who could not repair it would be stuck. A **complete** entry is +protected the moment it exists, including one whose locator matches nothing: that is the case the +append-a-correction rule is for. + **What this costs: one entry, once.** Under the deleted boundaries, the 2026-08-04 `mechanical-check-skipped-before-review` row (row D) was a *correct* in-place amend. Under this convention it is not: the correct move was an entry. That is the entire historical cost — one @@ -109,18 +123,22 @@ hardening* — it earns its own row under the one-row-per-hardening rule — whi a row's *text*. The two never compete now that both are appends: a `pending` row's resolution appends a row, and a correction to any row's wording appends an entry. -**Why supersession leaves recurrence alone.** A falsified row's *hardening* still stands — the -rule, test or lint it landed is untouched by the change that falsified its narration. Ejecting -it from the lineage would misreport the class's history to `harden-finding`'s recurrence step -and to any later reader of the ledger. - -This holds even in the case that looks like an exception. Because the scope covers a claim that -was *never true*, an entry may record that the row's **hardening itself** never existed or was -misclassified — and such a row still keeps counting, so a lineage can escalate from a rung that -was never landed. The boundary is therefore stated on mechanical behaviour rather than on the -hardening's truth: supersession changes text and changes nothing a tool reads, in every case, -which is a rule a reader can apply without first adjudicating whether a past hardening was real. -The consequence is owned by the reader being redesigned in §5, not by this convention. +**Why supersession leaves recurrence alone.** For a row whose narration was made stale *later*, +the *hardening* still stands — the rule, test or lint it landed is untouched by the change that +falsified its narration — so ejecting it from the lineage would misreport the class's history to +`harden-finding`'s recurrence step and to any later reader of the ledger. + +The never-true cases take the same answer for a different reason. Because the scope covers a claim +that was *never true*, an entry may record that the row's fingerprint is wrong, or that the row's +**hardening itself** never existed or was misclassified — and there the mechanical lineage is +**already** wrong: it reports a class or a rung that was never landed. Keeping such a row counting +preserves that misclassification; it does not protect accurate history, and this design accepts +that knowingly rather than claiming otherwise. The boundary is stated on mechanical behaviour +rather than on the hardening's truth — supersession changes text and changes nothing a tool reads, +in every case — because that is a rule a reader can apply without first adjudicating whether a past +hardening was real, and because the alternative reopens an edit path into the table. A lineage can +therefore escalate from a rung that was never landed; the consequence is owned by the reader being +redesigned in §5, not by this convention. ### 2.2 The convention prose, and the block it describes @@ -137,13 +155,21 @@ only once at least one entry exists: `` is the day the entry is written, in `YYYY-MM-DD`. A row is located by date + fingerprint. **An entry applies to every row its locator matches** — uniqueness is not a -requirement, and an entry that matches two rows says the same thing about both. To narrow it to -one, add `""`, a quoted distinguishing fragment of that row's `finding`, in the -position shown immediately after the fingerprint; pick one containing no double quote. Omit it, +requirement, and an entry that matches two rows says the same thing about both. To narrow the +match, add `""`, a quoted fragment of that row's `finding` carrying no double quote, +in the position shown immediately after the fingerprint. **A fragment narrows the match set; it +singles out one row only where that row has one no sibling shares** — a sibling being another row +the same date and fingerprint match. Where it has none — an identical `finding`, one that is a +substring of a sibling's, or one whose every unique fragment carries a double quote — the entry +marks every matching row, its accurate siblings included, and no fragment prevents that. Omit it, quotes included, when you mean every row the pair matches — including when the pair matches only one. Name the claim that does not hold — saying whether it stopped holding or was never true — and cite where the current -answer lives; restating that answer here only makes the entry the next stale narration. **An entry applies only to matching rows dated on or before the entry's own date** — supersession +answer lives; restating that answer here only makes the entry the next stale narration. Neither +of those two fields may contain ` · `: that separator is what divides them, and free text carrying +it makes an entry parse two ways. A fragment is matched **literally and case-sensitively against +the row's `finding` as written in the file**, escapes and markup included — what you quote is what +is in the table, not what a renderer shows you. **An entry applies only to matching rows dated on or before the entry's own date** — supersession marks the past, so a row dated later never comes under an entry written before it. **A row's date is the day it is appended**, and the table is chronological: backdating a row is forbidden, which is what makes the date bound mean what it says. Nothing can verify the append day itself; the @@ -155,7 +181,10 @@ entry marks a row, and the last entry for a row is the one that governs** — wh more than one, later in the file wins and the earlier ones are history. **A later entry must therefore describe the row as it now stands, not only the newly found fault**, or it retires a still-accurate earlier entry from a reader's view. Entries are never edited, never removed, and -never reference one another. If a union merge +never reference one another — and they take the same floor as rows: once a line exists as a +complete entry it is protected, committed or not, while a line that is partial or does not yet +carry the shape above is still drafting and may be fixed. A mistyped locator in a complete entry +is corrected the same way everything else is, by appending. If a union merge leaves two `Superseded rows:` labels, keep one and keep every entry under it. No standing tool reads this block — to every grep and skill scanning the table a superseded row is unchanged, including one whose entry says its fingerprint is wrong; prose readers get the correction, @@ -188,16 +217,25 @@ is not. row, and stopped the author when no fragment could achieve it. That rule could not survive its own immutability guarantee: an entry unique when written is made ambiguous by a *later* row sharing its date and fingerprint, and entries are never edited or removed, so there was no legal -repair — the convention would have contained a state it forbade fixing. Two rows identical in -`finding` admit no distinguishing fragment at all, so the stop could also be unsatisfiable on the +repair — the convention would have contained a state it forbade fixing. A row can also have no +permitted distinguishing fragment at all — identical `finding`s are one way, and not the only one +(below) — so the stop could be unsatisfiable on the day it was written. Match semantics has neither failure: an entry means what it matches, a later row simply joins the set, and an author who wants one row narrows the locator. The fragment is free text and guarantees nothing about uniqueness — under match semantics it does not need to. It narrows the match set; it is not a resolution procedure, and it is drawn from -`finding`, so **two rows identical in date, fingerprint and `finding` cannot be separated at all** -— an entry against such a pair marks every one of them, accurate siblings included. Short of that -case there is no state the locator stops on: many matches are valid and zero is inert. Requiring a +`finding`, so **a row can be singled out only when it has a permitted distinguishing fragment** — +a fragment of its own `finding`, containing no double quote, that no sibling matched by the same +date and fingerprint also contains. Where a row has none, an entry naming it marks every sibling +too, accurate ones included. Identical `finding`s are the obvious case and not the only one, and +the condition is **per row, not per pair**: a `finding` that is a substring of its sibling's has no +fragment the sibling does not also carry — every substring of `foo` is a substring of `foobar` — +while the sibling still has one; and a row whose every unique fragment carries a double quote is +barred by the no-double-quote rule — `foo"` against `foo`, where `"`, `o"`, `oo"` and `foo"` are +the only text the sibling lacks. Sharing a quote does not bar a row: `alpha"x` and `alpha"y` +separate on `x` and `y`. Short of that case there is no state the locator stops on: +many matches are valid and zero is inert. Requiring a fragment on every entry was rejected — it is redundant on the twenty other pairs, and a uniform three-field locator would still not guarantee more than this one does. @@ -261,9 +299,16 @@ claim that was never true, so the entry field is "what is false", and the entry the two it is. An instruction to name "what stopped being true" would have no accurate value for the never-true case, and an author would either omit the field or misdescribe it. -**Format.** Prose only — no machine-readable fields. A supersession entry that a tool parses -becomes wire format in every scaffolded ledger, and the reader that would consume it does not -exist yet and is being redesigned elsewhere (§5). +**Format.** No **standing consumer** parses an entry — that is the decision, and stating it that +way matters, because §6's one-time checks do parse one. The line has a shape: the ` · ` separator, +the optional quoted fragment, two non-empty fields, a calendar-valid date. §6 relies on exactly +that shape, and forbidding ` · ` inside a field is a constraint the shared prose ships. What is +refused is a field a **tool reads on an ongoing basis**, because that is what would become wire +format every scaffolded ledger had to keep honouring; a **validation-only grammar** binds this +change's checks and nothing after them. An entry no tool ever reads is still a line with a format, +and pretending otherwise would misdescribe what §6 does. The reader that would consume an entry on +an ongoing basis does not exist yet and is being redesigned elsewhere (§5); designing its input +format now, before it exists, is what this refuses. ## 3. The two cases the convention must resolve @@ -325,15 +370,17 @@ the entire price. | `plugins/dev-workflow/commands/workflow-init.md` | §2.1 and §2.2 prose, into the inline ledger-header template — no block, since a scaffolded ledger has no entries | | `plugins/dev-workflow/.claude-plugin/plugin.json` | `0.8.1` → `0.8.2` | | `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2` | -| `todos.md` | the source row **The hardening ledger has no supersession convention** marked done and rewritten in the past tense, so it no longer reads as an open gap; **plus** a new parked row for the uncovered case — a hardening later *removed* — carrying its trigger, the first rung actually removed. **plus** a third row: wire §6's inert-entry assertion into `AGENTS.md`'s quality battery, triggered by the first inert entry found after this change lands — **with the rider that any standing check must be diff-scoped**, for the reason §6 gives: a whole-block scan is unsatisfiable once an immutable inert entry exists. A named read confirms all three: no present-tense claim that the ledger lacks a convention, and both parked rows present with their triggers | +| `todos.md` | the source row **The hardening ledger has no supersession convention** marked done and rewritten in the past tense, so it no longer reads as an open gap; **plus** a new parked row for the uncovered case — a hardening later *removed* — carrying its trigger, the first rung actually removed. **plus** a third row: wire §6's check-1d property into `AGENTS.md`'s quality battery, triggered by the first inert entry found after this change lands — **with the rider that any standing check must be diff-scoped *and* must pass on §2.2's sanctioned repair**, for the reason §6 gives: a whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding that no appended entry be inert fails on the very move the convention prescribes for a mistyped locator. A named read confirms all three: no present-tense claim that the ledger lacks a convention, and both parked rows present with their triggers | | `docs/coding-workflow.md` | line 204's append-only sentence rewritten to state the rule **absolutely** — "strictly append-only: a **row** is never edited" — with the supersession move appended. It previously said "history is never rewritten"; an intermediate draft of this spec qualified it to *merged* history, which would have reinstated a pre-merge amendable class. No merged/unmerged qualifier appears in the landed wording. Found by the standing falsification lens, outside every path this change otherwise touches | | `docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` | its inherited open question widened from wrong-fingerprint to include phantom-hardening rows (that story's §5 Open questions) | | `docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md` | a header note marking it a historical snapshot — it carries the un-narrowed rule three times. Executed plans are records of what was done and are **not** rewritten to match later rules; the note is how the falsification sweep resolves them | -| `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` | **six** amendments, each with explicit old-condition accounting; the disposition labels differ per amendment, which is expected. **Pass 1** — AC 4 (kept/moved/dropped). **Pass 3** — the append-only rule narrowed (kept/narrowed/dropped). **Pass 4** — §4's invariants 11 and 12 made unconditional and §5's open question resolved to both surfaces, with §6's size following. **Pass 5** — the desired outcome brought into line with AC 4, which still promised a reader could tell whether *any* row describes current behaviour. **Pass 7** — the absolute rule **restored** (restored/dropped/kept), reversing pass 3. **Pass 9** — AC 2 reworded, because "the row no longer reads as current behaviour" was satisfiable only by the edit the same criterion forbids. Attribution names the pass whose findings prompted each change. §1 is also in the past tense, which is framing rather than an amendment | +| `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md` | **eight** amendments, one per prompting pass, each with explicit old-condition accounting; the disposition labels differ per amendment, which is expected. **Pass 1** — AC 4 (kept/moved/dropped). **Pass 3** — the append-only rule narrowed (kept/narrowed/dropped). **Pass 4** — §4's invariants 11 and 12 made unconditional and §5's open question resolved to both surfaces, with §6's size following. **Pass 5** — the desired outcome brought into line with AC 4, which still promised a reader could tell whether *any* row describes current behaviour. **Pass 7** — the absolute rule **restored** (restored/dropped/kept), reversing pass 3. **Pass 9** — AC 2 reworded, because "the row no longer reads as current behaviour" was satisfiable only by the edit the same criterion forbids. **Pass 18** — AC 4 narrowed a second time, to the corrections actually *recorded*, with the removed-hardening exclusion named; pass 5 had brought the desired outcome to that wording and left AC 4 behind, so this closes the pair. **Pass 19** — AC 4 again, naming the inseparable-sibling case, where a correction present against a sibling would otherwise read as belonging to an accurate row; §2 was moved to point at AC 4 rather than keep its own copy of the limits, which is pass 5's lesson applied. Attribution names the pass whose findings prompted each change. §1 is also in the past tense, which is framing rather than an amendment | **Parity, with sentinels a check can locate.** The shared region runs from the line -`# Hardening log` **inclusive** to the line ending `…and nothing checks the difference.` -**inclusive** — the last sentence of §2.2's prose. The region **includes** the four-space +`# Hardening log` **inclusive** to the line ending `and nothing checks the difference.` +**inclusive** — the last sentence of §2.2's prose. Both sentinels are quoted here as **literal +text**: a check greps for exactly those bytes, so neither code span may carry an editorial +ellipsis or any other stand-in. The region **includes** the four-space indented format example, now a single line, and **excludes** the `Superseded rows:` block, the `Columns:` paragraph and the table. Inside that region the two files are byte-identical modulo hard-wrap position. Both sentinels are @@ -373,8 +420,9 @@ profile change moves it. The story is `docs/superpowers/stories/2026-08-04-harde **This section states properties, not commands.** Each check below gives what must be true, the observation that fails without the change, and the **oracle** — what a correct implementation must be able to distinguish. The executable form is written at execution time, carried in the plan with -one label per check, and reviewed by **Gate B against the real diff**, where a check has something -to be checked against. Earlier drafts carried the shell here; over six Gate-A passes that shell +one label per check, and **supplied to the Gate-B reviewer alongside the real diff**, where a check +has something to be checked against. It is not committed, so it is not itself inside the reviewed +range — §8 records what that leaves uncovered. Earlier drafts carried the shell here; over six Gate-A passes that shell produced the large majority of findings while §1–§3 and §5 drew none, and a spec is the wrong place to review an implementation that has no diff to run against. @@ -414,21 +462,79 @@ requirement, mechanised. paragraph is not edited and that both surfaces carry it identically; both halves must be checked. *Falsifying observation:* change one word of it in either surface and this must fail. Verified today: both surfaces already carry it byte-identically. -*Oracle:* the delimiters bounding the paragraph must be asserted **present and unique in all three -inputs before any comparison is taken** — a checksum-style comparison returns a value for empty -input, so a guard on the checksum alone can never fire and two unreadable inputs compare equal. - -**1d — no entry this change appends is inert.** Every entry **added by the change under review** -must have a locator matching at least one table row dated on or before the entry's own date. -Matching two is fine — that is match semantics — so the bound is *at least one*, never *exactly -one*. Pre-existing entries are out of scope: §2.2 says an inert entry is never removed or edited, -so a whole-block scan would be unsatisfiable the first time the typo case fires — appending the -correction leaves the inert one in the scan, and deleting it breaks the absolute rule. -*Falsifying observation:* an entry whose fingerprint is mistyped must fail; one whose date precedes -the row it names must fail. -*Oracles* — **five** things a correct implementation must distinguish, each of which a draft got wrong: -- **Which entries are in scope.** Entries added by this change, computed against the base ref — - not every entry in the file. +*Oracles:* +- **Four inputs, not three.** Each current surface is compared against **its own** base version, + and current parity between the two surfaces is asserted separately. Three inputs — one base + paragraph as the reference for both — establishes "untouched" only by leaning on the separately + stated fact that the two surfaces are byte-identical *today*: were the base template to carry any + variation, editing it to match the ledger's bytes would pass a three-input comparison while that + template's paragraph had in fact changed from its own base. +- **Guarded, and bounded before comparing.** The delimiters bounding the paragraph must be asserted + **present and unique in every input before any comparison is taken** — a checksum-style + comparison returns a value for empty input, so a guard on the checksum alone can never fire and + two unreadable inputs compare equal. + +**1d — the change's entry is not inert.** The entry §3.1 mandates, added by the change under +review, matches at least one table row dated on or before its own date — its fragment applied if +it carries one. Matching two rows is fine; the bound is *at least one*, never *exactly one*. + +**One entry, and no claim about any other.** The property is deliberately about *the* mandated +entry rather than about every entry the change adds, because the quantified version is one §2.2 +forbids satisfying. A mistyped locator is a complete entry the moment it exists, entries are never +removed, and §2.2's sanctioned repair is to append a corrected one and leave the inert line +standing — so any property of the form "no added entry is inert" fails on the convention's own +prescribed move. Nor can a check ask whether the corrected entry *repairs* the mistyped one: that +asks which row the author meant, which the ledger does not encode, and the identity that would +encode it is the wire format §2.2 refuses. The same reasoning puts pre-existing entries out of +scope: a whole-block scan is unsatisfiable the first time the typo case fires. What this leaves +uncovered is recorded in §8. + +**Old-condition accounting for the narrowing.** The broader property carried three requirements. +*Dropped and recorded:* that no added entry is inert — unsatisfiable against §2.2's own repair +(§8). *Dropped and recorded:* that an inert added entry be followed by a non-inert one — the +repair relation is authorial intent the ledger does not encode (§8). *Dropped, and it is the one +that was not obvious:* the alignment rule requiring the base entry sequence to remain an **exact +prefix** also asserted that pre-existing entries were preserved, in order, unedited. Nothing checks +that now. It is dropped because this base carries no entries at all, so there is nothing to +preserve — but the guarantee does not return on its own for any later change, and no check +validates pre-existing entry immutability (§8). +*Falsifying observation:* mistype the mandated entry's fingerprint and this must fail; so must an +entry dated before the row it names. The typo-then-correct pair must **pass** — the mandated entry +is the corrected one, and that is the convention working, not a defect. +*Oracles* — **seven** things a correct implementation must distinguish, each of which a draft got wrong: +- **Which entry is in scope.** The mandated entry, and only entries this change **adds**, computed + against the base ref — not every entry in the file. For this change the base carries no block at + all, so every entry present is one it added. +- **An entry from a line that is not one.** Candidates are the **non-blank** lines of the interval + from the `**Superseded rows:**` label to the `Columns:` paragraph, both exclusive. The single + structural blank line separating the list from that paragraph is required by CommonMark — without + it `Columns:` renders inside the list item — so blank lines are not candidates; every non-blank + one is. A candidate is an entry when it carries the entry shape: the `- ` marker, a `YYYY-MM-DD` + date, the literal `· supersedes `, a row date, a backticked fingerprint, an **optional** quoted + fragment, and two remaining ` · `-separated prose fields, **both non-empty** and neither + containing ` · ` — which §2.2 forbids precisely so the split is unambiguous. Both dates must be + **calendar-valid**, not merely `YYYY-MM-DD`-shaped: `2026-02-30` parses lexically, orders + lexically, and is not a day anything was appended on. A candidate that + does not parse must **fail the check**, never be dropped from the candidate set — dropping it + reports "every entry recognised is fine" while establishing nothing about the one that matters. + This is the read-failure-from-a-clean-pass mistake on the entry half of the match, which the + row-side oracles below never cover. The non-empty requirement is what stops a locator-valid line + carrying no claim and no citation from counting as an entry at all — §2.2 obliges both fields, + and a line satisfying only the locator is malformed rather than merely uninformative. +- **Matching, to the extent the mandated entry uses it.** The mandated entry is fragmentless, and + the property treats one match and many alike, so two distinctions are load-bearing and the rest + are not. **Zero from at-least-one:** zero is inert and must fail, one or many must pass — a + checker demanding *exactly one* implements the guarantee §2.2 withdrew and would still pass this + change, which is why the many case needs a fixture even though this entry does not produce it. + **The on-or-before bound, at the boundary:** rows dated before, **on**, and after the entry's + date. Fixtures: the fragmentless locator matching **both** `2026-07-18` `docs-drift` rows, and + the three date positions. + + *Not validated by this change, deliberately.* Fragment-narrowed matching and the comparison + domain §2.2 pins — literal, case-sensitive, against the raw `finding` as written, escapes and + markup included — are convention semantics no entry here exercises. Requiring fixtures for them + would drive a general matcher this change has no use for. §8 records that nothing validates them; + the first entry that carries a fragment is what needs them. - **A row from a non-row.** A candidate must be a complete seven-column row: **exactly** the expected number of unescaped delimiters, terminal delimiter included. Rejecting only short lines lets a longer malformed line supply a `finding`; rejecting neither lets a truncated @@ -451,11 +557,19 @@ entry written before it, which §2.2 forbids. *Falsifying observation:* a row dated earlier than the row above it must fail. *Oracle:* this checks **recorded order, not append-day truth**. A row appended today and labelled with a date after the current tail passes. Nothing can verify the append day; the prohibition is -instruction-backed and this is a floor under it. The extraction must also distinguish "no dated -rows found" from "ledger unreadable". - -**1f — a named read.** A reader confirms the entry names the claim that does not hold, and that its -citation resolves to exactly one paragraph in `CLAUDE.md`. This half is a read, **not** a parse: +instruction-backed and this is a floor under it. Dates must be **calendar-valid**, not merely +well-shaped — `2026-02-30` sorts correctly and is not a date — since a lexical-only comparison +would let an impossible value satisfy both this property and 1d's on-or-before bound. The +extraction must also distinguish "no dated rows found" from "ledger unreadable". + +**1f — a named read.** A reader confirms **four** things about the entry, one per requirement §2.2 +puts on entry text: that it names the claim that does not hold; that it says **which cause** +applies — the claim stopped holding, or was never true; that it **cites without restating**, +pointing at where the current answer lives rather than copying that answer into the marker; and +that its citation resolves to exactly one paragraph in `CLAUDE.md`. The middle two are why this +read is not optional: an entry that names no cause, or that restates the mechanism and so becomes +the next stale narration, satisfies every mechanical property above. +This half is a read, **not** a parse: nothing validates it, and every mechanical property above would pass an entry whose prose said nothing useful. Stated rather than implied, because the mechanical half is what a green `battery+check` would otherwise rest on. @@ -465,7 +579,7 @@ nothing useful. Stated rather than implied, because the mechanical half is what The parity claim in §4 is what the both-surfaces decision rests on, and check 1 would pass with the template untouched. -**2a — presence, per surface, independently.** Each of the thirty-three anchors below must occur +**2a — presence, per surface, independently.** Each of the thirty-five anchors below must occur **exactly once** in each of `docs/hardening-log.md` and `plugins/dev-workflow/commands/workflow-init.md`. @@ -492,7 +606,7 @@ neither file, so 2a fails first. - **Occurrences, not matching lines.** After a paragraph join, two copies of an anchor in one paragraph are on one line; a line-counting test reports `1` and the exactly-once claim measures nothing. -- **Anchors are data, not code.** Two begin with `-` and many contain backticks; whatever runs them +- **Anchors are data, not code.** One begins with `-` and five contain backticks; whatever runs them must pass them as literal patterns and must not let a shell interpret them. An unescaped backtick silently degraded an anchor to a prefix and still exited 0. @@ -521,7 +635,7 @@ Each is the distinguishing text of one decision §2 settles. 12. `A hardening later removed is out of scope` 13. `**Correcting a row.**` 14. `present only once at least one entry exists` -15. ``block above the `Columns:` `` +15. `` block above the `Columns:` `` 16. `one appended line per supersession` 17. `- · supersedes ` 18. `` `` is the day the entry is written, in `YYYY-MM-DD` `` @@ -540,6 +654,8 @@ Each is the distinguishing text of one decision §2 settles. 31. `must therefore describe the row as it now stands` 32. `Entries are never edited, never removed, and never reference one another` 33. `keep one and keep every entry under it` +34. `singles out one row only where that row has one no sibling shares` +35. `once a line exists as a complete entry it is protected` ### Check 3 — the block is where §2.2 says, and only there Checks 1 and 2 leave §2.2's layout decisions unvalidated: check 1 matches the entry anywhere in the @@ -555,9 +671,12 @@ label does not appear at all. be established. *Oracles:* -- **Cardinality before ordering.** Each of the label, the `Columns:` paragraph and the entry must - resolve to exactly one position; two labels — which a union merge can produce — must fail rather - than silently taking the first. +- **Cardinality before ordering, evaluated on this change.** Each of the label, the `Columns:` + paragraph and the entry must resolve to exactly one position; two labels — which a union merge + can produce — must fail rather than silently taking the first. The evaluation point matters: + this runs once, on this change, before any merge involving it. A duplicate arriving later is + outside it — §8 records that no standing check looks again, and §2.2's repair for a duplicated + label keeps one label and every entry under it. - **The full ordering, not a prefix of it.** `label < entry < Columns:` alone permits the block above `# Hardening log`, or a second entry below the table. The end sentinel must be pinned as the lower bound and every entry-shaped line checked against the interval. @@ -577,7 +696,7 @@ touches, now that `harden-finding` is out of the change surface. decide without side effects — cited paths, quoted passages **byte-for-byte including markdown emphasis markers**, stated counts, the non-uniqueness claim in §2.2, §4's parity sentinels (unique and locatable in both files; does the region contain what §4 says and exclude what it - says?), §6's thirty-three anchors (does each occur exactly once per surface; is any a substring + says?), §6's thirty-five anchors (does each occur exactly once per surface; is any a substring of another?), §3.1's citation, and the syntax and balance of standalone fenced and indented blocks. Both source files are hard-wrapped, so compare quoted markdown with whitespace normalised and report a wrap artifact as a wrap artifact. Report what the sweep found separately @@ -620,14 +739,17 @@ statement in it; do not report its incompleteness. - **A row can be corrected in prose while its mechanical identity stays wrong.** §2.1 keeps a superseded row counting even when the entry records that its fingerprint was wrong, or that the hardening it claims never existed at all. A lineage can therefore escalate from a rung - that was never landed. This is deliberate — ejecting the row would misreport the class's - history — and the consequence is handed to the guard-scope story (§5), whose inherited open - question now covers both cases. + that was never landed, and the class history the ledger reports mechanically stays wrong. This + is deliberate, but what it preserves in these two cases is a **known misclassification, not + accurate history**: the reason is that the alternative reopens an edit path into the table and + that the rule must stay applicable without adjudicating whether a past hardening was real. The + consequence is handed to the guard-scope story (§5), whose inherited open question now covers + both cases. - **What check 2's anchor list does not cover — a partial list, and it is not exhaustive.** The rule is exact: an anchor is the distinguishing text of one decision, so **a decision with no anchor is not detected if it is deleted identically from both surfaces**. What is *not* exact is - any enumeration of which decisions those are. Known unanchored today: the - distinguishing-row-fragment narrowing and its no-double-quote condition, and §2.2's calibration + any enumeration of which decisions those are. Known unanchored today: the no-double-quote + condition on a fragment — the narrowing it qualifies now carries anchor 34 — and §2.2's calibration that append-day truth is unverifiable while the check enforces only non-decreasing recorded dates. **Others almost certainly exist and are not listed here.** The layout rationale, the prose-only decision and §2's reasoning generally sit outside the shared region and were never in @@ -654,17 +776,65 @@ statement in it; do not report its incompleteness. - **A live entry in the scaffolded template is not detected.** Check 3 tests for a stray `**Superseded rows:**` *label*; a template carrying an entry-shaped line without one passes it, and check 1's named read never opens the template. No check covers it. -- **Rows identical in date, fingerprint and `finding` cannot be told apart, and an entry marks all - of them.** The locator is date + fingerprint, narrowed only by a fragment of `finding`; where two - rows share all three there is no discriminator, so an entry naming one names every one — its - accurate siblings included. If only one such row's narration were false, the convention could not - mark it alone, which cuts against what the design is for. **No instance exists**: no such pair is - in the ledger today, and the case is hypothetical. A discriminator was considered and rejected — - an ordinal or a second column fragment would add wire format to the one thing §2.2 keeps - deliberately prose-only, to serve a case that has never occurred. - **Reopen trigger:** the first real identical pair whose truth diverges — two rows sharing date, - fingerprint and `finding` where one narration is falsified and the other still holds. Until then - this is a recorded limitation, not an open question. +- **A row with no permitted distinguishing fragment cannot be told apart from its siblings, and an + entry marks all of them.** The locator is date + fingerprint, narrowed only by a quoted fragment + of `finding` carrying no double quote. A row can be singled out only when it has such a fragment + that no sibling sharing its date and fingerprint also contains; where it has none, an entry + naming it names every one of them — its accurate siblings included. Identical `finding`s are the + obvious case; the condition is broader and holds **per row, not per pair** — a `finding` that is + a substring of its sibling's has no fragment the sibling lacks while the sibling still has one, + and a row whose every unique fragment carries a double quote (`foo"` against `foo`) is barred by + the no-double-quote rule, while two rows merely *containing* a quote (`alpha"x`, `alpha"y`) + separate normally. If only one such row's narration were false, the convention could not + mark it alone, which cuts against what the design is for. **No instance exists**: checked against + the twenty-two current rows, the one repeating date-and-fingerprint pair (`2026-07-18` + + `docs-drift`) leaves both its rows separable, and the case is hypothetical. A discriminator was + considered and rejected — an ordinal or a second column fragment would add wire format to the one + thing §2.2 keeps deliberately prose-only, to serve a case that has never occurred. + **Reopen trigger:** the first real case whose truth diverges — a row with no permitted + distinguishing fragment against a sibling, where one narration is falsified and the other still + holds. Until then this is a recorded limitation, not an open question. +- **The checks themselves are never reviewed by a gate, and never fingerprinted.** They are written + at execution time and are not committed — nothing standing runs them, so committing them would + add a file the repository carries and never executes. The consequence is that the only executable + artifact in this change sits outside the Gate-B range: the reviewer sees the check source only + because the plan requires it in `additionalContext`, which is a reviewer reading a paste, not a + gate comparing a diff. Their other guards are a self-test that must be shown able to fail, + `shellcheck`, and a counter-check per label. The evidence a `battery+check` mode rests on is + therefore produced by unfingerprinted code, and a later reader cannot recover which version of a + check produced a recorded result. +- **An extra inert entry appended alongside the mandated one is detected by nothing.** Check 1d + asks about **the** entry §3.1 mandates, not about every entry the change adds, so a second entry + whose locator matches no row rides along unexamined. Two reasons, and only the first is about + this change. Quantifying over every added entry is unsatisfiable by construction: §2.2's + sanctioned repair for a mistyped locator is to append a corrected entry and leave the inert one + standing, so the property would fail on the convention's own prescribed move. And the repair + relation itself is uncheckable in general — whether a later entry corrects an earlier one is a + question about which row the author *meant*, which the ledger does not encode and which no + mechanical test can recover; the identity that would encode it is the wire format §2.2 refuses. + The state is **outside the prescribed change**, which appends exactly one entry — but a + nonconforming implementation can produce it and every stated check will accept it. Calling that + unreachable would be circular: it is unreachable only if the implementation does what it was + told, which is the thing a check exists to stop assuming. Three further things nothing here + validates, named rather than left to inference: **fragment-narrowed matching** and the + **comparison domain** §2.2 pins, since no entry in this change carries a fragment; and + **pre-existing entry immutability** — the narrowing that removed 1d's alignment rule removed with + it the only assertion that base entries survive unedited and in order. This base carries no + entries, so there is nothing to preserve today; the guarantee does not return by itself for a + later change. +- **Two states absolutism makes unrepairable, and one of them a check can strand.** Nothing is + ever edited or removed, so a mistake that reaches the file stays in it. An **inert entry** is + the benign case: §2.2 sanctions leaving it and appending a correction, and §6 check 1d is worded + to pass on exactly that. A **backdated row** is not: no append can restore non-decreasing dates, + so check 1e — and any standing successor to it — would fail on that ledger from then on, with no + sanctioned repair. The prohibition on backdating is therefore load-bearing rather than advisory, + and it is instruction-backed only. This change appends no row, so it cannot enter that state; + the twenty-two current rows were checked and their dates do not decrease. **Check 1e is therefore + not implemented.** It validates the ledger's pre-existing chronology rather than anything this + change does — it passes before and after, and its falsifying observation requires mutating the + table by hand. Running it would report a green that means nothing about the diff. No chronology + validation exists as a result, one-time or standing; the parked `todos.md` row that covers wiring + a standing check covers this too. - **A superseded row is unchanged to every tool.** The column-2 recurrence grep, any count over the table, and `harden-finding`'s escalation all see the row exactly as before — including one whose entry says its fingerprint is wrong. @@ -693,5 +863,34 @@ statement in it; do not report its incompleteness. `unverified-enforcement-claim` row, not in `scripts/check-invariants.sh` itself, which carries no such comment — so an example later rewritten flush-left with a real date would become indistinguishable from an entry to any reader built over this block. +- **Four things found at the closing review and deliberately held, not fixed.** Gate A closed on + pass 23 by a decision taken before it ran, so these are recorded here rather than repaired — the + alternative was another unreviewed revision, which is the state the closing pass existed to end. + Each is actionable by whoever writes the plan and the executable checks. Gate B compares the + **implementation range only**. Neither the plan nor the scratch check source is inside it — the + plan lands earlier, as prose, under §5's exemption, and the checks are never committed at all; + both reach the reviewer as context, which is a reader given a document rather than a gate + comparing a diff. That is the division §6 states and the residual above records. + - **1d's matching oracle does not name row-date equality.** It names zero-from-at-least-one and + the on-or-before bound. A checker that compared only the fingerprint would still pass the + mandated entry, the two-row fixture and all three date positions — and would report an entry + non-inert whose locator matches no row, which is the property 1d exists to decide. The + executable check must validate **exact row-date and fingerprint equality before applying the + eligibility bound**, with a fixture whose only defect is a wrong locator row date. + - **Two sites still carry the pre-narrowing "prose-only" claim.** §5 says the correction is + prose-only and that nothing consumes it, and this section's discriminator-rejected bullet says + §2.2 keeps the entry deliberately prose-only. The accurate claim, after §2.2's Format paragraph + was narrowed, is **no standing machine consumer** — the syntax *is* a standing convention that + every future author must honour, and what §6 adds is a validation-only parser creating no + ongoing compatibility promise. Where those two sites and §2.2 disagree, §2.2 governs. + - **1d's scope oracle reads as plural.** "The mandated entry, and only entries this change adds" + can be read as quantifying over every added entry, which is the unsatisfiable property the + narrowing removed. The intended selection: identify the §3.1-mandated entry in current content, + prove it was added relative to the base, and **ignore every other added entry**. + - **The format-example guard is understated as "shape alone".** For this change, 1d confines + candidates to the label-to-`Columns:` interval and check 3 rejects entry-shaped lines outside + it, so location guards it too — once. The claim is right about **standing** enforcement and + wrong about this change's validation; the future risk is a reader or check that scans dated + lines without respecting the interval. - **It changes nothing about how the gate hook counts.** §3.1 records that 0.8.0 already changed it; this design only marks the row that still describes the old behaviour. diff --git a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md index 100ad9d..b718855 100644 --- a/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md +++ b/docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md @@ -62,7 +62,9 @@ is no definition to get wrong. A reader who opens any ledger row can tell which of its claims have been recorded as no longer holding and where the current answer lives, and a row falsified by a later change can be marked as such without breaking either standing rule, both of which hold in their original absolute -form: never edit a row, and one row per hardening. +form: never edit a row, and one row per hardening. Acceptance criterion 4 carries the two limits +on that reading — completeness is not promised, and a row that cannot be told apart from a +sibling is marked together with it. *(Amended 2026-08-10 from Gate-A spec pass 5, finding 11. Accounting per the AGENTS.md Don't — **kept:** determination from the ledger alone, and both standing rules. **Narrowed:** "can tell @@ -72,6 +74,12 @@ a row current — nothing validates completeness — so the outcome was promisin deliberately does not deliver. **Dropped:** nothing. AC 4 was amended this way at pass 1; this site was missed then and is brought into line now.)* +*(Amended again 2026-08-11 from Gate-A spec passes 18 and 19, and human-confirmed. Accounting per +the AGENTS.md Don't — **kept:** the outcome as pass 5 left it, unchanged in substance. **Moved:** +the two limits on what a reader can determine are stated once, in AC 4, and pointed at from here — +pass 5's lesson was that this site and AC 4 drift apart when each carries its own copy. **Dropped:** +nothing.)* + ## 3. Acceptance criteria - [x] Before design resumes on this story, whoever picks it up proposes both axes and the mode @@ -89,13 +97,32 @@ site was missed then and is brought into line now.)* *(Narrowed 2026-08-05 from Gate-A spec pass 3; **restored 2026-08-10** from pass 7, both human-confirmed. The criterion now reads as it originally did; the accounting for the narrowing and its reversal is in §1.)* -- [ ] A reader can determine, from the ledger alone, which of a row's claims no longer hold and - where the current answer lives. +- [ ] A reader can determine, from the ledger alone, which of a row's claims have been **recorded** + as no longer holding and where the current answer lives. Completeness is not promised: + nothing validates that a falsified claim was ever recorded, and a hardening that is later + *removed* has no sanctioned entry at all — the case the convention names as out of scope. + Nor is per-row resolution promised: where a row cannot be told apart from a sibling, an entry + marks every row it matches, accurate ones included, and the reader reads the correction + against all of them. *(Amended 2026-08-05 from Gate-A spec pass 1, finding 4, and human-confirmed. Accounting per the AGENTS.md Don't — **kept:** determination from the ledger alone. **Moved:** current behaviour lives at the cited artifact, deliberately, per cite-don't-restate. **Dropped:** "a row to trust", because no ledger row ever provided current behaviour and the criterion demanded what the genre cannot supply.)* + *(Amended again 2026-08-11 from Gate-A spec pass 18, finding 3, and human-confirmed. + Accounting per the AGENTS.md Don't — **kept:** determination from the ledger alone, and the + citation half. **Narrowed:** "which of a row's claims no longer hold" → "which have been + recorded as no longer holding", which is what the design supplies and what §2's desired + outcome has said since pass 5; this site was missed then. **Named:** the removed-hardening + exclusion, which the design leaves unmarkable, so the criterion no longer promises a marker + for it. **Dropped:** nothing.)* + *(Amended again 2026-08-11 from Gate-A spec pass 19, finding 3, and human-confirmed. + Accounting per the AGENTS.md Don't — **kept:** everything the pass-18 wording holds, all + three clauses. **Named:** the inseparable-sibling case, where the criterion could otherwise + be read as promising a *per-row* answer the locator cannot give — pass 18 closed a gap where + a correction might be missing; this closes one where a correction present against a sibling + is read as belonging to an accurate row, which is a wrong answer rather than an absent one. + **Dropped:** nothing.)* ## 4. Affected AGENTS.md invariants From bc645d830915afb8161e5c020fd34bda83c2f03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:10:17 +0200 Subject: [PATCH 5/6] Give the hardening ledger a supersession convention (0.8.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A row's narration can be falsified by a later change or wrong when written, and neither standing rule sanctioned a fix: editing broke "never edit a row", appending broke "one row per hardening". Both rules now stand unchanged and the correction is a third move — a `Superseded rows` entry appended above the `Columns:` paragraph, locating the row by date + fingerprint, naming what is false and where the current answer lives. Supersession marks a row's text and never alters mechanical behaviour: the row keeps its fingerprint, keeps matching the column-2 grep, and keeps counting. No standing tool reads the block, which the ledger header says in as many words. The convention lands in both surfaces — the live ledger and `/workflow-init`'s inline template — byte-identical over the region from `# Hardening log` to the line ending "and nothing checks the difference." The template carries no `Superseded rows:` label, since a freshly scaffolded ledger has no entries. The 2026-07-20 `truncated-tool-output-read-as-complete` row, which still narrated pre-0.8.0 counting behaviour as current, carries the first entry. `todos.md` closes the source row and parks two follow-ups: a hardening that is itself *removed* (explicitly out of scope here), and wiring §6's entry validation plus the unimplemented chronology check into the quality battery. The design spec rides in under CLAUDE.md §5's same-commit rule. Nine Gate-B passes corrected its narration about what its own checks prove — in both directions, overclaim and understatement — including several claims that earlier passes of this same cycle had made stale by fixing a check and leaving its description behind. Evidence · docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md - Battery: the full AGENTS.md § Commands chain, exit 0 — shellcheck over all six shell files, the hook suite under sh and under dash, check-invariants.test.sh (123 assertions) and check-invariants.sh, check-version-bump.test.sh (36 assertions) and check-version-bump.sh, and `claude plugin validate . --strict`. - Check that fails without the change: C1d, "the mandated entry is not inert", run against docs/hardening-log.md as it stands at BASE f9ed886 — exit 1, diagnostic "undecidable interval: 0 label(s), 1 Columns: paragraph(s)": there is no Superseded rows block, so no entry exists to match any row. Against the committed tree the same check, unmodified, exits 0. C1a fails there too and is the wider of the two. - Counterfactual: were the claim false, C1d would exit non-zero with the diagnostic belonging to the dimension that failed — "undecidable interval" where no block exists (the pre-change case above), "the interval holds no candidate line" for a block with no entry, a parse diagnostic naming the offending line for a malformed entry or an uncalendared date, and a locator/eligibility diagnostic reporting rows matched and rows eligible where the locator names no row or the row postdates the entry. The wiring could have produced those observations: the same check exits 1 on the pre-change tree and on twelve constructed inputs each differing from the committed ledger in exactly one advertised dimension — eleven of them ledgers, the twelfth a directory standing in for an unreadable one — and exits 0 on six that must pass, including entry-good (a second inert entry below the mandated one) and rows-two-matching (two rows the locator matches). It reads the real ledger rather than input it supplies itself, and the pre-change run proves the branch is reachable. - What that wiring rests on, after Gate-B passes 1 to 4. C1d identifies the mandated entry by its own date, row date and fingerprint and only then proves this change added it, so entry-good passes because the entry is the right one rather than because it happens to come first. The two input files are told apart by FILENAME rather than by `FNR == NR`, so an empty or unreadable ledger is reported as such instead of silently being analysed as the base. Every check stops before parsing once an input is unreadable, so no awk is handed a directory, whose treatment differs by implementation and would otherwise make the result host-dependent. Anything on stderr is an unconditional matrix disagreement decided before the PASS/FAIL comparison, so an infrastructure failure can no longer satisfy an expected-FAIL row. No fixture can silently degrade into a weaker one: of the sixteen built from the baseline, fourteen are produced by line substitution or insertion and assert that the anchor matched exactly once and that the result differs from its input, while the four that append a line — two of those fourteen among them — additionally assert the property the fixture exists to exhibit: rows-escapes that its escape-heavy row round-trips the parser byte-identically, rows-two-matching that the locator matches exactly two complete rows, entry-bad-date-locator that exactly one complete row carries the calendar-invalid 2026-02-30, and entry-outside-block that the entry occurs once and below `Columns:`. And every fixture that mutates part of the locator — the two calendar-validation rows, and the wrong-fingerprint and wrong-row-date rows — is run with its mutated value passed in, so identification and, where it applies, the on-or-before bound both succeed, leaving the property the fixture names as the only thing that can reject it. Run with the defaults instead, all four were rejected earlier, at identification, and would have failed identically with the property they test deleted outright. - Mutation tests, each confirming the check fails for its stated reason: suppressing each of the four appends makes the fixture builder exit 1 with that assertion's own diagnostic; replacing the calendar check with a shape-only one flips exactly the two calendar rows to passing and leaves the other twenty-eight unchanged; and deleting the locator's row-date and fingerprint equality flips five rows, the wrong-fingerprint and wrong-row-date rows among them. The matrix also runs the C2 and C3 failure paths the plan's fixture table names — an unreadable ledger against C2 and C3, and the entry-moved-below-the-table fixture against C3 — which until Gate-B pass 3 had been exercised only by hand. All 30 matrix rows agree under sh and under dash. The harness diverges from the plan's fixture table in one value, deliberately, and the plan is NOT amended. Plan line 243 specifies `entry-bad-date-entry` as `2026-02-30`; the harness uses `2026-08-32`. Reason: `2026-02-30` is lexically *earlier* than the locator's `2026-07-20` row, so with calendar validation removed it is still rejected — by the on-or-before bound — and the fixture proves nothing about the property it names. The value must be calendar-invalid **and** lexically on or after the locator row date; `2026-08-32` is both, and mutation-testing confirms that a shape-only date check flips exactly that row and `entry-bad-date-locator`. The plan landed in f9ed886, outside this commit's range, and is the record of what was intended at execution time; this repo does not rewrite executed plans to match later rules. Gate B: nine passes, spec and quality run as separate single-branch calls from pass 2 onward after both reviewers wrote each other's findings files in pass 1. Pass 9 clean on both branches. Of 27 Blocker/Major findings, 16 were in the never-committed scratch harness, 10 in the design spec's narration, 1 in the plan, and none in the shipped ledger, template, manifest, changelog or todos. --- docs/hardening-log.md | 66 ++++++ ...05-hardening-ledger-supersession-design.md | 197 ++++++++++++------ .../dev-workflow/.claude-plugin/plugin.json | 2 +- plugins/dev-workflow/CHANGELOG.md | 7 + .../dev-workflow/commands/workflow-init.md | 62 ++++++ todos.md | 38 ++-- 6 files changed, 297 insertions(+), 75 deletions(-) diff --git a/docs/hardening-log.md b/docs/hardening-log.md index 1683a44..093a112 100644 --- a/docs/hardening-log.md +++ b/docs/hardening-log.md @@ -7,6 +7,72 @@ this project's `docs/hardening-taxonomy.md`; column 2 is the recurrence-grep tar Never edit a row; resolve a `pending` row by appending a new row (same fingerprint, `ref` naming the prior row's date + anchor). +A row records a hardening claim as of its date, and its narration may be found wrong or +made stale later. When a row's text no longer describes reality — falsified by a +later change, or wrong when it was written — append a `Superseded rows` entry rather +than editing it. One later change is **excluded**: a hardening that is itself removed, +for which this convention supplies no move at all (see the end of this paragraph). This +holds for every row without exception: the existing `Never edit a row` rule is +absolute, and correcting a row is always an append. The rule is bound to rows, not to +commits: once text exists as a row it is never edited, committed or not. Drafting +before a row exists — an editor buffer, a line not yet written — is below the +rule's resolution, and nothing checks one. Resolving a `pending` row also appends — +that is a new hardening, not a correction to a row's text. Supersession marks a row's +**text** and never alters mechanical behaviour, including when the entry records that +the row's hardening claim was itself false: the row keeps its fingerprint, keeps +matching the column-2 grep, and keeps counting. A hardening later removed is out of +scope. + +**Correcting a row.** Corrections live in a `Superseded rows` block above the +`Columns:` paragraph — a `**Superseded rows:**` label carrying one appended line per +supersession, present only once at least one entry exists: + + - · supersedes `` "" · what is false · where the current answer is + +`` is the day the entry is written, in `YYYY-MM-DD`. A row is located by date + +fingerprint. **An entry applies to every row its locator matches** — uniqueness is +not a requirement, and an entry that matches two rows says the same thing about both. +To narrow the match, add `""`, a quoted fragment of that row's `finding` +carrying no double quote, in the position shown immediately after the fingerprint. **A +fragment narrows the match set; it singles out one row only where that row has one no +sibling shares** — a sibling being another row the same date and fingerprint match. +Where it has none — an identical `finding`, one that is a substring of a sibling's, +or one whose every unique fragment carries a double quote — the entry marks every +matching row, its accurate siblings included, and no fragment prevents that. Omit it, +quotes included, when you mean every row the pair matches — including when the pair +matches only one. Name the claim that does not hold — saying whether it stopped +holding or was never true — and cite where the current answer lives; restating that +answer here only makes the entry the next stale narration. Neither of those two fields +may contain ` · `: that separator is what divides them, and free text carrying it +makes an entry parse two ways. A fragment is matched **literally and case-sensitively +against the row's `finding` as written in the file**, escapes and markup included — +what you quote is what is in the table, not what a renderer shows you. **An entry +applies only to matching rows dated on or before the entry's own date** — +supersession marks the past, so a row dated later never comes under an entry written +before it. **A row's date is the day it is appended**, and the table is chronological: +backdating a row is forbidden, which is what makes the date bound mean what it says. +Nothing can verify the append day itself, and nothing checks that dates never decrease; +the rule is stated and read. An entry whose locator matches no such row is **inert**: +it governs nothing and is not an error to repair in place — append a new entry with a +locator that matches, and leave the inert one standing as history, like every other +entry. **An entry marks a row, and the last entry for a row is the one that governs** +— where a row carries more than one, later in the file wins and the earlier ones are +history. **A later entry must therefore describe the row as it now stands, not only the +newly found fault**, or it retires a still-accurate earlier entry from a reader's view. +Entries are never edited, never removed, and never reference one another — and they +take the same floor as rows: once a line exists as a complete entry it is protected, +committed or not, while a line that is partial or does not yet carry the shape above is +still drafting and may be fixed. A mistyped locator in a complete entry is corrected +the same way everything else is, by appending. If a union merge leaves two `Superseded +rows:` labels, keep one and keep every entry under it. No standing tool reads this +block — to every grep and skill scanning the table a superseded row is unchanged, +including one whose entry says its fingerprint is wrong; prose readers get the +correction, mechanical readers do not, and nothing checks the difference. + +**Superseded rows:** + +- 2026-08-12 · supersedes 2026-07-20 `truncated-tool-output-read-as-complete` · its `ref` states that an incomplete pass still increments the counter, including a failed review returning `{success: false}`; true when written, and it no longer holds for every pass · CLAUDE.md §5, the paragraph opening `` **What this does not do.** The hook counts on `PostToolUse` `` + Columns: `date` (YYYY-MM-DD), `fingerprint` (canonical class), `finding` (short, escape `\|`, one line), `source` (gate-a|gate-b|bot|manual), `severity` (blocker|major|minor|nit), `rung` (e.g. `2 lint`, `4 test`, `1 prose`, diff --git a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md index fdc288f..fb1f411 100644 --- a/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -20,13 +20,18 @@ those decisions were revised — that history lives in ## 1. The gap -`docs/hardening-log.md`'s header carries two standing rules: never edit a row, and one row per -hardening. When a row's text stops describing reality, neither move is sanctioned — -editing breaks the first rule, and appending a row that records no hardening breaks the second. +**This section describes the state *before* this design landed.** §2 onward is the current +rule; past tense here records what motivated it, not anything still true. The story does the +same, for the same reason. -The gap is live. The 2026-07-20 `truncated-tool-output-read-as-complete` row narrates the gate -hook counting every incomplete pass, which is pre-0.8.0 behaviour, and a reader who trusts it -is misled about how the hook counts today. +`docs/hardening-log.md`'s header carried two standing rules: never edit a row, and one row per +hardening. When a row's text stopped describing reality, neither move was sanctioned — +editing broke the first rule, and appending a row that records no hardening broke the second. +Both rules still stand unchanged; what §2.2 adds is a third move. + +The gap was live. The 2026-07-20 `truncated-tool-output-read-as-complete` row narrated the gate +hook counting every incomplete pass, which is pre-0.8.0 behaviour, and a reader who trusted it +was misled about how the hook counts today. That row is the one §3.1's entry supersedes. ## 2. The convention @@ -172,8 +177,8 @@ the row's `finding` as written in the file**, escapes and markup included — wh is in the table, not what a renderer shows you. **An entry applies only to matching rows dated on or before the entry's own date** — supersession marks the past, so a row dated later never comes under an entry written before it. **A row's date is the day it is appended**, and the table is chronological: backdating a row is forbidden, which -is what makes the date bound mean what it says. Nothing can verify the append day itself; the -rule is stated and a check enforces the weaker property that dates never decrease. An entry +is what makes the date bound mean what it says. Nothing can verify the append day itself, and +nothing checks that dates never decrease; the rule is stated and read. An entry whose locator matches no such row is **inert**: it governs nothing and is not an error to repair in place — append a new entry with a locator that matches, and leave the inert one standing as history, like every other entry. **An @@ -284,8 +289,8 @@ convention meant all along rather than a guard bolted onto it. **What the bound does not close.** Row dates are days, not instants, so a row appended *the same day* as the entry, after it, still satisfies "on or before" and does come under that entry. -Backdating is forbidden and mechanically checked, so that day is the whole exposure: activation is -bounded to the entry's own date rather than made impossible. Closing it would need a finer +Backdating is forbidden — by instruction only, since nothing checks it — so that day is the whole +exposure: activation is bounded to the entry's own date rather than made impossible. Closing it would need a finer timestamp in the row format, which §2.2 rejects as wire format. The residual is one day wide, requires the same date *and* the same fingerprint, and is the only way an entry's match set can grow after it is written. @@ -370,7 +375,7 @@ the entire price. | `plugins/dev-workflow/commands/workflow-init.md` | §2.1 and §2.2 prose, into the inline ledger-header template — no block, since a scaffolded ledger has no entries | | `plugins/dev-workflow/.claude-plugin/plugin.json` | `0.8.1` → `0.8.2` | | `plugins/dev-workflow/CHANGELOG.md` | one entry for `0.8.2` | -| `todos.md` | the source row **The hardening ledger has no supersession convention** marked done and rewritten in the past tense, so it no longer reads as an open gap; **plus** a new parked row for the uncovered case — a hardening later *removed* — carrying its trigger, the first rung actually removed. **plus** a third row: wire §6's check-1d property into `AGENTS.md`'s quality battery, triggered by the first inert entry found after this change lands — **with the rider that any standing check must be diff-scoped *and* must pass on §2.2's sanctioned repair**, for the reason §6 gives: a whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding that no appended entry be inert fails on the very move the convention prescribes for a mistyped locator. A named read confirms all three: no present-tense claim that the ledger lacks a convention, and both parked rows present with their triggers | +| `todos.md` | the source row **The hardening ledger has no supersession convention** marked done and rewritten in the past tense, so it no longer reads as an open gap; **plus** a new parked row for the uncovered case — a hardening later *removed* — carrying its trigger, the first rung actually removed. **plus** a third row covering **both** unwired properties: §6's check-1d entry validation *and* the chronology check 1e, which §8 records as never implemented — wire both into `AGENTS.md`'s quality battery, triggered by the first inert entry found after this change lands — **with the rider that any standing check must be diff-scoped *and* must pass on §2.2's sanctioned repair**, for the reason §6 gives: a whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding that no appended entry be inert fails on the very move the convention prescribes for a mistyped locator. A named read confirms all three: no present-tense claim that the ledger lacks a convention, and both parked rows present with their triggers | | `docs/coding-workflow.md` | line 204's append-only sentence rewritten to state the rule **absolutely** — "strictly append-only: a **row** is never edited" — with the supersession move appended. It previously said "history is never rewritten"; an intermediate draft of this spec qualified it to *merged* history, which would have reinstated a pre-merge amendable class. No merged/unmerged qualifier appears in the landed wording. Found by the standing falsification lens, outside every path this change otherwise touches | | `docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` | its inherited open question widened from wrong-fingerprint to include phantom-hardening rows (that story's §5 Open questions) | | `docs/superpowers/plans/2026-08-04-hardening-round-0-8-0-and-pr-21.md` | a header note marking it a historical snapshot — it carries the un-narrowed rule three times. Executed plans are records of what was done and are **not** rewritten to match later rules; the note is how the falsification sweep resolves them | @@ -383,7 +388,10 @@ text**: a check greps for exactly those bytes, so neither code span may carry an ellipsis or any other stand-in. The region **includes** the four-space indented format example, now a single line, and **excludes** the `Superseded rows:` block, the `Columns:` paragraph and the table. -Inside that region the two files are byte-identical modulo hard-wrap position. Both sentinels are +Inside that region the two files are **byte-identical, hard wraps included** — the text is +generated once and inserted into both, so the line breaks fall in the same places, and §6's check +2b compares the two regions raw. "Identical modulo wrap position" would be the weaker claim, and +weaker than what is checked: a rewrap of one surface alone fails 2b. Both sentinels are unique in both files once the change lands; before it, the end sentinel exists in neither, which §6's check 2 treats as a failure rather than as an empty region — and, since check 2 now asserts presence per surface before parity, one that its anchor greps fail on first. @@ -399,8 +407,10 @@ confined to `workflow-init.md` — invariant 12 still binds, since that path is ## 5. What this design hands to another story `docs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md` owns the -redesign of how a recurrence decision reads the ledger. §2.2 leaves a prose-only correction -that nothing consumes — including a note that a row's fingerprint is wrong, and, since the +redesign of how a recurrence decision reads the ledger. §2.2 leaves a correction with **no +standing machine consumer** — the syntax is a standing convention every future author must +honour, and §6 adds only a validation-only parser making no ongoing compatibility promise — +including a note that a row's fingerprint is wrong, and, since the scope covers a claim that was never true, a note that the row's **hardening itself** never existed or was misclassified. §2.1 keeps such a row counting on purpose, so a lineage can escalate from a rung that was never landed. Both are one family — a row whose mechanical @@ -417,9 +427,19 @@ profile change moves it. The story is `docs/superpowers/stories/2026-08-04-harde **Battery:** the full command in `AGENTS.md` § Commands. -**This section states properties, not commands.** Each check below gives what must be true, the -observation that fails without the change, and the **oracle** — what a correct implementation must -be able to distinguish. The executable form is written at execution time, carried in the plan with +**This section states properties, not commands.** Each *implemented* check below gives what must be +true, a falsifying observation, and the **oracle** — what a correct implementation must be able to +distinguish. + +**The falsifying observations are of two kinds, and conflating them overstates what a green run +means.** `1a`, `1d`, `2a`, `2b` and `3` **fail on the untouched base**, so their green is evidence +this change did something. `1b` and `1c` are **protective**: they hold on the untouched base by +construction and fail only under a mutation that damages what they guard, so their green is +evidence nothing was broken, never evidence anything was done — a plan that read them as +counterfactuals would call a broken check satisfied. `1e` is **not implemented** and passes before +and after, contributing no observation at all; and `1f` is a named read rather than an executable +label, with nothing to confirm before the entry exists. The plan carries the same split as a table, +per label. The executable form is written at execution time, carried in the plan with one label per check, and **supplied to the Gate-B reviewer alongside the real diff**, where a check has something to be checked against. It is not committed, so it is not itself inside the reviewed range — §8 records what that leaves uncovered. Earlier drafts carried the shell here; over six Gate-A passes that shell @@ -438,7 +458,12 @@ the mutation it exists to catch. ### Check 1 — the correction is reachable from the ledger alone -**Six** properties, 1a through 1f. All must hold, the named read included. +**Six** properties, 1a through 1f. **Five of them must hold for this change** — 1a–1d and 1f, the +named read included. **1e is specified here but deliberately not implemented**, for the reason §8 +records: it validates the ledger's *pre-existing* chronology rather than anything this change does, +so it passes before and after and would report a green that means nothing about the diff. It is +written out because a standing successor will need it, not because this change runs it — and no +chronology validation exists in the meantime. **1a — the entry exists and names the right row.** The `Superseded rows` block carries an entry dated `$D` superseding `2026-07-20` with fingerprint `truncated-tool-output-read-as-complete`, and @@ -502,9 +527,15 @@ validates pre-existing entry immutability (§8). entry dated before the row it names. The typo-then-correct pair must **pass** — the mandated entry is the corrected one, and that is the convention working, not a defect. *Oracles* — **seven** things a correct implementation must distinguish, each of which a draft got wrong: -- **Which entry is in scope.** The mandated entry, and only entries this change **adds**, computed - against the base ref — not every entry in the file. For this change the base carries no block at - all, so every entry present is one it added. +- **Which entry is in scope.** **The** entry §3.1 mandates, and no other — identified in current + content by its own date, row date and fingerprint, and only *then* proven absent at the base ref. + **The two steps are ordered.** Selecting whatever is new and treating that as the mandated entry + inverts them, and lets an unrelated or mistyped earlier entry decide the verdict. Every other + added entry is ignored, a second inert one included: quantifying over all of them is the + unsatisfiable property the narrowing removed, since §2.2's sanctioned repair for a mistyped + locator leaves an inert entry standing forever. For this change the base carries no block at all, + so every entry present is one it added — which makes the two steps indistinguishable *here* and + is exactly why the order has to be stated rather than inferred from this case. - **An entry from a line that is not one.** Candidates are the **non-blank** lines of the interval from the `**Superseded rows:**` label to the `Columns:` paragraph, both exclusive. The single structural blank line separating the list from that paragraph is required by CommonMark — without @@ -527,8 +558,11 @@ is the corrected one, and that is the convention working, not a defect. checker demanding *exactly one* implements the guarantee §2.2 withdrew and would still pass this change, which is why the many case needs a fixture even though this entry does not produce it. **The on-or-before bound, at the boundary:** rows dated before, **on**, and after the entry's - date. Fixtures: the fragmentless locator matching **both** `2026-07-18` `docs-drift` rows, and - the three date positions. + date. Fixtures: a ledger carrying **two** complete rows that the *mandated* locator matches — + both dated `2026-07-20` with `truncated-tool-output-read-as-complete` — plus the three date + positions. The real `2026-07-18` + `docs-drift` pair cannot serve here despite being the + ledger's one genuinely non-unique pair: the mandated entry's locator does not match it, so a + many-match fixture has to be constructed on the locator the entry actually carries. *Not validated by this change, deliberately.* Fragment-narrowed matching and the comparison domain §2.2 pins — literal, case-sensitive, against the raw `finding` as written, escapes and @@ -553,11 +587,13 @@ is the corrected one, and that is the convention working, not a defect. having examined nothing. **1e — recorded dates never decrease down the table.** Backdating a row would let it fall under an -entry written before it, which §2.2 forbids. -*Falsifying observation:* a row dated earlier than the row above it must fail. -*Oracle:* this checks **recorded order, not append-day truth**. A row appended today and labelled -with a date after the current tail passes. Nothing can verify the append day; the prohibition is -instruction-backed and this is a floor under it. Dates must be **calendar-valid**, not merely +entry written before it, which §2.2 forbids. **Not implemented — the whole of 1e is a specification +for a future standing check, and nothing below describes anything that runs today.** +*Falsifying observation:* a row dated earlier than the row above it would have to fail. +*Oracle, for whoever implements it:* it would check **recorded order, not append-day truth**. A row +appended today and labelled with a date after the current tail would pass. Nothing can verify the +append day; the prohibition is instruction-backed, and such a check would be a floor under it — +a floor that does not exist today. Dates must be **calendar-valid**, not merely well-shaped — `2026-02-30` sorts correctly and is not a date — since a lexical-only comparison would let an impossible value satisfy both this property and 1d's on-or-before bound. The extraction must also distinguish "no dated rows found" from "ledger unreadable". @@ -596,9 +632,13 @@ surface, under a heading claiming it reached both. neither file, so 2a fails first. *Oracles:* -- **Wrap-insensitive presence, indentation-exact parity.** Both surfaces are hard-wrapped at - different columns and most anchors straddle a line break in at least one, so presence must be - tested against a paragraph-joined view. Parity must compare leading indentation and blank-line +- **Wrap-insensitive presence, byte-exact parity.** Both surfaces are hard-wrapped, and most + anchors straddle a line break *within* a surface, so **presence** must be tested against a + paragraph-joined view or a multi-word anchor is missed on a technicality. **Parity is the + opposite**: the two regions are compared raw, byte for byte, line breaks included — they are + generated once and inserted into both, so they do not wrap differently, and joining them before + comparing would let a rewrap of one surface alone pass. The wrap-insensitivity belongs to 2a and + must not leak into 2b. Parity must compare leading indentation and blank-line structure **exactly**: the four-space prefix is the only thing distinguishing the format example from a live entry, and a normalisation that flattened it would let one surface turn the example into entry-shaped content and still report equal. The join must therefore leave indented blocks @@ -658,9 +698,15 @@ Each is the distinguishing text of one decision §2 settles. 35. `once a line exists as a complete entry it is protected` ### Check 3 — the block is where §2.2 says, and only there -Checks 1 and 2 leave §2.2's layout decisions unvalidated: check 1 matches the entry anywhere in the -file, and check 2's region deliberately *excludes* the block, the `Columns:` paragraph and the -table. +Checks 1 and 2 leave most of §2.2's layout decisions unvalidated — but not all, and the difference +matters. Check 1 confines its candidates to the label-to-`Columns:` interval and treats an absent, +duplicated or reversed interval as undecidable rather than empty, so the block's existence and its +position relative to `Columns:` are already established there. What check 1 does **not** establish +is the end sentinel as the block's lower bound, the absence of entry-shaped lines *outside* the +interval, the blank-line structure around the list, or the template's freedom from a label. Check +2's region deliberately *excludes* the block, the `Columns:` paragraph and the table, so it +establishes none of those either. **Check 3 is exactly that remainder** — not a second pass over +what check 1 already did. **Property.** In the ledger, in this order: the convention's end sentinel, then exactly one `**Superseded rows:**` label, then the change's entry, then the `Columns:` paragraph. Every @@ -750,8 +796,8 @@ statement in it; do not report its incompleteness. anchor is not detected if it is deleted identically from both surfaces**. What is *not* exact is any enumeration of which decisions those are. Known unanchored today: the no-double-quote condition on a fragment — the narrowing it qualifies now carries anchor 34 — and §2.2's calibration - that append-day truth is unverifiable while the check enforces only non-decreasing recorded - dates. **Others almost certainly exist and are not listed here.** The layout rationale, the + that append-day truth is unverifiable and that nothing checks the weaker non-decreasing-dates + property either. **Others almost certainly exist and are not listed here.** The layout rationale, the prose-only decision and §2's reasoning generally sit outside the shared region and were never in scope. Nothing anchors the prose *between* anchors on either surface. @@ -790,11 +836,15 @@ statement in it; do not report its incompleteness. the twenty-two current rows, the one repeating date-and-fingerprint pair (`2026-07-18` + `docs-drift`) leaves both its rows separable, and the case is hypothetical. A discriminator was considered and rejected — an ordinal or a second column fragment would add wire format to the one - thing §2.2 keeps deliberately prose-only, to serve a case that has never occurred. + thing §2.2 deliberately leaves with **no standing machine consumer**, to serve a case that has + never occurred. **Reopen trigger:** the first real case whose truth diverges — a row with no permitted distinguishing fragment against a sibling, where one narration is falsified and the other still holds. Until then this is a recorded limitation, not an open question. -- **The checks themselves are never reviewed by a gate, and never fingerprinted.** They are written +- **The checks themselves are reviewed only as pasted context, never as files in a gate's range, + and never fingerprinted.** Gate B does read them — this change's Gate-B cycle raised sixteen + findings against them, which is the paste working — but it reads a paste rather than comparing a + diff, so nothing ties what was reviewed to what was run. They are written at execution time and are not committed — nothing standing runs them, so committing them would add a file the repository carries and never executes. The consequence is that the only executable artifact in this change sits outside the Gate-B range: the reviewer sees the check source only @@ -803,9 +853,12 @@ statement in it; do not report its incompleteness. `shellcheck`, and a counter-check per label. The evidence a `battery+check` mode rests on is therefore produced by unfingerprinted code, and a later reader cannot recover which version of a check produced a recorded result. -- **An extra inert entry appended alongside the mandated one is detected by nothing.** Check 1d - asks about **the** entry §3.1 mandates, not about every entry the change adds, so a second entry - whose locator matches no row rides along unexamined. Two reasons, and only the first is about +- **An extra inert entry appended alongside the mandated one is invisible to check 1d, and caught + once by check 3.** Check 1d asks about **the** entry §3.1 mandates, not about every entry the + change adds, so a second entry whose locator matches no row rides past *1d* unexamined. Check 3's + cardinality oracle is what does see it: it requires the entry to resolve to exactly one position, + so a second entry-shaped line fails that check — on this change, once, before any merge involving + it, and never again afterwards. Two reasons for 1d's silence, and only the first is about this change. Quantifying over every added entry is unsatisfiable by construction: §2.2's sanctioned repair for a mistyped locator is to append a corrected entry and leave the inert one standing, so the property would fail on the convention's own prescribed move. And the repair @@ -813,7 +866,8 @@ statement in it; do not report its incompleteness. question about which row the author *meant*, which the ledger does not encode and which no mechanical test can recover; the identity that would encode it is the wire format §2.2 refuses. The state is **outside the prescribed change**, which appends exactly one entry — but a - nonconforming implementation can produce it and every stated check will accept it. Calling that + nonconforming implementation can produce it, and every stated check *other than* check 3's + cardinality oracle will accept it. Calling that unreachable would be circular: it is unreachable only if the implementation does what it was told, which is the thing a check exists to stop assuming. Three further things nothing here validates, named rather than left to inference: **fragment-narrowed matching** and the @@ -856,17 +910,24 @@ statement in it; do not report its incompleteness. pre-existing property of the ledger's merge driver, not something this change introduces — named here because an earlier draft described the failure as one writer "overwriting" another, which union never does. -- **The format example is not an entry, and nothing enforces that distinction beyond its - shape.** It is a four-space indented code block using `` and `` placeholders, - so no line-anchored grep for a dated entry reaches it. That is the whole guard: POSIX grep has +- **The format example is not an entry. For this change two checks guard the distinction; + beyond it, only shape does.** It is a four-space indented code block using `` and + `` placeholders, so no line-anchored grep for a dated entry reaches it. On this + change, location guards it as well: check 1d confines its candidates to the + label-to-`Columns:` interval, and check 3 rejects entry-shaped lines anywhere outside that + interval. Both run once, on this change. **No standing check looks again**, so for every + later reader shape is the whole guard — and shape is a weak one: POSIX grep has no code-block awareness — the limitation recorded in `docs/hardening-log.md`'s 2026-07-26 `unverified-enforcement-claim` row, not in `scripts/check-invariants.sh` itself, which carries no such comment — so an example later rewritten flush-left with a real date would become indistinguishable from an entry to any reader built over this block. -- **Four things found at the closing review and deliberately held, not fixed.** Gate A closed on - pass 23 by a decision taken before it ran, so these are recorded here rather than repaired — the - alternative was another unreviewed revision, which is the state the closing pass existed to end. - Each is actionable by whoever writes the plan and the executable checks. Gate B compares the +- **Four things found at the closing review and held rather than repaired — since acted on, as + recorded under each.** Gate A closed on pass 23 by a decision taken before it ran, so these were + recorded here rather than repaired — the alternative was another unreviewed revision, which is + the state the closing pass existed to end. Each was actionable by whoever wrote the plan and the + executable checks, and Gate B is where all four were: the first and third by the checks + themselves, the fourth by correcting the residual above, and the second by correcting both + sites it names. None is still held. Gate B compares the **implementation range only**. Neither the plan nor the scratch check source is inside it — the plan lands earlier, as prose, under §5's exemption, and the checks are never committed at all; both reach the reviewer as context, which is a reader given a document rather than a gate @@ -876,21 +937,33 @@ statement in it; do not report its incompleteness. mandated entry, the two-row fixture and all three date positions — and would report an entry non-inert whose locator matches no row, which is the property 1d exists to decide. The executable check must validate **exact row-date and fingerprint equality before applying the - eligibility bound**, with a fixture whose only defect is a wrong locator row date. - - **Two sites still carry the pre-narrowing "prose-only" claim.** §5 says the correction is - prose-only and that nothing consumes it, and this section's discriminator-rejected bullet says - §2.2 keeps the entry deliberately prose-only. The accurate claim, after §2.2's Format paragraph + eligibility bound**, with a fixture whose only defect is a wrong locator row date. *Done:* the + check compares both fields before the bound; `entry-wrong-rowdate` and + `entry-wrong-fingerprint` are those fixtures, and each is run with its own mutated locator so + that identification succeeds and the comparison is what rejects it. Deleting the comparison + flips both to passing — checked, at Gate-B pass 4, because until then both fixtures were + rejected earlier, at identification, and would have passed this item while proving nothing. + - **Two sites carried the pre-narrowing "prose-only" claim — corrected at Gate-B pass 4, no + longer held.** §5 said the correction is prose-only and that nothing consumes it, and this + section's discriminator-rejected bullet said §2.2 keeps the entry deliberately prose-only. + Both now read as below. The accurate claim, after §2.2's Format paragraph was narrowed, is **no standing machine consumer** — the syntax *is* a standing convention that every future author must honour, and what §6 adds is a validation-only parser creating no ongoing compatibility promise. Where those two sites and §2.2 disagree, §2.2 governs. - - **1d's scope oracle reads as plural.** "The mandated entry, and only entries this change adds" - can be read as quantifying over every added entry, which is the unsatisfiable property the - narrowing removed. The intended selection: identify the §3.1-mandated entry in current content, - prove it was added relative to the base, and **ignore every other added entry**. - - **The format-example guard is understated as "shape alone".** For this change, 1d confines - candidates to the label-to-`Columns:` interval and check 3 rejects entry-shaped lines outside - it, so location guards it too — once. The claim is right about **standing** enforcement and - wrong about this change's validation; the future risk is a reader or check that scans dated - lines without respecting the interval. + - **1d's scope oracle read as plural — the check was corrected at Gate-B pass 3, the oracle's own + wording only at pass 6.** "The mandated entry, and only entries this change adds" can be read as + quantifying over every added entry, which is the unsatisfiable property the narrowing removed. + The intended selection: identify the §3.1-mandated entry in current content, prove it was added + relative to the base, and **ignore every other added entry**. *Done, twice over:* the check + selects by entry date, row date and fingerprint, requires exactly one such candidate, and only + then asserts it is absent at the base — the two steps in that order; and §6's oracle now states + that order rather than the plural. Recorded as two steps because the first fix left the second + undone for three passes, with this item marked done in between. + - **The format-example guard was understated as "shape alone" — corrected at Gate-B pass 3, + no longer held.** For this change, 1d confines candidates to the label-to-`Columns:` interval + and check 3 rejects entry-shaped lines outside it, so location guards it too — once. The + claim was right about **standing** enforcement and wrong about this change's validation; the + residual bullet above now says both. The future risk it names is unchanged: a reader or check + that scans dated lines without respecting the interval. - **It changes nothing about how the gate hook counts.** §3.1 records that 0.8.0 already changed it; this design only marks the row that still describes the old behaviour. diff --git a/plugins/dev-workflow/.claude-plugin/plugin.json b/plugins/dev-workflow/.claude-plugin/plugin.json index 7ce8394..b3d1e9f 100644 --- a/plugins/dev-workflow/.claude-plugin/plugin.json +++ b/plugins/dev-workflow/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "dev-workflow", "displayName": "Cross-Model Review Workflow", - "version": "0.8.1", + "version": "0.8.2", "description": "Spec-driven workflow with two independent cross-model review gates, an append-only hardening ledger with an escalation ladder, and repo-enforced quality. Requires the superpowers plugin.", "author": { "name": "Daniel Sänger", diff --git a/plugins/dev-workflow/CHANGELOG.md b/plugins/dev-workflow/CHANGELOG.md index 659e881..2c9cd1b 100644 --- a/plugins/dev-workflow/CHANGELOG.md +++ b/plugins/dev-workflow/CHANGELOG.md @@ -22,6 +22,13 @@ unambiguously, still fails. Deleting only a plugin's *manifest* while the direct keeps shipping fails too. AGENTS.md invariant 12 carries the complete list. +## 0.8.2 + +- `workflow-init`: the scaffolded ledger header now carries a supersession convention — + a row whose narration is later found wrong or made stale is corrected by appending a + `Superseded rows` entry, never by editing the row. Both standing rules hold unchanged: + never edit a row, one row per hardening. + ## 0.8.1 - **Three sentences added to the §5 gate protocol, and to the template `/workflow-init` diff --git a/plugins/dev-workflow/commands/workflow-init.md b/plugins/dev-workflow/commands/workflow-init.md index 7f35bb7..341387c 100644 --- a/plugins/dev-workflow/commands/workflow-init.md +++ b/plugins/dev-workflow/commands/workflow-init.md @@ -643,6 +643,68 @@ this project's `docs/hardening-taxonomy.md`; column 2 is the recurrence-grep tar Never edit a row; resolve a `pending` row by appending a new row (same fingerprint, `ref` naming the prior row's date + anchor). +A row records a hardening claim as of its date, and its narration may be found wrong or +made stale later. When a row's text no longer describes reality — falsified by a +later change, or wrong when it was written — append a `Superseded rows` entry rather +than editing it. One later change is **excluded**: a hardening that is itself removed, +for which this convention supplies no move at all (see the end of this paragraph). This +holds for every row without exception: the existing `Never edit a row` rule is +absolute, and correcting a row is always an append. The rule is bound to rows, not to +commits: once text exists as a row it is never edited, committed or not. Drafting +before a row exists — an editor buffer, a line not yet written — is below the +rule's resolution, and nothing checks one. Resolving a `pending` row also appends — +that is a new hardening, not a correction to a row's text. Supersession marks a row's +**text** and never alters mechanical behaviour, including when the entry records that +the row's hardening claim was itself false: the row keeps its fingerprint, keeps +matching the column-2 grep, and keeps counting. A hardening later removed is out of +scope. + +**Correcting a row.** Corrections live in a `Superseded rows` block above the +`Columns:` paragraph — a `**Superseded rows:**` label carrying one appended line per +supersession, present only once at least one entry exists: + + - · supersedes `` "" · what is false · where the current answer is + +`` is the day the entry is written, in `YYYY-MM-DD`. A row is located by date + +fingerprint. **An entry applies to every row its locator matches** — uniqueness is +not a requirement, and an entry that matches two rows says the same thing about both. +To narrow the match, add `""`, a quoted fragment of that row's `finding` +carrying no double quote, in the position shown immediately after the fingerprint. **A +fragment narrows the match set; it singles out one row only where that row has one no +sibling shares** — a sibling being another row the same date and fingerprint match. +Where it has none — an identical `finding`, one that is a substring of a sibling's, +or one whose every unique fragment carries a double quote — the entry marks every +matching row, its accurate siblings included, and no fragment prevents that. Omit it, +quotes included, when you mean every row the pair matches — including when the pair +matches only one. Name the claim that does not hold — saying whether it stopped +holding or was never true — and cite where the current answer lives; restating that +answer here only makes the entry the next stale narration. Neither of those two fields +may contain ` · `: that separator is what divides them, and free text carrying it +makes an entry parse two ways. A fragment is matched **literally and case-sensitively +against the row's `finding` as written in the file**, escapes and markup included — +what you quote is what is in the table, not what a renderer shows you. **An entry +applies only to matching rows dated on or before the entry's own date** — +supersession marks the past, so a row dated later never comes under an entry written +before it. **A row's date is the day it is appended**, and the table is chronological: +backdating a row is forbidden, which is what makes the date bound mean what it says. +Nothing can verify the append day itself, and nothing checks that dates never decrease; +the rule is stated and read. An entry whose locator matches no such row is **inert**: +it governs nothing and is not an error to repair in place — append a new entry with a +locator that matches, and leave the inert one standing as history, like every other +entry. **An entry marks a row, and the last entry for a row is the one that governs** +— where a row carries more than one, later in the file wins and the earlier ones are +history. **A later entry must therefore describe the row as it now stands, not only the +newly found fault**, or it retires a still-accurate earlier entry from a reader's view. +Entries are never edited, never removed, and never reference one another — and they +take the same floor as rows: once a line exists as a complete entry it is protected, +committed or not, while a line that is partial or does not yet carry the shape above is +still drafting and may be fixed. A mistyped locator in a complete entry is corrected +the same way everything else is, by appending. If a union merge leaves two `Superseded +rows:` labels, keep one and keep every entry under it. No standing tool reads this +block — to every grep and skill scanning the table a superseded row is unchanged, +including one whose entry says its fingerprint is wrong; prose readers get the +correction, mechanical readers do not, and nothing checks the difference. + Columns: `date` (YYYY-MM-DD), `fingerprint` (canonical class), `finding` (short, escape `\|`, one line), `source` (gate-a|gate-b|bot|manual), `severity` (blocker|major|minor|nit), `rung` (e.g. `2 lint`, `4 test`, `1 prose`, diff --git a/todos.md b/todos.md index 5a64a95..385800d 100644 --- a/todos.md +++ b/todos.md @@ -48,19 +48,33 @@ driven by recurrence rather than by enthusiasm. failure message, a silent Bash event and the fallback emitter, not against every emitting branch. Closing it needs a selective `rm` shim and per-branch composition goldens. *Trigger: a disclosure or advice bug that the current rows do not catch.* -- [ ] **The hardening ledger has no supersession convention.** `docs/hardening-log.md`'s - header says never edit a row, and one row per hardening — so when a row's "what this - does NOT do" narration is later falsified by a feature change, there is no sanctioned - move: editing breaks the first rule and appending breaks the second. The 2026-07-20 - row now describes pre-0.8.0 counting behaviour as current. The 2026-07-20 *spec* took - a version-qualified supersession note and that worked; the ledger needs the same - convention written into its header, or an explicit "rows are historical, read the - newest row for current behaviour" statement. - **TRIGGER FIRED (2026-08-04):** the 2026-07-20 row now teaches pre-0.8.0 counting - behaviour as current — the second falsified row this trigger names. Story: +- [x] **The hardening ledger had no supersession convention.** **DONE in 0.8.2.** + `docs/hardening-log.md`'s header said never edit a row, and one row per hardening — so + when a row's "what this does NOT do" narration was later falsified by a feature change, + there was no sanctioned move: editing broke the first rule and appending broke the + second. Both rules now stand unchanged, and the correction is a third move: a + `Superseded rows` entry appended above the `Columns:` paragraph, marking the row by + date + fingerprint and naming what is false and where the current answer lives. The + convention is in the ledger header and in `/workflow-init`'s inline template, and the + 2026-07-20 row — which taught pre-0.8.0 counting behaviour as current — carries the + first entry. Design: + `docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md`. + **TRIGGER FIRED (2026-08-04):** the 2026-07-20 row taught pre-0.8.0 counting + behaviour as current — the second falsified row this trigger named. Story: `docs/superpowers/stories/2026-08-04-hardening-ledger-supersession-story.md`. - *Trigger: the next row falsified by a - later change — this is the second.* +- [ ] **A hardening that is later *removed* has no sanctioned supersession move.** The convention + in `docs/hardening-log.md`'s header covers a row whose narration was falsified later or was + wrong when written, and names a removed hardening as explicitly out of scope. No instance + exists. *Trigger: the first rung actually removed.* +- [ ] **Nothing standing validates a supersession entry, and no chronology check exists.** The + checks in `docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md` §6 ran + once, on the change that introduced the convention, and then stopped; §6's 1e was not + implemented at all, since it validates the ledger's pre-existing chronology rather than that + change. Wiring both into `AGENTS.md`'s quality battery is the follow-up — **with the rider + that any standing check must be diff-scoped *and* must pass on §2.2's sanctioned repair**: a + whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding + that no appended entry be inert fails on the very move the convention prescribes for a + mistyped locator. *Trigger: the first inert entry found after this change lands.* - [ ] **Locator selects the `text` element by RAW BYTE comparison of `type`.** A Unicode-escaped spelling of `text` is legal JSON meaning `text` and is not selected; with no other element the class is `no-result` (fail-closed, so discarded rather than From ed9834ba23145a692bc39269f9b2b4913696a5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20S=C3=A4nger?= <20968534+dsnger@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:39:02 +0200 Subject: [PATCH 6/6] docs(todos): park the two Gate-B cycle findings and CodeRabbit's story-AC finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three trigger-gated, none actioned here. - reviewType: full raced two writers onto both findings slots on PR #23's pass 1; per-branch files did not stop it, and every acceptance condition still passed because all four are shape checks. Fingerprint unverified-enforcement-claim, rung P std by the guard-scope precheck against the 2026-08-04 row, not by count. - §5 gives the finding-line severity by example only; pass 3 returned IMPORTANT and passed every check. Fingerprint prompt-vague-criteria, rung P std, no prior row. - CodeRabbit: the story's AC 1 restates §5's profile procedure lossily. Accepted, left unfixed — the criterion is satisfied and closed, so rewriting it edits a record of what was agreed rather than changing behaviour. The first two ride with the reviewer-availability fallback story, which amends the same §5 region, so one Gate B covers all three edits. --- todos.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/todos.md b/todos.md index 385800d..0549bbf 100644 --- a/todos.md +++ b/todos.md @@ -75,6 +75,43 @@ driven by recurrence rather than by enthusiasm. whole-block scan is unsatisfiable once an immutable inert entry exists, and a check demanding that no appended entry be inert fails on the very move the convention prescribes for a mistyped locator. *Trigger: the first inert entry found after this change lands.* +- [ ] **`reviewType: full` races two writers onto both findings slots, and per-branch files do + not stop it.** §5 prescribes one file per branch *because* `full` runs the spec and quality + reviewers in parallel from one `additionalContext` — but nothing binds a reviewer to its own + slot. On PR #23's Gate-B pass 1 both reviewers wrote **both** paths: the reply carried four + protocol lines instead of two, spec reported `7 / 5` and quality `9 / 9`, and the disk held + `9 / 9`. The face that makes it dangerous: **every acceptance condition still passed** — + terminator present, count matching, nothing-but-finding-lines, both branch files present — + because all four are *shape* checks and provenance is outside them. The spec branch's seven + findings were gone and no check could say so. Fix candidate: make **sequential + single-branch calls** (`reviewType: spec`, then `quality`) the documented default in §5 and + in `/workflow-init`'s template — eliminating the concurrency rather than detecting it. + Evidence: sixteen consecutive single-branch calls across passes 2–9 of that cycle, no + recurrence. Fingerprint `unverified-enforcement-claim`, rung `P std` — the fitting rung, not + an escalation: the guard-scope precheck against the 2026-08-04 row (whose guard is *"the + exhaustiveness statement"*, for a sentence naming what a mechanism does not cover) puts this + shape outside it, so the count alone does not escalate. + *Trigger: rides with the reviewer-availability fallback story — next in queue, amending the + same §5 region, so one Gate B covers all three edits.* +- [ ] **§5 gives the finding-line severity by example only, never as a closed set.** The gate + prompt shows `MAJOR | high | …` and tells the reader to filter to Blocker/Major, but never + states the four permitted tokens, and the acceptance rule validates shape — terminator, + count, one-finding-per-line — not the severity's value space. On PR #23's Gate-B pass 3 the + quality branch returned all four findings at severity `IMPORTANT`; the file was otherwise + well-formed, so it passed every check and the Blocker/Major filter had to be applied by + interpretation. Fix candidate: pin the enum in §5's finding-line spec and in + `/workflow-init`'s template. Fingerprint `prompt-vague-criteria`, rung `P std`, no prior row. + *Trigger: rides with the reviewer-availability fallback story, with the row above.* +- [ ] **The supersession story's AC 1 restates `CLAUDE.md` §5's profile-change procedure instead + of referencing it.** Raised by CodeRabbit on PR #23 and accepted as accurate: the criterion + spells out propose-axes → pause for confirmation → write the header, and does so *lossily* — + it omits §5's renewed override, the profile-log line, and the rule that an axis change voids + every prior override. Left unfixed there on the same ground as the plan divergences: the + criterion is **satisfied and checked off**, so rewriting it edits a closed record of what was + agreed at intake rather than changing any future behaviour. The story's own convention is to + amend with explicit old-condition accounting, which is a human call. + *Trigger: the next amendment to that story for any other reason — fold it in with accounting + rather than opening the file for this alone.* - [ ] **Locator selects the `text` element by RAW BYTE comparison of `type`.** A Unicode-escaped spelling of `text` is legal JSON meaning `text` and is not selected; with no other element the class is `no-result` (fail-closed, so discarded rather than