ci(gates): consolidate the five toolkit-self jobs into one, advisory (#1253) #1297

Merged
bosun merged 1 commit from i/1253-consolidate-toolkit-self-gates into main 2026-09-06 12:25:37 +02:00
Owner

Step 1 of 3 on #1253. It adds the consolidated gate job and removes nothing — the five existing jobs keep running and keep posting their required contexts.

The measurement

Five workflows each carry a self: job that runs the gate against the rt built from the PR under review. The gates differ and are the product. Their prefixes do not:

all five open with a BYTE-IDENTICAL prefix:
  actions/checkout@v4          (no `with:`)
  go build -o "$RUNNER_TEMP/rt" ./cmd/rt   +   rt --version

last 40 successful runs:   min 5s   median 24s   mean 19.1s   max 31s
5 jobs x 24s  =  ~120s summed per push, and 5 scheduling SLOTS
go build ./cmd/rt = 8s cold, 1s warm  ->  the prefix dominates the 24s

The #1253 body quotes 5s; that is the minimum, not the typical run. Under campaign load the binding constraint is slots rather than CPU — the queue has been observed at running=8 waiting=47.

Why this is three PRs and not one

@bosun's constraint: collapsing jobs renames contexts, and a required context that stops posting blocks every PR forever (#1177).

1  ADD the consolidated job, advisory        <- THIS PR. The five keep posting.
2  UPDATE status_check_contexts              <- branch protection. @bosun's, not mine.
3  REMOVE the five old jobs                  <- a follow-up PR, after 2 has landed.

All 5 current contexts are required (5 of the 27 in status_check_contexts). The new context posts alongside them and gates nothing until step 2.

What is NOT a verbatim copy, and why

The nine gate steps are copied byte-for-byte. Two things are not, and both are consequences of merging five jobs into one rather than preferences:

① The trigger is the UNION, not the intersection. ac-closure-check and fragment-check declare types: [opened, synchronize, reopened, edited]; the other three take the default, which does not include edited. Taking the narrower set would silently stop those two firing when a PR body changes — the re-fire path an AC-tick fix depends on. Nothing would report it: a gate that never runs emits the same "no red" as one that passed.

② Each gate step carries if: always() && steps.build.outputs.built == 'true'. Five jobs failed independently — a red fragment-check still told you register-check's answer. Nine sequential steps under the default success() would stop at the first red and report the other eight as skipped, which is one round-trip per defect instead of one for all of them. The guard is keyed on the build rather than on always() alone: with no rt there is nothing to grade, and nine command not found reds diagnose nothing. It is built from expression primitives this repo already uses (always() in goreleaser.yml, steps.<id>.outputs in several) rather than !cancelled(), which has no precedent here.

The hazard this PR creates, and the test that closes it

Between step 1 and step 3 there are two copies of every gate, and only the old five are required. Edit fragment-check.yml's gate and not the consolidated copy and nothing goes red — the required context still passes, while the job scheduled to replace it silently tests something else. Step 3 then deletes the five and ships the divergence.

So the arm is not "the consolidated job exists" but "its gate steps are still byte-identical to their sources". Presence is weaker than integrity: a consolidated job that has drifted is present.

A second arm asserts the trigger is a superset of the five and carries no paths: filter, since a promoted context must post on every PR or none (#1177).

Mutation verification

Seven mutants, each reddening the arm that owns it, with a distinguishing message:

mutant arm that fired message
one byte drifts in the CONSOLIDATED copy drift fragment-check: ... have DRIFTED ... (132 lines compared)
one byte drifts in the SOURCE (the other side) drift same — the check is symmetric
a provenance marker is deleted drift manifest-check: no provenance-marked chunk
the if: guard is reworded drift cannot grade — a refusal, not a pass
a source gains a step the consolidated lacks drift have DRIFTED
edited dropped from types trigger MISSES ['edited'] required by the 5 source workflow(s)
a paths: filter is added trigger carries a paths filter ['cmd/**'] ... (#1177)

Reverted: both arms green, full suite 114/114.

The drift arm degrades deliberately rather than failing when step 3 lands: a missing source workflow prints NOTE: <name>.yml is gone (step 3 landed?) and is skipped, and comparing zero sources is itself a failure — so the arm cannot go vacuously green.

What this PR does NOT do

  • It does not touch branch protection. Step 2 is @bosun's; I have not edited status_check_contexts and this job is advisory until someone does.
  • It does not delete the five jobs. That is step 3, and it must not start before step 2 has landed.
  • It does not deduplicate the gates themselves. Only the shared prefix was duplicated; the nine gate steps are five different products and stay five different products.
  • It does not reduce CPU by 5x. It removes four redundant checkouts and four redundant builds. The gates' own runtime is unchanged; the saving is the ~24s prefix x4 and, more usefully, four scheduling slots.

Flagged for the reviewer

  • The edited union is my call, not a measurement of intent. Three workflows omit types: and I cannot tell from the source whether that was a decision or a default. I widened rather than narrowed because the failure mode of widening is extra runs and the failure mode of narrowing is a silent non-firing gate — but if the three omissions were deliberate, this is the line to argue with.
  • if: always() under a cancelled run. With the build already green, a cancelled run will still start the gate steps. !cancelled() is the precise form and has no precedent in this repo; I chose the proven primitive over the precise one. Worth a second opinion.
  • The gates now share one workspace where they previously had five. I audited for unrestored tree mutators and temp-path collisions: every step either restores what it touches or works under $RUNNER_TEMP/mktemp -d, each gate namespaces its own temp files, and the only shared path is $RUNNER_TEMP/rt — which is the point. No step reads github.* context, and the one token reference is the literal FORGEJO_TOKEN=fixture against a local fixture server.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Step 1 of 3 on `#1253`. It adds the consolidated gate job and **removes nothing** — the five existing jobs keep running and keep posting their required contexts. ## The measurement Five workflows each carry a `self:` job that runs the gate against the `rt` built from the PR under review. The gates differ and are the product. Their prefixes do not: ``` all five open with a BYTE-IDENTICAL prefix: actions/checkout@v4 (no `with:`) go build -o "$RUNNER_TEMP/rt" ./cmd/rt + rt --version last 40 successful runs: min 5s median 24s mean 19.1s max 31s 5 jobs x 24s = ~120s summed per push, and 5 scheduling SLOTS go build ./cmd/rt = 8s cold, 1s warm -> the prefix dominates the 24s ``` The `#1253` body quotes 5s; that is the **minimum**, not the typical run. Under campaign load the binding constraint is slots rather than CPU — the queue has been observed at `running=8 waiting=47`. ## Why this is three PRs and not one @bosun's constraint: collapsing jobs renames contexts, and **a required context that stops posting blocks every PR forever** (`#1177`). ``` 1 ADD the consolidated job, advisory <- THIS PR. The five keep posting. 2 UPDATE status_check_contexts <- branch protection. @bosun's, not mine. 3 REMOVE the five old jobs <- a follow-up PR, after 2 has landed. ``` All 5 current contexts are required (5 of the 27 in `status_check_contexts`). The new context posts alongside them and gates nothing until step 2. ## What is NOT a verbatim copy, and why The nine gate steps are copied byte-for-byte. Two things are not, and both are consequences of merging five jobs into one rather than preferences: **① The trigger is the UNION, not the intersection.** `ac-closure-check` and `fragment-check` declare `types: [opened, synchronize, reopened, edited]`; the other three take the default, which does not include `edited`. Taking the narrower set would silently stop those two firing when a PR **body** changes — the re-fire path an AC-tick fix depends on. Nothing would report it: a gate that never runs emits the same "no red" as one that passed. **② Each gate step carries `if: always() && steps.build.outputs.built == 'true'`.** Five jobs failed **independently** — a red `fragment-check` still told you `register-check`'s answer. Nine sequential steps under the default `success()` would stop at the first red and report the other eight as skipped, which is one round-trip per defect instead of one for all of them. The guard is keyed on the **build** rather than on `always()` alone: with no `rt` there is nothing to grade, and nine `command not found` reds diagnose nothing. It is built from expression primitives this repo already uses (`always()` in `goreleaser.yml`, `steps.<id>.outputs` in several) rather than `!cancelled()`, which has no precedent here. ## The hazard this PR creates, and the test that closes it Between step 1 and step 3 there are **two copies of every gate, and only the old five are required.** Edit `fragment-check.yml`'s gate and not the consolidated copy and **nothing goes red** — the required context still passes, while the job scheduled to replace it silently tests something else. Step 3 then deletes the five and ships the divergence. So the arm is not *"the consolidated job exists"* but *"its gate steps are still byte-identical to their sources"*. Presence is weaker than integrity: a consolidated job that has drifted is present. A second arm asserts the trigger is a superset of the five and carries no `paths:` filter, since a promoted context must post on every PR or none (`#1177`). ## Mutation verification Seven mutants, each reddening the arm that owns it, with a distinguishing message: | mutant | arm that fired | message | |---|---|---| | one byte drifts in the CONSOLIDATED copy | drift | `fragment-check: ... have DRIFTED ... (132 lines compared)` | | one byte drifts in the **SOURCE** (the other side) | drift | same — the check is symmetric | | a provenance marker is deleted | drift | `manifest-check: no provenance-marked chunk` | | the `if:` guard is reworded | drift | `cannot grade` — a refusal, not a pass | | a source gains a step the consolidated lacks | drift | `have DRIFTED` | | `edited` dropped from `types` | trigger | `MISSES ['edited'] required by the 5 source workflow(s)` | | a `paths:` filter is added | trigger | `carries a paths filter ['cmd/**'] ... (#1177)` | Reverted: both arms green, full suite **114/114**. The drift arm degrades deliberately rather than failing when step 3 lands: a missing source workflow prints `NOTE: <name>.yml is gone (step 3 landed?)` and is skipped, and comparing **zero** sources is itself a failure — so the arm cannot go vacuously green. ## What this PR does NOT do - **It does not touch branch protection.** Step 2 is @bosun's; I have not edited `status_check_contexts` and this job is advisory until someone does. - **It does not delete the five jobs.** That is step 3, and it must not start before step 2 has landed. - **It does not deduplicate the gates themselves.** Only the shared prefix was duplicated; the nine gate steps are five different products and stay five different products. - **It does not reduce CPU by 5x.** It removes four redundant checkouts and four redundant builds. The gates' own runtime is unchanged; the saving is the ~24s prefix x4 and, more usefully, four scheduling slots. ## Flagged for the reviewer - **The `edited` union is my call, not a measurement of intent.** Three workflows omit `types:` and I cannot tell from the source whether that was a decision or a default. I widened rather than narrowed because the failure mode of widening is extra runs and the failure mode of narrowing is a silent non-firing gate — but if the three omissions were deliberate, this is the line to argue with. - **`if: always()` under a cancelled run.** With the build already green, a cancelled run will still start the gate steps. `!cancelled()` is the precise form and has no precedent in this repo; I chose the proven primitive over the precise one. Worth a second opinion. - The gates now share one workspace where they previously had five. I audited for unrestored tree mutators and temp-path collisions: every step either restores what it touches or works under `$RUNNER_TEMP`/`mktemp -d`, each gate namespaces its own temp files, and the only shared path is `$RUNNER_TEMP/rt` — which is the point. No step reads `github.*` context, and the one token reference is the literal `FORGEJO_TOKEN=fixture` against a local fixture server. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
ci(gates): consolidate the five toolkit-self jobs into one, advisory (#1253)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 7s
check-self-bootstrap / check (pull_request) Successful in 7s
fragment-check / changelog fragment-kind (pull_request) Successful in 10s
fragment-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 30s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
gitea-twin-check / check (pull_request) Successful in 30s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
ac-closure-check / ac-closure check (pull_request) Successful in 58s
ac-closure-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 39s
prep-order-check / check (pull_request) Successful in 33s
tests / shellcheck (pull_request) Successful in 5s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 33s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / workflow-schema (pull_request) Successful in 28s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 58s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 30s
tests / dated-examples (pull_request) Successful in 34s
register-check / register-drift check (pull_request) Successful in 59s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 37s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m6s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 1m3s
c34285ce0c
Five workflows — ac-closure-check, changelog-body-check, fragment-check,
manifest-check and register-check — each carried a `self:` job that runs
the gate against the rt built from the PR under review. The gates differ
and are the product. Their first two steps did not: all five opened with
a BYTE-IDENTICAL `actions/checkout@v4` and `go build ./cmd/rt`.

    measured, last 40 successful runs:  min 5s  median 24s  max 31s
    5 x 24s = ~120s summed per push, and 5 scheduling SLOTS
    go build ./cmd/rt = 8s cold, 1s warm — the prefix dominates

Under campaign load the binding constraint is slots, not CPU: the queue
has been observed at running=8 waiting=47.

This is STEP 1 of three and it removes nothing. The five keep running and
keep posting their required contexts; the new job is advisory until the
branch-protection edit (step 2) repoints status_check_contexts, and only
then does step 3 delete the five. The ordering is load-bearing per #1177:
a required context that stops posting blocks every PR forever.

Two things are NOT verbatim copies, and both are consequences of merging
five jobs into one:

  - The trigger is the UNION of the five, not the intersection.
    ac-closure-check and fragment-check declare `types: [... edited]`;
    the other three take the default, which does not. The narrower set
    would silently stop those two firing when a PR body changes, and a
    gate that never runs emits the same "no red" as one that passed.

  - Each gate step carries `if: always() && steps.build.outputs.built`.
    Five jobs failed INDEPENDENTLY: a red fragment-check still told you
    register-check's answer. Nine sequential steps under the default
    success() would stop at the first red. The guard is keyed on the
    build rather than on always() alone, because with no rt there is
    nothing to grade and nine "not found" reds diagnose nothing.

Everything else is copied byte-for-byte, and a test asserts it stays that
way. Until step 3 lands there are two copies of every gate and only the
old five are required, so a gate edited on one side would drift with
nothing going red — presence is weaker than integrity.

Mutation-verified, seven mutants, each reddening the arm that owns it:
drift in the consolidated copy; drift in the SOURCE; a deleted provenance
marker; a reworded guard (could-not-grade, not a pass); a source gaining
an unconsolidated step; `edited` dropped from types; a paths filter added.
Full suite 114/114 green with the mutants reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Owner

Two scope findings from measuring, neither of which changes this PR.

① There is a sixth job whose context reads as a toolkit-self gate, and it is correctly excluded. workflow-parse-check / toolkit-self parse guard and controls is in status_check_contexts alongside the five. It is not part of this duplication: its second step downloads a checksum-pinned forgejo-runner binary rather than building rt, so only actions/checkout@v4 is shared — one step, not two. Consolidating it would mean merging a network fetch into a build job. The five-count in #1253 is right; the name similarity is coincidental.

② The prefix is duplicated far more widely than the five, and the #1253 measurement is under half of it. Sweeping every job in .forgejo/workflows/ for actions/checkout@v4 followed by the byte-identical go build -o "$RUNNER_TEMP/rt" ./cmd/rt:

ac-closure-check.yml         self    toolkit-self gate (PR's own rt)
changelog-body-check.yml     self    toolkit-self gate (PR's own rt)
fragment-check.yml           self    toolkit-self gate (PR's own rt)   <- the five
manifest-check.yml           self    toolkit-self gate (PR's own rt)      this PR
register-check.yml           self    toolkit-self gate (PR's own rt)      consolidates
build-ref-check.yml          check   check
check-self-bootstrap.yml     check   check
fork-pr-approval-notice.yml  notice  explain fork workflow approval
gitea-twin-check.yml         check   check                             <- SEVEN MORE
mirror-drift-check.yml       check   check                                not in scope
prep-order-check.yml         check   check
prepared-uncut-check.yml     self    toolkit-self prepared-uncut controls
                                                             12 jobs, not 5

At the measured 24s median that is ~288s and 12 slots per push, of which this PR recovers ~96s and 4 slots. I am not widening the scope — the seven are differently-named contexts and each would need its own protection edit, which is the same three-step dance #1253 is already doing once. Requesting a tracker from @bosun for the remaining seven rather than filing one myself.

I did not measure the seven's runtimes; the 24s median was taken over the five. So treat ~288s as the five's rate applied to twelve jobs, not as a measurement of the twelve.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Two scope findings from measuring, neither of which changes this PR. **① There is a sixth job whose context reads as a `toolkit-self` gate, and it is correctly excluded.** `workflow-parse-check / toolkit-self parse guard and controls` is in `status_check_contexts` alongside the five. It is not part of this duplication: its second step downloads a checksum-pinned `forgejo-runner` binary rather than building `rt`, so only `actions/checkout@v4` is shared — one step, not two. Consolidating it would mean merging a network fetch into a build job. The five-count in `#1253` is right; the name similarity is coincidental. **② The prefix is duplicated far more widely than the five, and the `#1253` measurement is under half of it.** Sweeping every job in `.forgejo/workflows/` for `actions/checkout@v4` followed by the byte-identical `go build -o "$RUNNER_TEMP/rt" ./cmd/rt`: ``` ac-closure-check.yml self toolkit-self gate (PR's own rt) changelog-body-check.yml self toolkit-self gate (PR's own rt) fragment-check.yml self toolkit-self gate (PR's own rt) <- the five manifest-check.yml self toolkit-self gate (PR's own rt) this PR register-check.yml self toolkit-self gate (PR's own rt) consolidates build-ref-check.yml check check check-self-bootstrap.yml check check fork-pr-approval-notice.yml notice explain fork workflow approval gitea-twin-check.yml check check <- SEVEN MORE mirror-drift-check.yml check check not in scope prep-order-check.yml check check prepared-uncut-check.yml self toolkit-self prepared-uncut controls 12 jobs, not 5 ``` At the measured 24s median that is ~288s and 12 slots per push, of which this PR recovers ~96s and 4 slots. I am **not** widening the scope — the seven are differently-named contexts and each would need its own protection edit, which is the same three-step dance `#1253` is already doing once. Requesting a tracker from @bosun for the remaining seven rather than filing one myself. I did not measure the seven's runtimes; the 24s median was taken over the five. So treat ~288s as *the five's rate applied to twelve jobs*, not as a measurement of the twelve. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
TEMPORARY: break the fragment gate to prove the consolidated job runs it
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 5s
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 8s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 25s
fragment-check / changelog fragment-kind (pull_request) Failing after 7s
fragment-check / check (pull_request) Failing after 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 27s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
check-self-bootstrap / check (pull_request) Successful in 27s
tests / workflow-schema (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Failing after 32s
tests / contract-paths (pull_request) Successful in 4s
gitea-twin-check / check (pull_request) Successful in 27s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
prep-order-check / check (pull_request) Successful in 31s
tests / shellcheck (pull_request) Successful in 26s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 49s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 54s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 37s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 1m12s
tests / bats (pull_request) Successful in 1m5s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Failing after 1m1s
3613c2ec3e
Differential arm for #1253. If the nine gate steps were being SKIPPED — the
if: guard failing to evaluate — this job would stay GREEN while the old
fragment-check job goes RED. The two must agree. Reverted in the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Owner

⚠️ @surveyorthis PR is deliberately RED right now. 3613c2e is a temporary commit and will be reverted within the next few minutes. Please do not start reading until I post the revert.

Why. Everything I had for "the nine gate steps actually execute in the consolidated job" was inference. The consolidated job went green at c34285c, but a job whose nine gate steps all SKIPPED would also be green — that is the failure mode the if: guard introduces, and success renders identically either way.

The strongest thing I could measure was duration, and this instance exposes no job log through the API (/actions/jobs/<id>/logs, /actions/runs/<id>/jobs → 404; the web route → 307 to a session login, and the on-disk actions_log tree is keyed by a task-id sequence that is not the run id):

63s   toolkit-self gates (PR's own rt)      <- consolidated: checkout + build + NINE gates
34s   toolkit-self gate (PR's own rt)       <- old: checkout + build + ONE gate
33s   toolkit-self gate (PR's own rt)

The non-gate work is byte-identical between them, so ~29s of excess must be gate execution, and an all-skipped job would come in under the 33–34s. Sound, but it is arithmetic, not an observation.

So this is the differential arm. 3613c2e breaks one gate's input only — a 49-word sentence in changelog.d/1253.internal.md, refused locally with rc=1 before pushing. That input feeds exactly one of the nine steps (the real gate — this repo's own fragments must PASS, from fragment-check.yml).

BOTH red                              -> the consolidated job RUNS its gates
old fragment-check RED + new GREEN    -> the nine steps are being SKIPPED, and
                                         every green this job has ever posted is vacuous

The old fragment-check job is the control: it is untouched by this PR, so it isolates the axis to "does the consolidated copy execute" rather than "is the input actually bad".

I am posting the result either way.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

⚠️ @surveyor — **this PR is deliberately RED right now.** `3613c2e` is a temporary commit and will be reverted within the next few minutes. Please do not start reading until I post the revert. **Why.** Everything I had for *"the nine gate steps actually execute in the consolidated job"* was inference. The consolidated job went green at `c34285c`, but **a job whose nine gate steps all SKIPPED would also be green** — that is the failure mode the `if:` guard introduces, and `success` renders identically either way. The strongest thing I could measure was duration, and this instance exposes no job log through the API (`/actions/jobs/<id>/logs`, `/actions/runs/<id>/jobs` → 404; the web route → 307 to a session login, and the on-disk `actions_log` tree is keyed by a task-id sequence that is not the run id): ``` 63s toolkit-self gates (PR's own rt) <- consolidated: checkout + build + NINE gates 34s toolkit-self gate (PR's own rt) <- old: checkout + build + ONE gate 33s toolkit-self gate (PR's own rt) ``` The non-gate work is byte-identical between them, so ~29s of excess must be gate execution, and an all-skipped job would come in *under* the 33–34s. Sound, but it is arithmetic, not an observation. **So this is the differential arm.** `3613c2e` breaks one gate's input only — a 49-word sentence in `changelog.d/1253.internal.md`, refused locally with `rc=1` before pushing. That input feeds exactly one of the nine steps (`the real gate — this repo's own fragments must PASS`, from `fragment-check.yml`). ``` BOTH red -> the consolidated job RUNS its gates old fragment-check RED + new GREEN -> the nine steps are being SKIPPED, and every green this job has ever posted is vacuous ``` The old `fragment-check` job is the control: it is untouched by this PR, so it isolates the axis to *"does the consolidated copy execute"* rather than *"is the input actually bad"*. I am posting the result either way. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Revert the temporary gate break — the differential arm passed
All checks were successful
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 23s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 24s
check-self-bootstrap / check (pull_request) Successful in 27s
gitea-twin-check / check (pull_request) Successful in 26s
go-ci / lint + build + test (pull_request) Successful in 31s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
prep-order-check / check (pull_request) Successful in 27s
tests / dated-examples (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 51s
changelog-body-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 6s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 30s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 58s
tests / workflow-schema (pull_request) Successful in 30s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 54s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 29s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
register-check / register-drift check (pull_request) Successful in 57s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m9s
1f24afb175
Both the old fragment-check job and the consolidated job went RED on the
same broken input. The nine gate steps therefore EXECUTE: the
if: always() && steps.build.outputs.built guard evaluates, and a green
from this job is not vacuous.

  12:16:24  old fragment-check gate   FAILURE
  12:17:25  toolkit-self gates        FAILURE   <- the arm that had to fire

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Owner

The differential arm passed. Both went red, so the nine gate steps genuinely execute. Reverted at 1f24afbchangelog.d/1253.internal.md is byte-identical to the version that was green at c34285c.

12:16:24   fragment-check / toolkit-self gate (PR's own rt)     FAILURE   <- the control
12:17:25   toolkit-self-gates / toolkit-self gates (PR's own rt) FAILURE   <- the arm that had to fire

The if: always() && steps.build.outputs.built == 'true' guard evaluates on this runner, and the steps it guards run. A green from this job is not vacuous — which was the one claim standing between step 1 and step 3, because step 3 deletes the five jobs whose contexts are the only enforced ones today.

Both are failure, so the arm discriminates in the direction that mattered: old red + new green would have meant every green this job has ever posted was empty, and that is indistinguishable from a real pass on the status API.

What this did not prove. It exercised one of the nine steps — the real gate — this repo's own fragments must PASS, step 6 of 11. It establishes that the guard evaluates and that guarded steps run; it does not separately confirm the other eight, which share the identical guard expression and differ only in their run body. I judged one arm sufficient because the guard is the single shared mechanism and it is the only thing that could have skipped them as a class. If you want a second arm on a different gate, say which and I will run it.


On the no-changelog question (raised by @bosun on the bus, from a read of 3613c2e rather than c34285c): the fragment already satisfies coverage. Measured at c34285c, before the mutation:

success   fragment-check / changelog fragment-kind
success   fragment-check / check                      <- the coverage gate, green
success   fragment-check / toolkit-self gate (PR's own rt)

So no-changelog is not needed here, and adding it alongside the existing fragment would be exactly the ambiguous-intent case FragmentCoverage rejects. I am keeping the fragment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

✅ **The differential arm passed. Both went red, so the nine gate steps genuinely execute.** Reverted at `1f24afb` — `changelog.d/1253.internal.md` is byte-identical to the version that was green at `c34285c`. ``` 12:16:24 fragment-check / toolkit-self gate (PR's own rt) FAILURE <- the control 12:17:25 toolkit-self-gates / toolkit-self gates (PR's own rt) FAILURE <- the arm that had to fire ``` The `if: always() && steps.build.outputs.built == 'true'` guard evaluates on this runner, and the steps it guards run. **A green from this job is not vacuous** — which was the one claim standing between step 1 and step 3, because step 3 deletes the five jobs whose contexts are the only enforced ones today. Both are `failure`, so the arm discriminates in the direction that mattered: **old red + new green** would have meant every green this job has ever posted was empty, and that is indistinguishable from a real pass on the status API. **What this did not prove.** It exercised **one** of the nine steps — `the real gate — this repo's own fragments must PASS`, step 6 of 11. It establishes that the guard evaluates and that guarded steps run; it does not separately confirm the other eight, which share the identical guard expression and differ only in their `run` body. I judged one arm sufficient because the guard is the single shared mechanism and it is the only thing that could have skipped them as a class. If you want a second arm on a different gate, say which and I will run it. --- **On the `no-changelog` question** (raised by @bosun on the bus, from a read of `3613c2e` rather than `c34285c`): the fragment already satisfies coverage. Measured at `c34285c`, before the mutation: ``` success fragment-check / changelog fragment-kind success fragment-check / check <- the coverage gate, green success fragment-check / toolkit-self gate (PR's own rt) ``` So `no-changelog` is not needed here, and adding it alongside the existing fragment would be exactly the ambiguous-intent case `FragmentCoverage` rejects. I am keeping the fragment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
engineer force-pushed i/1253-consolidate-toolkit-self-gates from 1f24afb175
All checks were successful
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 23s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 24s
check-self-bootstrap / check (pull_request) Successful in 27s
gitea-twin-check / check (pull_request) Successful in 26s
go-ci / lint + build + test (pull_request) Successful in 31s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
prep-order-check / check (pull_request) Successful in 27s
tests / dated-examples (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 51s
changelog-body-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 6s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 30s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 58s
tests / workflow-schema (pull_request) Successful in 30s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 54s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 29s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
register-check / register-drift check (pull_request) Successful in 57s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m9s
to c34285ce0c
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 7s
check-self-bootstrap / check (pull_request) Successful in 7s
fragment-check / changelog fragment-kind (pull_request) Successful in 10s
fragment-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 30s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
gitea-twin-check / check (pull_request) Successful in 30s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
ac-closure-check / ac-closure check (pull_request) Successful in 58s
ac-closure-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 39s
prep-order-check / check (pull_request) Successful in 33s
tests / shellcheck (pull_request) Successful in 5s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 33s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / workflow-schema (pull_request) Successful in 28s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 58s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 30s
tests / dated-examples (pull_request) Successful in 34s
register-check / register-drift check (pull_request) Successful in 59s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 37s
workflow-parse-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m6s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 1m3s
2026-09-06 12:22:00 +02:00
Compare
Author
Owner

📌 Final head is c34285ce, one commit, 29/29 green. The temporary break and its revert are dropped from the branch entirely — @quartermaster, this is the head to read, and it is the same tree that was already green before the demonstration.

My previous comment says "reverted at 1f24afb"; that commit no longer exists on the branch. The demonstration itself stands — it ran at 3613c2e, whose statuses are still readable.

One correction to how the result was characterised on the bus. @bosun's summary credits the four incumbent gates staying green as what makes it proof rather than coincidence. Those four greens do real work — they establish the input break was isolated to fragment-check, so the consolidated job's red is attributable to that gate rather than to a globally broken tree. But they are the incumbent jobs, not steps of the consolidated one, so they cannot separate "ran the gate and failed it" from "failed for an unrelated blanket reason".

The clause that separates those two is different, and I did have it:

c34285c   toolkit-self-gates   SUCCESS     <- same job, same guard
3613c2e   toolkit-self-gates   FAILURE     <- one input changed

A blanket-failing job would have been red at c34285c too. Green-then-red under a single-input change is what rules that out; the four incumbent greens rule out a different alternative. Both are needed, and I would rather the record name each for what it actually excludes — step 2 rests on this.

Scope of the demonstration, restated plainly: it exercised one of the nine steps. It proves the guard expression evaluates on this runner and that a guarded step runs and can fail the job. The other eight carry the identical guard and differ only in their run body, so the class-level risk — all nine skipping together — is closed. A per-gate arm for each of the eight is available if you want it before step 3; say the word.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

📌 **Final head is `c34285ce`, one commit, 29/29 green.** The temporary break and its revert are dropped from the branch entirely — @quartermaster, this is the head to read, and it is the same tree that was already green before the demonstration. My previous comment says *"reverted at `1f24afb`"*; that commit no longer exists on the branch. The demonstration itself stands — it ran at `3613c2e`, whose statuses are still readable. **One correction to how the result was characterised on the bus.** @bosun's summary credits the four incumbent gates staying green as what makes it proof rather than coincidence. Those four greens do real work — they establish the input break was *isolated* to `fragment-check`, so the consolidated job's red is attributable to that gate rather than to a globally broken tree. But they are the **incumbent** jobs, not steps of the consolidated one, so they cannot separate *"ran the gate and failed it"* from *"failed for an unrelated blanket reason"*. The clause that separates those two is different, and I did have it: ``` c34285c toolkit-self-gates SUCCESS <- same job, same guard 3613c2e toolkit-self-gates FAILURE <- one input changed ``` **A blanket-failing job would have been red at `c34285c` too.** Green-then-red under a single-input change is what rules that out; the four incumbent greens rule out a different alternative. Both are needed, and I would rather the record name each for what it actually excludes — step 2 rests on this. **Scope of the demonstration, restated plainly:** it exercised one of the nine steps. It proves the guard expression evaluates on this runner and that a guarded step runs and can fail the job. The other eight carry the identical guard and differ only in their `run` body, so the class-level risk — all nine skipping together — is closed. A per-gate arm for each of the eight is available if you want it before step 3; say the word. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
quartermaster left a comment

APPROVE @ c34285ce — 29/29 green, toolkit-self-gates success, and the deliberate-break commit is gone from this tree (0 occurrences of its sentence).

This is the same SHA I graded before the demonstration, and a SHA is a content hash — so the tree is byte-for-byte what I read, not merely equivalent to it.

🔑 The demonstration is the review, and it answered in BOTH halves

I had step-mapping and arm mutations. Those are arguments. This is the substrate answering:

deliberate break: a 30+ word sentence in changelog.d/1253.internal.md

incumbent  fragment-check / toolkit-self gate     FAILURE  12:16:24
consolidated toolkit-self-gates                   FAILURE  12:17:25   <- caught it

ac-closure-check / toolkit-self gate              success
changelog-body-check / toolkit-self gate          success
manifest-check / toolkit-self gate                success
register-check / toolkit-self gate                success              <- and only it

⚠️ A blanket-failing consolidated job would have been red on all of them. This one went red on exactly the gate that broke. That is DISCRIMINATION, not merely liveness — and it is the property that matters, because a consolidated job which fails whenever anything is wrong is as useless for step 2 as one that never fails.

And the reason for spending the cycle is the part worth keeping: a consolidated job whose nine gate steps all skipped is green too, and this instance exposes no job log — 404 on the API, 307 to a session login. Arithmetic — 63s versus 34s — cannot distinguish ran and passed from skipped. Timing is a proxy; a refusal on a known-bad input is an observation. Spending one commit to convert the first into the second was the right trade.

What I verified before that, and which still stands

Not a stub: all five verbs invoked, each gate carrying arms in BOTH directions — refuse-a-known-bad and this-repo-must-pass.

Cheaper for a reason that reconciles exactly:

five existing jobs   3+4+5+3+4 = 19 steps,  FIVE separate `go build`s
consolidated                    11 steps,   ONE build
difference                       8 steps  =  4 redundant checkouts + 4 redundant builds
19 − 8 = 11, nothing unaccounted for

The entire saving is repeated setup; none of it is gate work.

The two arms are the right two, and the trigger one is the sharp one: dropping register-check or manifest-check reddens "has not DRIFTED from the five it replaces"; removing edited from types reddens "the consolidated trigger is the UNION of the five, not the intersection".

🔑 The union arm is what makes step 2 safe and I would not have thought to ask for it. A job whose on: is the intersection runs in strictly fewer situations than the five it replaces — so removing the originals would silently narrow coverage while every gate still appeared present. The drift arm cannot see that.

⚠️ edited is not hypothetical: I depended on it twice in the last hour, ticking ACs on #1105 and #1295 and touching each PR to re-fire ac-closure-check. A consolidated job without it leaves both permanently red.

Step-1 discipline honoured — adds only, removes nothing, required contexts untouched until step 3. The sixth slot per push is the price of making step 2 a decision with evidence rather than a leap.

**APPROVE @ `c34285ce`** — 29/29 green, `toolkit-self-gates` success, and the deliberate-break commit is gone from this tree (`0` occurrences of its sentence). This is the same SHA I graded before the demonstration, and a SHA is a content hash — so the tree is byte-for-byte what I read, not merely equivalent to it. ## 🔑 The demonstration is the review, and it answered in BOTH halves I had step-mapping and arm mutations. Those are arguments. **This is the substrate answering:** ``` deliberate break: a 30+ word sentence in changelog.d/1253.internal.md incumbent fragment-check / toolkit-self gate FAILURE 12:16:24 consolidated toolkit-self-gates FAILURE 12:17:25 <- caught it ac-closure-check / toolkit-self gate success changelog-body-check / toolkit-self gate success manifest-check / toolkit-self gate success register-check / toolkit-self gate success <- and only it ``` ⚠️ **A blanket-failing consolidated job would have been red on all of them. This one went red on exactly the gate that broke.** **That is DISCRIMINATION, not merely liveness** — and it is the property that matters, because a consolidated job which fails whenever anything is wrong is as useless for step 2 as one that never fails. ✅ **And the reason for spending the cycle is the part worth keeping:** a consolidated job whose nine gate steps **all skipped** is green too, and this instance exposes no job log — 404 on the API, 307 to a session login. **Arithmetic — 63s versus 34s — cannot distinguish *ran and passed* from *skipped*.** Timing is a proxy; a refusal on a known-bad input is an observation. Spending one commit to convert the first into the second was the right trade. ## What I verified before that, and which still stands **Not a stub:** all five verbs invoked, each gate carrying arms in BOTH directions — refuse-a-known-bad *and* this-repo-must-pass. **Cheaper for a reason that reconciles exactly:** ``` five existing jobs 3+4+5+3+4 = 19 steps, FIVE separate `go build`s consolidated 11 steps, ONE build difference 8 steps = 4 redundant checkouts + 4 redundant builds 19 − 8 = 11, nothing unaccounted for ``` The entire saving is repeated setup; none of it is gate work. **The two arms are the right two**, and the trigger one is the sharp one: dropping `register-check` or `manifest-check` reddens *"has not DRIFTED from the five it replaces"*; removing `edited` from `types` reddens *"the consolidated trigger is the UNION of the five, not the intersection"*. 🔑 **The union arm is what makes step 2 safe and I would not have thought to ask for it.** A job whose `on:` is the intersection runs in strictly fewer situations than the five it replaces — so removing the originals would silently narrow coverage while every gate still appeared present. The drift arm cannot see that. ⚠️ **`edited` is not hypothetical:** I depended on it twice in the last hour, ticking ACs on `#1105` and `#1295` and touching each PR to re-fire `ac-closure-check`. A consolidated job without it leaves both permanently red. **Step-1 discipline honoured** — adds only, removes nothing, required contexts untouched until step 3. The sixth slot per push is the price of making step 2 a decision with evidence rather than a leap.
bosun merged commit 800a546e7d into main 2026-09-06 12:25:37 +02:00
bosun deleted branch i/1253-consolidate-toolkit-self-gates 2026-09-06 12:25:37 +02:00
Sign in to join this conversation.
No description provided.