Skip to content

fix(B4): the roadmap remedy had zero callers, and the hook that would run it exits 2 on its own line 4 - #3048

Open
noahgift wants to merge 3 commits into
mainfrom
bse/roadmap-precommit
Open

fix(B4): the roadmap remedy had zero callers, and the hook that would run it exits 2 on its own line 4#3048
noahgift wants to merge 3 commits into
mainfrom
bse/roadmap-precommit

Conversation

@noahgift

@noahgift noahgift commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Ticket#3047 (bse-001, needs-pmat-id — BSE mints no pmat ids). Refs PMAT-980, G-6, #2874.

Claim — B4 (roadmap churn). Two defects, both "a result nothing measured": the repo's
own remedy had zero callers, and the hook that would call it could not run.

Measured before (basis=)

guard-tree is red on #3025, instance A's only MERGEABLE open PP-066 PR, for one entry:

basis=$ bash scripts/check_roadmap_diff_additive.sh $(git merge-base origin/main pr3025) pr3025
VIOLATION reserialised: id=PMAT-1077 (bytes differ, no field actually changed)
reserialised=1 first_ids=PMAT-1077
roadmap-diff: base=818 head=824 added=6 lifecycle=36 reserialised=1 deleted=0

guard-tree failed: failuregate exit 1 (run 34200927021, jobs 101979274909 / 101989986640).

basis=$ git grep -n 'roadmap_trim' origin/main
contracts/apr-roadmap-additive-diff-v1.yaml  4 hits — prose + a `remedy:` field
docs/audits/impl-PMAT-980-receipt.md         2 hits — prose
scripts/check_roadmap_diff_additive.sh:25,421  the RED footer's own text
scripts/roadmap_trim.py                        its own docstring
→ ZERO call sites in a hook, a Makefile target or a workflow.
basis=$ ls -l /bin/sh ; /bin/sh .githooks/pre-commit ; echo $?
/bin/sh -> dash
.githooks/pre-commit: 4: set: Illegal option -o pipefail
2

#!/bin/sh + set -euo pipefail. Following the install line in the hook's own header
refused every commit before cargo fmt — an unpassable gate, which is why
core.hooksPath on this box points at a hand-installed chain instead.

Change

  • check_roadmap_diff_additive.sh --staged judges the INDEX. git write-tree makes the
    staged content a real tree object; roadmap_diff.py already resolves any ref via
    git show <ref>:<file>. So this is the identical rule set CI applies, against exactly the
    bytes about to be committed — one implementation, two entry points. A second copy of the rule
    list here is precisely how bashrs#266 happened.
  • The base is plain merge-base(origin/main, HEAD), not resolve_base, and the reason is
    written at the divergence: resolve_base's push-shape branch exists because a commit judged
    against itself passes vacuously, which cannot arise when the head is the index tree. On a
    branch freshly cut from main it returns the tip's PARENT and attributes main's own entries to
    this commit.
  • .githooks/pre-commit: bash shebang; roadmap guard first (cheapest check, costliest miss).
    rc 2 = "this box cannot judge" → warn, do not block: guard-tree still refuses, so nothing
    goes silently green.

Self-test (both polarities)

bash scripts/check_roadmap_diff_additive.sh --self-test22/22 rows (was 18).
Rows 19–22 are --staged, run in a scratch repo carrying the real scripts in a real layout —
REPO_ROOT comes from BASH_SOURCE, so the guard under test is genuinely this file running
somewhere it was not born, not a seam that exists only for tests. The scratch repos set
core.hooksPath=/dev/null: the case table judges this guard, not the developer's hook install.

row polarity
19 an appended entry in the index → PASS added=1
20 a re-serialisation in the index → FAIL, naming roadmap_trim.py
21 no staged roadmap → fast PASS, by name
22 HEAD is the origin/main tip → base is the TIP, asserted by sha, not its parent

Mutation (RED → GREEN)

Run locally; both mutations are caught, and by the row designed to catch them.

