Skip to content

Gate-B fingerprint covers the index, not just the worktree (0.5.0)#8

Merged
dsnger merged 29 commits into
mainfrom
gate-b-index-tree
Jul 20, 2026
Merged

Gate-B fingerprint covers the index, not just the worktree (0.5.0)#8
dsnger merged 29 commits into
mainfrom
gate-b-index-tree

Conversation

@dsnger

@dsnger dsnger commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes the Gate-B false-positive recorded in todos.md: git commit commits the index, but every fingerprint component described the worktree. Staging a change and then reverting the file on disk left the fingerprint unchanged, so the gate reported "satisfied" for content nobody reviewed.

Verified before the fix, in a throwaway repo:

printf 'v2\n' > app.ts && git add app.ts   # index: v2
printf 'v1\n' > app.ts                     # worktree: back to HEAD's bytes
# -> git diff HEAD empty, worktree tree unchanged, hash matches -> "satisfied"
# -> the commit carries v2

⚠️ Upgrading invalidates any in-flight review, once

The fingerprint's composition changed, so a fingerprint recorded by 0.4.x cannot match one computed by 0.5.0. The first Gate-B-applicable commit attempt after updating reports "cannot confirm". A single review pass clears it.

This is expected, not a bug — please don't read it as the hook having broken. (A WIP: commit or a docs-only commit bypasses the comparison and is unaffected.) Also in CHANGELOG.md under 0.5.0.

Staging now invalidates a review. git add of already-reviewed content changes the index tree, so the fingerprint changes. The committed bytes are unchanged, making this a false invalidation — accepted under invariant 2 ("loose in the firing direction"), and the reminder says staging alone can cause it. One clean pass clears it. This was a real decision, recorded in the spec: the previous behaviour was asserted by a test as though it were a principle, but it had only ever fallen out of an implementation choice.

What changed

  • Index tree as a third component, taken from the effective index (GIT_INDEX_FILE when set, else the git-dir index) before git add -A brings the throwaway index up to the worktree. A relative GIT_INDEX_FILE is normalized against $repo_root, because git resolves it against the repo top-level while the hook's cp resolves against its own cwd.
  • An uncomputable fingerprint now fails closed. The failure value is a constant that never matches — including against itself — replacing a date+PID nonce that could collide under PID reuse and report satisfied.
  • Three reminder messages rewritten. The stale branch is reached by five indistinguishable states, so it names the state ("cannot confirm") rather than asserting a cause; the satisfied branch claims fingerprint equality rather than that Codex read the bytes (it compares a fingerprint of disk; the reviewer reads a git range).
  • Documentation synced across nine sites; 0.5.0; two hardening-ledger rows.

Testing

180 assertions (was 145). New coverage: the divergence repro, ambient alternate-index shapes, three-way-divergent tracked .context, and fault injection for checksum, seed-copy, git-dir and git diff failures — each fault held across both the stored and the recomputed fingerprint, since a fault applied only at commit time proves nothing.

Load-bearing guards are mutation-verified: dropping the index write-tree, the -f on git rm --cached, the .context exclusion, the relative-index normalization, or the unavailable guard each turns a specific test red.

Known gaps, parked rather than hidden

  • 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, the buffered-stream redirect). Enumerated by derivation from the code — three earlier attempts understated it from memory.
  • Commands that retarget git (GIT_INDEX_FILE=, -C, --git-dir, …) can still commit content the hook never fingerprinted. Split to its own story with default-deny as the settled approach, after five successive blocklist bypasses showed that enumerating spellings loses.
  • The hook fingerprints disk while mcp__codex__review reads a git range, so staged-but-never-committed content can be fingerprinted as reviewed. Pre-existing; closing it would mandate a WIP commit per review.

Review record

Gate A: 15 passes on the spec, 3 on the plan. Gate B: every code commit, 14 rounds. Plus a whole-branch review that mutation-tested the hook (7 experiments, 2 survivors, both fixed). Final pass clean on both tracks.

Spec: docs/superpowers/specs/2026-07-19-gate-b-index-tree-design.md · Plan: docs/superpowers/plans/2026-07-19-gate-b-index-tree.md

