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/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/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/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 new file mode 100644 index 0000000..fb1f411 --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-hardening-ledger-supersession-design.md @@ -0,0 +1,969 @@ +# 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 — 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 +`.context/codex-reviews/gate-a-spec-pass-*-dispositions.md`, one file per review pass. + +## 1. The gap + +**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. + +`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 + +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 + +```markdown +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. +``` + +| Clause | The state it settles | +|---|---| +| `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 | +| `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 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. + +**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 +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 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.** 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 + +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. + +```markdown +**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. +``` + +**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. +- 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 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. 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. 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 **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. + +**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 — 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. + +**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.** 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 + +### 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` `` +``` + +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. (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 — a precedent the convention supersedes + +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.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 **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 | +| `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. 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, 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. + +**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. +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/`. + +## 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 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 +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. + +## 6. Validation + +**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 *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 +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 + +**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 +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. +*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** 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 + 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: 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 + 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 + `| | |` 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. **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". + +**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. + +### 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. + +**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`. + +**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, 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 + 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.** 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. + +**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` +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 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 +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, 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. +- **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` — the only invariant-11 surface this change +touches, now that `harden-finding` is out of the change surface. + +## 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**, 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-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 + 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, 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 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 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. + + **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. +- **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 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 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 + 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 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 + 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 *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 + **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. +- **Check 1's content half is a human read.** The greps establish that an entry exists carrying + the expected fingerprint and that its locator matches the row it names; nothing establishes that the + entry says anything true or useful. +- **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. +- **`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. 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 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 + 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. *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 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/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..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 @@ -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,9 +81,22 @@ 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. **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 e9ac5ca..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 @@ -1,17 +1,19 @@ # 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 -`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, @@ -23,26 +25,104 @@ 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, 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 | | 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. + +**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. +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. 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 +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.)* + +*(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 -- [ ] 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. -- [ ] 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 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 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 @@ -51,17 +131,28 @@ row falsified by a later change can be marked as such without breaking either st - `## 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.)* 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..0549bbf 100644 --- a/todos.md +++ b/todos.md @@ -48,19 +48,70 @@ 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.* +- [ ] **`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