Skip to content

ci(deps): carry licence rows forward for bumped action pins - #806

Merged
wenzowski merged 1 commit into
mainfrom
sbom-actions/carry-20260901T110320Z
Sep 1, 2026
Merged

ci(deps): carry licence rows forward for bumped action pins#806
wenzowski merged 1 commit into
mainfrom
sbom-actions/carry-20260901T110320Z

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Opened by sbom-actions-currency.

sbom-check refuses an action pin with no row in mise-tasks/sbom-actions.tsv, and Renovate cannot write that table — so every action bump opens red and holds the prConcurrentLimit: 1 slot (CLOUD-1213).

Each row here is the same repository's existing row with only the sha changed. It was carried forward only after every candidate licence file was confirmed byte-identical between the mapped commit and the new one, so the judgement already recorded still describes those exact bytes. Nothing was re-derived.

A pin whose licence files differ, or whose repository has no row at all, is not here — it is reported as a warning in the run log for a person to read.

Refs: CLOUD-1213

DO-NOT-CLOSE CLOUD-1213

CLOUD-1213 built the workflow that opened this PR; this PR is its product, not its completion. Closing it here would move the row on every carry, and its first acceptance clause is not met — see the row.

Every row here was copied verbatim from the same repository's
existing row with only the sha changed, after confirming every
candidate licence file is byte-identical between the two commits.
No licence was re-derived and no verdict inferred.

Refs: CLOUD-1213
@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1213 `sbom-check`'s licence table needs a human on every action pin move, and only a person can regenerate it

Why

Found while fixing CLOUD-1207 and deliberately not repaired there, because
the refusal is the design working rather than a defect.

sbom-check's second half: mise-tasks/sbom-actions.tsv maps each pinned
action's licence by repo AND sha together, and its own header is explicit
that this is the point — "a table row whose sha is stale is exactly the drift…*
A renovate bump that does not record the new commit's license fails the gate
*rather than silently degrading the document." Regenerating the table is
mise run sbom plus a commit, which the update lane cannot do, having decided
for good reason never to check a bot's head out under a write token.
mise-action-floor states the same shape as an accepted cost in prose:
"such a PR then sits open and red until somebody closes it."

What #771 already settled, so this row is smaller than when it was filed

The original body carried two concerns. The second — that the lane burned CI
indefinitely while waiting for that person, and said nothing about it — is
fixed: the freeze arm drafts a red head, halts the rebases, and comments once
naming the failing checks. The lane's own header now states the bound rather
than claiming a scope its gates refuse.

So what remains is only the first: should this class need a person at all?
Waiting is now free, which is what makes this a design question rather than a
bill.

A correction to the measurement above (2026-08-31)

This row said *"Measured on #676: 34 × *sbom-action-unmapped, every pin in the
tree." That is wrong and the argument does not need it. sbom-check.sh:358-371
loops per pin and matches repo@sha as one key, so only a moved pin is
unmapped. #676 moves exactly two — jdx/mise-action v4.2.4→v4.2.5 and
linear/linear-release-action v0.15.1→v0.16.0. The cost of the human step is
two table rows per bump, not a full regeneration, which makes option 2 cheaper
than this row assumed and is why the decision below can be made at all.

The options

  1. Teach something in the lane to regenerate the table. Needs a write to the
    bot's head — the property the lander was built to avoid — or a follow-up
    commit from a trusted lane.
  2. Accept it. A licence is a fact about a commit, and recording it deliberately
    is defensible; the cost is now bounded to one frozen PR and one comment.
  3. Key the table by action name rather than sha. Rejected — it deletes the
    drift detector that makes a committed licence table defensible at all.

Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Source of truth (§1). **mise-tasks/sbom-check.sh's unmapped loop and
    mise-tasks/sbom-actions.tsv's header.
  • Computable predicate (§2). mise run sbom-check exits 0 on a head whose
    action pins moved, with no hand edit between the bump and the check. Option 1
    is the decision (2026-08-31): a workflow on main, shaped like
    .github/workflows/lock-currency.yml, reads the open renovate/* PR's pins,
    fetches each new pin's LICENSE bytes at its sha, and opens its own PR
    against main adding the rows. Option 2 is declined — it leaves the lane's
    throughput bounded by whoever notices a freeze. Option 3 stays rejected.
  • Effect (§3). No command-surface change.
  • Output & exit (§5). Pointer-only, unchanged — the gate already emits
    path:line and a count, never a licence or a holder.
  • **Commit / bump (§6). **ci → no bump.
  • **Test obligation (§7). **mise-tasks/sbom-check.sh is frozen by
    V-SHELL-RULE-EDITED, so any change reaching it takes the port-and-retire
    shape: the predicate as a policy/*.rego module plus a
    crates/batten/tests/*.rs tier, with one conserves arm per deleted path.
  • Blockers (§8). None.
{
  "source_of_truth": "mise-tasks/sbom-actions.tsv",
  "gate": { "task": "sbom-check", "exits": [0, 1] },
  "commit_type": "ci",
  "blockers": [],
  "tests": [
    {
      "file": "tests/sbom-check.bats",
      "mutation": "drop the per-pin unmapped loop, so a moved action pin reports mapped"
    }
  ]
}

REOPENED 2026-09-01. Closed against #788 with §2's Option 1 never built — git grep -l sbom-actions -- .github/ returns nothing across 25 workflow files — and the class reproduced immediately on #676, which was still holding the prConcurrentLimit: 1 slot four hours later and had to be cleared by hand (77fbad3b). The freeze half of this row IS done and stays credited; what is reopened is Option 1. See the comment thread for the measurement and for a correction to this body's own pin arithmetic.

Acceptance

  • A github-actions bump reaches green on sbom-check with no hand edit
    between the bump and the check.
  • The instance in hand is cleared in the same change: the two rows ci(deps): update actions #676 needs
    are on main, renovate-frozen is off it, and the prConcurrentLimit: 1
    slot is free for the 18 updates queued behind it in Dependency Dashboard #494.

Review in Linear

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 11:25
@wenzowski

Copy link
Copy Markdown
Contributor

/fast-forward

@wenzowski
wenzowski merged commit 7470498 into main Sep 1, 2026
5 of 6 checks passed
@wenzowski
wenzowski deleted the sbom-actions/carry-20260901T110320Z branch September 1, 2026 11:58
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…ceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: `mutant-census` enumerates `mise-tasks/*.sh` and
`policy/*.rego`, so a deleted program is not in the census at all. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

131 suites after the deletion, 686.8s serial on this container. The saving is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the rest of the totals' difference is machine noise and is not claimed.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…ceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: `mutant-census` enumerates `mise-tasks/*.sh` and
`policy/*.rego`, so a deleted program is not in the census at all. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

129 suites after the deletion, 706.1s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired two more suites in between, and a corpus derived from the older report
would publish a cost for a suite that is gone. The saving is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the rest of the totals' difference is those two suites plus machine noise, and
is not claimed.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: `mutant-census` enumerates `mise-tasks/*.sh` and
`policy/*.rego`, so a deleted program is not in the census at all. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

129 suites after the deletion, 706.1s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired two more suites in between, and a corpus derived from the older report
would publish a cost for a suite that is gone. The saving is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the rest of the totals' difference is those two suites plus machine noise, and
is not claimed.

BREAKING CHANGE: `config::Config` and `resolve::Resolved` each gain a
`bot_lane` field, so any code constructing either with a struct literal must
name it. `semver check` reports it as `constructible_struct_adds_field` and it
is declared rather than worked around: the alternative is `#[non_exhaustive]`,
which is a break in its own right and a wider one — it would close both structs
to literal construction for every field, not just this one.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: `mutant-census` enumerates `mise-tasks/*.sh` and
`policy/*.rego`, so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

129 suites after the deletion, 706.1s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired two more suites in between, and a corpus derived from the older report
would publish a cost for a suite that is gone. The saving is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the rest of the totals' difference is those two suites plus machine noise, and
is not claimed.

BREAKING CHANGE: `config::Config` and `resolve::Resolved` each gain a
`bot_lane` field, so any code constructing either with a struct literal must
name it. `semver check` reports it as `constructible_struct_adds_field` and it
is declared rather than worked around: the alternative is `#[non_exhaustive]`,
which is a break in its own right and a wider one — it would close both structs
to literal construction for every field, not just this one.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

127 suites after the deletion, 748.7s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired four more suites in between, `mutant` and `mutant-census` among them,
and a corpus derived from an older report publishes a cost for a suite that is
gone. The saving this change can claim is `tests/bot-issue.bats`' own 2.9s share
of the corpus it was measured in, 0.3%; the totals differ by more in both
directions across runs, and that is those four suites plus machine noise rather
than anything this branch did.

BREAKING CHANGE: `config::Config` and `resolve::Resolved` each gain a
`bot_lane` field, so any code constructing either with a struct literal must
name it. `semver check` reports it as `constructible_struct_adds_field` and it
is declared rather than worked around: the alternative is `#[non_exhaustive]`,
which is a break in its own right and a wider one — it would close both structs
to literal construction for every field, not just this one.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

127 suites after the deletion, 748.7s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired four more suites in between, `mutant` and `mutant-census` among them,
and a corpus derived from an older report publishes a cost for a suite that is
gone. The saving this change can claim is `tests/bot-issue.bats`' own 2.9s share
of the corpus it was measured in, 0.3%; the totals differ by more in both
directions across runs, and that is those four suites plus machine noise rather
than anything this branch did.

BREAKING CHANGE: `config::Config` and `resolve::Resolved` each gain a
`bot_lane` field, so any code constructing either with a struct literal must
name it. `semver check` reports it as `constructible_struct_adds_field` and it
is declared rather than worked around: the alternative is `#[non_exhaustive]`,
which is a break in its own right and a wider one — it would close both structs
to literal construction for every field, not just this one.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
#806 landed with. `sbom-actions-currency`'s generated body gains
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 1, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-1213's first acceptance clause. `auto-bot-land` merged only
`renovate/**`, so every carry PR needed a person — which is what produced
#806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove.

THE PAIRING IS THE POINT. The two lanes have different authors: Renovate
opens its own PRs, and the carry lane is opened by `sbom-actions-currency`
under the default token. A single `BOT_LOGIN` no longer answers, and the
lazy widening — any known prefix against any known login — would admit
`renovate[bot]` on a `sbom-actions/` branch and this workflow's own token
on a `renovate/` one, neither of which either lane can produce. `LANES` is
therefore a table of PAIRS and a PR must satisfy one row whole.

CLOUD-867's origin test is untouched: `head_repository.full_name` is still
what a fork cannot forge, still checked separately, and the prefix stays a
filter rather than the trust boundary.

Driven over a six-PR fixture before landing, and it caught a real bug: the
first spelling read `.prefix` inside a pipe where `.` had already rebound
to the branch string, which `jq` refuses at runtime — a workflow that
would have errored on its first tick. With the lane bound, both lanes are
admitted, both cross-pairings are refused, a fork-headed PR is refused and
a human's branch is refused.

What still gates a carry PR is unchanged and is not review: the full
required check set must be green, and `batten claim carry` bounds what the
branch may contain — only the licence table differs, every added row names
a repo the base already maps with an identical licence and holder so only
the sha moves, and no other tracked path differs from the merge base.

Admits: b93a99d9da1523b9d9bd398f5b8c302d989357d8214bffe6e09c5e6aa30e9d12
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: 0e089a9
Admits-epoch: 25c305c35f01ee4c99bceeacd1efc47509463adf09d3e5923196f59089cc2d98
Admits-author: alec@wenzowski.com
Admits-prev: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-answer-lost: CLOUD-1213's first acceptance clause stays open and every licence-carry PR keeps needing a person to land it — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. The lane can now be landed honestly either way, so what is lost is the automation rather than the correctness; the human asked for the automation explicitly.
Admits-answer-precondition: The change extends this lane's on-green auto-merge from `renovate/**` to `sbom-actions/**`, which is a workflow trigger scope, a job `if:` and the resolve step's author test — all of them inside this file and expressible nowhere else. No verb writes a workflow, and the branch scope has to sit on the trigger rather than only in the `if:` for the reason this file already records (CLOUD-493: a job condition is evaluated after the run exists, so 1131 runs in 25 hours were created only to skip). It lands in a pull request, which is what the class asks for, and the diff is what a reviewer reads to see exactly which branches can now merge unattended.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because a GitHub Actions workflow IS its own owning surface — there is no generator and no config table that emits this file. R-RESTORE-IT is rejected because restoring the committed bytes restores the single-lane scope, which is the thing being changed rather than a fault to undo.

Refs: CLOUD-1213, CLOUD-1295, CLOUD-867
wenzowski added a commit that referenced this pull request Sep 2, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 2, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 2, 2026
CLOUD-1213's first acceptance clause. `auto-bot-land` merged only
`renovate/**`, so every carry PR needed a person — which is what produced
#806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove.

THE PAIRING IS THE POINT. The two lanes have different authors: Renovate
opens its own PRs, and the carry lane is opened by `sbom-actions-currency`
under the default token. A single `BOT_LOGIN` no longer answers, and the
lazy widening — any known prefix against any known login — would admit
`renovate[bot]` on a `sbom-actions/` branch and this workflow's own token
on a `renovate/` one, neither of which either lane can produce. `LANES` is
therefore a table of PAIRS and a PR must satisfy one row whole.

CLOUD-867's origin test is untouched: `head_repository.full_name` is still
what a fork cannot forge, still checked separately, and the prefix stays a
filter rather than the trust boundary.

Driven over a six-PR fixture before landing, and it caught a real bug: the
first spelling read `.prefix` inside a pipe where `.` had already rebound
to the branch string, which `jq` refuses at runtime — a workflow that
would have errored on its first tick. With the lane bound, both lanes are
admitted, both cross-pairings are refused, a fork-headed PR is refused and
a human's branch is refused.

What still gates a carry PR is unchanged and is not review: the full
required check set must be green, and `batten claim carry` bounds what the
branch may contain — only the licence table differs, every added row names
a repo the base already maps with an identical licence and holder so only
the sha moves, and no other tracked path differs from the merge base.

Admits: b93a99d9da1523b9d9bd398f5b8c302d989357d8214bffe6e09c5e6aa30e9d12
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: 0e089a9
Admits-epoch: 25c305c35f01ee4c99bceeacd1efc47509463adf09d3e5923196f59089cc2d98
Admits-author: alec@wenzowski.com
Admits-prev: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-answer-lost: CLOUD-1213's first acceptance clause stays open and every licence-carry PR keeps needing a person to land it — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. The lane can now be landed honestly either way, so what is lost is the automation rather than the correctness; the human asked for the automation explicitly.
Admits-answer-precondition: The change extends this lane's on-green auto-merge from `renovate/**` to `sbom-actions/**`, which is a workflow trigger scope, a job `if:` and the resolve step's author test — all of them inside this file and expressible nowhere else. No verb writes a workflow, and the branch scope has to sit on the trigger rather than only in the `if:` for the reason this file already records (CLOUD-493: a job condition is evaluated after the run exists, so 1131 runs in 25 hours were created only to skip). It lands in a pull request, which is what the class asks for, and the diff is what a reviewer reads to see exactly which branches can now merge unattended.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because a GitHub Actions workflow IS its own owning surface — there is no generator and no config table that emits this file. R-RESTORE-IT is rejected because restoring the committed bytes restores the single-lane scope, which is the thing being changed rather than a fault to undo.

Refs: CLOUD-1213, CLOUD-1295, CLOUD-867
wenzowski added a commit that referenced this pull request Sep 2, 2026
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
wenzowski added a commit that referenced this pull request Sep 2, 2026
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
wenzowski added a commit that referenced this pull request Sep 2, 2026
CLOUD-1213's first acceptance clause. `auto-bot-land` merged only
`renovate/**`, so every carry PR needed a person — which is what produced
#806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove.

THE PAIRING IS THE POINT. The two lanes have different authors: Renovate
opens its own PRs, and the carry lane is opened by `sbom-actions-currency`
under the default token. A single `BOT_LOGIN` no longer answers, and the
lazy widening — any known prefix against any known login — would admit
`renovate[bot]` on a `sbom-actions/` branch and this workflow's own token
on a `renovate/` one, neither of which either lane can produce. `LANES` is
therefore a table of PAIRS and a PR must satisfy one row whole.

CLOUD-867's origin test is untouched: `head_repository.full_name` is still
what a fork cannot forge, still checked separately, and the prefix stays a
filter rather than the trust boundary.

Driven over a six-PR fixture before landing, and it caught a real bug: the
first spelling read `.prefix` inside a pipe where `.` had already rebound
to the branch string, which `jq` refuses at runtime — a workflow that
would have errored on its first tick. With the lane bound, both lanes are
admitted, both cross-pairings are refused, a fork-headed PR is refused and
a human's branch is refused.

What still gates a carry PR is unchanged and is not review: the full
required check set must be green, and `batten claim carry` bounds what the
branch may contain — only the licence table differs, every added row names
a repo the base already maps with an identical licence and holder so only
the sha moves, and no other tracked path differs from the merge base.

Admits: b93a99d9da1523b9d9bd398f5b8c302d989357d8214bffe6e09c5e6aa30e9d12
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: 0e089a9
Admits-epoch: 25c305c35f01ee4c99bceeacd1efc47509463adf09d3e5923196f59089cc2d98
Admits-author: alec@wenzowski.com
Admits-prev: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-answer-lost: CLOUD-1213's first acceptance clause stays open and every licence-carry PR keeps needing a person to land it — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. The lane can now be landed honestly either way, so what is lost is the automation rather than the correctness; the human asked for the automation explicitly.
Admits-answer-precondition: The change extends this lane's on-green auto-merge from `renovate/**` to `sbom-actions/**`, which is a workflow trigger scope, a job `if:` and the resolve step's author test — all of them inside this file and expressible nowhere else. No verb writes a workflow, and the branch scope has to sit on the trigger rather than only in the `if:` for the reason this file already records (CLOUD-493: a job condition is evaluated after the run exists, so 1131 runs in 25 hours were created only to skip). It lands in a pull request, which is what the class asks for, and the diff is what a reviewer reads to see exactly which branches can now merge unattended.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because a GitHub Actions workflow IS its own owning surface — there is no generator and no config table that emits this file. R-RESTORE-IT is rejected because restoring the committed bytes restores the single-lane scope, which is the thing being changed rather than a fault to undo.

Refs: CLOUD-1213, CLOUD-1295, CLOUD-867
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