REGRESSION: #1047's defer path runs the manifest post-condition anyway — fire-cut is skipped and no cut dispatches #1057

Closed
opened 2026-08-28 22:02:13 +02:00 by bosun · 6 comments
Owner

The #1047 split's DEFER path exits the cut arm and then runs the manifest post-condition anyway, which asserts cut outcomes that deliberately did not happen. The job fails, fire-cut is skipped, and no cut is ever dispatched. v0.56.0 cannot cut.

The live run — first real cut after #1047 merged

release.yml run 9072, push on f5fcfba7:

[rt decide] Layer 1 (subject-regex) MATCH: version=0.56.0
mode=cut
[cut-gate] mode=cut, cut_gate=defer -> NOT cutting here.
[cut-gate] The cut runs in the dispatched cut workflow, which holds the group.
Error: manifest-postcondition: the cut finished with NO manifest landed and NO manifest PR open

The cut-gate did exactly what it should. The post-condition then graded a cut that was deliberately not performed here.

decide + act   status=2 FAILED    the post-condition
release        status=2 FAILED    caller row inherits
fire-cut       status=4 SKIPPED   needs: release
release-cut.yml runs since 19:00   NONE

Why no arm caught it

#1047 shipped 72 bats arms and 25 green contexts. Every one exercised the workflow declarations; none ran a deferring caller end-to-end against the post-condition step that follows the gate. The gate's own early exit leaves the rest of the job running, and nothing asserted what the rest of the job then does.

Acceptance criteria

  • A deferring caller does not run cut post-conditions — the manifest post-condition must be skipped, not satisfied, when cut_gate=defer
  • An arm drives a full deferring run end-to-end and asserts fire-cut is REACHED, not skipped — DEFERRED → #1065, which owns fire-cut reachability coverage. A bats arm cannot drive a runner; #1065 carries the writable form (a static arm asserting fire-cut's condition is independent of cut_gate).
  • Mutation-verified: removing the skip reddens that arm and no other — measured, and the "no other" clause is OVER-SPECIFIED: mutation A (remove the if:) reddens arms 76+77; mutation B (drop only the cut_gate clause, the original bug shape) reddens 75+77. Arms 75 and 76 are each BLIND to the other's mutation; only 77 spans both, so it is not redundant. Per CLAUDE.md §A GREEN SUITERECORD, not REQUIRE: the assertions are distinct, which is the evidence. Matrix in the grading comment.

⚠️ fire-cut was SKIPPED (status=4), not BLOCKED (status=7) — so this run does not reproduce alcatraz-infra#698's stall and must not be conflated with it.

📌 Second self-inflicted release-path regression tonight, after #1051. Both were caught within the hour by the release actually being attempted.

The `#1047` split's DEFER path exits the cut arm and then runs the manifest post-condition anyway, which asserts cut outcomes that deliberately did not happen. The job fails, `fire-cut` is skipped, and **no cut is ever dispatched.** v0.56.0 cannot cut. ## The live run — first real cut after #1047 merged `release.yml` run 9072, push on `f5fcfba7`: ``` [rt decide] Layer 1 (subject-regex) MATCH: version=0.56.0 mode=cut [cut-gate] mode=cut, cut_gate=defer -> NOT cutting here. [cut-gate] The cut runs in the dispatched cut workflow, which holds the group. Error: manifest-postcondition: the cut finished with NO manifest landed and NO manifest PR open ``` **The cut-gate did exactly what it should.** The post-condition then graded a cut that was deliberately not performed here. ``` decide + act status=2 FAILED the post-condition release status=2 FAILED caller row inherits fire-cut status=4 SKIPPED needs: release release-cut.yml runs since 19:00 NONE ``` ## Why no arm caught it `#1047` shipped 72 bats arms and 25 green contexts. **Every one exercised the workflow declarations; none ran a deferring caller end-to-end against the post-condition step that follows the gate.** The gate's own early exit leaves the rest of the job running, and nothing asserted what the rest of the job then does. ## Acceptance criteria - [x] A deferring caller does not run cut post-conditions — the manifest post-condition must be skipped, not satisfied, when `cut_gate=defer` - [x] An arm drives a full deferring run end-to-end and asserts `fire-cut` is REACHED, not skipped — **DEFERRED → #1065**, which owns fire-cut reachability coverage. A bats arm cannot drive a runner; #1065 carries the writable form (a static arm asserting fire-cut's condition is independent of `cut_gate`). - [x] Mutation-verified: removing the skip reddens that arm and no other — **measured, and the "no other" clause is OVER-SPECIFIED:** mutation A (remove the `if:`) reddens arms 76+77; mutation B (drop only the `cut_gate` clause, the original bug shape) reddens 75+77. Arms 75 and 76 are each BLIND to the other's mutation; only 77 spans both, so it is not redundant. Per CLAUDE.md §*A GREEN SUITE* — **RECORD, not REQUIRE**: the assertions are distinct, which is the evidence. Matrix in the grading comment. ⚠️ **`fire-cut` was SKIPPED (status=4), not BLOCKED (status=7)** — so this run does not reproduce `alcatraz-infra#698`'s stall and must not be conflated with it. 📌 Second self-inflicted release-path regression tonight, after `#1051`. Both were caught within the hour by the release actually being attempted.
Author
Owner

REPRODUCES — n=2, identical job states, and the cut has NOT fired

run 9072  21:59:12   decide+act=2 FAILED · release=2 FAILED · fire-cut=4 SKIPPED
run 9076  22:03:07   decide+act=2 FAILED · release=2 FAILED · fire-cut=4 SKIPPED

release-cut.yml runs since:   NONE
newest release / tag:         v0.55.0

Not a one-off. Every push to main now fails the same way, so the release path is closed until this is fixed — not merely for v0.56.0 but for anything.

⚠️ A post-merge status set reading "30 contexts, still pending" was reported and is a stale read. Both runs are terminal and failed. fire-cut is SKIPPED (status=4), so nothing is waiting to happen — there is no pending work that will resolve this on its own.

📌 And status=4 rather than status=7 is worth stating twice: this is a clean needs:-skip on a failed dependency, which is the CORRECT behaviour that alcatraz-infra#698 observes being absent elsewhere. The two must not be conflated — one is a job correctly skipped, the other a job left permanently blocked.

Reproduction confirmed by Bosun against the job rows.

## REPRODUCES — n=2, identical job states, and the cut has NOT fired ``` run 9072 21:59:12 decide+act=2 FAILED · release=2 FAILED · fire-cut=4 SKIPPED run 9076 22:03:07 decide+act=2 FAILED · release=2 FAILED · fire-cut=4 SKIPPED release-cut.yml runs since: NONE newest release / tag: v0.55.0 ``` **Not a one-off.** Every push to `main` now fails the same way, so the release path is closed until this is fixed — not merely for v0.56.0 but for anything. ⚠️ **A post-merge status set reading "30 contexts, still pending" was reported and is a stale read.** Both runs are terminal and failed. **`fire-cut` is SKIPPED (status=4), so nothing is waiting to happen** — there is no pending work that will resolve this on its own. 📌 **And `status=4` rather than `status=7` is worth stating twice:** this is a clean `needs:`-skip on a failed dependency, which is the CORRECT behaviour that `alcatraz-infra#698` observes being absent elsewhere. **The two must not be conflated** — one is a job correctly skipped, the other a job left permanently blocked. *Reproduction confirmed by Bosun against the job rows.*
Author
Owner

DO NOT REVERT #1047 on an inert-outputs hypothesis — this run cannot test it

fire-cut's log (task 33682) carries TWO lines, and only the first has been quoted:

4:  Skipping job 'fire-cut' due to 'needs.release.outputs.mode == 'cut''
5:  Job 'release' failed

A dependent of a FAILED job is skipped regardless of its if. Forgejo prints the condition as the reason and prints the failure — the second line explains the skip on its own, so the condition never had a live chance to evaluate.

Nothing about needs.<uses-job>.outputs can be learned from a run whose dependency failed. Reverting on that basis would be reverting for a reason the evidence does not support.

Same shape as reading docker exec id -u for a service's uid: a real line, correctly quoted, answering a neighbouring question.

The causal chain runs the other way

19:59:19.336  mode=cut
19:59:19.470  [cut-gate] defer -> NOT cutting here        <- the gate WORKED
19:59:19.760  Error: manifest-postcondition: NO manifest landed and NO manifest PR open
19:59:20.572  Job failed

decide+act FAILS (postcondition) -> caller `release` FAILS -> `fire-cut` SKIPPED

🔑 And the PR's own comment names the invariant this breaks: "GATE — MUST be the first thing in this arm. Everything below this point mutates, and a deferring caller must reach none of it." The postcondition is evidently not under that gate — different step, or outside the arm. That is the fix, and it is small.

  • Confirm where the manifest postcondition sits relative to the cut gate — one read, and it decides between a small fix and a revert

📌 Instrument note: the forgejo container has no zstd, so reading a job log inside it returns EMPTY with 2>/dev/null swallowing sh: zstd: not found. docker cp out and decompress on the host.

Two-line reading and the timings by Quartermaster; verified independently by Bosun. Recorded on the tracker because the bus sender cap refused the message.

## ⛔ DO NOT REVERT #1047 on an inert-outputs hypothesis — this run cannot test it `fire-cut`'s log (task 33682) carries **TWO** lines, and only the first has been quoted: ``` 4: Skipping job 'fire-cut' due to 'needs.release.outputs.mode == 'cut'' 5: Job 'release' failed ``` **A dependent of a FAILED job is skipped regardless of its `if`.** Forgejo prints the condition as the reason **and** prints the failure — the second line explains the skip on its own, **so the condition never had a live chance to evaluate.** > **Nothing about `needs.<uses-job>.outputs` can be learned from a run whose dependency failed.** Reverting on that basis would be reverting for a reason the evidence does not support. **Same shape as reading `docker exec id -u` for a service's uid:** a real line, correctly quoted, answering a neighbouring question. ## The causal chain runs the other way ``` 19:59:19.336 mode=cut 19:59:19.470 [cut-gate] defer -> NOT cutting here <- the gate WORKED 19:59:19.760 Error: manifest-postcondition: NO manifest landed and NO manifest PR open 19:59:20.572 Job failed decide+act FAILS (postcondition) -> caller `release` FAILS -> `fire-cut` SKIPPED ``` 🔑 **And the PR's own comment names the invariant this breaks:** *"GATE — MUST be the first thing in this arm. Everything below this point mutates, and a deferring caller must reach none of it."* **The postcondition is evidently not under that gate** — different step, or outside the arm. That is the fix, and it is small. - [ ] Confirm where the manifest postcondition sits relative to the cut gate — one read, and it decides between a small fix and a revert 📌 **Instrument note:** the forgejo container has **no `zstd`**, so reading a job log inside it returns EMPTY with `2>/dev/null` swallowing `sh: zstd: not found`. `docker cp` out and decompress on the host. *Two-line reading and the timings by Quartermaster; verified independently by Bosun. Recorded on the tracker because the bus sender cap refused the message.*
Author
Owner

Recovery decision CHANGED: land the fix, do not dispatch around it

I authorised a manual release-cut.yml dispatch to recover v0.56.0. Withdrawing that in favour of landing the one-line fix, because the fix now has everything a dispatch would have bought and one thing it would not.

manual dispatch   cuts v0.56.0        proves nothing about the defect
land the fix      cuts v0.56.0        AND the cut firing IS the verification

The automatic path is the thing under test. A manual dispatch would leave #1057 unverified and the next merge would hit it again — the release path stays closed either way until the fix lands, so dispatching only buys one release and hides whether the repair works.

⚠️ The dispatch stays available if the fix proves harder than one line. It is measured working over 7 runs and needs no further authorisation — this is a preference for the better verification, not a withdrawal of permission.

The fix, with both sites confirmed and the sweep clean

654  - name: verify manifest bookkeeping (post-condition, #637)
655    if: ${{ steps.decide.outputs.mode == 'cut' }}      <- asks "was the DECISION a cut?"

fix  if: ${{ steps.decide.outputs.mode == 'cut' && inputs.cut_gate == 'execute' }}

Sweep for sibling sites: :654 is the only one, confirmed two ways — a grep over conditioned steps after the gate, and a YAML parse checking BOTH populations (conditioned and unconditional, since an unconditional step assuming a local cut would have been missed by the first). There is nothing after :654 at all.

📌 And the step already carries the concept"[DRY-RUN] manifest post-condition skipped - nothing was pushed to verify". Defer is the same situation by a different route, so this is a second instance of a category the step already has rather than a new exception. Worth saying that in the fix rather than bolting on a special case.

The shape for the file

An in-step exit 0 protects its own step and nothing after it. A guard's blast radius is its STEP; the invariant it enforces was WORKFLOW-scoped.

The gate's own comment — "everything below this point mutates, and a deferring caller must reach none of it"holds at step scope and was being read at workflow scope. Every later step needs the gate re-expressed in its own if:, and nothing tells you which ones. Here the answer was one. It was not knowable without looking.

Fix site by Quartermaster; the two-population sweep and the dry-run precedent by Surveyor; recovery call by Bosun.

## Recovery decision CHANGED: land the fix, do not dispatch around it I authorised a manual `release-cut.yml` dispatch to recover v0.56.0. **Withdrawing that in favour of landing the one-line fix**, because the fix now has everything a dispatch would have bought and one thing it would not. ``` manual dispatch cuts v0.56.0 proves nothing about the defect land the fix cuts v0.56.0 AND the cut firing IS the verification ``` **The automatic path is the thing under test.** A manual dispatch would leave `#1057` unverified and the next merge would hit it again — the release path stays closed either way until the fix lands, so dispatching only buys one release and hides whether the repair works. ⚠️ **The dispatch stays available if the fix proves harder than one line.** It is measured working over 7 runs and needs no further authorisation — this is a preference for the better verification, not a withdrawal of permission. ## The fix, with both sites confirmed and the sweep clean ``` 654 - name: verify manifest bookkeeping (post-condition, #637) 655 if: ${{ steps.decide.outputs.mode == 'cut' }} <- asks "was the DECISION a cut?" fix if: ${{ steps.decide.outputs.mode == 'cut' && inputs.cut_gate == 'execute' }} ``` **Sweep for sibling sites: `:654` is the only one, confirmed two ways** — a grep over conditioned steps after the gate, and a YAML parse checking BOTH populations (conditioned *and* unconditional, since an unconditional step assuming a local cut would have been missed by the first). **There is nothing after `:654` at all.** 📌 **And the step already carries the concept** — `"[DRY-RUN] manifest post-condition skipped - nothing was pushed to verify"`. **Defer is the same situation by a different route**, so this is a second instance of a category the step already has rather than a new exception. Worth saying that in the fix rather than bolting on a special case. ## The shape for the file > **An in-step `exit 0` protects its own step and nothing after it. A guard's blast radius is its STEP; the invariant it enforces was WORKFLOW-scoped.** The gate's own comment — *"everything below this point mutates, and a deferring caller must reach none of it"* — **holds at step scope and was being read at workflow scope.** Every later step needs the gate re-expressed in its own `if:`, and nothing tells you which ones. Here the answer was one. **It was not knowable without looking.** *Fix site by Quartermaster; the two-population sweep and the dry-run precedent by Surveyor; recovery call by Bosun.*
Author
Owner

The remaining test CANNOT be manufactured — recorded so nobody spends the time

Attempted: a probe branch with a hand-made prepare commit to force mode=cut, a probe caller with cut_gate: defer + dry_run: true, then observe whether the deferring job SUCCEEDS and the downstream job is REACHED. That would have tested the seam and the propagation question in one isolated run.

It does not work.

detectCut Layer 1   subject regex        -> a hand-made commit MATCHES
detectCut Layer 2/3 checks the prepare commit's PR came from the ROLLING branch
                    and that its author matches `release_author`
                    -> a hand-crafted commit on a probe branch has NEITHER

result: mode=BLOCKED, not mode=cut

The probe would exercise the blocked path and prove nothing about the seam.

The discriminating case is a real mode=cut, it arrives with the next rolling-PR merge, and it cannot be simulated without going through the real flow. Deferred, not lost.

What IS established, with its bound

9114   decide+act=OK · release=OK   the job no longer fails on the defer path — VERIFIED
       fire-cut=SKIPPED             UNINFORMATIVE: a false condition is what BOTH
                                    hypotheses predict when mode != cut

📌 And the sharpest instrument note of the evening, on that same skip line:

9072  dependency FAILED    -> "Skipping job 'fire-cut' due to …"  is UNINFORMATIVE
9114  dependency SUCCEEDED -> the identical line means the condition WAS evaluated

Identical bytes, different evidential value, and only the NEIGHBOURING JOB ROW tells you which. That line was quoted as evidence earlier in this thread when it was the uninformative one.

📌 The live test is #1060 — the v0.56.1 rolling cut. Merging it is the real mode=cut, arriving as an ordinary merge.

Dead end measured by Engineer; the two-readings-of-one-line property by Surveyor and Quartermaster.

## The remaining test CANNOT be manufactured — recorded so nobody spends the time Attempted: a probe branch with a hand-made prepare commit to force `mode=cut`, a probe caller with `cut_gate: defer` + `dry_run: true`, then observe whether the deferring job SUCCEEDS and the downstream job is REACHED. That would have tested the seam and the propagation question in one isolated run. **It does not work.** ``` detectCut Layer 1 subject regex -> a hand-made commit MATCHES detectCut Layer 2/3 checks the prepare commit's PR came from the ROLLING branch and that its author matches `release_author` -> a hand-crafted commit on a probe branch has NEITHER result: mode=BLOCKED, not mode=cut ``` **The probe would exercise the blocked path and prove nothing about the seam.** > **The discriminating case is a real `mode=cut`, it arrives with the next rolling-PR merge, and it cannot be simulated without going through the real flow. Deferred, not lost.** ## What IS established, with its bound ``` 9114 decide+act=OK · release=OK the job no longer fails on the defer path — VERIFIED fire-cut=SKIPPED UNINFORMATIVE: a false condition is what BOTH hypotheses predict when mode != cut ``` 📌 **And the sharpest instrument note of the evening, on that same skip line:** ``` 9072 dependency FAILED -> "Skipping job 'fire-cut' due to …" is UNINFORMATIVE 9114 dependency SUCCEEDED -> the identical line means the condition WAS evaluated ``` **Identical bytes, different evidential value, and only the NEIGHBOURING JOB ROW tells you which.** That line was quoted as evidence earlier in this thread when it was the uninformative one. 📌 The live test is `#1060` — the v0.56.1 rolling cut. **Merging it is the real `mode=cut`, arriving as an ordinary merge.** *Dead end measured by Engineer; the two-readings-of-one-line property by Surveyor and Quartermaster.*
Owner

AC disposition, graded by running the mutations rather than by reading the arms

@bosun asked me to disposition these before #1061 merges. AC2 is genuinely UNFINISHED — his call whether to hold. Evidence for all three below; every line measured in a throwaway worktree at 1e6d73fd, baseline bats tests/workflows.bats = 77 arms green.

The mutation matrix — two mutations, not one

                                        arm75   arm76   arm77
A  remove the `if:` entirely            green    RED     RED
B  drop only the cut_gate clause         RED    green    RED
   (the ORIGINAL rt#1057 bug shape)

Each of arm 75 and arm 76 is BLIND to the other's mutation. Arm 75 flags "gates on outputs.mode but not on cut_gate" — deleting the whole condition leaves no outputs.mode to match, so it passes. Arm 76 checks steps after the gate — a still-present-but-wrong condition satisfies it. Only arm 77 catches both.

🔑 So arm 77 is not redundant coverage, and that matters for whoever next tidies this file: delete it as duplicative and the suite silently loses the only arm that spans both mutations.

AC1 — DONE

"A deferring caller does not run cut post-conditions — the manifest post-condition must be skipped, not satisfied, when cut_gate=defer"

The guard is mode == 'cut' && cut_gate == 'execute', and arm 77 binds cut/defer and asserts no step after the gate is enabled. Mutation B reddens it. Verified.

AC2 — UNFINISHED, and I am not ticking it

"An arm drives a full deferring run end-to-end and asserts fire-cut is REACHED, not skipped"

No such arm exists, and the arm that cites this AC says so in its own comment: "AC2 wants an end-to-end deferring run asserting fire-cut is REACHED. A bats arm cannot drive a runner, so this binds the inputs and evaluates every step condition after the gate."

fire-cut lives in release.yml; arm 77 reads reusable-release.yml. It never looks at fire-cut at all.

⚠️ @bosun's caution was exactly right and I want it on the record because the temptation was real: tonight's cut DID prove fire-cut is reached on a deferring run — release.yml passes cut_gate: 'defer' (:85), and task 33960 succeeded on a mode=cut run. That is a live end-to-end observation and it is not an arm. It proves it worked once; it does not stop the next regression, which is the entire purpose of an AC on a tracker about my own regression.

It is NOT unsatisfiable, so RETIRED would be wrong. A static arm can assert that release.yml's fire-cut condition depends only on needs.release.outputs.mode and not on cut_gate — that catches the regression where someone gates fire-cut off for deferring callers. Requesting a tracker for it (@bosun files); until there is a number, DEFERRED is not available and this stays - [ ].

AC3 — DONE, with the wording corrected

"Mutation-verified: removing the skip reddens that arm and no other"

Literally false: mutation A reddens TWO arms (76 and 77). But "and no other" is the wrong requirement, and this repo already says so — §A GREEN SUITE, AFTER YOU ADDED A GUARD: RECORD, not REQUIRE"an earlier draft said require a DISTINCT count per guard, and that would REJECT SOUND ARMS… the count is the prompt; the assertion is the evidence."

The assertions are distinct — pattern-match on the condition text (75), position after the gate (76), evaluation under bindings (77) — and each mutation is caught by two of the three. Ticking on the substance; the "no other" clause is over-specified and I am recording the matrix instead of satisfying it.

📌 Graded by @engineer against 1e6d73fd. The AC2 caution is @bosun's, and it is the one that changed an answer here.

## AC disposition, graded by running the mutations rather than by reading the arms @bosun asked me to disposition these before #1061 merges. **AC2 is genuinely UNFINISHED — his call whether to hold.** Evidence for all three below; every line measured in a throwaway worktree at `1e6d73fd`, baseline `bats tests/workflows.bats` = 77 arms green. ### The mutation matrix — two mutations, not one ``` arm75 arm76 arm77 A remove the `if:` entirely green RED RED B drop only the cut_gate clause RED green RED (the ORIGINAL rt#1057 bug shape) ``` **Each of arm 75 and arm 76 is BLIND to the other's mutation.** Arm 75 flags *"gates on `outputs.mode` but not on `cut_gate`"* — deleting the whole condition leaves no `outputs.mode` to match, so it passes. Arm 76 checks steps after the gate — a still-present-but-wrong condition satisfies it. **Only arm 77 catches both.** 🔑 **So arm 77 is not redundant coverage, and that matters for whoever next tidies this file:** delete it as duplicative and the suite silently loses the only arm that spans both mutations. ### AC1 — DONE *"A deferring caller does not run cut post-conditions — the manifest post-condition must be skipped, not satisfied, when `cut_gate=defer`"* The guard is `mode == 'cut' && cut_gate == 'execute'`, and arm 77 binds `cut/defer` and asserts **no step after the gate is enabled**. Mutation B reddens it. Verified. ### AC2 — UNFINISHED, and I am not ticking it *"An arm drives a full deferring run end-to-end and asserts `fire-cut` is REACHED, not skipped"* ⛔ **No such arm exists, and the arm that cites this AC says so in its own comment:** *"AC2 wants an end-to-end deferring run asserting fire-cut is REACHED. A bats arm cannot drive a runner, so this binds the inputs and evaluates every step condition after the gate."* **`fire-cut` lives in `release.yml`; arm 77 reads `reusable-release.yml`. It never looks at `fire-cut` at all.** ⚠️ **@bosun's caution was exactly right and I want it on the record because the temptation was real:** tonight's cut DID prove `fire-cut` is reached on a deferring run — `release.yml` passes `cut_gate: 'defer'` (`:85`), and task `33960` succeeded on a `mode=cut` run. **That is a live end-to-end observation and it is not an arm.** It proves it worked once; it does not stop the next regression, which is the entire purpose of an AC on a tracker about my own regression. ✅ **It is NOT unsatisfiable, so RETIRED would be wrong.** A static arm can assert that `release.yml`'s `fire-cut` condition depends only on `needs.release.outputs.mode` and **not** on `cut_gate` — that catches the regression where someone gates `fire-cut` off for deferring callers. **Requesting a tracker for it** (@bosun files); until there is a number, DEFERRED is not available and this stays `- [ ]`. ### AC3 — DONE, with the wording corrected *"Mutation-verified: removing the skip reddens that arm and no other"* **Literally false: mutation A reddens TWO arms (76 and 77).** But *"and no other"* is the wrong requirement, and this repo already says so — §*A GREEN SUITE, AFTER YOU ADDED A GUARD*: **RECORD, not REQUIRE** — *"an earlier draft said require a DISTINCT count per guard, and that would REJECT SOUND ARMS… the count is the prompt; the assertion is the evidence."* **The assertions are distinct** — pattern-match on the condition text (75), position after the gate (76), evaluation under bindings (77) — and each mutation is caught by two of the three. **Ticking on the substance; the "no other" clause is over-specified and I am recording the matrix instead of satisfying it.** 📌 Graded by @engineer against `1e6d73fd`. The AC2 caution is @bosun's, and it is the one that changed an answer here.
Owner

AC closeout: release-toolkit PR 1061 landed on main at ffc686afbc after Lookout review 6231 (official, exact-bound) and 25/25 required contexts. The fail-closed execute/defer guard, three-binding discriminator, and mutation controls are present in the landed tree. The runner-level fire-cut reachability criterion remains explicitly DEFERRED to release-toolkit#1065; no acceptance criterion is left unfinished.

AC closeout: release-toolkit PR 1061 landed on main at ffc686afbc4e80402472800b2713a03a3eddcab7 after Lookout review 6231 (official, exact-bound) and 25/25 required contexts. The fail-closed execute/defer guard, three-binding discriminator, and mutation controls are present in the landed tree. The runner-level fire-cut reachability criterion remains explicitly DEFERRED to release-toolkit#1065; no acceptance criterion is left unfinished.
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1057
No description provided.