Summary by CodeRabbit

  • New Features

    • Gate B now accounts for staged content when checking whether reviewed changes remain current.
    • Unavailable fingerprints now fail safely instead of being treated as valid matches.
    • Updated Gate B messages more clearly distinguish confirmed, unconfirmed, and unavailable states.
  • Documentation

    • Clarified fingerprinting rules, excluded content, staging behavior, and workflow guarantees across the documentation.
  • Chores

    • Updated the development workflow plugin to version 0.5.0.
    • Expanded validation coverage for review invalidation and failure scenarios.

dsnger added 25 commits July 19, 2026 15:44
Spec: Gate A clean at pass 15 (~108 findings; the command-retargeting guard
was split to its own story at pass 13). Plan: Gate A clean at pass 3.
Replace the date+PID failure nonce with a constant `unavailable` marker and
enforce never-matching at the comparison sites, so two failing invocations
cannot compare equal and report satisfied. Buffer the component stream and
checksum it only on full success, so a partial stream is never hashed.
git commit commits the INDEX, but both hash components described the
worktree, so staging a change and reverting the file on disk reported Gate B
satisfied on unreviewed content. Add a tree id taken from the effective index
before the throwaway index is brought up to the worktree.

Staging already-reviewed content now invalidates a review — a false
invalidation, accepted under invariant 2. Test 3f asserted the opposite as a
principle and is rewritten to assert the decision.
A prefix assignment on a SHELL FUNCTION call (rev/commitpre) persists in the shell after the call returns, unlike the same prefix on an external command. GIT_INDEX_FILE leaked out of section 27 and corrupted section 28s three-way .context divergence fixture, silently defeating its guard: mutating the hooks git rm -rfq --cached to -rq left the suite fully green.

Fixes: wrap the three leaking calls in a subshell so the assignment cannot escape; add a guard assertion before section 28 asserting GIT_INDEX_FILE is unset, so a future regression fails loudly; fix a related snapshot-ordering flakiness in section 28s real index bytes unchanged assertion (index snapshot was taken before two read-only git commands that can trigger a racy-clean stat-cache rewrite). Updated the plan (Task 2 Step 2, section 27) to match and to name the trap.
The shell-side eff_index test/cp resolved a relative GIT_INDEX_FILE
against the hook cwd, while every git call used -C repo_root; git
itself resolves it against the top-level. From a subdirectory this
silently took the absent-index carve-out and hashed a constant empty
tree, a false Gate B satisfied. Normalize eff_index against repo_root.

Also contains five PATH=... rev leaks in codex-gate.test.sh section 24
(prefix assignment on a shell function persists after return in POSIX
sh, same class as the GIT_INDEX_FILE leak fixed earlier for section
27) and adds a guard asserting PATH is restored after the section.
The stale branch is reached by five distinct states, so it names the state
(cannot confirm) rather than claiming the tree changed. The empty-state branch
admits a fingerprint that failed to write or read. The satisfied branch drops
'actually reviewed what you are committing' for fingerprint equality: the hook
hashes disk, the reviewer reads a git range.
…ality and systemMessage clauses in Gate B tests

Finding 1: the empty-state branch carried two overlapping comments (old + new); merged into one that attributes the state to an absent fingerprint. Finding 2: the satisfied-message test asserted only the bare word fingerprint; now pins the actual equality clause plus its other required clauses. Finding 3: the stale-message test never pinned the systemMessage; added an exact-match assertion and an absence check for the old causal label. Plan doc corrected to match.
…ison

Per-clause greps only catch a regression someone thought to enumerate; a
meaning-reversing edit (inserting "do not ", swapping "Usually" for
"Always", appending an unasserted causal claim) stayed green. Compare the
complete additionalContext and systemMessage against a fixture instead, with
$passes/$floor/$fresh pinned deterministically per branch. Also qualifies
the empty-state comment: a failed REPLACEMENT write preserves the older
fingerprint and lands in the stale branch, not this one - only a first
write with nothing to fall back to reaches empty-state. Plan doc updated to
match the implemented test code.
Amend invariant 3 to name all three components and why the index one exists.
Correct every site that described the hash as worktree-only or claimed the
tree-hash proves what was reviewed, and settle the story's open questions.
Task 4's own comment block still said "BOTH components" and enumerated
only the tracked diff and worktree tree, though the fingerprint now has
three (Gate B finding, spec+quality passes both flagged it).
Gate B (pass 2, both spec and quality) flagged "what a commit would
actually carry" as an overclaim: spec sec:2 already narrows this to
"the index as it stands when the hook is invoked" and calls out the
compound-command mutation as a separate, parked defect. Apply that
same scoping everywhere the unqualified claim was made.
Gate B found the same unverified-enforcement-claim that Task 3 removed from the
satisfied message: "any change by any tool invalidates" is false (.context/ and
ignored paths are excluded, post-invocation mutations are unseen), and "what is
being committed is what was reviewed" claims the hook cannot support - it compares
a fingerprint of disk while the reviewer reads a git range. Also reflows an
AGENTS.md line break that split an inline code span.
That phrasing was my own replacement for an earlier overclaim, and it is one too.
The fingerprint covers the effective index PLUS untracked and unstaged worktree
content; a plain git commit carries only the index. It is a deliberate superset so
the gate errs toward firing, and the prose now says that at all three sites
(README, the hook header, architecture.md). Also: only UNTRACKED ignored paths are
excluded - a tracked file counts even if it matches .gitignore - and
getting-started no longer promises that any file change invalidates.
Gate B found the same overclaim in two spellings my greps had missed:
"everything a commit could carry" (AGENTS.md invariant 3) and "everything that
could end up in a commit" (the hash comment). Both now use the canonical wording.

Fourth round of this defect on one task, and the cause each time was searching
for a phrase rather than the claim: I grepped "what a commit would carry" and
missed two synonyms. Found this round by sweeping for totality words near
commit/fingerprint/hash instead.
getting-started.md showed "3 on current code"; the hook emits "3 on current
fingerprint" since Task 3 renamed it. A doc quoting output the tool does not
produce is worse than vague prose - a reader compares it against their terminal.

Also retires the last comments equating fingerprint coverage with commit payload.
Not changed: the shipped satisfied message. Its "(unchanged since that review)"
modifies the FINGERPRINT, which is exactly what equality proves, and the text is
pinned in the approved spec and its golden-fixture test.
The fresh count tracks passes carrying the CURRENT fingerprint; it does not
establish that Codex read those bytes. Phrase now gone repo-wide.
…im rule

Gate B on Task 6 found both. The narrowed tree-unavailable row and the ledger
row listed only git add/write-tree as untested seams; mktemp -d and the
throwaway-index git rm --cached are untested too. Both now list all four.

The recurrence row used rung=pending with a ref pointing at no todos.md item,
breaking the ledger contract. The rung is now real: a Dont in AGENTS.md, sibling
to the existing manifest-claims rule, pairing a stated rule with a by-meaning
grep recipe and saying plainly that nothing runs it in CI.

Also records a checker false positive found by real use: check-invariants.sh
greps the working tree, so gitignored scratch quoting a violating pattern fails
it. Writing that row literally reproduced the bug, which is noted in the row.
Final whole-branch review, which mutation-tested the hook and found two mutants
surviving.

Finding 1 (Important): the approved spec pinned a tree_hash() snippet that omits
the relative-GIT_INDEX_FILE normalization added later at Gate B (92a23f0). That is
CLAUDE.md 5s "a fix that changes specified behaviour updates the spec in the same
commit" - the docs-drift class this repo records against itself - so the snippet
and its rationale are now in the spec.

The untested-seam list is rewritten by DERIVING it from the code: four seams have
stubs, seven do not, and the two write-tree calls are distinct sites. Three earlier
versions enumerated it from memory and understated it every time; the row now says
so. The ledger also records that only the -f flag of the .context removal is
mutation-covered, not the removal itself.

Nits: architecture.md no longer cites a hardcoded hook line number (it had already
drifted), and getting-started no longer stacks two parentheticals.
…ontext exclusion, the inert unavailable guard on a second unhashable pass, and section 24 state leaks (stub_dir, REAL_GIT)
Final Gate B. The state-leak fix guarded PATH and REAL_GIT but not the temp
directory itself - dropping `rm -rf "$stub_dir"` left every assertion green, the
same partial fix one more time. Now mutation-verified.

The ledger row still said the index-component .context exclusion was untested;
a85395f added that test, so the record was stale within its own task. The seam
inventory said FOUR tested shapes while naming five.

Not changed: invariant 12. Gate B reported it violated, but it diffed against an
intermediate commit; CI runs check-version-bump on pull_request with the PRs
base_ref, and against the real merge-base with main the check passes.
…tion

Same class, second occurrence: closing this story the battery again had to be
run with .superpowers/sdd/ moved aside by hand. Not a one-off confusing red but
a remembered manual step on a command AGENTS.md presents as what CI runs.
Recorded as a candidate note toward the rows eventual escalation trigger, not
acted on - the row stays parked.
@cursor

cursor Bot commented Jul 20, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dsnger, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82a234ac-6330-4798-a4e3-66370d60a85d

📥 Commits

Reviewing files that changed from the base of the PR and between 01051d2 and dfa8167.

📒 Files selected for processing (4)
  • AGENTS.md
  • docs/hardening-log.md
  • docs/superpowers/plans/2026-07-19-gate-b-index-tree.md
  • plugins/dev-workflow/hooks/codex-gate.test.sh
📝 Walkthrough

Walkthrough

Gate-B now fingerprints the effective index and included worktree content, fails closed when hashing is unavailable, detects staged/worktree divergence, updates messages and tests, and aligns specifications, documentation, plugin metadata, and backlog records.

Changes

Gate-B index-tree fingerprinting

Layer / File(s) Summary
Fingerprint computation and Gate-B verdicts
plugins/dev-workflow/hooks/codex-gate.sh
tree_hash() now combines tracked content, effective-index, and worktree tree data, returns unavailable on failure, and updates freshness and reminder comparisons.
Failure, divergence, and message validation
plugins/dev-workflow/hooks/codex-gate.test.sh
Tests cover hashing failures, unborn repositories, index/worktree divergence, alternate indexes, .context/ exclusion, exact messages, and unavailable state storage.
Implementation design and acceptance criteria
docs/superpowers/plans/..., docs/superpowers/specs/..., docs/superpowers/stories/...
Plans and specifications define the fingerprint algorithm, failure behavior, message contracts, test matrix, acceptance criteria, and related parked command-retargeting work.
Invariant, documentation, and release updates
AGENTS.md, README.md, docs/..., plugins/dev-workflow/..., todos.md
Documentation and invariants describe index-inclusive fingerprinting; release metadata, hardening records, workflow guidance, and backlog entries are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Hook as codex-gate.sh
  participant Git
  participant Index as Throwaway index
  participant State as Gate-B state
  Hook->>Git: Resolve effective index and repository data
  Hook->>Index: Build index and worktree trees
  Index->>Git: Write tree identifiers
  Hook->>State: Compare fingerprint and update freshness
  State-->>Hook: Emit satisfied or unconfirmed message
Loading

Possibly related PRs

Poem

I’m a bunny counting trees in a row,
Index and worktree, watch them grow.
If hashes fail, I thump “unknown!”
No false match shall ever be won.
Docs and tests hop neatly in line—
Gate-B’s little burrow now shines.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: Gate-B now fingerprints the index in addition to the worktree.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/architecture.md (1)

76-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix hard line wrap.

The sentence is prematurely wrapped, leaving a very short line for dangerous direction,. Reflowing the paragraph would improve readability.

♻️ Proposed fix
-(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 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
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture.md` around lines 76 - 78, Reflow the paragraph in
docs/architecture.md around “A false ✓ is the dangerous direction” so the
sentence is wrapped at a more natural width, avoiding the short standalone line
while preserving the wording and meaning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 206: Update the reverse-order regex alternation in the documented grep
command to include “any change” alongside “everything,” “anything,” and “all
content,” preserving the existing forward-order patterns and exclusions.

In `@docs/superpowers/plans/2026-07-19-gate-b-index-tree.md`:
- Around line 517-518: Update the documented test checks around the
codex-gate.test.sh invocations, including the checks at the referenced later
sections, to capture the runner’s exit status separately from the grep failure
count. Assert that the script exits successfully and that the count of lines
matching ^FAIL is zero, rather than relying on the grep pipeline’s status.

---

Nitpick comments:
In `@docs/architecture.md`:
- Around line 76-78: Reflow the paragraph in docs/architecture.md around “A
false ✓ is the dangerous direction” so the sentence is wrapped at a more natural
width, avoiding the short standalone line while preserving the wording and
meaning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b83a610-ef91-430c-9199-4fc011f30f17

📥 Commits

Reviewing files that changed from the base of the PR and between c3f2e8f and 01051d2.

📒 Files selected for processing (15)
  • AGENTS.md
  • README.md
  • docs/architecture.md
  • docs/getting-started.md
  • docs/hardening-log.md
  • docs/superpowers/plans/2026-07-19-gate-b-index-tree.md
  • docs/superpowers/specs/2026-07-19-gate-b-index-tree-design.md
  • docs/superpowers/stories/2026-07-18-gate-b-hash-staged-worktree-divergence-story.md
  • docs/superpowers/stories/2026-07-19-command-retargeting-guard-story.md
  • plugins/dev-workflow/.claude-plugin/plugin.json
  • plugins/dev-workflow/CHANGELOG.md
  • plugins/dev-workflow/commands/workflow-init.md
  • plugins/dev-workflow/hooks/codex-gate.sh
  • plugins/dev-workflow/hooks/codex-gate.test.sh
  • todos.md

Comment thread AGENTS.md Outdated
Comment thread docs/superpowers/plans/2026-07-19-gate-b-index-tree.md Outdated
dsnger added 2 commits July 20, 2026 08:51
CI caught what macOS hid: `cmp` on .git/index failed on Linux. Byte-identity is
not a property the hook guarantees - git rewrites the index stat cache during
ordinary read-only operations, and the hook runs `git diff HEAD`. The earlier
comment blamed placement, but moving the snapshot could not fix it either.

`git ls-files --stage` compares mode, object id, stage and path for every entry,
which is the property actually claimed - the hook must not change what the users
index MEANS - and it is immune to benign stat-cache rewrites. Strictly stronger
than the byte check on entry coverage. Mutation-verified: dropping GIT_INDEX_FILE
from the hooks `git rm --cached` so it hits the real index turns it red.
Both verified by direct probe, by me rather than a triage subagent: this PR
touches AGENTS.md, plugins/ and commands/, so process-pr-reviews
instruction-path precheck skips delegation.

1. The overclaim grep recipe was asymmetric - its reverse-order arm omitted
   `any change`, so "the fingerprint guarantees any change" escaped. Probed
   before and after; now caught. This strengthens the rung recorded in the
   2026-07-19 unverified-enforcement-claim row rather than needing a new one.

2. `... | grep -cE "^FAIL"` reports the greps status, not the test runners, so
   a suite that died part-way would read as 0 failures and be logged green -
   in the steps used to prove this branch guards were load-bearing. All three
   sites now capture exit= separately and assert both. New ledger row.
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes the Gate-B false-positive where git commit commits the index but the fingerprint only described the worktree: staging a change and reverting the file on disk left the fingerprint unchanged, so the gate reported satisfied on content nobody reviewed. It adds the index tree as a third component in tree_hash(), replaces the date+PID failure nonce with a constant unavailable marker (fail-closed, never self-matching), and rewrites the three reminder messages to name the state rather than assert a cause.

  • codex-gate.shtree_hash() now emits three components into a buffered stream (tracked diff, index tree SHA, worktree tree SHA); on any failure the function returns the literal unavailable, and the comparison sites and fresh-count handler each guard explicitly against it. A relative ambient GIT_INDEX_FILE is normalised against $repo_root before the shell-side [ ! -e ] and cp calls.
  • codex-gate.test.sh — 180 assertions (was 145): the staged-vs-worktree divergence repro (section 26), ambient alternate-index shapes including relative-path normalisation (section 27), three-way .context divergence (section 28), five fault-injection shapes spanning both fingerprint sites (section 24), golden-fixture comparisons for all three reminder branches (section 29), and unstorable-state shapes (section 30).
  • Documentation — nine files updated (AGENTS.md, README, architecture, getting-started, hardening-log, CHANGELOG, workflow-init, specs, plans); version bumped to 0.5.0.

Confidence Score: 4/5

The hook implementation is correct and safe to merge; the only concern is a stale grep pattern in the test suite that fails on non-GNU-grep systems.

The core implementation is sound: three-component buffered stream, unavailable fail-closed contract, relative GIT_INDEX_FILE normalisation, and -f flag for three-way divergence are all correctly written and mutation-verified. The single finding is that a grep pattern in test 18 had its first alternative rendered stale by this PR's message rewording, leaving the test dependent on a GNU grep BRE extension on non-Linux systems. The hook runtime itself is unaffected.

plugins/dev-workflow/hooks/codex-gate.test.sh line 415 — grep pattern for the Finding 9 fresh-count assertion needs its first alternative updated to the new message wording.

Important Files Changed

Filename Overview
plugins/dev-workflow/hooks/codex-gate.sh Core fix is correct: three-component buffered stream, unavailable marker fail-closed at both comparison sites and the fresh-count handler, relative GIT_INDEX_FILE normalisation, and -f flag on git rm --cached for three-way divergence. No logic errors found.
plugins/dev-workflow/hooks/codex-gate.test.sh Comprehensive coverage with 180 assertions; one pre-existing grep pattern at line 415 became portability-fragile in this PR: the first alternative "1 cover the CURRENT tree" no longer appears in any message and the test now depends on | as BRE alternation (a GNU grep extension) to match via the second alternative.
plugins/dev-workflow/CHANGELOG.md 0.5.0 entry is accurate and complete, covering all four behaviour changes with correct descriptions of the upgrade invalidation and false-invalidation trade-off.
plugins/dev-workflow/.claude-plugin/plugin.json Version bumped to 0.5.0 as required by invariant 12.
AGENTS.md Invariant 3 updated to name all three fingerprint components; new Don't added about not overstating what a gate proves, including a by-meaning grep recipe rather than another banned phrase.
docs/architecture.md Accurately describes the three-component fingerprint (tracked diff, effective-index tree, worktree tree), the false-invalidation on staging, and the limitation that the hook fingerprints disk while the reviewer reads a git range.
docs/hardening-log.md Two new rows correctly record the false-negative-gate finding and the repeated unverified-enforcement-claim pattern, with accurate accounts of what the new tests cover and explicitly do not cover.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as PostToolUse review
    participant T as tree_hash
    participant S as gateB state file
    participant P as PreToolUse commit

    R->>T: compute fingerprint
    T->>T: git diff HEAD tracked diff
    T->>T: cp eff_index to tmp_index
    T->>T: git rm --cached -f .context
    T->>T: git write-tree INDEX tree SHA
    T->>T: git add -A excluding .context
    T->>T: git write-tree WORKTREE tree SHA
    T->>T: checksum buffered stream
    T-->>R: h or unavailable
    R->>S: store h update freshCount
    P->>S: read reviewed fingerprint
    P->>T: compute current fingerprint
    T-->>P: current or unavailable
    alt current or reviewed is unavailable
        P-->>P: STOP cannot confirm
    else reviewed not equal current
        P-->>P: STOP cannot confirm
    else passes below floor
        P-->>P: WARN below floor
    else all checks pass
        P-->>P: Gate B satisfied
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant R as PostToolUse review
    participant T as tree_hash
    participant S as gateB state file
    participant P as PreToolUse commit

    R->>T: compute fingerprint
    T->>T: git diff HEAD tracked diff
    T->>T: cp eff_index to tmp_index
    T->>T: git rm --cached -f .context
    T->>T: git write-tree INDEX tree SHA
    T->>T: git add -A excluding .context
    T->>T: git write-tree WORKTREE tree SHA
    T->>T: checksum buffered stream
    T-->>R: h or unavailable
    R->>S: store h update freshCount
    P->>S: read reviewed fingerprint
    P->>T: compute current fingerprint
    T-->>P: current or unavailable
    alt current or reviewed is unavailable
        P-->>P: STOP cannot confirm
    else reviewed not equal current
        P-->>P: STOP cannot confirm
    else passes below floor
        P-->>P: WARN below floor
    else all checks pass
        P-->>P: Gate B satisfied
    end
Loading

Comments Outside Diff (1)

  1. plugins/dev-workflow/hooks/codex-gate.test.sh, line 415 (link)

    P2 The first alternative in this grep pattern is now dead. This PR changed the satisfied message from "cover the CURRENT tree" to "cover the CURRENT content fingerprint", so 1 cover the CURRENT tree matches nothing. The test now depends on \| being treated as alternation in BRE mode — a GNU grep extension that is not available on BSD/macOS systems with a strict POSIX grep. Before this PR the first alternative matched on all systems; now \| is load-bearing.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: plugins/dev-workflow/hooks/codex-gate.test.sh
    Line: 415
    
    Comment:
    The first alternative in this grep pattern is now dead. This PR changed the satisfied message from `"cover the CURRENT tree"` to `"cover the CURRENT content fingerprint"`, so `1 cover the CURRENT tree` matches nothing. The test now depends on `\|` being treated as alternation in BRE mode — a GNU grep extension that is not available on BSD/macOS systems with a strict POSIX grep. Before this PR the first alternative matched on all systems; now `\|` is load-bearing.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
plugins/dev-workflow/hooks/codex-gate.test.sh:415
The first alternative in this grep pattern is now dead. This PR changed the satisfied message from `"cover the CURRENT tree"` to `"cover the CURRENT content fingerprint"`, so `1 cover the CURRENT tree` matches nothing. The test now depends on `\|` being treated as alternation in BRE mode — a GNU grep extension that is not available on BSD/macOS systems with a strict POSIX grep. Before this PR the first alternative matched on all systems; now `\|` is load-bearing.

```suggestion
printf '%s' "$out" | grep -q 'of which 1 cover the CURRENT content fingerprint' && pass "fresh count reports only 1 pass covers current fingerprint (Finding 9)" || fail "fresh count reports only 1 pass covers current fingerprint (Finding 9)"
```

Reviews (1): Last reviewed commit: "test: compare real-index ENTRIES, not by..." | Re-trigger Greptile

dsnger added 2 commits July 20, 2026 09:00
All three valid, all mine.

1. The grep recipe false-positived on its own repo: `commit` matches `committed`,
   so an unrelated adoption comment hit. Word boundaries added, with a note
   saying why they are load-bearing.
2. The three verification sites shared a fixed /tmp/suite.out, so concurrent runs
   could read each others results. Per-run mktemp now, and the command RETURNS
   success only when the runner status and the FAIL count are both zero rather
   than printing them to eyeball. Verified negatively too: a runner that exits 3
   without printing FAIL now reports failure, where the old idiom said green.
3. The ledger row claimed those commands assert both values when they only
   printed them - an unverified-enforcement-claim inside the row recording that
   very class. The commands now do assert, so the claim is true rather than
   softened, and the row records the miss.
Greptile, PR #8: the Finding-9 assertion at test.sh:415 had a stale first
alternative - this PRs message rewrite renamed "cover the CURRENT tree" to
"CURRENT content fingerprint", so it matched nothing and the assertion rested
entirely on `\|`, which is alternation only under GNU-style BRE and literal
under POSIX. It would have failed on a stock BSD grep (invariant 4: machines we
do not control). Now a -F match on the real wording.

Notable: this passed locally only because this machines `grep` is ugrep 7.5.0,
not BSD grep - so earlier BSD-grep conclusions in this session were made on the
wrong implementation. CI (GNU) hid it too.

Three pre-existing BRE alternations in the same file converted to -E. Small,
local to a file this PR already touches.

Also Gate B MINOR: the plans Expected: lines still described count-only output
after the commands began asserting both values.
@dsnger

dsnger commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Greptile: accepted and fixed — good catch, and it was hiding behind two layers.

codex-gate.test.sh:415 asserted grep -q '1 cover the CURRENT tree\|of which 1'. This PR renamed that message to "cover the CURRENT content fingerprint", so the first alternative matched nothing and the assertion rested entirely on \| — alternation under GNU-style BRE, literal under POSIX. On a stock BSD grep the whole pattern is a literal string that never matches, and the test fails. Invariant 4 is explicit that this runs on machines we do not control.

Two reasons it stayed invisible:

  • CI runs GNU grep, so \| works there.
  • Locally it passed because this machine's grep is ugrep 7.5.0, not BSD grep — which also means some earlier "BSD grep" reasoning in this session was done against the wrong implementation. Worth knowing for anyone reading the parked check-invariants.sh row, which has a competing BSD-grep theory in it.

Fixed by pinning the real wording with grep -qF 'of which 1 cover the CURRENT content fingerprint' — no regex dialect involved at all. The three other pre-existing \| BRE alternations in the same file are converted to -E; pre-existing, but one character each in a file this PR already touches, so fixed here rather than deferred.

Validated by me directly rather than by a triage subagent: this PR touches AGENTS.md, plugins/ and commands/, which trips the instruction-path precheck in /dev-workflow:process-pr-review.

Also in the same commit, from Gate B: the plan's Expected: lines still described count-only output after those commands began asserting both the runner status and the FAIL count.

Suite: exit=0 FAIL=0 (180 assertions).

@dsnger
dsnger merged commit f5548dc into main Jul 20, 2026
2 checks passed
@dsnger
dsnger deleted the gate-b-index-tree branch July 20, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant