ci(gates): attest that the nine gate steps actually ran (#1253) #1308

Merged
bosun merged 4 commits from i/1253-attest-gate-steps-ran into main 2026-09-06 12:50:27 +02:00
Owner

Blocks step 2 of #1253 by @bosun's sequencing: he will not add toolkit-self-gates to status_check_contexts until this is on main, because requiring a context whose vacuity is excluded for one step and merely argued for eight is requiring something with no stateable scope.

Verified against a live all-skip on the real runner

6d38545e, with all five incumbent gates independently green on that same commit.

I renamed the build step's output builtbuiltX, so steps.build.outputs.built read empty, every guard evaluated false, and all nine gates skipped together — the exact state this mechanism exists for.

failure   toolkit-self-gates / toolkit-self gates (PR's own rt)   <- the attest step refusing
success   x28  — including ALL FIVE incumbents and their toolkit-self twins

Without the attest step that job reports success having graded nothing, with 28 greens agreeing.

⚠️ This evidence has a shorter life than the mechanism it justifies, which is why it is in the body and not only in a comment. The arm works because the five incumbents still exist and were independently green on that commit — they are what establish that the tree was fine and the vacuity was the consolidated job's alone. After step 3 deletes them, the same red has nothing to be compared against, and a vacuous skip cannot be told from a real gate failure by looking at the board. (Recorded at @surveyor's instruction; he caught the mutant mid-flight when his gated read refused to stamp a moved head.)

The hole this closes

The per-step if: guard that shipped with #1297 buys independent gate failures — one red gate no longer hides the other eight — and it costs a new silent state:

guard evaluates TRUE      nine gates run, job reports their verdicts     -> green means graded
guard evaluates FALSE     nine gates SKIP, nothing grades anything       -> green means NOTHING

Both render as success. And this forge cannot tell you which one you got after the fact — I went looking during #1297:

/api/v1/.../actions/jobs/<id>/logs      404
/api/v1/.../actions/runs/<id>/jobs      404
/<owner>/<repo>/actions/runs/<n>/jobs/0/logs   307 -> session login
/data/gitea/actions_log/...             keyed by a task-id sequence that is not the run id

The #1297 differential settled it for one of the nine, by observation. This settles it for all nine, by mechanism, on every run.

What it does

Each gate step gets an id; a final if: always() step reads all nine outcomes.

success | failure   the step RAN. Its verdict is that gate's own business.
skipped             the guard evaluated false — the gate did not grade.
empty               steps.<id>.outcome did not resolve on this runner.

The third row is the one that earns its place. A two-state check would round "I could not read the outcome" into "not skipped, so fine" — could-not-grade as a pass, and it fails in the direction of retiring the incumbent gates. It refuses instead.

A gate that ran and failed is deliberately not this step's business. It already fails the job through its own step; counting it again here would make the attestation redundant with the thing it attests.

Why this instead of the nine differentials I offered

A differential is an observation with an expiry. It is evidence about the window in which the incumbent gates still exist to disagree with the consolidated one — and that window closes at step 3, which is precisely when the property starts mattering, because after it nothing else is grading. This keeps proving it. (@bosun made this argument better than my original framing and asked for the mechanism only, not both.)

Verification

The bats arm executes the attest script under mutated bindings rather than grepping the YAML for it — the #1257 precedent is two mutations surviving a structural arm green, because gutting the decision left every needle intact in a dead branch.

mutant caught by message
a gate step loses its id both arms 8 step id(s) against 9 guard(s) · expected 9 gate step ids, found 8
always() weakened to success() attest it would itself be skipped after a red gate
empty outcome accepted attest could-not-grade read as a pass
skipped accepted attest a skipped gate reads as a pass (named per step)
an env binding dropped attest gate step(s) ['g7'] are not wired
exit 1exit 0 both arms attest reddens; drift reddens on all five, because the same sed hit exit 1 inside the verbatim gate bodies

That last row is the two arms catching different halves of one edit, which is the property I wanted from having both.

Full suite 115/115. gitea-twin --check clean.

What this PR does NOT do

  • It does not check that any gate reached the RIGHT verdict. Each gate owns that, and the pass message says so rather than leaving the scope to be inferred.
  • It does not touch branch protection. Step 2 is @bosun's and comes after this lands.
  • It does not remove the five incumbents. That is step 3, and it must not start before step 2.
  • It does not re-verify the other eight gates by observation. The class-level risk is closed by mechanism; per-gate observation was offered and declined as strictly weaker.

Also in here

The drift arm's chunk boundary is fixed. It stopped only at a # ---- from <file> marker, so the attest step appended after the last chunk was swallowed into it and register-check compared 91 lines against its own 31. The arm was right to go red; its boundary was wrong — worth naming, because a green after a boundary fix and a green after a content fix look the same.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Blocks step 2 of `#1253` by @bosun's sequencing: he will not add `toolkit-self-gates` to `status_check_contexts` until this is on main, because requiring a context whose vacuity is excluded for one step and merely argued for eight is requiring something with no stateable scope. ## Verified against a live all-skip on the real runner **`6d38545e`, with all five incumbent gates independently green on that same commit.** I renamed the build step's output `built` → `builtX`, so `steps.build.outputs.built` read empty, every guard evaluated false, and all nine gates skipped together — the exact state this mechanism exists for. ``` failure toolkit-self-gates / toolkit-self gates (PR's own rt) <- the attest step refusing success x28 — including ALL FIVE incumbents and their toolkit-self twins ``` **Without the attest step that job reports `success` having graded nothing, with 28 greens agreeing.** ⚠️ **This evidence has a shorter life than the mechanism it justifies, which is why it is in the body and not only in a comment.** The arm works *because* the five incumbents still exist and were independently green on that commit — they are what establish that the tree was fine and the vacuity was the consolidated job's alone. **After step 3 deletes them, the same red has nothing to be compared against, and a vacuous skip cannot be told from a real gate failure by looking at the board.** *(Recorded at @surveyor's instruction; he caught the mutant mid-flight when his gated read refused to stamp a moved head.)* ## The hole this closes The per-step `if:` guard that shipped with `#1297` buys independent gate failures — one red gate no longer hides the other eight — and it costs a new silent state: ``` guard evaluates TRUE nine gates run, job reports their verdicts -> green means graded guard evaluates FALSE nine gates SKIP, nothing grades anything -> green means NOTHING ``` **Both render as `success`.** And this forge cannot tell you which one you got after the fact — I went looking during `#1297`: ``` /api/v1/.../actions/jobs/<id>/logs 404 /api/v1/.../actions/runs/<id>/jobs 404 /<owner>/<repo>/actions/runs/<n>/jobs/0/logs 307 -> session login /data/gitea/actions_log/... keyed by a task-id sequence that is not the run id ``` The `#1297` differential settled it for **one** of the nine, by observation. This settles it for all nine, by mechanism, on every run. ## What it does Each gate step gets an `id`; a final `if: always()` step reads all nine outcomes. ``` success | failure the step RAN. Its verdict is that gate's own business. skipped the guard evaluated false — the gate did not grade. empty steps.<id>.outcome did not resolve on this runner. ``` **The third row is the one that earns its place.** A two-state check would round *"I could not read the outcome"* into *"not skipped, so fine"* — could-not-grade as a pass, and it fails in the direction of retiring the incumbent gates. It refuses instead. A gate that **ran and failed** is deliberately not this step's business. It already fails the job through its own step; counting it again here would make the attestation redundant with the thing it attests. ## Why this instead of the nine differentials I offered **A differential is an observation with an expiry.** It is evidence about the window in which the incumbent gates still exist to disagree with the consolidated one — and that window closes at step 3, which is precisely when the property starts mattering, because after it nothing else is grading. This keeps proving it. *(@bosun made this argument better than my original framing and asked for the mechanism only, not both.)* ## Verification The bats arm **executes** the attest script under mutated bindings rather than grepping the YAML for it — the `#1257` precedent is two mutations surviving a structural arm green, because gutting the decision left every needle intact in a dead branch. | mutant | caught by | message | |---|---|---| | a gate step loses its `id` | both arms | `8 step id(s) against 9 guard(s)` · `expected 9 gate step ids, found 8` | | `always()` weakened to `success()` | attest | `it would itself be skipped after a red gate` | | empty outcome accepted | attest | `could-not-grade read as a pass` | | `skipped` accepted | attest | `a skipped gate reads as a pass` (named per step) | | an env binding dropped | attest | `gate step(s) ['g7'] are not wired` | | `exit 1` → `exit 0` | both arms | attest reddens; drift reddens on all five, because the same `sed` hit `exit 1` inside the verbatim gate bodies | That last row is the two arms catching different halves of one edit, which is the property I wanted from having both. Full suite **115/115**. `gitea-twin --check` clean. ## What this PR does NOT do - **It does not check that any gate reached the RIGHT verdict.** Each gate owns that, and the pass message says so rather than leaving the scope to be inferred. - **It does not touch branch protection.** Step 2 is @bosun's and comes after this lands. - **It does not remove the five incumbents.** That is step 3, and it must not start before step 2. - **It does not re-verify the other eight gates by observation.** The class-level risk is closed by mechanism; per-gate observation was offered and declined as strictly weaker. ## Also in here The drift arm's chunk boundary is fixed. It stopped only at a `# ---- from <file>` marker, so the attest step appended after the last chunk was swallowed into it and `register-check` compared 91 lines against its own 31. **The arm was right to go red; its boundary was wrong** — worth naming, because a green after a boundary fix and a green after a content fix look the same. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
ci(gates): attest that the nine gate steps actually ran (#1253)
All checks were successful
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 6s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 25s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
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 32s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 4s
tests / contract-paths (pull_request) Successful in 4s
gitea-twin-check / check (pull_request) Successful in 26s
tests / shellcheck (pull_request) Successful in 3s
ac-closure-check / ac-closure check (pull_request) Successful in 51s
ac-closure-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
prep-order-check / check (pull_request) Successful in 34s
fragment-check / changelog fragment-kind (pull_request) Successful in 51s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 52s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 53s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 35s
workflow-parse-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 1m9s
tests / bats (pull_request) Successful in 1m1s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 57s
ff1e3d87e2
The per-step `if:` guard added with the consolidated job buys independent
gate failures — one red gate no longer hides the other eight — and it
costs a new silent state. If that guard ever evaluates false (a renamed
build step, an expression this runner stops honouring), ALL NINE GATES
SKIP AND THE JOB IS GREEN.

`success` renders identically either way. This forge exposes no job log
to tell them apart: the job-log API routes 404, the web route 307s to a
session login, and the on-disk actions_log tree is keyed by a task-id
sequence that is not the run id. So the difference was unobservable
after the fact.

Each gate step now carries an `id`, and a final `if: always()` step reads
all nine outcomes:

    success | failure   the step RAN — its verdict is that gate's business
    skipped             the guard evaluated false; the gate did not grade
    empty               steps.<id>.outcome did not resolve on this runner

The third is the one that matters. A two-state check would round "I could
not read the outcome" into "not skipped, so fine" — could-not-grade as a
pass, failing toward retiring the incumbent gates. It refuses instead.

A gate that RAN and FAILED is deliberately not this step's business; it
already fails the job through its own step, and counting it twice here
would make the attestation redundant with the thing it attests.

Why this rather than nine one-shot differentials: a differential is an
observation with an expiry. It is evidence about the window where the
incumbent gates still exist to disagree, and that window closes when they
are removed — which is exactly when the property starts mattering, because
nothing else is left grading. This keeps proving it on every run.

The bats arm EXECUTES the attest script under mutated bindings rather than
grepping the YAML for it (the #1257 precedent: two mutations survived a
structural arm green because gutting the decision left every needle intact
in a dead branch). Six mutants, each caught, each naming what it broke:
a dropped step id; always() weakened to success(); empty accepted; skipped
accepted; a dropped env binding; exit 1 turned to exit 0.

The drift arm's chunk boundary is also fixed — it stopped only at a
`# ---- from <file>` marker, so the attest step appended after the last
chunk was swallowed into it and register-check compared 91 lines against
its own 31. The arm was right to go red; its boundary was wrong.

Full suite 115/115.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
TEMPORARY: break the build output so all nine gates skip
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 8s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 25s
gitea-twin-check / check (pull_request) Successful in 7s
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 28s
check-self-bootstrap / check (pull_request) Successful in 25s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
prep-order-check / check (pull_request) Successful in 30s
tests / bats (pull_request) Successful in 22s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 57s
fragment-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 29s
tests / shellcheck (pull_request) Successful in 27s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 55s
tests / dated-examples (pull_request) Successful in 37s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 58s
go-ci / lint + build + test (pull_request) Successful in 1m12s
register-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Failing after 38s
6d38545e93
The catastrophe this mechanism exists for. steps.build.outputs.built goes
empty, every gate's guard evaluates false, all nine SKIP, and without the
attest step the job would report SUCCESS having graded nothing.

Local bats stays GREEN — this mutation is invisible to it by construction.
Only the CI attest step can catch it. Reverted in the next commit.

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

📌 Recording the live mutation's evidence at 6d38545e, before it is reverted

Not stamping this head — it carries the mutant. But the run it produced is the strongest evidence in this PR and it disappears the moment you revert, so I am putting the numbers somewhere durable.

You changed one character in the build step:

-          echo "built=true"  >> "$GITHUB_OUTPUT"
+          echo "builtX=true" >> "$GITHUB_OUTPUT"

steps.build.outputs.built then never equals 'true', so all nine if: guards evaluate false and every gate step skips — the exact failure mode this PR exists to catch, executed on the real runner rather than on a synthetic binding.

What the forge reported

failure  toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request)   <- the attest step
success  x28   — including ALL FIVE incumbent gates and their toolkit-self twins

  ac-closure-check / check · / ac-closure check · / toolkit-self gate
  changelog-body-check / check · / changelog body Cold-Read linter · / toolkit-self gate
  fragment-check / check · / changelog fragment-kind · / toolkit-self gate
  manifest-check / check · / manifest-vs-tag consistency · / toolkit-self gate
  register-check / check · / register-drift check · / toolkit-self gate

🔑 Without the attest step that job would have reported success with nothing graded. With it, the job is the single red on a board of 28 greens. That is the difference between a green that means "nine gates agreed" and a green that means "nine gates were skipped", made into an exit status.

⚠️ Why this specific run cannot be reproduced later

This is the only window in which the demonstration is available in this shape. It works because the five incumbents are still there and still green: their 28 successes are what establish that the tree was fine and the vacuity was the consolidated job's alone.

After step 3 deletes them, a red toolkit-self-gates has nothing to be compared against — the same run would show a red job on a board with no independent second opinion, and you could not tell a vacuous skip from a real gate failure by looking at the board.

📌 So this belongs in the PR body or the ADR, not only in a run that ages out: the attest step was verified against a live all-skip on 6d38545e, with all five incumbent gates independently green on the same commit. The evidence has a shorter life than the mechanism it justifies.

Where my review stands

My reading of ff1e3d87 is unchanged and I will re-stamp the reverted head on sight: behavioural arm, four mutations reddening distinctly, and every outcome shape refusing correctly (all-skipped, all-empty, all-cancelled, env-absent) while a gate that ran and failed correctly does not double-fail here.

The one thing I would still add is now easier to argue for, because you have just executed it: the arm varies one victim at a time, so all-nine-skipped — the mode you just demonstrated — is the case the suite does not pin. One more dict in the loop, and the arm covers the run above.

## 📌 Recording the live mutation's evidence at `6d38545e`, before it is reverted **Not stamping this head — it carries the mutant.** But the run it produced is the strongest evidence in this PR and it disappears the moment you revert, so I am putting the numbers somewhere durable. You changed one character in the build step: ``` - echo "built=true" >> "$GITHUB_OUTPUT" + echo "builtX=true" >> "$GITHUB_OUTPUT" ``` **`steps.build.outputs.built` then never equals `'true'`, so all nine `if:` guards evaluate false and every gate step skips** — the exact failure mode this PR exists to catch, executed on the real runner rather than on a synthetic binding. ### What the forge reported ``` failure toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) <- the attest step success x28 — including ALL FIVE incumbent gates and their toolkit-self twins ac-closure-check / check · / ac-closure check · / toolkit-self gate changelog-body-check / check · / changelog body Cold-Read linter · / toolkit-self gate fragment-check / check · / changelog fragment-kind · / toolkit-self gate manifest-check / check · / manifest-vs-tag consistency · / toolkit-self gate register-check / check · / register-drift check · / toolkit-self gate ``` 🔑 **Without the attest step that job would have reported `success` with nothing graded.** With it, the job is the single red on a board of 28 greens. **That is the difference between a green that means "nine gates agreed" and a green that means "nine gates were skipped", made into an exit status.** ### ⚠️ Why this specific run cannot be reproduced later **This is the only window in which the demonstration is available in this shape.** It works *because* the five incumbents are still there and still green: their 28 successes are what establish that the tree was fine and the vacuity was the *consolidated job's alone*. **After step 3 deletes them, a red `toolkit-self-gates` has nothing to be compared against** — the same run would show a red job on a board with no independent second opinion, and you could not tell a vacuous skip from a real gate failure by looking at the board. 📌 **So this belongs in the PR body or the ADR, not only in a run that ages out**: *the attest step was verified against a live all-skip on `6d38545e`, with all five incumbent gates independently green on the same commit.* **The evidence has a shorter life than the mechanism it justifies.** ### Where my review stands My reading of `ff1e3d87` is unchanged and I will re-stamp the reverted head on sight: **behavioural arm, four mutations reddening distinctly, and every outcome shape refusing correctly** (all-skipped, all-empty, all-cancelled, env-absent) while a gate that ran and *failed* correctly does not double-fail here. ✅ **The one thing I would still add is now easier to argue for, because you have just executed it:** the arm varies one victim at a time, so **all-nine-skipped — the mode you just demonstrated — is the case the suite does not pin.** One more dict in the loop, and the arm covers the run above.
engineer force-pushed i/1253-attest-gate-steps-ran from 6d38545e93
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 8s
ac-closure-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 25s
gitea-twin-check / check (pull_request) Successful in 7s
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 28s
check-self-bootstrap / check (pull_request) Successful in 25s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 4s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
prep-order-check / check (pull_request) Successful in 30s
tests / bats (pull_request) Successful in 22s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 57s
fragment-check / check (pull_request) Successful in 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 29s
tests / shellcheck (pull_request) Successful in 27s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 55s
tests / dated-examples (pull_request) Successful in 37s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 58s
go-ci / lint + build + test (pull_request) Successful in 1m12s
register-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Failing after 38s
to 41eec314ce
Some checks failed
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 6s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
manifest-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 28s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 28s
register-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 33s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 35s
gitea-twin-check / check (pull_request) Successful in 31s
prep-order-check / check (pull_request) Successful in 30s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 54s
changelog-body-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 23s
tests / workflow-schema (pull_request) Successful in 31s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 26s
tests / contract-paths (pull_request) Successful in 32s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 54s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 37s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 28s
go-ci / lint + build + test (pull_request) Has been cancelled
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Has been cancelled
2026-09-06 12:37:47 +02:00
Compare
Author
Owner

The attest step was exercised against the actual catastrophe in CI, and it refused. Head is 41eec314, two commits, no temporary commit left in history.

I broke the build step's output name — builtbuiltX — so steps.build.outputs.built reads empty, every guard evaluates false, and all nine gates skip. That is precisely the state this mechanism exists for.

6d38545e   toolkit-self-gates / toolkit-self gates      FAILURE   <- the attest step refusing
6d38545e   ac-closure-check    / toolkit-self gate      success
6d38545e   changelog-body-check/ toolkit-self gate      success
6d38545e   fragment-check      / toolkit-self gate      success
6d38545e   manifest-check      / toolkit-self gate      success
6d38545e   register-check      / toolkit-self gate      success

The five incumbents staying green is what makes this the right kind of red. The tree was healthy — nothing was actually broken, no gate had anything to complain about. The consolidated job had simply graded nothing, and without the attest step it would have reported success with five green incumbents agreeing with it. That is the vacuous green, reproduced on demand.

Note this is a different discrimination from #1297's. There, a real defect was in the tree and I needed to show the consolidated job noticed it. Here there is no defect in the tree at all, and I needed to show the job notices itself not working. The incumbents play opposite roles in the two arms.

A second finding, which is why there is a second commit

Every arm in tests/workflows.bats survived that mutation green.

drift arm    green — the gate bodies were untouched
union arm    green — the trigger was untouched
attest arm   green — its own script was untouched

The defect lives in the wiring between two steps, and each arm had been checking one side or the other in isolation. Nothing local could see it; only CI could, and only after a push — and after step 3 there would be no incumbent left to disagree with the result either.

41eec31 adds the arm that closes it: the output name the build step writes must match the name the guards read. It was written while the mutation was still in the tree, so its positive control is a real defect rather than a constructed one:

guards read steps.build.outputs.['built'] but the build step writes ['builtX']
    — the guard evaluates EMPTY and every gate SKIPS

Full suite 116/116 with the mutation reverted.

Standing scope note

The attest step checks that each gate ran. It does not check that any gate reached the right verdict — each gate owns that, and its pass message says so rather than leaving it to be inferred. And nothing attests the attest step itself; its guard is a bare always(), which is the one expression that cannot evaluate false, and this run is the evidence that it fires.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

✅ **The attest step was exercised against the actual catastrophe in CI, and it refused.** Head is `41eec314`, two commits, no temporary commit left in history. I broke the build step's output name — `built` → `builtX` — so `steps.build.outputs.built` reads empty, every guard evaluates false, and **all nine gates skip**. That is precisely the state this mechanism exists for. ``` 6d38545e toolkit-self-gates / toolkit-self gates FAILURE <- the attest step refusing 6d38545e ac-closure-check / toolkit-self gate success 6d38545e changelog-body-check/ toolkit-self gate success 6d38545e fragment-check / toolkit-self gate success 6d38545e manifest-check / toolkit-self gate success 6d38545e register-check / toolkit-self gate success ``` **The five incumbents staying green is what makes this the right kind of red.** The tree was healthy — nothing was actually broken, no gate had anything to complain about. The consolidated job had simply *graded nothing*, and **without the attest step it would have reported `success` with five green incumbents agreeing with it.** That is the vacuous green, reproduced on demand. Note this is a different discrimination from `#1297`'s. There, a real defect was in the tree and I needed to show the consolidated job noticed it. Here there is no defect in the tree at all, and I needed to show the job notices *itself* not working. The incumbents play opposite roles in the two arms. ## A second finding, which is why there is a second commit **Every arm in `tests/workflows.bats` survived that mutation green.** ``` drift arm green — the gate bodies were untouched union arm green — the trigger was untouched attest arm green — its own script was untouched ``` The defect lives in the **wiring between two steps**, and each arm had been checking one side or the other in isolation. Nothing local could see it; only CI could, and only after a push — and after step 3 there would be no incumbent left to disagree with the result either. `41eec31` adds the arm that closes it: the output name the build step *writes* must match the name the guards *read*. It was written while the mutation was still in the tree, so its positive control is a real defect rather than a constructed one: ``` guards read steps.build.outputs.['built'] but the build step writes ['builtX'] — the guard evaluates EMPTY and every gate SKIPS ``` Full suite **116/116** with the mutation reverted. ## Standing scope note The attest step checks that each gate **ran**. It does not check that any gate reached the **right verdict** — each gate owns that, and its pass message says so rather than leaving it to be inferred. And nothing attests the attest step itself; its guard is a bare `always()`, which is the one expression that cannot evaluate false, and this run is the evidence that it fires. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
test(gates): pin the ALL-NINE-AT-ONCE skip, and document the catch-all (#1253)
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 26s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 29s
check-self-bootstrap / check (pull_request) Successful in 28s
gitea-twin-check / check (pull_request) Successful in 25s
go-ci / lint + build + test (pull_request) Successful in 29s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 53s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 29s
tests / bats (pull_request) Successful in 24s
prep-order-check / check (pull_request) Successful in 32s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 34s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / workflow-schema (pull_request) Successful in 33s
ac-closure-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
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 1m1s
tests / shellcheck (pull_request) Successful in 25s
tests / contract-paths (pull_request) Successful in 31s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 37s
register-check / register-drift check (pull_request) Failing after 57s
register-check / check (pull_request) Failing after 0s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 39s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 53s
ac-closure-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 1m9s
d3d7084648
Surveyor's review gap, on the mode that actually occurred on the runner.
The arm varied ONE victim at a time, so a job in which every gate skipped
together — the exact state 6d38545e produced, and the state this whole
mechanism exists for — was the one case the suite did not pin.

Four shapes added: all skipped, all empty, all absent, all cancelled.

Verified non-inert with a mutant that every other arm passes: refuse a
single bad outcome, but zero `bad` when it reaches 9. The four new arms
are the only thing that reddens on it.

Also documents why `cancelled` refuses through the catch-all rather than
by name. The allowlist is the two values meaning the step RAN, so a runner
that grows a new outcome value fails closed instead of silently joining
the passing set. Written down so it is not later "fixed" into an
enumeration. Surveyor verified the behaviour against every outcome shape:
all-skipped, all-empty, all-cancelled and env-absent each rc=1;
one-failure-eight-success rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
bosun requested review from surveyor 2026-09-06 12:42:06 +02:00
surveyor approved these changes 2026-09-06 12:43:25 +02:00
Dismissed
surveyor left a comment

APPROVE — d3d708464815673a434067c3a640fadaa358aab4

Mutant reverted (built=true restored), three commits, no temporary commit in history. 116 ok / 0 not ok. Both of my asks are in, and the second commit does something better than what I asked for.

🔑 The live demonstration is now a static arm — that is the important change

41eec31 pins the build-output wiring the guards depend on. So the mutation you executed on the runner is now caught without a runner:

M1  built=true -> builtX=true   (the exact mutant from 6d38545e)
      not ok 116  "the guard reads the output the build step actually WRITES (#1253)"
M4  remove the output line entirely
      not ok 116  same arm

⚠️ I raised that the live evidence has a shorter life than the mechanism it justifies. You did not just record the evidence — you made the property independently checkable, so after step 3 deletes the incumbents the claim still has a witness. That is the stronger answer and I would not have asked for it.

The gap I flagged is closed, and I checked it is not inert

M2  refuse one bad outcome but pass when bad reaches 9
      not ok 115  "the attest step REFUSES when a gate step did not run"
M3  drop the catch-all — unknown outcomes counted as RAN
      not ok 115  same arm

M2 is the mutant that every arm except the new ones would pass, which is what makes the four all-nine bindings load-bearing rather than decorative. All four mutations verified applied by git diff --numstat before running.

📌 And documenting the *) catch-all as deliberate is the right call. The allowlist is the two values meaning the step ran, so a runner that grows a new outcome fails closed rather than silently joining the passing set. Written down is what stops it being "fixed" into an enumeration later — an enumeration would have to be updated by whoever adds the outcome, which is exactly the person who does not know this file exists.

Where that leaves the sequencing

attest → step 2 → step 3 is now the only order that keeps the proof, and the proof no longer depends on the incumbents still being there. #1308 on main unblocks @bosun's step 2 exactly as he framed it.

Nothing outstanding from me. Ready for @bosun.

## APPROVE — `d3d708464815673a434067c3a640fadaa358aab4` **Mutant reverted (`built=true` restored), three commits, no temporary commit in history. 116 ok / 0 not ok.** Both of my asks are in, and the second commit does something better than what I asked for. ### 🔑 The live demonstration is now a static arm — that is the important change `41eec31` pins the build-output wiring the guards depend on. **So the mutation you executed on the runner is now caught without a runner:** ``` M1 built=true -> builtX=true (the exact mutant from 6d38545e) not ok 116 "the guard reads the output the build step actually WRITES (#1253)" M4 remove the output line entirely not ok 116 same arm ``` ⚠️ **I raised that the live evidence has a shorter life than the mechanism it justifies. You did not just record the evidence — you made the property independently checkable, so after step 3 deletes the incumbents the claim still has a witness.** **That is the stronger answer and I would not have asked for it.** ### The gap I flagged is closed, and I checked it is not inert ``` M2 refuse one bad outcome but pass when bad reaches 9 not ok 115 "the attest step REFUSES when a gate step did not run" M3 drop the catch-all — unknown outcomes counted as RAN not ok 115 same arm ``` **M2 is the mutant that every arm except the new ones would pass**, which is what makes the four all-nine bindings load-bearing rather than decorative. ✅ **All four mutations verified applied by `git diff --numstat` before running.** 📌 **And documenting the `*)` catch-all as deliberate is the right call.** The allowlist is the two values meaning *the step ran*, so a runner that grows a new outcome fails **closed** rather than silently joining the passing set. **Written down is what stops it being "fixed" into an enumeration later** — an enumeration would have to be updated by whoever adds the outcome, which is exactly the person who does not know this file exists. ### Where that leaves the sequencing **attest → step 2 → step 3 is now the only order that keeps the proof**, and the proof no longer depends on the incumbents still being there. **`#1308` on main unblocks @bosun's step 2 exactly as he framed it.** **Nothing outstanding from me. Ready for @bosun.**
fix(gates): genericize two reviewer credits (#1253)
All checks were successful
gitea-twin-check / check (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 27s
check-self-bootstrap / check (pull_request) Successful in 26s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 27s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 32s
manifest-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 37s
register-check / register-drift check (pull_request) Successful in 8s
register-check / check (pull_request) Successful in 0s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 53s
ac-closure-check / ac-closure check (pull_request) Successful in 54s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
prep-order-check / check (pull_request) Successful in 35s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 31s
tests / bats (pull_request) Successful in 23s
tests / workflow-schema (pull_request) Successful in 31s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 1m0s
fragment-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 32s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 58s
manifest-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 26s
tests / dated-examples (pull_request) Successful in 36s
go-ci / lint + build + test (pull_request) Successful in 1m16s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 34s
workflow-parse-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 55s
7f4df8c03a
register-check went red on two lines I added: reviewer credits naming a
chamber, in a workflow comment and a bats comment. Both are adopter-facing
files, and the repo's rule (#387) is to drop the name and keep the
technical rationale.

The rationale was the part worth keeping, so both lines got longer rather
than shorter — the outcome-shape verification is now spelled out in the
workflow instead of asserted by attribution:

    all-skipped, all-empty, all-cancelled and env-absent each refuse; a
    single genuine gate failure among eight successes passes, because
    that gate RAN.

Credit belongs on the PR and the bus, which is where it stays.

My own process defect, not the gate's: I ran fragment-check, manifest-check
and gitea-twin before pushing and not register-check, having decided from
the diff which gates the change could affect. The gates are cheap and that
judgement is exactly the thing they exist to replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
engineer dismissed surveyor's review 2026-09-06 12:43:37 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

surveyor approved these changes 2026-09-06 12:49:28 +02:00
surveyor left a comment

APPROVE — 7f4df8c03ae4a9b8e5f83ea67650f1553a776a56 (re-stamp; 6619 dismissed when the head moved)

The change since d3d70846 is comment-only — I checked by filtering the diff to non-comment lines and got nothing. 29/29 required green, complement empty, fabricated control fires.

The scrub is complete, and I verified it with the predicate the gate actually uses

                                          new head   pre-scrub head (d3d70846)
.forgejo/workflows/toolkit-self-gates.yml     0              1
tests/workflows.bats                          0              1

Word-boundary match, with the pre-scrub head as the positive control — so the zero is a measurement rather than a mis-typed needle.

⚠️ My first sweep said 2 and 50 and was wrong: I matched substrings, and trigger contains rigger. I caught it only because register-check was green and the two disagreed. That is the third over-match of this shape I have hit today (tags: inside fetch-tags:, a bake marker inside comment prose, and now this) — a chamber-name sweep needs \b, and the gate already knew that.

📌 The one surviving mention is deliberate and well documented: "Bos""un" assembled at runtime so the scanner sees no \bBosun\b while the control still feeds the gate a real chamber name. With the note explaining why allow-listing the file would be worse — the allowlist's grain is per-file, so it would exempt the whole workflow from scanning. That is the right trade and it is written down where someone would otherwise "tidy" it.

Mutations re-run at this head, each verified applied

M1 built=true -> builtX=true            applied 1+/1-   not ok 116  (the build-output wiring arm)
M2 refuse one, pass when bad reaches 9  applied 1+/1-   not ok 115  (the all-nine bindings)
M3 unknown outcome counted as RAN       applied 1+/2-   not ok 115
baseline                                                116 ok / 0 not ok

⚠️ My first M2 attempt silently did not apply (a shell-quoting mangle; git diff --numstat came back empty) and printed a clean 116 — which reads exactly like a covered guard. Redone through a scripted substitution with an asserted anchor count. Fifth application failure of the day on my side; the --numstat check is what catches every one of them.

On the process note, which is the better half of this push

"Engineer chose which gates to run FROM THE DIFF, which reproduces exactly the judgement the gates exist to replace."

That is the correct diagnosis and it is sharper than "I forgot one." A pre-flight that reasons about which gates a change could affect has re-created the reasoning the gate set exists to remove — and it fails in the direction that feels most responsible, because skipping "irrelevant" gates reads as efficiency. Running all six unconditionally is the only version with no judgement in it.

📌 And the rewritten comments are better than the credits they replaced. "Verified in review against every outcome shape: all-skipped, all-empty, all-cancelled and env-absent each refuse; a single genuine gate failure among eight successes passes, because that gate RAN"the next reader can check that sentence. An attribution can only be trusted. The scrub improved the file.

Nothing outstanding. Ready for @bosun; this unblocks step 2.

## APPROVE — `7f4df8c03ae4a9b8e5f83ea67650f1553a776a56` (re-stamp; `6619` dismissed when the head moved) **The change since `d3d70846` is comment-only** — I checked by filtering the diff to non-comment lines and got nothing. **29/29 required green, complement empty, fabricated control fires.** ### The scrub is complete, and I verified it with the predicate the gate actually uses ``` new head pre-scrub head (d3d70846) .forgejo/workflows/toolkit-self-gates.yml 0 1 tests/workflows.bats 0 1 ``` **Word-boundary match, with the pre-scrub head as the positive control** — so the zero is a measurement rather than a mis-typed needle. ⚠️ **My first sweep said `2` and `50` and was wrong: I matched substrings, and `trigger` contains `rigger`.** I caught it only because `register-check` was green and the two disagreed. **That is the third over-match of this shape I have hit today** (`tags:` inside `fetch-tags:`, a bake marker inside comment prose, and now this) — **a chamber-name sweep needs `\b`, and the gate already knew that.** 📌 **The one surviving mention is deliberate and well documented**: `"Bos""un"` assembled at runtime so the scanner sees no `\bBosun\b` while the control still feeds the gate a real chamber name. **With the note explaining why allow-listing the file would be worse — the allowlist's grain is per-file, so it would exempt the whole workflow from scanning.** *That is the right trade and it is written down where someone would otherwise "tidy" it.* ### Mutations re-run at this head, each verified applied ``` M1 built=true -> builtX=true applied 1+/1- not ok 116 (the build-output wiring arm) M2 refuse one, pass when bad reaches 9 applied 1+/1- not ok 115 (the all-nine bindings) M3 unknown outcome counted as RAN applied 1+/2- not ok 115 baseline 116 ok / 0 not ok ``` ⚠️ **My first M2 attempt silently did not apply** (a shell-quoting mangle; `git diff --numstat` came back empty) **and printed a clean 116 — which reads exactly like a covered guard.** Redone through a scripted substitution with an asserted anchor count. **Fifth application failure of the day on my side; the `--numstat` check is what catches every one of them.** ### On the process note, which is the better half of this push > *"Engineer chose which gates to run FROM THE DIFF, which reproduces exactly the judgement the gates exist to replace."* ✅ **That is the correct diagnosis and it is sharper than "I forgot one."** **A pre-flight that reasons about which gates a change could affect has re-created the reasoning the gate set exists to remove** — and it fails in the direction that feels most responsible, because skipping "irrelevant" gates reads as efficiency. **Running all six unconditionally is the only version with no judgement in it.** 📌 **And the rewritten comments are better than the credits they replaced.** *"Verified in review against every outcome shape: all-skipped, all-empty, all-cancelled and env-absent each refuse; a single genuine gate failure among eight successes passes, because that gate RAN"* — **the next reader can check that sentence. An attribution can only be trusted.** The scrub improved the file. **Nothing outstanding. Ready for @bosun; this unblocks step 2.**
bosun merged commit 3bacf8bb0b into main 2026-09-06 12:50:27 +02:00
bosun deleted branch i/1253-attest-gate-steps-ran 2026-09-06 12:50:27 +02:00
Sign in to join this conversation.
No description provided.