mutation effect verdict
M1 base ← resolve_base HEAD row 22 rc 0→1 (main's own re-serialisation attributed to this commit); rows 19,20 rc→2 SELF-TEST FAILED
M2 run_check … || true (the gate cannot fail) row 20 rc 1→0 SELF-TEST FAILED
restored 22/22 rows

Row 22 asserts the PASS and the printed base sha, so it cannot pass for the wrong reason.

Queries at the change site

The git grep roadmap_trim census and the dash probe are both quoted in the file headers they
justify — check_roadmap_diff_additive.sh's usage block and .githooks/pre-commit's shebang
comment — so the next reader who is tempted to "simplify" either one meets the measurement first.

Predicted after

A pmat work add re-serialisation is refused at git commit, naming the one-command remedy,
instead of surfacing as a red required check ~11 min after push. Six of eight open pp-066 PRs
carry roadmap mints and queue up behind this same wall as they rebase.

Writes

scripts/check_roadmap_diff_additive.sh, .githooks/pre-commit. No roadmap.yaml, no
workflow, no crates/, no PP-066 row file. Two files.

Known, and deliberately not fixed here

core.hooksPath on this box is /home/noah/src/aprender/.git/hooks-apr, an apr-agent chain that
execs the pmat-managed $GIT_DIR/hooks/pre-commit and never reads .githooks/. So this hook
is correct in the repo and inert on this machine until git config core.hooksPath .githooks
— which is the same defect class one layer up, and is why the audit doc records it as measured
rather than as done. Changing a developer's local hooks install is not a change to make
unilaterally inside a PR; guard-tree remains the authority either way.

Closes #3047.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EeUX4ymyt8XhjXbtTuNH8L

…run it exits 2 on its own line 4 (#3047)

`guard-tree` is red on PP-066 #3025 — instance A's only MERGEABLE open PR — for
one entry:

    $ bash scripts/check_roadmap_diff_additive.sh $(git merge-base origin/main pr3025) pr3025
    VIOLATION reserialised: id=PMAT-1077 (bytes differ, no field actually changed)
    roadmap-diff: base=818 head=824 added=6 lifecycle=36 reserialised=1 deleted=0

`pmat work add` re-serialises the whole roadmap on every call. This repo already
knows that, already detects it, and already ships the remedy — and the remedy had
ZERO call sites: every `git grep roadmap_trim` hit was prose (a contract field, a
receipt, this guard's own RED footer). Detection lived only in CI, so the loop was
commit → push → wait 11 min for guard-tree → read a footer → run trim → push again.

The hook that would have closed that loop could not run at all. `.githooks/pre-commit`
was `#!/bin/sh` with `set -euo pipefail`; /bin/sh is dash here, so following the
install line in its own header refused EVERY commit before a single check ran:

    $ /bin/sh .githooks/pre-commit ; echo $?
    .githooks/pre-commit: 4: set: Illegal option -o pipefail
    2

An unpassable gate is not a gate. It is why core.hooksPath on this box points
somewhere else entirely.

WHAT CHANGED

- `check_roadmap_diff_additive.sh --staged` judges the INDEX. `git write-tree`
  makes the staged content a real tree object and roadmap_diff.py already resolves
  any ref via `git show <ref>:<file>`, so this is the identical rule set CI applies,
  run against exactly the bytes about to be committed. One implementation, two entry
  points — a second copy of the rule list is how bashrs#266 happened.
- The base is plain merge-base(origin/main, HEAD), NOT resolve_base, and the reason
  is written at the divergence: resolve_base's push-shape branch exists because a
  COMMIT judged against itself passes vacuously, which cannot arise when the head is
  the index tree. On a branch freshly cut from main it would return the tip's PARENT
  and attribute main's own entries to this commit.
- `.githooks/pre-commit`: bash shebang, and the roadmap guard runs FIRST (cheapest
  check, costliest miss). rc 2 = "this box cannot judge" warns and does not block,
  because guard-tree still refuses — nothing goes silently green.

FALSIFIED, NOT ASSERTED

Rows 19-22 of the case table, both polarities, hermetic (the scratch repos set
core.hooksPath=/dev/null so the table judges this guard, not the developer's hook
install). 22/22.

Two mutations, each caught:
  M1 base <- resolve_base       → row 22 rc=0→1 (main's re-serialisation misattributed),
                                  rows 19,20 rc→2.  SELF-TEST FAILED.
  M2 `run_check ... || true`    → row 20 rc=1→0 (the gate cannot fail). SELF-TEST FAILED.
Restored: 22/22 rows.

Row 22 is the discriminating one: it asserts the PASS *and* that the printed base is
the origin/main tip, so it cannot pass for the wrong reason.

Closes #3047. Refs PMAT-980, G-6, #2874, #3025.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUX4ymyt8XhjXbtTuNH8L
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3048 head=3804b0c8b6c75c88eee874bfa4dff9cc0c161323 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

…dogfood

`pmat work add` was not used, for the two reasons this ticket exists:

1. It allocates from the roadmap it can see. Max across origin/main and every
   open pp-066 head is PMAT-1079, and three instances are minting concurrently,
   so an auto-mint races. 1090 leaves 1080-1089 as headroom. A gap is opaque; a
   collision is data loss (and 12 duplicate ids on stale heads is what turned
   `ci / security` red on #3031 and #3001).

2. It re-serialises the whole file — the defect this ticket fixes.

The new `--staged` mode judged its own commit, which is the point:

    $ bash scripts/check_roadmap_diff_additive.sh --staged
    === roadmap.yaml STAGED diff is additive: base=c04eda87d (merge-base(origin/main, HEAD))
        head=8e4ceee37 (index tree) ===
    roadmap-diff: base=818 head=819 added=1 lifecycle=0 reserialised=0 deleted=0
    PASS

    $ git diff --cached --stat
    docs/roadmaps/roadmap.yaml | 19 +++++++++++++++++++

added=1, reserialised=0, 0 deletions. That is the diff the rule asks for, and
`pmat work add` cannot produce it — which is why the remedy script exists and
why nothing calling it made the rule unenforceable in practice.

Refs #3047, PMAT-1090.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUX4ymyt8XhjXbtTuNH8L
@noahgift

noahgift commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

quorum-review (AD-04): three PASS — agreed

{
 "ticket": "PMAT-1090",
 "head": "6b90c14437ad98588b1fa3a9c5e79eb93ef94da4",
 "width": 3,
 "executor": "agy",
 "agreed": true,
 "lanes": [
  {
   "lane": 1,
   "verdict": "PASS",
   "findings": 0
  },
  {
   "lane": 2,
   "verdict": "PASS",
   "findings": 1
  },
  {
   "lane": 3,
   "verdict": "PASS",
   "findings": 0
  }
 ]
}

Three independent agy lanes judged the diff against PMAT-1090 and the receipt.
AGREED: lane 1=PASS, lane 2=PASS, lane 3=PASS. No lane raised a finding with
cited or measured grounding; the single asserted finding restates the ticket.

Refs #3047, PMAT-1090.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUX4ymyt8XhjXbtTuNH8L
@noahgift
noahgift enabled auto-merge September 8, 2026 12:15
@noahgift
noahgift added this pull request to the merge queue Sep 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bse-001 BSE-001 build-system enhancement / unblocker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B4: roadmap_trim.py has zero callers, and .githooks/pre-commit exits 2 on line 4 — #3025's required check is red for a cause no hook can catch

1 participant