Backlog of stories, follow-ups, and prerequisites referenced by
docs/hardening-log.md (pending rows point here by ref).
This repo now improves itself reactively only. A change starts because a finding
surfaced — through the gates, a PR bot, or real use — goes through
dev-workflow:harden-finding, and lands via the normal PR flow. There are no
proactive self-improvement sweeps: no auditing our own files looking for things to
harden, no pre-emptively broadening a checker, no tidying passes.
Everything parked below stays parked until its trigger condition fires or Daniel explicitly pulls it. A parked item is not a to-do list to work down; several are deliberately deferred trade-offs, and re-opening one without its trigger is the anticipation-driven escalation the ladder exists to prevent.
Why this is written down: self-initialization is exactly the phase that generates appetite for more self-work, and the ledger's own escalation rules assume changes are driven by recurrence rather than by enthusiasm.
-
Locator: TWO quadratic paths —
skipval's container walk and the record accumulator.substr(s,i,1)is O(len) per call in BWK awk, so a large VALID sibling container beforetool_responseis quadratic: 3.2 s at 200 KB, 11.5 s at 400 KB, in one synchronous hook invocation. Only the 1 Mi-unit ceiling stops it, and a payload just under the ceiling still costs tens of seconds — so the ceiling is load-bearing rather than a formality. Found at Gate B pass 2 on 0.8.0, after two other quadratics in the same scan were fixed. The candidate fixes are a jump-based walk (linear for realistic shapes, still quadratic for many-sibling-container payloads), a work budget scaled by payload length, or lowering the ceiling — all three are design calls, which is why this is a row and not a patch. Second path, found at pass 3:s = s $0 "\n"rebuilds the accumulated input once per input line, so a newline-rich (pretty-printed) payload is quadratic in line count independently of the container walk — 0.35 s at 4k lines, 2.69 s at 16k. Chunked accumulation reduces but does not remove it; the two paths share a fix only if the scan stops indexing the payload withsubstr. Trigger: a report of a slow hook, or any change that raises the ceiling. -
A5 marker matrix and A6 composition coverage are narrower than the approved plan. The marker-lifecycle rows run through one emitter pair rather than both, omit the mixed pending-disclosure/background-advice write-failure combinations, and P9-9's pending-delete-failure row is skipped by name because no operation-specific fault is available (one permission governs both operations on
.context/, and a directory at the pending path is not seen as pending). A6 composition is exact-tested against a failure message, a silent Bash event and the fallback emitter, not against every emitting branch. Closing it needs a selectivermshim and per-branch composition goldens. Trigger: a disclosure or advice bug that the current rows do not catch. -
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: aSuperseded rowsentry appended above theColumns: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. -
The supersession convention names a repair for a mistyped locator and for nothing else an entry can get wrong.
docs/hardening-log.md's header forbids entries from referencing one another, requires the governing entry to describe its row as it now stands, and forbids restating the current answer — but the only repair it spells out is for a mistyped locator ("corrected the same way everything else is, by appending"). An entry that is malformed in any other way has no named move, and entries are protected "committed or not", so editing is out. Found in use, not theorised: this round appended three entries that referred to other entries, and Gate-B pass 6 caught them. The repair applied — append a later, self-contained governing entry and leave the malformed lines standing as history — is a reading of the locator rule generalized, and it leaves a reader who stops at the first matching entry looking at a forbidden cross-reference. Any fix must keep entries immutable, which is the constraint that makes this awkward in the first place. Trigger: a third round appending a malformed entry, or the first reader observed acting on a superseded entry instead of the governing one. -
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 intoAGENTS.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: fullraces two writers onto both findings slots, and per-branch files do not stop it. §5 prescribes one file per branch becausefullruns the spec and quality reviewers in parallel from oneadditionalContext— 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 reported7 / 5and quality9 / 9, and the disk held9 / 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, thenquality) 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. Fingerprintunverified-enforcement-claim, rungP 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: re-pointed 2026-08-16 atdocs/superpowers/stories/2026-08-14-sequential-branch-calls-hook-story.md. The reviewer-availability story closed without shipping the sequential-calls rider — it is a hook change, not a §5 prose edit, and the unchanged hook counts each call as a pass. -
§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 severityIMPORTANT; 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. Fingerprintprompt-vague-criteria, rungP std, no prior row. CLOSED 2026-08-16 — §5 and the template now state the enum as a closed set, andscripts/check-invariants.shcheck 4c asserts it is present exactly once in each copy (and, in the command file, inside the scaffolded template's own section). The reader normalizes an out-of-enum token toMAJORrather than discarding the pass, which is what PR #23's fourIMPORTANTfindings needed. -
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
textelement by RAW BYTE comparison oftype. A Unicode-escaped spelling oftextis legal JSON meaningtextand is not selected; with no other element the class isno-result(fail-closed, so discarded rather than miscounted, but still a wrong verdict on a legal payload). Same for escaped spellings of thetype/textkeys. Characterized by a regression row and stated in spec §3.1; closing it means decoding thetypevalue for equality while still returning the selectedtextin its original escaped bytes, since the matcher depends on those. Trigger: a serializer observed emitting escaped key or type spellings. -
Two consumer Gate-B cycles closed with no usable fingerprint persisted, while counting and cycle-recognition worked. Field-reported from
infinite-portfolio-canvas(~60+ Gate-B cycles, kit 0.8.0 since mid-run); evidence is that repo'stodos.md§ Tooling revalidation, which carries four dated measurements, anddocs/field-reports/2026-08-16-canvas-a1-a5-field-report.mditem 1. Two shapes, both measured withCLAUDE_CODE_MCP_AUTO_BACKGROUND_MS="0"already set — so this is not the backgrounding cause that entry started with, and it does not collapse into the preflight-check row in § Next. (a) 2026-08-11, cycle5a4bf38, 24 counted passes..context/codex-gate.gateBheld the literalunavailableandcodex-gate.freshCounta0at session start. Read the hook before repeating the consumer's inference here: a staleunavailableis not sticky — every counted pass recomputestree_hash()and overwrites the state file unconditionally (codex-gate.sh, the counted-pass branch), setting the fresh streak to 1 on a computable hash and to 0 only when the hash is uncomputable at that moment. So the observation does not show a cycle poisoned by its starting state. What it does show is narrower than "the hash was uncomputable at every pass": the persisted value wasunavailableat the observed endpoints, and the write itself is best-effort — the state file is written under2>/dev/null || true, so a later computable hash that failed to persist is indistinguishable from one never computed. Uncomputable hashes and a failing write are both live candidates, and the row picks neither. Conditions were the cleanest available — pass 13 Blocker/Major-free on both branches, both branches on byte-identical content, nothing edited between the last pass and the--amend, the closing tree equal to the reviewed WIP tree (b2757ff), and the hook's own diagnostic checklist green point by point (.context/andTMPDIRwritable,shasumpresent,git statusruns, 49 GiB free). Both state files were gone after the amend, so the hook read the amend correctly as cycle-closing. Recognition and counting work; what the reported states do not separate is computation from persistence. (b) 2026-08-16, cyclef09286b, counter at 21. The count alone says nothing about the fingerprint — the hook increments it independently of whether a hash was computed or persisted — so read this as an observation and not as proof that storage worked. That STOP was correct (a comment correction sat between pass 13 and the reset — real drift). Unexplained is the close: after a fresh pass 14, clean on both branches, the--amendreported "no fingerprint is recorded for this cycle", which is thereviewed-empty branch — the state file absent, not stale. The consumer's own narrowing is the ordering "WIP commit → pass →--amend", where the closing amend no longer sees the fingerprint of the pass immediately before it. What this round did not establish, which is most of the mechanism: whether the hash was computable at any given pass of (a), whether a computed hash failed to persist, and why (b)'s close found no fingerprint. The observations are the persisted values at the endpoints, the counts, and the messages — nothing between them. Per-pass computation, per-pass persistence and the storedreviewedvalue at each STOP are all unknown. Reading the hook at the site that computes the hash and returnsunavailableis the consumer's own named next step and was not done here. One lead worth carrying: (b)'s STOP arrived at agit reset --softcall, which reaches the reset path only ifis_commitmatched that command string — so the loose-grep row below is a candidate contributor to (b), and the two should be read together rather than separately. Trigger: a second consumer reporting an unhealable fingerprint, or the next change to the fingerprint code path. A hook change either way, which is why this is a row. -
A mere mention of "commit" beside a
gitcommand closes the cycle and resets the pass counter.is_commit()greps the raw command string for(^|[^[:alnum:]])git[[:space:]].*commit(plugins/dev-workflow/hooks/codex-gate.sh:756), and on a non-WIP match the counters are reset. So a diagnosticechois not merely the "redundant warning" invariant 2 accepts as the price of firing loose — it destroys the floor silently. Measured in the field (canvas, 2026-08-04):git status --porcelain; echo "--- last commit body ---"; git log …in one Bash call dropped the counter from 5 to 0, and the real closing commit then reported "1 recorded pass". The consumer's memory notegate-b-echo-resets-pass-counternames why this is the dangerous direction: "the loss looks exactly like passes that never ran". Second shape, same call-string looseness, already tracked above:git add … && git commit …chained in one Bash call defeats the docs-only exemption, with two controlled data points isolating the variable —29da026(separategit add) → "N/A (docs-only)",c2fa18e(chained) → STOP. That is the timing class the compound-commands row tracks, and these are cross-consumer instances of it from a second repo, predating that row's occurrences 3 and 4. That row's occurrence count is deliberately left unedited here, so this field item carries exactly one disposition; whoever escalates it should count these. Third shape rejected as unverified: the report's heredoc-fires-a-false-STOP claim resolves, at its cited evidence, to §5's own warning about heredocs restated in the consumer'sCLAUDE.mdandtodos.md. No measured instance exists. Trigger: a second reported counter loss, or any change tois_commit. The fix is a parse rather than another pattern, so it is a hook change. -
The arms-race remedy exists as an observation and not as a procedure. When a series of passes stops converging because each correction enables the next finding of the same shape, the remedy the field found is to change what is being checked rather than to patch once more: scope the reviewer to changed regions, change the instrument's layer, or relocate the residual to the layer that already catches it — and STOP with a named state rather than run the next round. Verified at three sites in
infinite-portfolio-canvas: itsdocs/hardening-taxonomy.mdcorollary ("when a tool enters an arms race with its reviewer, it is checking at the wrong level"),.context/a5-t2a-resume.md(four consecutive passes each finding one more spelling the claim text seemed to cover; changing the instrument's FORM — printing the patterns, stating the cause as line- and grammar-local — took findings from nine to zero, and the residual became a named ticket with its own budget whose trigger is a relapse), and.context/gate-b-a5-t2a-dispositionen.md(six of six findings on the checker's grammar, none on product behaviour). Before this round the kit stated the same idea in one place only, and locally — the invariant-checker escalation row below ("adding one more regex arm per newly-discovered spelling is not the ladder working"); as of this round the story named below and the amended guard-scope story also state it, so the count is a before-picture and not a current inventory. Parked as a story, not written as prose, because it lands on a decision branch that is already under design: as a procedure it changes which rungharden-findingpicks when passes stop converging, and that branch is the subject of the guard-scope-precheck story. Story:docs/superpowers/stories/2026-08-17-arms-race-remedy-as-procedure-story.md, which inherits from and is inherited bydocs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md— recorded in both directions on purpose: one decision branch, one design, and whichever is picked up first must read the other rather than re-deriving it. Trigger: either story being picked up, or a third arms-race series observed. -
A handback says nothing about how much context produced it. When a gate loop stops and surfaces — §5's
clearly stuck → STOP and surface, or the new scope stop beside it — the human decides whether to continue in the same session or start fresh, and one input that could inform it is absent: how much context the surfacing agent had left. How much that input actually decides is unestablished, and the evidence below shows only that operators asked for it repeatedly, not that it changed an outcome. The field adopted it as a standing field, carried as a documented instruction and nothing stronger — nothing verifies that an agent supplies the figure, that a reader demands it, or that a self-reported percentage is accurate: the consumer'sdocs/handoff-cowork.mdcarriesKontext-% (einfordern, falls fehlend)at line 10 andKontext-% in Übergaben einfordernat line 72 — demand it, and demand it again when a handback arrives without it. This row exists because the round first rejected the item on a search that missed that file (it searched the consumer's.context/,todos.mdandCLAUDE.md, where the only percentages are pixel-coverage measurements), and Gate-B pass 1 caught the rejection. Two things are unsettled, which is why this is a row and not a sentence in §5: whether a self-reported context figure is reliable enough to steer a decision, and whether it belongs in §5's stop text, in the handoff-shaped artifacts, or both. Adding it to §5 mid-cycle would also have put a third rule into a paragraph already under review. Trigger: the next §5 edit touching the stop-and-surface text, or a session continued past a handback and lost to compaction where the figure would have said to start fresh. -
EXPERIMENTAL — proportionality for findings whose subject is a test instrument. Proposed rule: a finding about the instrument rather than about product behaviour gets one repair round, then is collected — unless it demonstrates a false-green on product behaviour, which keeps it iterating. Explicitly not field-proven; the report marks it as a proposal, and its motivation is that late cycles spent a growing share of passes on meta-instruments. Cross-project corroboration for the motivation only, from this repo's own history: PR #23's closing commit records that "of 27 Blocker/Major findings, 16 were in the never-committed scratch harness, 10 in the design spec's narration, 1 in the plan" (
7bbdb14) — 16 of 27 on the instrument. What is unproven is the remedy: capping repair rounds by the finding's subject weakens the Blocker/Major rule that makes the loop trustworthy, and the false-green carve-out is the whole difficulty rather than a footnote. Trigger: a cycle where instrument findings measurably starve product findings of passes, with both counted. -
EXPERIMENTAL — pre-split heuristic for oversized tranches. Proposed rule: a tranche exceeding a size signal (the report suggests >N new runners or steps) is split by subject at PLAN time. Its factual premise is rejected as unverified, not merely caveated: the report states that every large tranche in the field split anyway and always through an expensive stop-decide-re-record round-trip, and it cites no tranche records, sizes or measurements — none were found, and the round did not adopt the frequency or the cost claim. What is parked is the proposal only. The threshold is then the entire remaining question: a number chosen without measurement would split tranches that did not need it, and this repo has no measurement of its own to set one. Trigger: three tranches in one project splitting mid-execution, with their sizes recorded — that is the sample a threshold could be read from.
-
A failed Codex call counts as a pass — false ✓ in the firing direction. DONE in 0.8.0. The hook now reads the result before counting. Five classes (spec §3.3):
successandunrecognizedcount and store a fingerprint;failure(the envelope's immediately-first property issuccess: false),backgrounded(the harness notice anchor at the start of the located block) andno-result(an unambiguous determination that no located block yields a non-blank string) do neither. The candidate fix recorded here was right about the direction and wrong about the unknown:tool_response's real shape formcp__codex__*was established by capturing live payloads, which now ship as fixtures. What remains, and it is the accepted residual, not a leftover of this row: locating-uncertainty is fail-OPEN — an unwalkable structure, a repeated depth-1tool_response, or a payload past the scan bounds counts, with a once-per-workspace disclosure that the count was made without inspection. And the counter is still not evidence: classification cannot see whether the findings file was written, so an incomplete pass is discounted whatever the counter says. C1–C4 inplugins/dev-workflow/CHANGELOG.mdcarry the full residual list. -
jq-free parser stops at an escaped JSON quote. (Candidate path, recorded 2026-08-01: the result-classification story builds a POSIX awk locator with proper string-state and backslash-parity handling. Once that exists and is proven against the captured fixtures, this row's fix can likely reuse it rather than inventing a second escape-aware scanner. The scopes stay separate — that story does not touch
input_field— but whoever takes this row should look there first.) A payload containingecho \"quoted\" && git commit -m xdecodes to nothing, so no reminder fires — wrong direction under invariant 2, and only on machines withoutjq. Needs escape-aware decoding or a conservative raw-payload scan, plus tests for escaped quotes and backslashes. -
Compound commands hash the pre-mutation tree.
printf changed > tracked.txt && git commit -am xis one PreToolUse event: the hook hashes before the mutation runs, so the commit carries content the hash never saw. Consider treating any command segment precedinggit commitas uncertain and firing. Occurrence 2 (2026-07-30): same event-timing class, second consumer —is_docs_onlyrather thantree_hash. Commit 1950739 staged exactly onedocs/**.mdpath, so Gate B was N/A per CLAUDE.md §5's prose exemption, yet the hook emitted the Gate-B STOP. Cause, read from the source rather than inferred: the docs-only branch derives its file list asfiles=$(git -C "$repo_root" diff --cached --name-only)at PreToolUse, the commit was issued as a single Bash call whosegit addhad not run yet, so that list was empty — andis_docs_onlyopens with[ -n "$1" ] || return 1, which the branch's own comment states as intent ("Only when the file list is POSITIVELY confirmed docs-only; an empty list falls through to fire"). So the timing gap now has two consumers, and this one is a false positive — it fires when it need not, the safe direction under invariant 2 — where thetree_hashconsumer above is the dangerous direction. Counts toward this row's eventual trigger; not fixed now, and note that any fix must keep the empty-list fallthrough firing rather than trade a redundant warning for a missed one. Occurrence 3 (2026-08-14): same shape as occurrence 2, same consumer.git addandgit commitissued in one Bash call on PR #23's close; the staged set was empty atPreToolUse,is_docs_onlyfell through, and the Gate-B STOP fired on a docs-only commit. Like occurrence 2 a false positive — the safe direction. Occurrence 4 (2026-08-16): same shape again, on PR #24's review pass.git addandgit commitin one Bash call; the staged set was empty atPreToolUse,is_docs_onlyfell through, and the Gate-B STOP fired on a docs-only commit. A false positive like 2 and 3. Counted here even though it was observed rather than suffered, because the trigger is recurrence of the shape and an occurrence noticed by the person who caused it is still the shape recurring; excluding it would tune the count to who was watching. Four occurrences of the timing gap now, three of them benign; the dangeroustree_hashconsumer above is still the one that decides this row's priority. -
No regression test for a
git add/write-treefailure inside the throwaway index. Derived from the code, not recalled: sections 24a-24e stub FIVE failure shapes — every checksum tool failing silently, a checksum printing a token then failing, the seedcp,git diff HEAD, andrev-parse --absolute-git-dir. SEVEN have no targeted test:mktemp -d; the non-symbolic unresolvable-HEAD branch (theelse ok=0arm); the throwaway-indexgit rm -rfq --cached; the firstwrite-tree(index tree);git add -A; the secondwrite-tree(worktree tree); and failure of the redirect that creates the buffered stream. The twowrite-treecalls are distinct sites needing distinct tests — one covers the index component, the other the worktree component. Each needs a portable way to fail exactly one call without disturbing the rest; a selectivegitwrapper earlier onPATH(as 24d/24e already use) is the seam for the git ones. This row was written three times from memory and understated the gap every time — re-derive from the code before trusting it. -
Gate-B fingerprints disk; the reviewer reads history. A review pass records a fingerprint of the index and worktree, but
mcp__codex__reviewreads a git range — so content that is staged and never committed can be fingerprinted as reviewed without Codex having read it, and three such passes reach ✓. Raised at Gate A pass 8 of the index-tree story and deliberately deferred there: closing it means refusing to satisfy Gate B unless the index and worktree correspond to the reviewed range, i.e. mandating a WIP commit for every review. That redefines the gate rather than fixing a hash, so it needs its own story and its own decision. CLAUDE.md §5's WIP-commit flow is the current mitigation. -
check-invariants.shscans untracked scratch directories, so local scratch can fail it. It greps the working tree recursively, not the tracked set, so a gitignored scratch file that merely quotes a violating pattern trips it. Hit for real: the SDD scratch under.superpowers/held pasted test output in which the wordnpxsat next to a--yesflag inside one of the checker's OWN test descriptions, and the checker then reported an unpinned-npx violation against a repo containing no such call. (This row deliberately does not quote that string verbatim — doing so put the pattern into a tracked file and made the checker fail on this very commit, which is the bug demonstrating itself.) A false positive, so it is the safe direction — but it is confusing, and it makes "the battery is green" depend on what else happens to be on disk. Surfaced by real use during the index-tree story, not by a gate. Fix would be to scan tracked files (or honour.gitignore), with a reject/accept fixture for a violating pattern inside an ignored path. Occurrence 2 (candidate note, not a fix): recurring operator friction. Closing that same story, the battery had to be run with.superpowers/sdd/moved aside again — by hand, remembered rather than prompted. So this is not only a confusing one-off red: it is a step a human must know about and repeat, on a command AGENTS.md presents as "what CI runs". Two occurrences of the same class now; counts toward whatever trigger this row is eventually escalated on. -
Gate-pass counters are read-modify-write, with no serialization.
bump_countreads, increments and writes; twoPostToolUseevents arriving concurrently can lose an increment, and the fresh-count, fingerprint and pass-count writes are independent, so a reader can observe a mixed snapshot. This predates the result-classification change and applies to every counter — that change adds files with the same property, not new exposure, which is why it was documented as a stated contract there rather than fixed asymmetrically. Raised as MAJOR at Gate-A pass 2 of that design under the risk lens. Trigger: the first observed lost increment, or when batch/orchestrator work makes concurrent gate calls real — the parked story for that is where concurrency stops being hypothetical. Until then: known, stated, unexploited. -
The hook trusts
.context/and does not reject non-regular state targets. A globally installed hook creates and truncates files inside a repository-controlled directory, following symlinks; a hostile workspace could point a state file or marker at another user-writable path. Pre-existing for every state file the hook already writes — the result-classification change adds markers with identical properties, so fixing only the new ones would be inconsistent, and fixing all of them was out of that story's scope. Raised as MAJOR (medium confidence) at Gate-A pass 2 under the security lens. Any fix must keep invariant 1 (always exit 0) on the rejection path. Trigger: the first security-highstory touching the hook, or a real report of a hostile-workspace scenario. -
Temp-index writes land in the real object database.
git add -Aagainst the throwaway index writes loose blobs/trees into the user's repo (verified: 3 → 5 objects per review). Unreachable, so gc collects them, but a temporaryGIT_OBJECT_DIRECTORYwith the real store as an alternate would avoid the churn. -
The gate-claims Don't is correct and was not followed, twice. PR #21's C4 (an unqualified jq-parity criterion that outran what
field()compares for a malformed outer document) and C5 (a README claim that a typo cannot quietly unhook a gate) both fall inside the 2026-07-19AGENTS.mdDon't, whose operative instruction already requires exactly what they omitted — name the exact comparison the code performs, and delete any part of the sentence that outruns it. No textual repair exists, which is why these are parked rather than logged: a ledger row would have to name a hardening, and a rule needing no change means the failure was compliance, not wording. The 2026-08-04 amendment covers coverage enumerations (F8's shape) and reaches neither a positive parity claim nor a positive prevention claim. Trigger: a third compliance miss against that Don't, or a feasible mechanical rung emerging fromdocs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md.
Roadmap items below are kit-side only and carry the same trigger discipline as
Parked: each names the condition that starts it, and none starts early. Product-side
work — wiring a security battery, a validation lane, coded E2E — is deliberately absent.
That belongs in each product project's own todos.md once /workflow-init has run
there, not here: this repo ships the workflow, it does not hold another project's
backlog.
-
P2 — risk/security profiles, and the derived validation mode. Shipped: two human-confirmed axes in the story header, a mode derived as
max(risk, security), lens sets appended to the §5 gate prompts, and the Gate-B triviality skip narrowed to effective level 0. Spec:docs/superpowers/specs/2026-07-26-risk-security-validation-profiles-design.md. -
P6 — standalone security sections in the intake, spec and gate templates: DELIBERATELY REJECTED, not shipped. The profile is the heading: a standalone section would be a second surface to keep in sync with it (the docs-drift class), and it invites boilerplate-filling on stories where nobody knows what to write. Security content lives in the spec's decision record and risks discussion and in
AGENTS.mdinvariants; the security lens set is what asks about assets, trust boundaries, roles, external systems and abuse paths. Reopens when: field use shows high-security content scattering incoherently across specs — that recurrence is the trigger, not a fresh opinion. -
P5 light — stable AC-/SEC-IDs in the story and plan templates. Identifiers that survive from story to plan to review, so an acceptance criterion can be cited instead of re-described. Trigger: the first story that runs under profiles — the IDs exist to label what profiles produce, so the numbering scheme should meet a real profiled story before it gets a template slot.
-
/workflow-initpreflight checksCLAUDE_CODE_MCP_AUTO_BACKGROUND_MS. The variable keeps a >120 s gate call in the foreground so its result reaches the hook. The failure mode this row originally described was fixed in 0.8.0 — a backgrounded call carrying the recognized harness notice is now discarded, not counted. What the variable still buys is the residual the CHANGELOG names as C1: the notice is recognized in the wording it currently uses, so if that harness prose ever changes the call is counted fail-open instead, with a disclosure. The variable prevents the situation; the hook only recognizes today's spelling of it. The result-classification story documents it inREADME.md§ Setup only, deliberately — a preflight check is a second surface and was kept out of that diff. OBSERVATION (2026-08-04), and the row stays open. With the variable set to0, five Gate-A passes of the 2026-08-03 round ran 458 s, 550 s, 757 s, 663 s and 780 s; all five stayed in the foreground and returned ordinarysuccess: trueenvelopes the hook could read. No control run was made with the variable unset, so this is a correlation observed under one setting, not a demonstration that the variable held those calls in the foreground. What it does establish is that calls well past 120 s can return as ordinary foreground results here. The row's deliverable — a/workflow-initpreflight check — is unbuilt, so the row is not discharged by this. SECOND INDEPENDENT FIELD EVIDENCE (2026-08-17) — the row is ripe. A second consumer,infinite-portfolio-canvas, discovered the variable by measurement rather than fromREADME.md: six realmcp__codex__reviewpasses (57 findings, pass 6 clean on the quality branch) were each auto-backgrounded past 120 s, none reached the hook, and the closing commit drew a STOP against a structurally zero counter. Setting the variable to0in~/.claude/settings.json'senvblock fixed counting on the following cycles (5 counted passes, then 24). What the variable buys, separated, because this row previously blurred them: it keeps a long successful call in the foreground so its result reaches the hook and is counted at all — the 0 → 5 → 24 progression is that effect — and separately it removes dependence on C1's residual, the hook recognizing today's wording of the harness notice. Both are real; neither is the other. What this evidence does not show: a documentation gap.README.md§ Setup step 2b already documents the variable, its version requirement, that it is read at process start, the launch-environment caveat, both usable values and both outcomes — so the honest reading is that the shipped documentation did not reach an operator who then spent four dated measurement rounds rediscovering it, which is an argument for the preflight check this row's deliverable already is, not for more prose. No control run with the variable unset was made in that repo either. Evidence:docs/field-reports/2026-08-16-canvas-a1-a5-field-report.mditem 4 and that repo'stodos.md§ Tooling revalidation. Trigger: after that story lands (spec:docs/superpowers/specs/2026-07-31-failed-codex-call-counts-as-a-pass-design.md). -
Upstream-report candidate:
claude plugin update <bare-name>. The CLI docs document the bare plugin name as a valid argument forinstall/uninstall/updatealike, butclaude plugin update dev-workflowerrors "Plugin 'dev-workflow' not found" on CLI 2.1.x whiledev-workflow@dev-workflow-kitworks. README documents the qualified form as a workaround; file via/feedbackso the behaviour and the docs stop disagreeing. -
Upstream-report candidate:
mcp-codex-deverror envelopes swallow the reason. A failed gate call returnssuccess: falsewith noerror.code, so neither the operator nor the hook can tell an exhausted quota from a server defect; the cause is findable only in~/.codex/sessions/<Y>/<M>/<D>/rollout-*.jsonlunderrate_limits. Three confirmed instances, all frominfinite-portfolio-canvas's memory notecodex-empty-envelope-means-credits(2026-08-15/16), verified there this round: (1) an emptysuccess: falseenvelope with no code whilemcp__codex__healthstill reportedok: true— the rollout log carriedcredits.balance: "0",has_credits: false,codex_error_info: "usage_limit_exceeded"; it read as a server defect, and an A5/T2b Gate-B cycle was abandoned after nine passes with the cause open; (2) the OpenRouter route, the same wall in another shape —success: falsecarrying progress prose in thereviewfield,unexpected status 402 Payment Required: Insufficient credits,codex_error_info: "other"; (3) asuccess: trueenvelope carrying only progress prose and no answer line, i.e. an incomplete pass wearing a success envelope. The upstream ask is the report's own and is not widened here: carry the credit or quota reason in the envelope —NO_CREDITSand friends — which covers instances (1) and (2). Instance (3) is recorded as a limitation of that ask, not as a second proposal, because the report asked for a failure reason on asuccess: falseenvelope and asuccess: trueone is a different shape that a credit code cannot reach; the report also says to add nothing beyond its list. What the evidence supports about (3) is only what was seen: one degeneration emittedtask_completewith no error and produced no file, and one silent death emitted notask_completeat all — sotask_completealone did not separate a finished pass from an abandoned one in those cases. Whether any combination of artifact, answer line and log growth is necessary or sufficient was not established, and this row does not claim it. Kit-side relevance: the hook'sfailureclass can see that a gate call failed and never why, which is the same "no error code surfaced (unclassified)" shape this repo met in 0.8.x use. Deliberately not fixed here — both are fields in the server's envelope; file them againstmcp-codex-dev, which is Daniel's own server, not this repo's scope.
- P1 —
workflow-status→workflow-next, staged. Report where a story stands first; only once that read is reliably right does it get to recommend the next step. Staged deliberately: a "next" that is wrong is worse than no command at all, because it is followed. Trigger: 3–5 real stories completed in a product project — fewer than that and the state machine would be modelled on this repo's own atypical usage. - P7 —
workflow-doctor, extracted from the/workflow-initpreflight. Not a second implementation of the same checks: the point is a single shared check source that both the initializer and the doctor call, or the two drift and the doctor starts blessing setups init would reject. Trigger: the next setup incident, or before the second project init — whichever comes first; the second init is where a divergence would first cost someone real time. - P8 — passive metrics, read-only over the ledger and git. Analysis only: no new
state file, no instrumentation, nothing written back. It answers questions the
ledger already contains the data for (which fingerprints recur, how often a rung
holds).
TRIGGER FIRED (2026-08-04): the 2026-08-03 hardening round appended four ledger rows,
taking the ledger from 18 to 22 and crossing the 20-row arm; its fifth story takes the
story count to 10, crossing the other arm as well. Story:
docs/superpowers/stories/2026-08-04-passive-metrics-over-the-ledger-story.md, which carries this row's conditions with each marked kept, moved or dropped. Trigger: 10 stories or 20 ledger rows — below that the sample says more about the last week than about the workflow. -
/capture-findingas an intake extension ofharden-finding. CLOSED 2026-08-17 — folded into the Finding A story, which is now the single owner of the finding-to-ledger route. The trigger fired exactly as written: the canvas A1–A5 field report (docs/field-reports/2026-08-16-canvas-a1-a5-field-report.md) is a production finding set arriving from real use rather than from a gate or a bot. It fired and answered itself negatively: eleven items were routed by hand throughdev-workflow:intake,todos.mdrows, one upstream note and one reasoned rejection, and capture was never the part that hurt. What did hurt is durability and reach — a fixed finding surviving a compaction or a handoff, and a project that never opens PRs never reachingprocess-pr-reviewstep 5 — and both of those already belong to Finding A. A second command on that route is how the ledger acquires two formats, which is this row's own stated reason for existing as an extension; the honest conclusion is that the extension point is Finding A's design, not a command of its own.
-
Re-check
docs/prompt-standards.mdagainst the current model-specific prompting pages on every model-generation change (new Claude model in Claude Code, new Codex model for the gates). Includedocs/sparring-briefing.mdin that pass — it is a prompt artifact for the upstream advisor chat, and a model change on either side of it (sparring model or coding agent) can shift what its conventions should say. Concretely pending: the switch of the coding agent to the new Opus generation fires this row. -
Ad-hoc-brief paragraph synced into the scaffolded template. Done in the canvas-findings round's PR 2, the vehicle this row named. Not the verbatim repo paragraph, which turned out to be unportable: it links
docs/sparring-briefing.md(never scaffolded) and asserts this repo's own incident count. The template carries a downstream-neutral variant preserving both halves of the principle — briefs carry the checklist's habits, and nobody reviews a brief against all 12 items. -
Prompt-standards conformance checker — resolved the two
pendingledger rows (2026-07-25). Landed as checks 4a and 4b inscripts/check-invariants.sh, with fixtures in its regression suite, in the canvas-findings round's PR 1. (No fixture count is quoted here on purpose: an earlier draft said "42", the Gate-B fixes took it past that, and a hard-coded total at a doc site describing this checker is the very drift class the checker exists for.) Bothpendingrows were resolved by appending rung-2 rows dated 2026-07-26, never edited. Each new row states the exact spelling its check guards and what stays instruction-backed. Two things turned out differently than this entry assumed: word forms had to be in scope, because the motivatingdocs-driftoccurrence spelled its count as a word rather than a digit, and a digit-only check would have missed it entirely. (The exact phrase is deliberately not quoted here: check 4b reads a live count claim in any scanned*.md, and it caught this very entry when it was first written. The ledger may quote it because the ledger is excluded; editable prose should reword instead.) And the ledger itself had to be excluded from both checks, because a ledger that quotes defects self-rejects the checks that detect them. -
Each Gate cycle destroys the previous cycle's review record. Slot names (
gate-a-spec-pass-<p>,gate-b-<branch>-pass-<p>) carry no cycle-unique component, and §5 mandates deleting every target file before each call — correctly, since a surviving prior file is indistinguishable from a fresh one. The consequence is that a second cycle in the same repo silently erases the first cycle's findings artifacts. Observed, not theorised: the result-classification cycle's pass-1 call deleted the 2026-07-26 profiles cycle's 11 KBgate-a-spec-pass-1.md..context/is git-ignored, so it is unrecoverable. §5 anticipates concurrent calls racing on one slot and says so; it does not cover sequential cycles reusing them. Note the dispositions and resume-note companions have the same property. Any fix has to keep the pre-call delete — that check is load-bearing — so it is about naming (a cycle component in the slot) or archiving, not about relaxing the protocol. NOT FIRED (2026-08-04): the 2026-08-03 hardening round edits §5 prose and its template mirror, and changes no part of the §5 file protocol this row's trigger names — not the slot names, not the pre-call delete, not the terminator or acceptance rules. Recorded so a later reader can check the reading rather than re-derive it. SECOND OCCURRENCE, TRIGGER FIRED (2026-08-16). The reviewer-availability story's cycles destroyed a predecessor's findings file and its dispositions before the surviving 44 artifacts were archived by hand. That is the second observed destruction, after the 2026-07-26 profiles cycle. The row stays open — the fix is still naming (a cycle component in the slot) or archiving, never relaxing the pre-call delete, which is load-bearing. Trigger: FIRED. Was: the next round touching the §5 file protocol. -
Attribution for the human-exception record form. The
<handle>in aHuman exception:record is unverified, and §5 says so in the shipped text: nothing checks that it belongs to whoever decided, that a human was asked, or that the reason is honest. Hardening for what ships; needs no availability attestation. Trigger: the first record whose authorship is disputed or unattributable. -
External-authority zero-pass research. A signed commit or a protected-branch approval is the one design class the reviewer-availability cycles never tried, and its requirements are already named: a trusted signer identity and a role policy saying which identities may approve — an ordinary signer can be the author — plus an availability attestation from a party that is not the author, which neither mechanism supplies on its own. It is one untried direction worth reconsidering, not the only one that could work; the design space was never exhausted (
docs/superpowers/specs/2026-08-14-reviewer-availability-fallback-design.md§1.5). Trigger: a renewed need to close a gate cycle with no review — a second multi-day reviewer outage, or the operational bridges of that design's §7 proving unavailable. -
Tracked re-review debt. The human-exception form records a decision and creates no follow-up obligation, so "the re-review never happened" is not an observable event today. Stated as one deliberately. Trigger: a human explicitly asks for follow-up review on a recorded exception and that follow-up is later found not to have happened.
-
A recording mechanism for severity normalization. Rider (b) normalizes an unrecognized severity token to
MAJORand records nothing. The drift is visible to the reader at the moment the pass is validated — the findings file carries the original token on the finding line — but nothing is durable:.context/is git-ignored and slot collisions have destroyed findings here (row above). A companion record was designed and cut, at a measured cost: it needed a token-identity rule, a bijection audit, a logical-pass/attempt/credited-count identity model, edits to four shipped hook reminder strings, and adocs/hardening-log.mdsupersession row. Trigger: a pass is normalized and the drift goes unnoticed in review. -
The hook's
is_docs_onlyexempts any.mdpath outside a prompt directory, which is broader than §5's prose list (docs/**.md,README.md,MANIFEST.md). Found while siting a removed debt store. Trigger: a root.mdfile acquiring gate-relevant state. -
Tier-2 counting and containment. A same-family reviewer whose passes are worth counting, per
docs/superpowers/stories/2026-08-14-tier-2-same-family-reviewer-story.md. The unchanged hook counts each call as a pass, so counting is part of the problem, not a detail of it. Trigger: the tier-2 story being picked up, or a second multi-day reviewer outage. -
Finding B — a §5 version stamp, so a scaffolded CLAUDE.md can tell it lags the installed plugin. Split out of the canvas-findings round after two Gate-A passes showed it is a design, not a sentence. Spec questions: a semantic §5 locator (
/workflow-initmay append the section renumbered, so "no §5 heading" can misread a valid section and append a duplicate); per-state merge semantics (invariant 9 forbids a silent overwrite, and "re-run init to sync" promises what the command cannot give); stamp cardinality (absent, duplicate, malformed); and a binding real on every push path — the version-bump coupling first proposed was false, since invariant 12's checker ispull_request-only. A stamp is a wire format: shipping a provisional one writes legacy into every scaffolded file. The one known-stale instance (canvas) is being re-synced by hand, so this carries no schedule pressure. TRIGGER FIRED (2026-08-04): the 2026-08-03 hardening round edits the §5 inline template. Story:docs/superpowers/stories/2026-08-04-section-5-version-stamp-story.md, which carries this row's conditions with each marked kept, moved or dropped. Trigger: the next round that touches the §5 template. -
harden-finding's recurrence rule is scope-blind. Rungs guard scopes; the skill's recurrence step does re-read the ledger, and the defect is that its decision branch keys on the fingerprint and the latest matching row's rung without letting that row's stated guard control the verdict. Sketched fix — before proposing escalation on a same-fingerprint recurrence, read the prior row's stated guard: outside it the prior mechanism never claimed that shape, so its rung did not fail — pick the fitting rung, do not escalate; inside it, the mechanism was meant to catch this and did not, so that is a regression to repair or strengthen. (An earlier draft had those branches inverted, which would have entrenched the bug it was filed against; Gate A caught it.) Evidence case 3 (2026-08-04): the 2026-08-03 hardening round ran the precheck as a standing manual instruction from Daniel — which is this row's own diagnosis, since a rule that exists only in chat is not one the skill carries — and still reached a wrong verdict twice by reading a single prior row's guard and stopping. Split todocs/superpowers/stories/2026-08-04-harden-finding-guard-scope-precheck-story.md; this row stays open because the fix it sketches has not landed. Trigger: the first human rejection of an over-escalation the 2026-07-26 rows predicted, or the next round touching the skill. -
Finding A — a route from a fixed finding to the ledger for projects that never open PRs. The only mandated ledger check lives in
process-pr-reviewstep 5, so a no-PR project never reaches it: canvas has 51 Gate-A pass files and 0 ledger rows. Cut from the canvas-findings round after drawing a Major on all five Gate-A passes; those findings are the story's opening evidence rather than a blank page: it cannot rest on same-session memory, because a compaction, interruption or handoff loses the fixed-finding set and nothing detects the loss; its scope must matchprocess-pr-reviewstep 5 exactly — check every accepted actionable fixed finding, but invokeharden-findingonly when a class matches or a new one is clearly warranted, which every approximating draft got wrong; and a durable handoff needs real design (identity, dedup, consumption semantics), which is why it was refused as a mid-round addition. It mintsmandatory-step-anchored-to-optional-pathwhen it lands — minting it earlier would leave a class no row uses. TRIGGER FIRED (2026-08-04): the 2026-08-03 hardening round edits §5. Story:docs/superpowers/stories/2026-08-04-ledger-route-without-pull-requests-story.md, which carries this row's conditions with each marked kept, moved or dropped. INHERITED 2026-08-17: this story is now the single owner of the finding-to-ledger route. The/capture-findingrow in § Someday closed into it after its own trigger fired and answered negatively — manual capture of eleven field findings worked, so the gap is durability across a compaction or handoff and reach into no-PR projects, both of which are already this story's scope. What it inherits is one constraint, not new scope: whatever route it designs is the only one, because a second capture entry point is how the ledger acquires two formats. Trigger: the next round that touches §5, or a project reporting an empty ledger across cycles that fixed findings. -
Escalation trigger for the invariant checker — read this before patching it. The checker asserts only the spellings its fixtures cover. Adding one more regex arm per newly-discovered spelling is not the ladder working; it is the same rung applied repeatedly. If a fifth unhandled spelling turns up in the wild, that is the recurrence, and the answer is a real YAML/shell parse logged as the next rung — not another patch. Anticipating that today would be escalating without recurrence, which the ladder exists to prevent. Count so far: the spellings found during development were fixed as part of building the rung and do not count toward the five.
-
Invariant checker does not see Docker images outside a
docker://action ref.FROM alpine:latestin a Dockerfile anddocker run alpinein a script are executable dependencies that invariant 5 covers, but every Docker rule is downstream of the action-ref scan, so neither is looked at. Raised by CodeRabbit on PR #2. Deferred rather than fixed there because it is a new surface (Dockerfiles, shelldocker run), not a gap in a spelling the checker already claims — and the ledger ref is worded to claim only the latter. Needs its own reject/accept fixtures. Part of that story:ci.yml'skoalaman/shellcheck:v0.11.0is tag-pinned by luck, not by the gate — a tag can be repointed, so digest-pinning it belongs to whoever takes the Docker surface on.