The joule ceiling is JCP's flagship enforcement, and the spec's contract is that
every decision is sealed, allow and deny alike. dispatch() did not honour that.
route() pre-filtered out any harness whose remaining budget could not cover the
task's quote, so an over-budget harness never reached runtime.decide() -- the
ceiling fired, the loop failed over, and no receipt recorded that it had. The
enforcement happened; the audit of it did not.
Replace route() with candidates(), which filters on capability only. dispatch()
now offers the task to each granted harness in turn, letting the runtime make and
seal the decision -- so a harness that cannot pay is denied on the record before
the loop fails over to one that can. Denied dispatches are still not debited
(Runtime::decide only debits on allow), so per-harness joule totals are unchanged.
One refusal deliberately seals nothing: a task whose kind no registered harness
holds. There is no grant to decide against, and JCP seals decisions about a
grant, not the absence of one. It is recorded denied in the RunReport instead.
Module doc and MAPPING.md now state both cases precisely rather than claiming a
blanket allow-and-deny seal.
Two regression tests: budget_denials_are_sealed_not_silently_skipped (the deny
lands in the ledger, verifies under the steward key, and is not debited) and
ungranted_kind_seals_nothing. All six pre-existing tests pass unchanged. The
background_agents example now shows 7 sealed decisions, up from 6 -- the seventh
being the budget denial that used to disappear.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
MAPPING.md records what this crate ingests: the background-agent orchestrator
pattern (tracker -> branch -> dispatch -> PR, coding agents in isolated
containers, local-handoff CLI doubling as an MCP server, BYO subscription,
event-triggered automations, chat/forge mention bindings). The pattern is
commodity; what no deployed instance produces is a signed, energy-accounted,
verifiable receipt of what ran, under whose authority, for how many joules.
The doc maps the pattern onto the reconcile loop, the Sandbox S0-S7 rungs its
container model sits low on (~S2/S3; the step up is an S5 microVM plus
sandbox-tee::energy), the Joule Table seams its UX surfaces never carried, and
the JCP grant its credential model stops short of (authentication vs authority)
- then scopes the frontier the category leaves empty.
examples/background_agents.rs drives it end to end: two backends handle
`implement` and the fleet fails over when the first exhausts its joule budget; a
`review` backend flakes twice and lands on the third attempt, charged for every
attempt; a `deploy` task is refused at routing because no backend holds that
capability. Six sealed decisions, all verifying under the steward key.
Tests and clippy --all-targets clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>