Skip to content

bootstrap-pin: generate the pin, stop failing PRs that cannot fix it, and terminate - #78

Merged
bdelanghe merged 4 commits into
mainfrom
claude/bootstrap-pin-autobump
Jul 31, 2026
Merged

bootstrap-pin: generate the pin, stop failing PRs that cannot fix it, and terminate#78
bdelanghe merged 4 commits into
mainfrom
claude/bootstrap-pin-autobump

Conversation

@bdelanghe

@bdelanghe bdelanghe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #77, which went red on FRESHNESS and could not do anything about it. Branches from main, not from #77 — the two are independent, but the merge order matters (see the note on #77: this one first, then #77 goes green with no change to its diff).

Three commits, and the second and third are both cases of the first being wrong in a way only running it would show.

1. Generate the pin, and stop failing PRs that cannot fix it

PIN and the SUM_* digests are one atomic pair. SUM_* is content-addressed and knowable on a branch; PIN is a commit, and cannot name a merge commit before that commit exists. So every PR touching a fetched file is red on FRESHNESS with no honest fix — recording the new digests early just moves the red to INTEGRITY. #75 hit it, #77 hit it again, and an expected-red teaches people to ignore red. Worse: org-defaults.yml also runs on push to main, so main went red the moment such a PR merged and stayed red until a human hand-typed a follow-up.

  • gen-bootstrap-pin.mjs generates the pin and digests, and backs the gate's --check. register-mcp: register each repo's MCP servers at user scope #71 and bootstrap: verify fetched scripts before executing, and bump the pin #72 were both a human hand-copying 64 hex characters; the gate that caught them decided "correct" in code separate from the code that produces correct, which is the same bug one level up.
  • FRESHNESS is asserted on push and reported (t.diagnostic) on pull_request. INTEGRITY stays hard everywhere.
  • A pin job regenerates on push to main and opens the bump PR itself, on a fixed branch so consecutive pushes update one PR rather than queueing PRs that pin superseded commits. Not auto-merged: the org requires a review on the default branch, and a generator that writes the trust anchor is the last place to drop that.

2. It did not terminate

That commit claims the loop terminates "because the bump only edits .claude/README.md, which is not in the fetch set, so it cannot invalidate the pin it just wrote." Running the loop showed that is true and not sufficient:

round event before after
1 a fetched file changes on main bump PR opened bump PR opened
2 that bump PR merges bump PR opened silent
3 anything at all pushes to main bump PR opened silent

The README-is-not-fetched argument establishes that the content stays consistent across a bump. It says nothing about the commit id — and the commit id is what the rewrite keyed on. next === source was the only guard, and after a bump lands next always differs, because PIN names the new commit rather than the one before it. An automation that opens a PR on every push to the default branch is worse than the expected-red it was written to remove.

So the decision moves off commit identity and onto content. planBump writes only when the pin is wrong — INTEGRITY broken, or STALE against this tree. A pin that trails HEAD but still serves byte-identical files is correct, and correct is a no-op.

3. The pin job made the workflow unparseable, and CI showed green

The job in commit 1 never ran. Neither did schema, the job that runs every test in this repo. Its gh pr create --body "..." put prose at column 0 inside a run: | block scalar; a non-empty line at column 0 ends the scalar, and the --- rule then starts a new YAML document:

.github/workflows/org-defaults.yml, line 74, column 1
could not find expected ':'

The first push of this PR showed seven checks, every one green, with the test suite never executed. GitHub does not report an unparseable workflow as a failing check — it records a run named by the file's raw path, and the jobs inside simply do not happen. A break that removes your gates does not look like a break; it looks like success with fewer lines.

  • The bump PR body moves to .github/bootstrap-pin-bump.md, passed with --body-file. Prose wants blank lines, a --- rule and a leading _, none of which survive inline in a block scalar — and keeping it in a file makes the body reviewable as prose, which is what it is.
  • workflows.test.mjs asserts, across all twelve workflows, that nothing but a top-level key or a comment starts at column 0. Not a YAML parser: node has none built in, this repo's tests take no dependencies, and the one structural property that broke is exactly checkable.

It runs from schema, i.e. from inside org-defaults.yml, so it fully covers the other eleven and covers this one only before a push. The other half is that schema is a required check — one that never reports blocks the merge, which is what catches this file being already broken on a branch. Neither half is sufficient alone; that is written into the test's header rather than left to be rediscovered.

