Skip to content

fix(landing): consult the required-check roster before moving main - #895

Merged
wenzowski merged 4 commits into
mainfrom
claude/prose-dispatch-gate-verify-u0fnx3
Sep 7, 2026
Merged

fix(landing): consult the required-check roster before moving main#895
wenzowski merged 4 commits into
mainfrom
claude/prose-dispatch-gate-verify-u0fnx3

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes CLOUD-1570.

DO-NOT-CLOSE CLOUD-1320
DO-NOT-CLOSE CLOUD-1579

Both are served by commits here and neither is completed by them. CLOUD-1320 is
the general rule — a row's line_sources must supply every path its module
classifies — and this PR only repairs its own module's instance of it.
CLOUD-1579 was filed from this branch and is untouched by it.

Why

protect-main requires exactly one context, final. Required status checks apply to direct pushes, and landing here is a direct push by the /fast-forward bot.

final declares needs: [ci, batten-check, bats, perf]. $CI_REQUIRED_CHECKS names 20. So branch protection fans in over 4 of 20, and fifteen required checks sit outside it — cross, commit-lint, zizmor, darwin-link, semver, windows, and nine action* contexts.

Measured on PR #880 head 6eb08e14:

check workflow run id completed
final ci.yml 34077564803 02:57:30
windows rust.yml 34077564864 03:01:50

final concluded 4m20s before windows. For that window the one required context was green while a required check was still running, and a /fast-forward posted in it would have moved main to an ungraded head with nothing on the host side refusing. That is how the row was found — by a maintainer commenting in exactly that window.

Three shapes are refuted by the tree, not by argument

The obvious fixes all fail, and the files say why before they are tried:

  • workflow_call from ci.yml. rust.yml's header: a job skipped by an if: still mints a check run concluded skipped; CI_ANSWERED_CONCLUSIONS excludes skipped on purpose, so a skipping required check leaves checks-green at exit 3 and land polling forever. A paths: filter is workflow-level only, and a workflow that does not trigger mints nothing — ABSENT, which CI_ABSENT_OK_CHECKS accepts. Calling those jobs from ci.yml, which triggers on every PR, converts absent into skipped.
  • Moving the four back into ci.yml. Same defect, and it re-buys the 64% of the CI bill CLOUD-398 slice 2 removed.
  • final reading the siblings by name. CLOUD-897, already walked and refused: "this job finished BEFORE the analyzer started on every recent merged PR … a step that cannot decide should not be in the one job branch protection requires."

The host cannot close this by construction. A path-filtered check must be ABSENT rather than skipped to avoid wedging the loop, and a ruleset cannot require an absent context. So the roster is enforceable only where the landing decision is made.

What changed

A third refusal in fast-forward.yml. That file already decides two things itself rather than delegating to the ruleset — the draft head (CLOUD-853) and the unreviewed fork head (CLOUD-867) — both on the stated reasoning that "the ruleset is precisely the thing measured not to hold." Like the draft step it is unreachable from land, which runs the same predicate before it comments; what it catches is a comment from any other OWNER/MEMBER/COLLABORATOR.

The predicate is the engine's, reused whole. mise run checks-green fetches the runs with gh api and hands them to batten checks green, a pure function of that TSV. Re-deriving "green" in the workflow would be the second authority CLOUD-363 measured: land counted cancelled as an answer, checks-green read it as red, and the two composed into "a branch wedge with no exit, and escaping it took a hand-minted SHA."

Exit 3 refuses, and so does exit 2. The adapter's contract is 0 green / 1 red / 2 could-not-look / 3 no answer yet. Only 0 lands. Could-not-look is refused because "a reading this gate cannot take is not a pass" is the posture checks-green already states for its own gh api failure.

Acquiring the binary. Building is not affordable — batten-check is the CI workflow's pole at 683s against this job's 5-minute budget. install.sh is the route: POSIX sh, no toolchain, no clone, every asset digest-verified with no flag to skip it. The two cheap gh api refusals run first, so a draft or unreviewed fork head is turned away before anything is installed.

The gate, and the two defects its own tier found

policy/landing-roster-guarded.rego deliberately does not assert that the fan-in covers the roster. That predicate is false by design and a gate asserting it would refuse the tree forever — the shape fixture-forks.rego records as the one that "gets an exception written for it, and the exception is what rots." It asserts the landing path asks the question at all.

The second commit is the compiled tier doing its job on the first:

The gate could not see its own subject deleted. The row named only .github/workflows/fast-forward.yml, on the reasoning that a module should not be handed files it does not judge. Measured: a rule whose declared sources match no file is SKIPPED, not evaluated over an empty document. So a branch whose whole content is git rm .github/workflows/fast-forward.yml passed a gate written to stop exactly that — left: [] against right: ["landing-roster-unguarded"]. Declaring the directory guarantees a source exists; the module still reads one key. The missing arm is deleted rather than kept, because a glob matching zero files is no source rather than an unreadable one.

And the prescribed channel probe cannot catch that class. I ran the unconditional-arm confirmation .claude/rules/policy-modules.md requires and it spoke — exit 2 — while the arm was dead. That probe confirms the module evaluates on the tree you run it against; it says nothing about whether a given arm is reachable, and nothing about a tree where the declared source is gone. Only the tier driving the engine over a fixture with the file removed could tell. Recorded on CLOUD-1320, which owns the general form; CLOUD-1276 is the adjacent shape where the rule evaluates and one clause cannot fire.

Its class is check read never. The vocabulary refused the first name — roster is not a declared subject — and check is "one named CI check, or a batten check run". read never is the pair patch read never already spends.

Verification

Measured end to end, not asserted:

  • The adapter, both directions. All twenty answered → exit 0, green — every required check terminal and green. windows in flight with nineteen graded → exit 3, pending — 1 required check(s) still running, 19 graded. That is the measured window, refused.
  • mise run policy-test61 bundles, 767 passed, 0 failed.
  • crates/batten/tests/it/landing_roster.rs5 passed, including the committed workflow's own bytes, the deleted-workflow case, and a strong anti-vacuity case where a different workflow carries the guard's text and must not satisfy the rule.
  • batten check --rule landing-roster-guarded — exit 0 over the committed tree.

Filed rather than fixed here

CLOUD-1579override request resolves the config before it issues, so a batten.toml edit that breaks the load cannot be repaired by an override; the only route is git restore, which discards the whole edit. Hit twice landing this. An admission is also one-shot per write, which makes a multi-row registration all-or-nothing by construction and is stated nowhere.

CLOUD-1571override request evaluates all 58 policy bundles when --rule names no row, costing 2m22s. Every protected-path write in this PR paid it.

CLOUD-1320 — the deletion trap above, with the note that the blast radius is CLOUD-843's migration: any of the ~130 gates being ported that anchors on a literal path inherits it.

What this does NOT do

ci.yml's fan-in comment is unchanged and correct: it claims only that pointing protection at final avoids a ruleset edit per job, which is true and which this preserves. Line 1087 already states the four moved jobs are held by CI_REQUIRED_CHECKS rather than by final.

batten.toml's [ci].required_checks = ["final"] is unchanged — it projects what the host enforces, and the host still enforces exactly that.

🤖 Generated with Claude Code

https://claude.ai/code/session_015H3zMrSGxu4K3fMZu4KFX1

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: cbe8caa5-7fe3-49c6-8032-c145bf01685c

📥 Commits

Reviewing files that changed from the base of the PR and between 65f08f5 and ea30794.

📒 Files selected for processing (6)
  • .github/workflows/fast-forward.yml
  • batten.toml
  • crates/batten/tests/it/landing_roster.rs
  • crates/batten/tests/it/main.rs
  • mise.toml
  • policy/landing-roster-guarded.rego

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wenzowski
wenzowski force-pushed the claude/prose-dispatch-gate-verify-u0fnx3 branch from 7ca736e to c14f4ca Compare September 7, 2026 07:37
`protect-main` requires exactly one context, `final`, and `final` declares
`needs: [ci, batten-check, bats, perf]` — four of the twenty names in
`CI_REQUIRED_CHECKS`. Branch protection passing is therefore not the roster
answering, and required status checks apply to direct pushes, which is what
`/fast-forward` performs.

Measured on PR #880 head 6eb08e1: `final` concluded 02:57:30 and `windows`
03:01:50. For that 4m20s the one required context was green while a required
check was still running, and a `/fast-forward` posted in it would have moved
`main` to an ungraded head. That is how the row was found.

Three shapes are refuted by the tree rather than by argument. A `workflow_call`
from ci.yml — or moving the four jobs back — makes a path-filtered check SKIP
where today it is ABSENT, and `skipped` is excluded from
`CI_ANSWERED_CONCLUSIONS` on purpose, so it wedges `land` at exit 3; that is
the defect rust.yml's split exists to avoid. And `final` reading the siblings
by name is CLOUD-897, already walked and refused. A path-filtered check must be
absent to avoid wedging the loop and a ruleset cannot require an absent
context, so the host cannot enforce this roster by construction.

So the enforcement point is the landing path, where fast-forward.yml already
decides two things itself rather than delegating to the ruleset — the draft
head and the unreviewed fork head — both on the reasoning that the ruleset is
the thing measured not to hold. The step reuses `mise run checks-green` whole:
re-deriving "green" here would be the second authority CLOUD-363 measured into
a branch wedge that took a hand-minted SHA to escape. Building is not
affordable at 683s against a 5-minute budget, so the released binary is
installed through install.sh.

policy/landing-roster-guarded.rego is the ratchet. It does not assert that the
fan-in covers the roster — that is false by design and would refuse the tree
forever — but that the landing path asks the question at all.

Verified: the adapter exits 0 over all twenty answered and exits 3 with
`windows` in flight and nineteen graded; the channel was confirmed with an
unconditional arm before the clean reading was trusted.

Refs: CLOUD-1570
Refs: CLOUD-1579

Admits: b1267e1e1dead053e5ff2f7044b1de9731a881b85cf4ddaae5f5ee3e7fc0f8f1
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .github/workflows/fast-forward.yml
Admits-anchor: call:6eb08e1468c244ea3a4a3791d482d15c83e6c6ab
Admits-epoch: d15646da656c93efe11540f87e16e245fd48630292cfe2d15ec53eb3ea9b49f6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The window CLOUD-1570 measured stays open: `final` fans in over 4 of the 20 names in `CI_REQUIRED_CHECKS`, so on PR #880's head `6eb08e14` the one context branch protection requires went green at 02:57:30 while `windows` ran until 03:01:50. In that 4m20s any OWNER/MEMBER/COLLABORATOR typing `/fast-forward` moves `main` to a head whose roster has not answered, and the host refuses nothing. Measured here twice through the adapter: all 20 answered exits 0, `windows` in flight with 19 graded exits 3.
Admits-answer-precondition: The class declares two other routes and both point at surfaces that cannot carry this change: `[[redirect]]` and `batten.toml` declare WHICH paths are protected and what to write instead, and neither can express a refusal STEP inside a workflow job. CLOUD-1570's fix is a step in `fast-forward.yml` running `mise run checks-green` before the fast-forward action, and there is no config spelling for it. The write is the entire content of the PR that closes CLOUD-1570, so a reviewer sees it in the diff it lands in.
Admits-answer-rejected-route: `config read first` points at `batten.toml`, which declares the protected set and the per-class redirects; it cannot hold a workflow job step, and widening the protected set is not the change. `patch run first` is `git restore`, which discards the edit and is the opposite of the change. Neither reaches a step inside `.github/workflows/fast-forward.yml`, which is where the enforcement point has to be: a path-filtered check must be ABSENT rather than skipped to avoid wedging `land`, and a ruleset cannot require an absent context, so the host structurally cannot enforce this roster.

Admits: c4b8b2d2f41d01067e23766049a33c17eb10e86ff7be36c8e53ade0024f2733f
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-anchor: call:6eb08e1468c244ea3a4a3791d482d15c83e6c6ab
Admits-epoch: d15646da656c93efe11540f87e16e245fd48630292cfe2d15ec53eb3ea9b49f6
Admits-author: alec@wenzowski.com
Admits-prev: 6130ede1c401d25e38e7c4a26a71c41bb83b88ca194b88815c896f5fff4e68f9
Admits-answer-lost: Non-negotiable rule 2: a rule without a runnable gate is half a change. Without these rows `policy/landing-roster-guarded.rego` is an unregistered file the engine never loads, so the roster guard added to `fast-forward.yml` could be deleted silently and nothing would refuse it — the regression the module exists to prevent. The two rows cannot be split: a module raising a token no `[[verdict]]` row declares fails the load, so registering one without the other takes the whole bundle down.
Admits-answer-precondition: A `[[rule]]` row and a `[[verdict]]` row can only be declared in `batten.toml` — it IS the policy authority and the verdict registry, so registering a gate has no other surface by construction. `[[redirect]]` names a narrower remedy per path class and cannot add rows to itself. This write registers `landing-roster-guarded` and its `check read never` class in ONE edit, which is the shape the previous attempt should have taken: split across two admissions it left the config in a half-registered state that would not load. It lands in the PR closing CLOUD-1570, visible in the diff.
Admits-answer-rejected-route: `config read first` points at `batten.toml`, the file being written; it is a read route and cannot register a rule or declare a class. `patch run first` is `git restore` — it was just SPENT correctly, to undo a class name the vocabulary refused (`roster` is not a declared subject), and running it again would discard this registration too. Neither can add a `[[rule]]` or a `[[verdict]]`, because that surface is `batten.toml` and nothing else.
…isible

The compiled tier found both defects in the gate landed a commit ago, which
is what that tier exists for.

A rule whose declared `line_sources` match NO file is SKIPPED rather than
evaluated over an empty document. The row named only
`.github/workflows/fast-forward.yml`, on the reasoning that a module should
not be handed files it does not judge — so a branch whose whole content is
`git rm .github/workflows/fast-forward.yml` made the gate not run at all and
passed clean. That is the dead-gate shape the module was written to refuse,
reached through the module's own declaration. The predicate's body is
`not guarded`, which holds over an absent file by construction, and it still
produced nothing: measured `left: []` against `right: ["landing-roster-unguarded"]`.

Declaring the directory guarantees a source exists, so the predicate fires on
a deleted landing workflow exactly as on an unguarded one. The narrowing that
matters — which file answers the question — is the module's and is unchanged.
The `missing` arm is deleted rather than kept: a glob matching zero files is
not an unreadable source but no source, so nothing reaches `lines`,
`documents` or `missing`, and the arm was unreachable.

THE UNCONDITIONAL-ARM PROBE CANNOT CATCH THIS, and it was run and passed
before the tier. A `violation` whose body is `true` confirms the module
evaluates on the tree it is run against; it says nothing about whether an arm
is reachable, and nothing about a tree where the declared source is gone.
Worth stating beside the rule that prescribes the probe.

The second defect was narrower: scanning the repo root with a one-rule subset
trips `check_registry_is_exhausted`, which reports every class the other rules
would have raised. The fidelity case copies the committed bytes into a fixture
instead — same assertion, and 0.038s rather than 4.077s.

Recorded on CLOUD-1320, which owns the general form; CLOUD-1276 is the
adjacent shape where the rule evaluates and one clause cannot fire.

Refs: CLOUD-1570
Refs: CLOUD-1320

Admits: 602066abacece58b19a380d2b9751de501d0c9bc1eb25c4c3b1ed4ba23559250
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-anchor: call:7b5bc4c966518336824938e3eb784f58dc1e8360
Admits-epoch: 765fdb3843f7629e80ce79d2be6516ae8d3cb4c2b0341c18f988959f8084e097
Admits-author: alec@wenzowski.com
Admits-prev: c4b8b2d2f41d01067e23766049a33c17eb10e86ff7be36c8e53ade0024f2733f
Admits-answer-lost: The gate cannot see its own subject being deleted. `landing-roster-guarded` exists to stop the roster check being removed from the landing path, and with the narrow `line_sources` the cheapest way to remove it — delete the file — is exactly the case the rule cannot reach, because the engine skips a rule with no matching source. That is the dead-gate shape the module was written to refuse, reached through the module's own declaration. Measured by `an_absent_landing_workflow_is_refused`, which returned no finding where one was owed.
Admits-answer-precondition: A `[[rule]]` row's `line_sources` can only be declared in `batten.toml`, so widening it has no other surface; `[[redirect]]` names a per-path remedy and cannot edit a rule. The change is one line — `.github/workflows/fast-forward.yml` becomes `.github/workflows/*.yml` — and it is forced by a measurement rather than chosen: the compiled tier showed a rule whose declared sources match NO file is skipped rather than evaluated, so with the narrow glob a branch DELETING the landing workflow made the gate not run and passed clean. It lands in the PR closing CLOUD-1570, visible in the diff.
Admits-answer-rejected-route: `config read first` points at `batten.toml`, the file being written, and is a read route that cannot change a rule's sources. `patch run first` is `git restore`, which would discard the whole registration landed earlier in this branch. Neither can widen a glob, because the rule table is `batten.toml` and nothing else.
`UNGUARDED_BODY` carries no quote, so `r#"…"#` is one delimiter wider than
the literal needs and `clippy::needless_raw_string_hashes` refuses it.
`GUARDED` keeps its hashes: it holds `SHA="$head_sha"`, which is the case
the wider form exists for.

Caught by `land`'s own local `verify` rather than by CI, which is the lap
working — the PR stayed draft and no runner was spent on a one-line fix.

Refs: CLOUD-1570
A code review found the third defect in this module, and it is the sharpest:
the module's own DOCUMENTATION defeated it.

`guarded` matched any line containing `checks-green`. The committed
fast-forward.yml carries that substring on FOUR comment lines — 218, 223, 235,
247, the block explaining why the predicate is the engine's and reused whole —
against ONE real invocation at 274. So deleting the roster step while leaving
its rationale, which is the ordinary shape of a "this step was flaky, dropping
it" edit, left the gate green over a landing path that no longer consults the
roster. That is precisely the regression the module exists to refuse, reached
through the module's own predicate. The more the guard was explained, the
deader it got.

`mise run checks-green` plus a comment exclusion discriminates an invocation
from every mention of one. The committed workflow still passes, now on line 274
rather than on its own commentary.

The existing anti-vacuity case rules out another FILE satisfying the rule; it
could not see another LINE in the same file doing it. Both tiers gain that case
— the load-time one over a fixture, and the compiled one driving the engine over
a workflow whose only mention is a comment. A second `#MUTANT` row makes the
comment exclusion itself mutable, and it reddens the new case rather than any
existing one, so neither mutation shadows the other.

Verified: policy test 62 bundles, 785 passed; the compiled tier 6/6; and
`batten check --rule landing-roster-guarded` still exits 0 over this tree.

Refs: CLOUD-1570

Admits: 307aeaa37c513256d3d454799b3630cb1586fd4085c8320206a0f1ce37d8a546
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: policy/landing-roster-guarded.rego
Admits-anchor: call:e27e9f49e7feb1f6b34e5291878846ee0dd0d0cc
Admits-epoch: 8bc2db336876b2ce4e32107f889d8142bd3bcc874fee5f67526429cb51694391
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The gate is defeated by its own documentation. `guarded` holds on any line containing `checks-green`, and `.github/workflows/fast-forward.yml` carries that substring on four COMMENT lines (218, 223, 235, 247) against one real invocation (274). So deleting the roster step while leaving its comment block — the ordinary shape of a "this step was flaky, dropping it" edit — leaves the module green over a landing path that no longer consults the roster. That is exactly the regression the module exists to refuse, reached through the module's own predicate. The fix anchors on `mise run checks-green` and excludes comment lines, which discriminates the invocation from every mention of it.
Admits-answer-precondition: The class's other two routes cannot carry this change. `config read first` points at `batten.toml`, which declares WHICH paths are protected and what to write instead; it cannot edit a predicate's body. `patch run first` is `git restore`, which would discard the module entirely. The change is one conjunct in `policy/landing-roster-guarded.rego`'s `guarded` rule, and it lands in the PR closing CLOUD-1570 where a reviewer sees it in the diff.
Admits-answer-rejected-route: `config read first` points at `batten.toml` and is a read route that cannot change a Rego predicate. `patch run first` is `git restore`, which discards the module this PR is adding rather than repairing its predicate. Neither reaches the body of a `.rego` rule, which lives only in the module file.
@wenzowski
wenzowski marked this pull request as ready for review September 7, 2026 18:57
@wenzowski
wenzowski force-pushed the claude/prose-dispatch-gate-verify-u0fnx3 branch from c14f4ca to ea30794 Compare September 7, 2026 18:57
@wenzowski
wenzowski marked this pull request as draft September 7, 2026 18:57
@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Refusing to fast-forward #895: it is a draft, so no required check has graded its head. main must not advance to a SHA CI never ran on (CLOUD-853). Mark it ready for review, let CI grade the head, then ask again.

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Refusing to fast-forward #895: it is a draft, so no required check has graded its head. main must not advance to a SHA CI never ran on (CLOUD-853). Mark it ready for review, let CI grade the head, then ask again.

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Refusing to fast-forward #895: it is a draft, so no required check has graded its head. main must not advance to a SHA CI never ran on (CLOUD-853). Mark it ready for review, let CI grade the head, then ask again.

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Refusing to fast-forward #895: it is a draft, so no required check has graded its head. main must not advance to a SHA CI never ran on (CLOUD-853). Mark it ready for review, let CI grade the head, then ask again.

@wenzowski
wenzowski marked this pull request as ready for review September 7, 2026 19:15

Copy link
Copy Markdown
Contributor Author

/fast-forward


Generated by Claude Code

@wenzowski
wenzowski merged commit ea30794 into main Sep 7, 2026
23 of 24 checks passed
@wenzowski
wenzowski deleted the claude/prose-dispatch-gate-verify-u0fnx3 branch September 7, 2026 19:28
wenzowski added a commit that referenced this pull request Sep 12, 2026
`grade()` maps a `failure` conclusion to `Answer::Refused`, whose own doc says
"the branch is no longer a direct descendant", and `lib.rs` printed exactly that.
The bot has FOUR refusal grounds and that sentence names one of them.

MEASURED, PR #895: the bot's log read `refusing #895: draft head, no graded
checks (CLOUD-853)`. `main` had not moved and the branch was a perfect
descendant. The head was a draft because `land` re-drafts on a failed lap — so
the loop created the refusing condition, reported it as an external one, and
narrated a remedy (rebase, re-verify, retry) that regenerates it. The reader went
looking for a moved trunk.

A FIFTH STATE THE ROW DOES NOT ENUMERATE, measured this session: the run failed
before any refusal logic ran. `fast-forward.yml` invokes `./install.sh`, v0.0.161
carried no binaries, the step died — conclusion `failure`, no refusal posted, and
`land` still said "no longer a direct descendant" and recommended a re-run. Every
implied fact was false and two laps were spent on it.

THE PORT MADE IT WORSE, WHICH IS WHY THE ROW'S SURFACE MOVED. CLOUD-1617 names
`mise-tasks/land.sh:2249`, retired in `a2916756`. The shell at least held the
bot's refusal in `$refused` and discarded its content; `fast_forward.rs` never
reads it at all — `answer()` sees only the run's conclusion.

A LOOKUP, NEVER A JUDGEMENT (rule 3). Each refusing arm of `fast-forward.yml`
posts a comment through the same endpoint this module already posts the directive
to, and each names the row it enforces. So the ground is read out of the bot's
own text by that key. Nothing here decides what prose MEANS.

`GROUNDS` is the four in one place a fifth must be added to. An unlisted key
resolves to `Unclassified`, which narrates as could-not-look — the safe
direction, and the one the predecessor did not have. `Unclassified` also covers
no-comment-at-all, which is what a run that died early looks like from here.

Each ground now carries the remedy that actually clears it: readying for a draft
head, because rebasing cannot; a review for an unreviewed fork head; waiting for
a matrix that has not graded. The exit code is unchanged — `Violation` already
stopped the lap, and the defect was never the stop, it was the sentence.

`startsWith`, never `contains`, on the refusal body: the directive and a human
quoting a refusal live in the same comment list, which is the discipline
`fast-forward.yml` applies to its own trigger after a comment DISCUSSING the
trigger fired it.

Four falsifiers, including the anti-vacuity term over the table itself: every
declared key must resolve to its own ground, so a lookup that resolves one row
and drops the rest cannot pass as caution.

Refs: CLOUD-1617
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant