Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a0151ce
docs(intake): add command-retargeting-guard story
dsnger Jul 19, 2026
ff4a25b
docs(superpowers): add the Gate-B index-tree spec and plan
dsnger Jul 19, 2026
294a879
fix(hook): make an uncomputable Gate-B fingerprint fail closed
dsnger Jul 19, 2026
2d0cde8
fix(hook): hash the index tree, not just the worktree
dsnger Jul 19, 2026
1c82189
fix(test): contain GIT_INDEX_FILE leak in codex-gate.test.sh section 27
dsnger Jul 19, 2026
92a23f0
fix(hook): resolve a relative ambient GIT_INDEX_FILE against repo_root
dsnger Jul 19, 2026
b5c330a
fix(hook): stop the Gate-B messages asserting causes they cannot know
dsnger Jul 19, 2026
ca554d1
fix(hook): merge overlapping empty-state comment; pin fingerprint-equ…
dsnger Jul 19, 2026
4dcf0a8
test(hook): pin the three Gate-B messages by exact golden-file compar…
dsnger Jul 19, 2026
5c6563d
docs: describe the fingerprint as index + worktree, not worktree alone
dsnger Jul 19, 2026
d188ecc
docs: fix stale two-component wording found by Gate B
dsnger Jul 19, 2026
4b1e3b3
docs: scope the fingerprint claim to hook-invocation time
dsnger Jul 19, 2026
478b434
docs(workflow-init): fix duplicated preposition in the .context exclu…
dsnger Jul 19, 2026
6008bd4
docs: stop architecture.md overclaiming what the fingerprint proves
dsnger Jul 19, 2026
51db56e
docs: stop calling the fingerprint "what a commit would carry"
dsnger Jul 19, 2026
02077ca
docs: remove the last two totality claims about the fingerprint
dsnger Jul 19, 2026
a0d1433
docs: fix a quoted hook message that no longer exists
dsnger Jul 19, 2026
c445477
docs(hook): last comment equating fresh-count with reviewed code
dsnger Jul 19, 2026
56593ab
chore(plugin): release 0.5.0
dsnger Jul 19, 2026
c686aad
docs(ledger): close the staged-vs-worktree row, park the review-range…
dsnger Jul 19, 2026
8b470fa
docs: correct two understated coverage claims and install the overcla…
dsnger Jul 19, 2026
6c5ba7b
docs: sync the spec to the shipped hook; derive the untested-seam list
dsnger Jul 19, 2026
a85395f
Close three mutation-tested gaps in codex-gate.test.sh: index-tree .c…
dsnger Jul 19, 2026
7abc2ee
test: guard the stub_dir cleanup; correct two stale coverage records
dsnger Jul 19, 2026
01051d2
docs(todos): record occurrence 2 of the check-invariants scratch fric…
dsnger Jul 20, 2026
23db534
test: compare real-index ENTRIES, not bytes (CI-only failure)
dsnger Jul 20, 2026
68849ae
fix: close two CodeRabbit findings from PR #8
dsnger Jul 20, 2026
f5c9070
fix: close three Gate-B findings on the PR-review fixes
dsnger Jul 20, 2026
dfa8167
fix: portable grep in the suite; align the plan expected-output lines
dsnger Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,20 @@ reader can judge whether it still holds.
2. **Loose in the firing direction.** On uncertainty, fire. A missed commit (false ✓)
is the dangerous direction; a redundant warning is the accepted price.
3. **Gate-B validity is content-derived, never event-derived.** Invalidation compares a
hash of the working tree — `git diff HEAD` plus a tree id written from a throwaway
index, so untracked paths, contents and file modes all count, minus `.context/`. An
fingerprint of the effective index plus the included worktree content, as of the
hook's invocation — a deliberate superset of any one commit's payload, so the gate
errs toward firing: `git diff HEAD` for tracked content, a tree id written from the
**effective index** (`GIT_INDEX_FILE` when set, else the git-dir index), and a tree
id written from a throwaway index brought up to the worktree — so untracked paths,
contents and file modes all count, minus `.context/`.
The index component exists because `git commit` commits the index: without it, staging
a change and reverting the file on disk read as unchanged and reported satisfied. An
event-derived check misses a file changed through Bash (`sed -i`, `git apply`,
codegen) and leaves a stale ✓ standing; so does a name-only view of untracked files,
or any hand-rolled walk that re-derives what `git write-tree` already gets right
(symlink targets, exotic path encodings, non-regular files).
(symlink targets, exotic path encodings, non-regular files). When the fingerprint
cannot be computed it is the literal `unavailable`, which never matches — including
against itself.
4. **POSIX `sh`, and `jq` is optional.** No bash-isms; correct behaviour via fallback
parsing when `jq` is absent. The hook runs on machines whose environment we do not
control, and CI invokes it with `sh`. Enforced mechanically by the lint command
Expand Down Expand Up @@ -188,6 +196,21 @@ reader can judge whether it still holds.
matches that word order, i.e. "convention-loaded" / "convention loading"; a sentence
reading "loaded by convention" is caught by the `declare[sd]?` arm only when it also
contains a form of *declare*, so read the hits rather than trusting the count.)
- **Never describe what a gate proves without checking what it actually compares.**
Prose that overstates a mechanism is this repo's most persistent defect, and it
regenerates: fixing the index-tree story took four Gate-B rounds because *each
correction introduced a subtler version of the same claim* — "the tree-hash proves
what was reviewed", then "what is being committed is what was reviewed", then "what a
commit would actually carry", then "everything a commit could carry". Every round
searched for the previous **phrase**, so a synonym survived. Search for the **claim**:
`grep -rniE '(everything|anything|all content|any change)[^.]{0,80}\b(commit|fingerprint|hash)\b|\b(commit|fingerprint|hash)\b[^.]{0,80}(everything|anything|all content|any change)' --include='*.md' --include='*.sh' . | grep -vE 'source-files/|docs/superpowers/'`
(The `\b` boundaries matter: without them `commit` matches `committed` and the
recipe reports its own false positives.) Two things this does NOT do, stated so
nobody mistakes it for a guard: nothing runs it
in CI — it is a recipe a human runs — and an overclaim phrased without those totality
words escapes it entirely. It raises the floor; it does not close the class. The
underlying rule is the check itself: for every sentence about a gate, name the exact
comparison the code performs, and delete any part of the sentence that outruns it.
- **Never rename or delete a doc section without grepping for references first.**
`ci.yml` once pointed at a deleted README section; `MANIFEST.md` listed a `CLAUDE.md`
that did not exist. Docs-drift is this plugin's own taxonomy class and this repo is
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Why each of these, and how to adapt them: [`docs/coding-workflow.md`](docs/codin
| `/dev-workflow:process-pr-review` | command — validates PR bot comments against the code and your invariants, replies to each, fixes regressions, tracks pre-existing issues. |
| `dev-workflow:finding-triage` | agent — read-only, fresh context, judges whether one PR-bot claim is actually true of the code. Used by the PR processor; never counts as a review gate. |
| `/dev-workflow:workflow-init` | command — scaffolds the per-project files, then interviews you to write `AGENTS.md`. |
| codex-gate hook | non-blocking reminders that count Gate A and Gate B passes, and verify a Gate-B review against the actual content of the working tree. Always exits 0. |
| codex-gate hook | non-blocking reminders that count Gate A and Gate B passes, and verify a Gate-B review against a fingerprint of the effective index plus the included worktree content, as of the hook's invocation — a deliberate superset of any one commit's payload, so the gate errs toward firing. Always exits 0. |

## Setup

Expand Down
24 changes: 17 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,25 @@ It doesn't, because that is blind to a file changed through Bash — `sed -i`,
`eslint --fix`, `git apply`, a codegen step — which emits no such event and would leave
a stale "reviewed" marker standing.

Instead the hook stores a hash of the working tree at review time and recomputes it at
commit: `git diff HEAD` for tracked content, plus a tree id written from a throwaway
index so untracked files count by path, content and mode. (Asking git for the tree
Instead the hook stores a fingerprint of the index and the working tree at review time
and recomputes it at commit: `git diff HEAD` for tracked content, a tree id for the
effective index, and a tree id written from a throwaway index brought up to the
worktree so untracked files count by path, content and mode. (Asking git for the tree
rather than walking the files in shell is deliberate — a hand-rolled walk has to
re-derive symlink targets, git's path quoting and non-regular files, and got all three
wrong before this was reduced to `git write-tree`.) Any change by any tool
invalidates; an edit-then-undo correctly stays valid, because what is being committed
*is* what was reviewed. A false ✓ is the dangerous direction, so the check is tied to
what is actually on disk rather than to what the harness happened to notice.
wrong before this was reduced to `git write-tree`.) Any change to *included* content,
made by any tool and present when the hook runs, invalidates — `.context/` and
untracked ignored paths are excluded by design (a *tracked* file still counts even if it
matches `.gitignore`). An unstaged edit-then-undo still matches, because it restores
the fingerprint — which says the content is unchanged since the review, not that Codex
read it: the hook compares a fingerprint of disk while the reviewer reads a git range
(the hook's own hard-floor comment, above `floor=3`, says the same). A false ✓ is the
dangerous direction,
so the check is tied to the effective index plus the included worktree content as of
the hook's invocation — a superset of any one commit's payload, chosen so the gate errs
toward firing — rather than to what the harness happened to notice. (A mutation after
that invocation, such as the compound `printf x > f && git commit -am y`, is still
unseen — a separate, parked defect, not this one.)

Two consequences worth knowing:

Expand Down
8 changes: 5 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ skipping locally only postpones the red.
**7. Gate B on the diff.** Claude makes a `WIP:`-prefixed commit (gives Codex a
range to read; the hook knows WIP doesn't end the cycle), then loops
`mcp__codex__review` the same way: three passes, final clean. Verification is by
**content** — any file change after the last review, even from a formatter, flips
it back to unsatisfied (staging alone doesn't; `git add` changes no content). On
`✓ Codex Gate B satisfied (3/3 cycle, 3 on current code)`, the real commit replaces
**content** — any change to included content present when the hook runs, even from a
formatter, flips it back to unsatisfied; `.context/` and untracked ignored paths are
excluded, and staging counts, because the fingerprint covers the index and that is what
a commit carries. On
`✓ Codex Gate B satisfied (3/3 cycle, 3 on current fingerprint)`, the real commit replaces
the WIP via `git commit --amend`.

**8. PR and bots.** Open the PR as usual; once the bots have commented, run
Expand Down
3 changes: 3 additions & 0 deletions docs/hardening-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ escape `\|`, one line), `source` (gate-a|gate-b|bot|manual),
| 2026-07-18 | unverified-enforcement-claim | six citable claims across one spec, its plan and its diff that something was enforced/caught/guaranteed where no mechanism did it — each caught by a gate, none by the author, one written into the same document that records the pattern | gate-a | major | P std | docs/prompt-standards.md item 11 (+ the same item in the workflow-init inline template). Mixed provenance: four from Gate A on the spec, one from Gate A on the plan, one from Gate B on the pre-merge diff; `source` records Gate A as the majority and the trigger |
| 2026-07-18 | docs-drift | a Gate-B fix reordered a precedence rule and added a terminal state in process-pr-review; the approved spec was never updated and disagreed until a PR bot found it | bot | major | P std | CLAUDE.md §5 Gate B, "a fix that changes specified behaviour updates the spec in the same commit" (+ the workflow-init inline template). Escalated from the 2026-07-18 `1 prose` row, whose AGENTS.md rule was scoped to manifest claims and could not reach spec-vs-implementation drift |
| 2026-07-19 | artifact-version-not-bumped | plugin changes merged without a version bump, so installed copies silently stayed stale — a machine ran 0.1.0 while main was at 0.4.0, the 0.4.0 bump had to be requested during PR #4, and main still carried two un-released plugin commits when this was written | manual | major | 2 lint | scripts/check-version-bump.sh + .test.sh, wired PR-only in ci.yml; AGENTS.md invariant 12. It verifies that a bump is PRESENT, not that it is right. It does not check: semantic correctness (a patch where a minor was due passes); direction (any different string passes, including a decrease); anything outside a pull_request event (a direct push to main bypasses it entirely); deletion of an entire plugin directory; any change to which directory the marketplace entry points at (rename, copy, or `source` repoint); two PRs branched from the same version each bumping to the same new one; and whether the version was ever released or tagged |
| 2026-07-19 | false-negative-gate | both Gate-B hash components described the worktree while `git commit` commits the index, so staging a change and reverting the file on disk reported satisfied on unreviewed content | gate-b | blocker | 4 test | plugins/dev-workflow/hooks/codex-gate.test.sh sections 24-30 + AGENTS.md invariant 3 (amended to name all three components). What the tests DO cover: the index component exists and moves on divergence, the failure marker never self-matches, and the ambient-alternate-index and three-way-`.context` states. What they do NOT: seven of `tree_hash`'s failure seams have no targeted test — `mktemp -d`, the non-symbolic unresolvable-HEAD branch, the throwaway-index `git rm --cached`, each `write-tree` separately, `git add -A`, and the buffered-stream redirect (enumerated in todos.md by derivation after three attempts understated it from memory); every divergence shape (they pin the mechanism, not exhaustive enumeration); mutation after the PreToolUse event, which is the parked compound-command row; command-local retargeting (`GIT_INDEX_FILE=`/`-C`/`--git-dir`), split to the 2026-07-19 command-retargeting-guard story; and a `git add`/`write-tree` failure inside the throwaway index, which stays parked |
| 2026-07-19 | unverified-enforcement-claim | across four Gate-B rounds on the index-tree documentation task, prose repeatedly claimed more than the hook guarantees, and each fix introduced a subtler version of the same overclaim — "the tree-hash proves what was reviewed", then "what is being committed is what was reviewed", then "what a commit would actually carry", then "everything a commit could carry"; every round searched for the previous PHRASE rather than the CLAIM, so a synonym survived each time | gate-b | major | 1 prose | AGENTS.md Don'ts, "Never describe what a gate proves without checking what it actually compares" — a rule plus a by-meaning grep recipe, sibling to the existing manifest-claims rule. Deliberately NOT another banned phrase: adding one phrase per round is the same rung a fifth time. What it does NOT do, in the rule text as well as here: nothing runs it in CI (a human runs it), and an overclaim phrased without those totality words escapes it. It raises the floor; it does not close the class |
| 2026-07-20 | verification-masks-failure | the plan's mutation-verification steps read `sh …codex-gate.test.sh 2>&1 \| grep -cE '^FAIL'`, which reports the GREP's status, not the runner's — a suite that died part-way, or exited non-zero without printing `FAIL`, would read as 0 and be recorded as green. Found by CodeRabbit on PR #8, in the very steps used to prove this branch's guards were load-bearing | bot | major | 1 prose | plan Task 1 Step 3 + the two later mutation steps now capture `exit=$?` separately into a variable and RETURN success only when the status and the FAIL count are both zero (per-run `mktemp` file, not a shared path), with the reason stated inline. An earlier revision of this row said they "assert both" when they only printed both and still returned grep's status — caught by Gate B, in the row recording this very class. Scope of the fix, plainly: it corrects the three sites in this plan and the rationale a future plan author reads — nothing checks new plans for the same shape, so a plan written tomorrow can reintroduce it |
Loading