Verification

  • 71/71 across org-defaults, workflows, session-start-dispatch, register-mcp, gen-bootstrap-pin, bootstrap-pin.
  • gen-bootstrap-pin.test.mjs walks the four rounds above against a fixture world, including the commit a merged bump lands as — a new id serving identical bytes, the case the old guard could not see. Deleting the gate fails that test and only that test; checked by deleting it, not assumed.
  • End to end on a scratch branch, both directions: with the generator as it stood in commit 1, round 2 writes and opens a second bump; with commit 2 it reports already serves this content and opens nothing, while --check still calls the tree correct.
  • workflows.test.mjs checked against the real break: restoring the previous org-defaults.yml fails it and names line 74.

Note on branch naming

The session's designated branch is claude/ci-chain-verification-r8bpoh, which already carries #77. This is a separate change off main, so it is on its own branch rather than stacked into that PR — say the word if you would rather have them together.

claude added 2 commits July 31, 2026 16:33
The gate added in #73 was right about the facts and wrong about where it
reported them. Every PR touching a fetched file went red on FRESHNESS, and
none of them could do anything about it: SUM_* is content-addressed and
knowable on a branch, but PIN is a COMMIT and cannot name the merge commit
before that commit exists. The pair has to agree with each other, so
recording the new digests early just moves the red to INTEGRITY. #75 hit
this, #77 hit it again, and an expected-red teaches people to ignore red.

Worse, and unmentioned so far: org-defaults.yml also runs on push to main,
so main went red the moment such a PR merged and stayed red until a human
hand-typed a follow-up. That is a failing default branch as routine.

Three changes, following the registry-graph.yml pattern already here:

- gen-bootstrap-pin.mjs generates the pin and digests, and backs the gate's
  --check. #71 and #72 were both a human hand-copying 64 hex characters;
  the gate that caught them decided "correct" in code separate from the
  code that produces correct, which is the same bug one level up.

- FRESHNESS is asserted on push and reported (t.diagnostic) on
  pull_request. INTEGRITY stays hard everywhere — a digest that does not
  describe its pin is a live bug on any branch, fixable where it is made.

- A `pin` job regenerates on push to main and opens the bump PR itself. It
  terminates: the bump only edits .claude/README.md, which is deliberately
  not in the fetch set, so it cannot invalidate the pin it just wrote. A
  fixed branch, not registry-graph's timestamped one, so consecutive pushes
  update one PR instead of queueing PRs that pin superseded commits.

Not auto-merged: the org requires a review on the default branch, and a
generator that writes the trust anchor is the last place to drop that.

Two bugs found while writing the tests, both now covered:

- renderBootstrap anchored on `\s*$`, which under the `m` flag matches the
  newline too — each rewrite ate the following blank line and shifted the
  document up. Inherited from the old parse-only regex, where it was
  harmless; it is not harmless in a writer.
- The documented confirm loop omitted stop-hook-git-check.sh, so anyone
  following the docs would never recompute that digest — how #72 shipped a
  stale one. Gated now, not just fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6WQtQtqLZg3gNeqJ3jTG
…pin is wrong

The previous commit claims the loop terminates "because the bump only edits
.claude/README.md, which is deliberately not in the fetch set, so it cannot
invalidate the pin it just wrote". That claim is true and it is not
sufficient, which running the loop showed in two commits:

  round 1  a fetched file changes on main    → bump PR opened     (correct)
  round 2  that bump PR merges               → bump PR opened     (wrong)
  round 3  anything at all pushes to main    → bump PR opened     (wrong)

The README-is-not-fetched argument establishes that the CONTENT stays
consistent across a bump. It says nothing about the commit id — and the
commit id is what the rewrite keyed on. `next === source` was the only guard,
and after a bump lands, `next` always differs from `source`, because PIN names
the new commit rather than the one before it. So main proposed a pin bump
after every push, forever, with no fetched file ever having changed. An
automation that opens a PR on every push to the default branch is worse than
the expected-red it was written to remove.

So the decision moves off commit identity and onto content. planBump writes
only when the pin is WRONG — INTEGRITY broken (the bootstrap is refusing a
file right now) or STALE (the pin serves different bytes than this tree). A
pin that trails HEAD but still serves byte-identical files is correct, and
correct is a no-op. Round 2 is then silent and the loop stops there.

Split out of main() rather than left inline: this is the termination argument
in code, and it is the part that was wrong, so a test has to be able to reach
it without a git repository to push around. gen-bootstrap-pin.test.mjs walks
the same four rounds against a fixture world, including the commit a merged
bump lands as — a new id serving identical bytes, which is precisely the case
the old guard could not see. Deleting the gate fails that test and only that
test; verified rather than assumed.

Also kept: a test that renderBootstrap alone still does not converge at round
2. That is not a defect in renderBootstrap — it rewrites what it is told to —
but it is the reason `next !== source` cannot be reused as a "something
changed" signal across commits, and it should fail loudly if anyone tries.

Verified end to end on a scratch branch, both directions: with the generator
as it stood in the previous commit, round 2 writes and opens a second bump;
with this one it reports "already serves this content" and opens nothing,
while `--check` still calls the tree correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6WQtQtqLZg3gNeqJ3jTG
…d green

The `pin` job added two commits ago never ran, and neither did `schema` — the
job that runs every test in this repo. Its `gh pr create --body "..."` put
prose at column 0 inside a `run: |` block scalar. A non-empty line at column 0
ends the scalar, and the `---` rule then starts a new YAML document, so
org-defaults.yml stopped being a workflow at all:

  .github/workflows/org-defaults.yml, line 74, column 1
  could not find expected ':'

What makes this worth more than a fix is how it presented. GitHub does not
report an unparseable workflow as a failing check on the PR — it records a run
named by the file's raw path, and the jobs inside simply do not happen. PR #78
showed seven checks, every one green, with the test suite never executed. A
break that removes your gates does not look like a break; it looks like
success with fewer lines.

Two changes:

- The bump PR body moves to .github/bootstrap-pin-bump.md and is passed with
  --body-file. Prose wants blank lines, a `---` rule and a leading `_`, and
  none of those can survive inline in a block scalar. Keeping it in a file
  also makes the body reviewable as prose, which is what it is.

- workflows.test.mjs asserts, for all twelve workflows, that nothing but a
  top-level key or a comment starts at column 0. Deliberately not a YAML
  parser: node has none built in, this repo's tests take no dependencies, and
  the one structural property that broke is exactly checkable. Verified
  against the real thing — restoring the previous file fails this test and
  names line 74.

The test runs from `schema`, i.e. from inside org-defaults.yml, so it fully
covers the other eleven and covers this one only before a push. The other half
of the argument is that `schema` is a required check: one that never reports
blocks the merge, which is what catches this file being already broken on a
branch. Neither half is sufficient alone, and that is written down in the
test's header rather than left for the next person to rediscover.

Found by checking why `schema` was absent from an all-green PR rather than by
reading the seven that were there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6WQtQtqLZg3gNeqJ3jTG
…ly wired

The header claimed the residual gap — this test cannot run when the workflow it
runs from is the broken one — was closed by `schema` being a required status
check. Checked before merging rather than after: it is not one.

  GET /repos/bounded-systems/.github/rules/branches/main
    pull_request, required_signatures, required_linear_history,
    non_fast_forward, deletion  — no required_status_checks

`.github` is absent from the `ci-green` ruleset, which names
`front-desk-scheduler` only, and nothing else on this repo's default branch
looks at CI. A PR here can merge with every check red.

That makes the sentence a mechanism asserted from how it ought to be wired
rather than from how it is — the same species of claim as "it terminates
because the bump only edits README.md" two commits ago, and as the seven green
checks that were green because the tests never ran. Three times in one chain is
enough to stop writing it down as settled.

So the header now states the gap is OPEN, dates the measurement, and names the
fix: `.github` meets `merge-gate.md`'s documented prerequisite for joining
`ci-green` — one always-run job with a stable name, which `schema` is — so the
change is to org/rulesets/ci-green.json in `.github-private`, not here. Left
for its own PR rather than smuggled in: this repo cannot edit that ruleset, and
a claim about org config belongs where the org config is reviewed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SG6WQtQtqLZg3gNeqJ3jTG
@bdelanghe
bdelanghe marked this pull request as ready for review July 31, 2026 17:08
@bdelanghe
bdelanghe merged commit c8293ef into main Jul 31, 2026
8 checks passed
@bdelanghe
bdelanghe deleted the claude/bootstrap-pin-autobump branch July 31, 2026 17:08
@bounded-systems-front-desk bounded-systems-front-desk Bot moved this from Todo to Done in Front Desk Jul 31, 2026
bdelanghe added a commit that referenced this pull request Jul 31, 2026
A session with both .github and .github-private attached got the org context
TWICE — two byte-identical copies of .github-private/claude/context.md, joined
by the dispatcher's "---" separator, so "Keep this LEAN, it counts against the
context window every session" was paid twice.

Neither hook is removable: without .github-private attached, .github's is the
only one that can reach the context; without .github attached, .github-private's
is. They collide only when BOTH are attached — exactly the session that
verifies the chain. The duplication is a property of merging, so it is fixed at
the merge point. mergeContexts compares trimmed text and keeps the first
spelling, since the two producers differ only by a trailing newline, and logs
what it dropped rather than absorbing it silently.

Merged after #78, which makes FRESHNESS a diagnostic on pull_request: this PR
changes a fetched file, so it could not have gone green before that landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants