Skip to content

fix(ci): define the deps-install seam the provisioner already calls - #798

Merged
wenzowski merged 4 commits into
mainfrom
claude/session-start-perf-al9rhq
Sep 1, 2026
Merged

wenzowski merged 4 commits into
mainfrom
claude/session-start-perf-al9rhq

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-1085.

batten is absent at SessionStart on a fresh container, so every mediated call fails open in
silence for the first ~2.5 minutes of a session, and for the whole of any session where the
SessionStart hook does not reach its build step. An absent reference monitor and a passing one are
indistinguishable from outside.

The seam was never missing — it was a dangling call

The environment's setup script runs mise run deps-install on every provision, before any session
starts, and soft-fails with a stderr line when the task is absent. This repository never defined
it.
mise tasks ls carried install-check and install:local and no deps* at all, so that
call has been landing on nothing for the life of the defect, printing
setup: 'deps-install' incomplete into a log nobody reads as a finding.

Defining the task is the entire fix. Nothing outside this repository changes, which is what
CLOUD-1085 §1 asks for: "the change is in the environment configuration that provisions a
container, which must invoke it before any hook fires"
— and that configuration already makes the
call. Its deps report sibling, which the same script calls at step 7, is defined here too.

install.sh, never install:local

The row is explicit that install:local is "a dev-clone convenience that supersedes a release
build, not a provisioning path"
. It builds the working tree's binary, so it assumes a Rust
toolchain and a 141-second compile (measured, CLOUD-1085) that provisioning cannot. One install
interface (§1); this is the caller it lacked. install:local stays where it is — it supersedes the
released binary with the working tree's for dev work, which is a different job.

Routing through mise run is load-bearing

The setup script shadows mise with a wrapper that prepends the GitHub API and asset hosts to
NO_PROXY and sets MISE_GITHUB_TOKEN from the session PAT, because the agent proxy answers 403
for third-party release repos. A task body inherits that environment. A bare ./install.sh in the
setup script would not reach the release API at all, so the seam and the proxy fix are the same
mechanism rather than two independent facts.

Shown able to fail (CLOUD-418)

Both arms exercised on this container, into a temp destination so the live binary was untouched:

arm result
success installs v0.0.137, verified=sha256, silent, exit 0
refusal emits the ::error:: naming which failure, propagates install.sh's own code

2 stays distinct from 1 per house style §7 — could-not-look (no curl, unreachable API) against a
refusal install.sh decided (bad digest, unwritable destination). Collapsing them would send a
reader with a network problem hunting a corrupted asset.

Two defects found and fixed while writing it

Both are recorded in comments beside the code rather than only here.

  1. A default inline run body executes under sh -e. The first draft used a bare
    ./install.sh followed by status=$?; the failing call aborted the task and the branch that
    reports which failure never ran — mise printed its own ERROR task failed and nothing else.
    The || status=$? capture is the shape tests/task-fail-closed.bats already admits
    (CLOUD-318). Note this differs from the shell = "bash -c" bodies
    .claude/rules/toolchain.md describes, which genuinely do not stop.
  2. Backticks inside the double-quoted messages would have been command substitution on the
    binary's own name. Quoting it the way every surrounding comment does would have run it.

The refusal message also does not claim nothing was written: install.sh installs and then
refuses when the destination is off PATH, so that wording is false in the failure mode most likely
to be hit. What is true in every arm — and what matters, since .claude/settings.json registers the
engine by bare name — is that the name does not resolve.

Not in this change

CLOUD-1085 §2(b) — the advisory when a session's SessionStart registration did not run — is the
other half of the row's acceptance and is engine work in contract.rs. It is not here yet.

Restoring .claude/container-setup.sh is explicitly out of scope; the design is one install script
and no harness-specific bootstrap, and #711/#713 took that decision deliberately.

🤖 Generated with Claude Code

https://claude.ai/code/session_0134WDLd3bJpLPxZyRS6zPW7


Generated by Claude Code

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1085 Deleting `.claude/container-setup.sh` left no bootstrap at all: `batten` is absent at SessionStart, so the engine mediates nothing for the whole session

Why

.claude/container-setup.sh was added by #709"Put the released batten on PATH before the session starts" — and deleted by #713 (65757c8, 2026-08-28T03:32:44Z) on #711's premise that the hardened one-liner makes a repo-side wrapper unnecessary. That premise is about what install.sh can do, and it is correct. What it does not establish is that anything invokes it.

Measured on the first session after that deletion, 2026-08-28, this container:

time event
04:37:21 .git/batten-receipts/session-start written — session-start.sh ran, so SessionStart fired
04:39:58 /root/.local/bin/batten created by mise run install:local (141 s build, per /tmp/session-start-batten-build.log)
04:40:48 first .git/batten-contract/<session> snapshot written

contract.rs seeds that snapshot on SessionStart (CLOUD-461), deliberately, so an autonomous session's first fetch+rebase cannot be recorded as the baseline. It was written at the first PostToolBatch instead, 3½ minutes late. The SessionStart registration of batten hook --harness claude-code therefore did not run: there was no binary on PATH for the host to execute.

So for the first ~2.5 minutes of every session on a container of this class, and for the whole of any session where install:local fails or is not reached, batten mediates nothing — every PreToolUse resolves to a command the host cannot run and fails open, silently. .claude/settings.json registers batten hook on all eight events by bare name; only .claude/hooks/session-start.sh is registered by path, and it is the sole reason a binary exists at all.

The failure is not loud. No host error surfaced in that session; the only hook message was container-preflight's own success line. An absent reference monitor and a passing one are indistinguishable from the outside, which is exactly the silent-absence case #711 refused inside install.sh ("Off PATH is a refusal, not a warning over exit 0") — the same defect, one layer out.

Why it is more than one container's setup

The repo now carries no bootstrap. install:local inside the SessionStart hook is the only thing installing batten, and it is a dev-clone convenience that supersedes a release build — not a provisioning path. That inverts CLOUD-824's posture, which moved the missing-binary report forward to provisioning time, where it can still be fixed.

It also blocks CLOUD-312 row 10 on its merits. #714 retires .claude/hooks/session-start.sh behind a [[hook.handler]], which is right under the intended design — one environment-agnostic install script, everything else driven by batten. But a handler is dispatched by the binary, so while nothing puts a released batten on PATH first, that move removes the last thing that installs it. Row 10's premise is this row's acceptance.

Not in scope: restoring the wrapper. The design is one install script and no harness-specific bootstrap; re-adding .claude/container-setup.sh would re-take a decision #711 and #713 made deliberately.


Refinement — Ready

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

  • **Source of truth (§1). **install.sh stays the one install interface and gains nothing; the change is in the environment configuration that provisions a container, which must invoke it before any hook fires. No second installer, in the repo or beside it.
  • Computable predicate (§2). Two, both decidable. (a) On a fresh container, command -v batten answers before the first SessionStart hook runs — observable after the fact as: the batten-contract snapshot for a session is written at that session's SessionStart, not at a later event. (b) A session whose SessionStart engine registration did not run is reported rather than silent: the first batten hook invocation of a session that finds no snapshot for it says so on its own advisory channel, which is free because the snapshot is already keyed per session.
  • **Effect (§3). **read. (b) reads and writes only the existing per-session snapshot store under $GIT_DIR; no new fact class, no new verb.
  • Generated artifacts (§4). None.
  • Output & exit (§5). Pointer-only and advisory. (b) rides SessionStart/PostToolBatch's existing advisory channel, never exit 2 — an absent reference monitor is news, not a verdict about the call in hand, and Event::carries_a_verdict is the authority.
  • **Commit / bump (§6). **fixpatch until 0.1.0.
  • Test obligation (§7). Shown able to fail per CLOUD-418: a fixture session whose snapshot store carries no entry for the current session id produces the advisory, and one that does produces silence. The measurement above is (a)'s reproduction and needs no fixture — it is a property of a real container and is checked by provisioning one.
  • Blockers (§8). None. relatedTo CLOUD-312 (row 10's premise is this row's acceptance), CLOUD-824 (which moved the missing-binary report to provisioning time), CLOUD-476 (which closed the absence this reopens).

Acceptance

  • A fresh container has batten on PATH before its first hook fires, and the SessionStart contract snapshot proves it by being written at SessionStart.
  • A session where that did not happen says so, once, rather than mediating nothing in silence.
  • No harness-specific bootstrap returns to the repository.

Found while landing #714, by asking whether row 10's stated premise — "a container puts batten on PATH before any hook fires" — holds on the container running it. It does not, and the reason is 65 minutes older than the session that measured it.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 35 minutes.

Check out review usage here.

View limit details

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

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: baf692c4-11ef-42e1-988b-8390f33d72fd

📥 Commits

Reviewing files that changed from the base of the PR and between a673667 and c52854e.

📒 Files selected for processing (4)
  • crates/batten/src/contract.rs
  • crates/batten/src/lib.rs
  • crates/batten/tests/contract_drift.rs
  • mise.toml

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

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

`batten` is absent at SessionStart on a fresh container, so every mediated
call fails open in silence for the first ~2.5 minutes of a session and for
the whole of any session where the SessionStart hook does not reach its
build step. The engine mediates nothing and nothing says so.

The bootstrap seam was never missing. The environment's setup script runs
`mise run deps-install` on every provision, before any session starts, and
soft-fails with a stderr line when the task is absent -- which this
repository has never defined. `mise tasks ls` carried `install-check` and
`install:local` and no `deps*` at all, so the call has been dangling for
the life of the defect.

Define it, and its `deps` report sibling that the same script calls. The
task runs `install.sh` -- the one install interface -- and never
`install:local`, which builds the working tree's binary and is a dev-clone
convenience rather than a provisioning path: it assumes a Rust toolchain
and a 141-second compile that provisioning cannot.

Routing through `mise run` is load-bearing rather than incidental. The
setup script shadows `mise` with a wrapper that prepends the GitHub API and
asset hosts to NO_PROXY and sets MISE_GITHUB_TOKEN from the session PAT,
because the agent proxy answers 403 for third-party release repos. A task
body inherits that environment; a bare `./install.sh` in the setup script
would not.

Verified both arms on this container. Success installs v0.0.137,
digest-verified, and is silent at exit 0. A refusal emits the `::error::`
line naming which failure it was and propagates install.sh's own code,
keeping could-not-look (2) distinct from a decided refusal (1).

Two defects found and fixed while writing it, both recorded in comments
beside the code: a default inline `run` body executes under `sh -e`, so the
first draft's bare call aborted the task before the branch that reports the
failure could run; and backticks inside the double-quoted messages would
have been command substitution on the binary's own name.

Refs: CLOUD-1085
CLOUD-1085's second half. The first closed the cause -- nothing installed
`batten` before the first hook fired -- and this closes the silence: a
session where that happened said nothing, and an absent reference monitor
is indistinguishable from a passing one from outside.

The reporter serves exactly two events and seeds the per-session snapshot
at whichever arrives first. So a seed at `PostToolBatch` means
`SessionStart` never reached the engine, and since the hosts register it by
bare name the usual cause is that no binary resolved when that event fired.
Measured on the container that produced the row: SessionStart receipt at
04:37:21, binary at 04:39:58, first snapshot at 04:40:48 -- three and a half
minutes in which every mediated call failed open and nothing reported it.

The mechanism is free. The snapshot is already keyed per session and
already written in that branch, so the same write that rate-limits the
drift notice rate-limits this one: emitted once per session, never again.

Pointer-only and advisory. It rides the existing channel, never exit 2 --
an absent monitor is news, not a verdict about the call in hand. It does
not claim which calls preceded it, because this process cannot see them,
and it names the provisioning step rather than a policy remedy.

RE-DECIDES ONE LANDED CASE, quoted in place rather than deleted.
`the_first_batch_of_a_session_seeds_the_snapshot_silently` asserted silence
on exactly this condition. Its reasoning -- a session that started after a
change has already read the new files -- is right about `SessionStart` and
wrong about the case it covered, so it moves to the mirror case and the
original becomes the discriminating one §7 asks for: no snapshot entry
produces the advisory, an entry produces silence.

One stated bound: a consumer that deliberately does not register
`SessionStart` would see this every session. The row specifies the simple
predicate and it is implemented as written; narrowing it needs the wiring,
which `doctor hooks` reads and this reporter should not.

Refs: CLOUD-1085
…ry advisory

`each_session_is_told_about_what_moved_under_it_and_not_about_the_rest`
asserted `pipe_notice() == None` for a session seeded now. The property it
owns is ISOLATION -- a session that started after a change is not nudged
about one it already has -- and `== None` is a proxy for that which also
couples the case to every other advisory the channel may ever carry.

The unmediated-start notice makes the coupling bite. This fixture drives
`PostToolBatch` only, which is by construction the condition that notice
reports, so the fresh session now receives it: a different notice about a
different fact, and the isolation claim is untouched.

Asserted over what the notice CLAIMS instead. The fresh session must not
name the moved file and must carry no change-set; the already-running one
is still told. That discriminates the same way and stops the next advisory
from failing a case that is not about it.

Refs: CLOUD-1085
… slips

Three gate refusals from the first land lap, all mine.

`inline-task-bodies-not-growing` refused mise.toml 31->33. Both new tasks
were nine-line inline bodies. The ratchet's own block records that the
wider literal was rejected precisely because a one-line `run` shim is the
campaign SUCCEEDING, and reading that made clear the bodies were wrong on
the merits rather than merely over budget: each caught install.sh's exit
status only to re-word it, which is a verdict replaced rather than
propagated (CLOUD-1090's shape, one file over).

Collapsed. `deps-install` is now the install script alone, so its own
diagnosis and its own 0/1/2 reach the caller intact instead of being
translated. `deps` is `command -v batten && batten --version`, where
`command -v` both answers the question -- which binary does PATH find, the
one the bare-name registrations resolve -- and supplies the failure exit
without a branch to write.

The comment describing the ratchet then tripped it a second time. The row
matches a literal substring with no anchoring and no regex column
(CLOUD-1058), so it reads prose as readily as code: spelling the pattern
out incremented the count again with no task added. Reworded to name the
shape in words, with that recorded in place so the next author does not
rediscover it. Both spellings are now level with base, 31 and 2.

clippy doc_markdown twice, same slip copied: an unbackticked event name in
contract.rs and again in the test's doc comment.

Refs: CLOUD-1085
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 05:43
@wenzowski
wenzowski force-pushed the claude/session-start-perf-al9rhq branch from d1b24e4 to c52854e Compare September 1, 2026 05:43
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit c52854e into main Sep 1, 2026
10 of 11 checks passed
@wenzowski
wenzowski deleted the claude/session-start-perf-al9rhq branch September 1, 2026 06:02
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-312 row 10. `.claude/hooks/session-start.sh` was the last by-path hook
registration this repository owned. It did three separable things and only one
of them was its own:

  ordering   -> the declaration order of ten `[[hook.handler]] on = "session-start"`
                rows in batten.toml. `dispatch` runs handlers in config order, so
                install < doctor < preflight is now readable from the authority
                instead of grepped out of a stubbed call log.
  the bound  -> `timeout_ms`, imposed by the parent. The script had none, on any
                step, so a hung `mise install` wedged the session.
  the report -> `mise.toml`'s `session:*` tasks, which keep the `step` helper's
                log-and-tail contract verbatim. The door does not provide it:
                stdout on a passing exit is advisory text, so a step that prints
                on success would announce itself every session (CLOUD-891).

Verified by dispatching the real rows, not by reasoning about them: exit 0, the
whole provisioning ran, and the reply carried exactly the two lines the retired
script also emitted. `crates/batten/tests/session_provisioning.rs` is the tier —
ten cases over the compiled binary against a fixture whose handlers are stubs,
proving order, the bound (0.749s against a 30s sleep), silence on a pass, the
reason reaching the session on a failure, and that stdout is never forwarded.

The bootstrap objection that pulled this from #714 is answered rather than
argued: `deps-install` (CLOUD-1085, #798) puts the released binary on PATH at
provisioning, before Claude launches, so there is something to dispatch with.
`session:batten` then replaces it with this checkout's build.

Two corrections the live dispatch measured, both of my own writing:
`signing-posture` prints on success, so registered bare it added a line the
retired script's `step` wrapper had swallowed — repointed at `session:signing`;
and the claim that only `session-census` may speak was wrong, because
`container-preflight` speaks too and did under the old script as well.

NOT READY TO LAND. Three cases in two governed suites grep the retired script
and go red: `tests/commit-attribution.bats:41` and
`tests/container-preflight.bats:131,138`. Measured over the whole corpus —
those three and nothing else. Both suites declare subjects that survive
(`hk.pkl`, `mise.toml`, `mise-tasks/container-preflight.sh`), so
`bats-tests-not-deleted` refuses deleting them and `shell-retirement` refuses
editing them: `only_drops_a_retired_reference` admits a removed line that names
a retired path, and a removed `@test` block's other lines name nothing.
`V-SHELL-RULE-EDITED` declares no override and no bypass_env.

Admits: dddb39ae4779d81289438ea653f09f6610f0945146dd12f8dab939d528de807b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 094058e
Admits-epoch: 121f33e1fca81092b00c148867959d9bf6cb54a82832540296b8b7f4fb2b7efb
Admits-author: alec@wenzowski.com
Admits-prev: 0d15402fe6e42f8c889653b827876c756c2d35c008671b41bf49c239f3b0360e
Admits-answer-lost: CLOUD-312 row 10 stays open, .claude/settings.json keeps the last by-path registration on any surface this repository owns, and 295 lines of bash keep ordering provisioning with no bound on any step. The verified work — ten rows, ten tasks, a compiled tier, and a live dispatch that returned exit 0 — would exist only in a container the platform reclaims.
Admits-answer-precondition: This commit writes ten hook.handler rows into batten.toml plus the corrections measured while dispatching them. The handler table is compiled-in config with no other surface: no mise task, rego module or CLI verb can add a row to it, so writing batten.toml directly is the only route, and every row lands in a diff a reviewer reads beside the script it replaces.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply: batten.toml is the owning surface for hook.handler and for its own comments. R-RESTORE-IT would discard the rows and leave the deleted script with nothing dispatching its steps.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… blocker

Three comments in `[tasks."test:bats"]` said the body "runs under /bin/sh with
no `set -e`". Measured false: a mise task declaring no `shell =` runs under
`sh -e`, and `shell = "bash -c"` is the every-line-runs case. The discriminator
is a DECLARED SHELL, not the file. This cost a debugging cycle in #798, where a
bare call aborted the task before its own error branch could run.

`.claude/rules/toolchain.md` names the contrast at the section readers
generalise from, so the next author does not re-derive the three comments.

`batten.toml`'s `[hook] exclusive` blocker note named `hooks-wiring-check` on
the `$HOME` surface. That stopped being the blocker when CLOUD-1167 landed
`input.tree.external`. The real blocker is four live sibling registrations, and
the row now carries the table plus the measurement `siblings: 2,
merged_siblings: 2`.

Admits: 12bc0d5675f0158bf1441a5f15ea8ad6ad04b6e46b25c30153d189a44185b023
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: c52854e
Admits-epoch: c81e3ad211cddfce765e42d79469223b10d01ff822929696d3914e643004f748
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The next reader scoping CLOUD-1160 is sent at a blocker that no longer exists instead of at the four live sibling registrations that are the real one. That already happened once this session and cost the row a wrong scoping before it was returned to Backlog.
Admits-answer-precondition: batten.toml is itself the owning surface for policy config, and the change is prose INSIDE it: the blocker note at :5185 names hooks-wiring-check on the $HOME surface, which stopped being the blocker when CLOUD-1167 landed input.tree.external. No other surface can express a correction to batten.toml's own comments, and the diff is three sentences a reviewer reads in place.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for its own comments; there is no narrower surface to redirect to. R-RESTORE-IT would discard the correction and leave the stale blocker naming a retired mechanism.
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-312 row 10. `.claude/hooks/session-start.sh` was the last by-path hook
registration this repository owned. It did three separable things and only one
of them was its own:

  ordering   -> the declaration order of ten `[[hook.handler]] on = "session-start"`
                rows in batten.toml. `dispatch` runs handlers in config order, so
                install < doctor < preflight is now readable from the authority
                instead of grepped out of a stubbed call log.
  the bound  -> `timeout_ms`, imposed by the parent. The script had none, on any
                step, so a hung `mise install` wedged the session.
  the report -> `mise.toml`'s `session:*` tasks, which keep the `step` helper's
                log-and-tail contract verbatim. The door does not provide it:
                stdout on a passing exit is advisory text, so a step that prints
                on success would announce itself every session (CLOUD-891).

Verified by dispatching the real rows, not by reasoning about them: exit 0, the
whole provisioning ran, and the reply carried exactly the two lines the retired
script also emitted. `crates/batten/tests/session_provisioning.rs` is the tier —
ten cases over the compiled binary against a fixture whose handlers are stubs,
proving order, the bound (0.749s against a 30s sleep), silence on a pass, the
reason reaching the session on a failure, and that stdout is never forwarded.

The bootstrap objection that pulled this from #714 is answered rather than
argued: `deps-install` (CLOUD-1085, #798) puts the released binary on PATH at
provisioning, before Claude launches, so there is something to dispatch with.
`session:batten` then replaces it with this checkout's build.

Two corrections the live dispatch measured, both of my own writing:
`signing-posture` prints on success, so registered bare it added a line the
retired script's `step` wrapper had swallowed — repointed at `session:signing`;
and the claim that only `session-census` may speak was wrong, because
`container-preflight` speaks too and did under the old script as well.

NOT READY TO LAND. Three cases in two governed suites grep the retired script
and go red: `tests/commit-attribution.bats:41` and
`tests/container-preflight.bats:131,138`. Measured over the whole corpus —
those three and nothing else. Both suites declare subjects that survive
(`hk.pkl`, `mise.toml`, `mise-tasks/container-preflight.sh`), so
`bats-tests-not-deleted` refuses deleting them and `shell-retirement` refuses
editing them: `only_drops_a_retired_reference` admits a removed line that names
a retired path, and a removed `@test` block's other lines name nothing.
`V-SHELL-RULE-EDITED` declares no override and no bypass_env.

Admits: dddb39ae4779d81289438ea653f09f6610f0945146dd12f8dab939d528de807b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 094058e
Admits-epoch: 121f33e1fca81092b00c148867959d9bf6cb54a82832540296b8b7f4fb2b7efb
Admits-author: alec@wenzowski.com
Admits-prev: 0d15402fe6e42f8c889653b827876c756c2d35c008671b41bf49c239f3b0360e
Admits-answer-lost: CLOUD-312 row 10 stays open, .claude/settings.json keeps the last by-path registration on any surface this repository owns, and 295 lines of bash keep ordering provisioning with no bound on any step. The verified work — ten rows, ten tasks, a compiled tier, and a live dispatch that returned exit 0 — would exist only in a container the platform reclaims.
Admits-answer-precondition: This commit writes ten hook.handler rows into batten.toml plus the corrections measured while dispatching them. The handler table is compiled-in config with no other surface: no mise task, rego module or CLI verb can add a row to it, so writing batten.toml directly is the only route, and every row lands in a diff a reviewer reads beside the script it replaces.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply: batten.toml is the owning surface for hook.handler and for its own comments. R-RESTORE-IT would discard the rows and leave the deleted script with nothing dispatching its steps.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… blocker

Three comments in `[tasks."test:bats"]` said the body "runs under /bin/sh with
no `set -e`". Measured false: a mise task declaring no `shell =` runs under
`sh -e`, and `shell = "bash -c"` is the every-line-runs case. The discriminator
is a DECLARED SHELL, not the file. This cost a debugging cycle in #798, where a
bare call aborted the task before its own error branch could run.

`.claude/rules/toolchain.md` names the contrast at the section readers
generalise from, so the next author does not re-derive the three comments.

`batten.toml`'s `[hook] exclusive` blocker note named `hooks-wiring-check` on
the `$HOME` surface. That stopped being the blocker when CLOUD-1167 landed
`input.tree.external`. The real blocker is four live sibling registrations, and
the row now carries the table plus the measurement `siblings: 2,
merged_siblings: 2`.

Admits: 12bc0d5675f0158bf1441a5f15ea8ad6ad04b6e46b25c30153d189a44185b023
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: c52854e
Admits-epoch: c81e3ad211cddfce765e42d79469223b10d01ff822929696d3914e643004f748
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The next reader scoping CLOUD-1160 is sent at a blocker that no longer exists instead of at the four live sibling registrations that are the real one. That already happened once this session and cost the row a wrong scoping before it was returned to Backlog.
Admits-answer-precondition: batten.toml is itself the owning surface for policy config, and the change is prose INSIDE it: the blocker note at :5185 names hooks-wiring-check on the $HOME surface, which stopped being the blocker when CLOUD-1167 landed input.tree.external. No other surface can express a correction to batten.toml's own comments, and the diff is three sentences a reviewer reads in place.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for its own comments; there is no narrower surface to redirect to. R-RESTORE-IT would discard the correction and leave the stale blocker naming a retired mechanism.

Refs: CLOUD-1085, CLOUD-1268, CLOUD-1160
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-312 row 10. `.claude/hooks/session-start.sh` was the last by-path hook
registration this repository owned. It did three separable things and only one
of them was its own:

  ordering   -> the declaration order of ten `[[hook.handler]] on = "session-start"`
                rows in batten.toml. `dispatch` runs handlers in config order, so
                install < doctor < preflight is now readable from the authority
                instead of grepped out of a stubbed call log.
  the bound  -> `timeout_ms`, imposed by the parent. The script had none, on any
                step, so a hung `mise install` wedged the session.
  the report -> `mise.toml`'s `session:*` tasks, which keep the `step` helper's
                log-and-tail contract verbatim. The door does not provide it:
                stdout on a passing exit is advisory text, so a step that prints
                on success would announce itself every session (CLOUD-891).

Verified by dispatching the real rows, not by reasoning about them: exit 0, the
whole provisioning ran, and the reply carried exactly the two lines the retired
script also emitted. `crates/batten/tests/session_provisioning.rs` is the tier —
ten cases over the compiled binary against a fixture whose handlers are stubs,
proving order, the bound (0.749s against a 30s sleep), silence on a pass, the
reason reaching the session on a failure, and that stdout is never forwarded.

The bootstrap objection that pulled this from #714 is answered rather than
argued: `deps-install` (CLOUD-1085, #798) puts the released binary on PATH at
provisioning, before Claude launches, so there is something to dispatch with.
`session:batten` then replaces it with this checkout's build.

Two corrections the live dispatch measured, both of my own writing:
`signing-posture` prints on success, so registered bare it added a line the
retired script's `step` wrapper had swallowed — repointed at `session:signing`;
and the claim that only `session-census` may speak was wrong, because
`container-preflight` speaks too and did under the old script as well.

NOT READY TO LAND. Three cases in two governed suites grep the retired script
and go red: `tests/commit-attribution.bats:41` and
`tests/container-preflight.bats:131,138`. Measured over the whole corpus —
those three and nothing else. Both suites declare subjects that survive
(`hk.pkl`, `mise.toml`, `mise-tasks/container-preflight.sh`), so
`bats-tests-not-deleted` refuses deleting them and `shell-retirement` refuses
editing them: `only_drops_a_retired_reference` admits a removed line that names
a retired path, and a removed `@test` block's other lines name nothing.
`V-SHELL-RULE-EDITED` declares no override and no bypass_env.

Admits: dddb39ae4779d81289438ea653f09f6610f0945146dd12f8dab939d528de807b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 094058e
Admits-epoch: 121f33e1fca81092b00c148867959d9bf6cb54a82832540296b8b7f4fb2b7efb
Admits-author: alec@wenzowski.com
Admits-prev: 0d15402fe6e42f8c889653b827876c756c2d35c008671b41bf49c239f3b0360e
Admits-answer-lost: CLOUD-312 row 10 stays open, .claude/settings.json keeps the last by-path registration on any surface this repository owns, and 295 lines of bash keep ordering provisioning with no bound on any step. The verified work — ten rows, ten tasks, a compiled tier, and a live dispatch that returned exit 0 — would exist only in a container the platform reclaims.
Admits-answer-precondition: This commit writes ten hook.handler rows into batten.toml plus the corrections measured while dispatching them. The handler table is compiled-in config with no other surface: no mise task, rego module or CLI verb can add a row to it, so writing batten.toml directly is the only route, and every row lands in a diff a reviewer reads beside the script it replaces.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply: batten.toml is the owning surface for hook.handler and for its own comments. R-RESTORE-IT would discard the rows and leave the deleted script with nothing dispatching its steps.

Refs: CLOUD-312, CLOUD-1085, CLOUD-898
wenzowski added a commit that referenced this pull request Sep 1, 2026
… blocker

Three comments in `[tasks."test:bats"]` said the body "runs under /bin/sh with
no `set -e`". Measured false: a mise task declaring no `shell =` runs under
`sh -e`, and `shell = "bash -c"` is the every-line-runs case. The discriminator
is a DECLARED SHELL, not the file. This cost a debugging cycle in #798, where a
bare call aborted the task before its own error branch could run.

`.claude/rules/toolchain.md` names the contrast at the section readers
generalise from, so the next author does not re-derive the three comments.

`batten.toml`'s `[hook] exclusive` blocker note named `hooks-wiring-check` on
the `$HOME` surface. That stopped being the blocker when CLOUD-1167 landed
`input.tree.external`. The real blocker is four live sibling registrations, and
the row now carries the table plus the measurement `siblings: 2,
merged_siblings: 2`.

Admits: 12bc0d5675f0158bf1441a5f15ea8ad6ad04b6e46b25c30153d189a44185b023
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: c52854e
Admits-epoch: c81e3ad211cddfce765e42d79469223b10d01ff822929696d3914e643004f748
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The next reader scoping CLOUD-1160 is sent at a blocker that no longer exists instead of at the four live sibling registrations that are the real one. That already happened once this session and cost the row a wrong scoping before it was returned to Backlog.
Admits-answer-precondition: batten.toml is itself the owning surface for policy config, and the change is prose INSIDE it: the blocker note at :5185 names hooks-wiring-check on the $HOME surface, which stopped being the blocker when CLOUD-1167 landed input.tree.external. No other surface can express a correction to batten.toml's own comments, and the diff is three sentences a reviewer reads in place.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for its own comments; there is no narrower surface to redirect to. R-RESTORE-IT would discard the correction and leave the stale blocker naming a retired mechanism.

Refs: CLOUD-1085, CLOUD-1268, CLOUD-1160
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-312 row 10. `.claude/hooks/session-start.sh` was the last by-path hook
registration this repository owned. It did three separable things and only one
of them was its own:

  ordering   -> the declaration order of ten `[[hook.handler]] on = "session-start"`
                rows in batten.toml. `dispatch` runs handlers in config order, so
                install < doctor < preflight is now readable from the authority
                instead of grepped out of a stubbed call log.
  the bound  -> `timeout_ms`, imposed by the parent. The script had none, on any
                step, so a hung `mise install` wedged the session.
  the report -> `mise.toml`'s `session:*` tasks, which keep the `step` helper's
                log-and-tail contract verbatim. The door does not provide it:
                stdout on a passing exit is advisory text, so a step that prints
                on success would announce itself every session (CLOUD-891).

Verified by dispatching the real rows, not by reasoning about them: exit 0, the
whole provisioning ran, and the reply carried exactly the two lines the retired
script also emitted. `crates/batten/tests/session_provisioning.rs` is the tier —
ten cases over the compiled binary against a fixture whose handlers are stubs,
proving order, the bound (0.749s against a 30s sleep), silence on a pass, the
reason reaching the session on a failure, and that stdout is never forwarded.

The bootstrap objection that pulled this from #714 is answered rather than
argued: `deps-install` (CLOUD-1085, #798) puts the released binary on PATH at
provisioning, before Claude launches, so there is something to dispatch with.
`session:batten` then replaces it with this checkout's build.

Two corrections the live dispatch measured, both of my own writing:
`signing-posture` prints on success, so registered bare it added a line the
retired script's `step` wrapper had swallowed — repointed at `session:signing`;
and the claim that only `session-census` may speak was wrong, because
`container-preflight` speaks too and did under the old script as well.

NOT READY TO LAND. Three cases in two governed suites grep the retired script
and go red: `tests/commit-attribution.bats:41` and
`tests/container-preflight.bats:131,138`. Measured over the whole corpus —
those three and nothing else. Both suites declare subjects that survive
(`hk.pkl`, `mise.toml`, `mise-tasks/container-preflight.sh`), so
`bats-tests-not-deleted` refuses deleting them and `shell-retirement` refuses
editing them: `only_drops_a_retired_reference` admits a removed line that names
a retired path, and a removed `@test` block's other lines name nothing.
`V-SHELL-RULE-EDITED` declares no override and no bypass_env.

Admits: dddb39ae4779d81289438ea653f09f6610f0945146dd12f8dab939d528de807b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 094058e
Admits-epoch: 121f33e1fca81092b00c148867959d9bf6cb54a82832540296b8b7f4fb2b7efb
Admits-author: alec@wenzowski.com
Admits-prev: 0d15402fe6e42f8c889653b827876c756c2d35c008671b41bf49c239f3b0360e
Admits-answer-lost: CLOUD-312 row 10 stays open, .claude/settings.json keeps the last by-path registration on any surface this repository owns, and 295 lines of bash keep ordering provisioning with no bound on any step. The verified work — ten rows, ten tasks, a compiled tier, and a live dispatch that returned exit 0 — would exist only in a container the platform reclaims.
Admits-answer-precondition: This commit writes ten hook.handler rows into batten.toml plus the corrections measured while dispatching them. The handler table is compiled-in config with no other surface: no mise task, rego module or CLI verb can add a row to it, so writing batten.toml directly is the only route, and every row lands in a diff a reviewer reads beside the script it replaces.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply: batten.toml is the owning surface for hook.handler and for its own comments. R-RESTORE-IT would discard the rows and leave the deleted script with nothing dispatching its steps.

Refs: CLOUD-312, CLOUD-1085, CLOUD-898
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