delegation-kit — safe delegated-agent execution for budget-bounded sessions

Delegated agents are cheap to dispatch and expensive to trust. Three failure surfaces dominate: shared mutable state (two committing agents race the git index and one sweeps the other’s staged files under the wrong message), interrupted long units (a usage-window wall fires mid-flight and the uncommitted investigation dies with the session), and untrustworthy self-reports (a sub-agent’s “passed” claim, or a gate quietly weakened to make its commit pass). The kit packages the supervisor-side protocol that closes all three, plus the two pieces that are mechanizable: a trustworthy budget verdict (usage-verdict) and a commit-shape gate over gate tampering (check-gate-tamper).

The kit carries the protocol templates, the budget-verdict tool, and the tamper gate; the consumer supplies its own validate battery, shared-file roster, and toolchain checks — those are project rule content and never ship.

The delegation model

One supervisor session dispatches Agent tasks and owns every ruling; agents execute briefs and surface anything the brief does not cover. templates/agent-execution.md owns the load-bearing rules — it is the procedure a session loads, and each bullet’s bold lead-in is the rule’s stable name (§One template, a resident pointer). This section keeps the mechanism contracts and only the rationale that earns spec residency (a failure surface, a calibration history, a bound that is correctness rather than preference), each paragraph citing its rule by name rather than restating it.

Boundary — this protocol stays fire-and-forget. An agent here executes a bounded brief and its result returns to the supervisor; a paused session that holds its working state and resumes in place when a question is answered is a different interaction model — resume-in-place orchestration, lifecycle-kit’s lead template (lifecycle-kit/SPEC.md §templates/lead.md), which reuses this protocol’s dispatch safety unchanged rather than replacing it.

The template’s Serialize on shared files; ≤DELEGATION_KIT_FAN_WIDTH-wide otherwise rule caps an unlocked fan-out at read-only work because the git index and HEAD are shared for every committing agent regardless of source-file disjointness — so the cap is a correctness bound, never a preference a consumer may configure up for committing agents (the knob’s derivation: §Layout and configuration).

The template’s Background + notification, never poll rule reads as universal but is not, and the sentence that scopes it is a fact rather than a role list: backgrounding survives a turn, and a dispatched agent’s turn end is its session end. Attested failure surface — a stage session read the rule, backgrounded its whole validate suite, ended its turn to report progress, and its child died with it: an entry stamp, no results, the suite re-run from scratch. Nothing reddened, because missing evidence is indistinguishable from pending evidence. Two phrasings were drafted and rejected on the way to the current one, and both failures are instructive: a role-neutral “never end your turn with detached work” forbids the supervisor pattern the same rule prescribes, and “await it in the foreground” names a lever the agent may not have — dispatches made without requesting backgrounding have been observed detached anyway, so a rule stated on foreground is unfollowable exactly where it is needed. That a subagent’s return terminates its background children was attested here and inferred from the harness docs, not stated by them, and a counter-observation has since narrowed it: a validate session backgrounded a shell producer, ended its turn, and the process kept running and kept writing its output file for minutes. Reaping holds for an Agent child and not for a shell one, so the rule’s mechanism is the unconditional loss of the observer, not the conditional loss of the work — and the surviving-producer case is the worse of the two, since it is validate-producer-liveness-unobservable’s hazard (an unread producer mutating shared files while the next actor moves) arriving through the backgrounding door. The rule is anchored on the invariant either way — an agent’s contract with its caller is its return value, so work whose result is not in the return did not happen as far as the caller is concerned — which is why the counter-observation cost the rule a sentence and not its content, exactly as this paragraph predicted a reaping change would.

Why that correction needed an affordance, not just a better reason. The session that produced the counter-observation reasoned correctly into the failure: it read the producer-liveness hazard, concluded it must not let the next actor proceed until the producer finished, and ended its turn to wait for the completion notification — the one act that destroys that channel for a dispatched role. A rule whose correct application requires a channel the reader does not have is defeated by careful readers and not only careless ones, so the template names the channel a dispatched role does have: an in-turn wait on the work’s own artifact. This is the case where the backgrounding rule and the liveness hazard meet and neither, alone, covered it.

The template’s Background + notification, never poll rule now carries a which primitive clause, and that clause’s residency here is a calibration history. The template first named the harness’s waiting primitive in the singular while the harness offers two forms with opposite reactivity, so a session picking one unaided picked by shape. Naming a form teaches a spelling; naming the property a wait must have and sorting the forms under it teaches the discriminator — a bound that is correctness rather than preference, because it is what survives a harness adding a third form.

Two waitable things, and only one of them has a path. The template splits what a session waits on and carries that mechanism itself. What is added here is that the split is complete rather than merely convenient, because the attested failure had no third case — and that the artifact’s home is the resume journal’s own scoping widened rather than a second rule, so the survivability grounds are stated once and cited twice.

The shell branch names its artifact, and the naming is the substance. The split above left that branch pointing at an artifact the session placed without saying which, so a session that wanted to comply had nothing to write, and of the long-running shell producers a session might background, most publish no record of any kind. The artifact is a launch-time liveness record: the child’s PID, captured by the launcher at the moment it backgrounds the child. The obvious repair — have the child touch a file when it finishes and wait on that — is refused, and the attested failure is exactly why: a completion marker answers is it done to a live observer, and the failure case is the one where the observer is gone. A liveness record answers is it still running to whoever arrives next, which is a question a dead session’s successor can still ask.

The record’s producer is the launcher, and the alternative is recorded as refused rather than merely not chosen. The alternative is per-producer self-claiming — the producer-liveness lock’s shape generalized so every long-running producer claims its own record — and it fails on three counts. It reaches only producers the consumer’s own tree owns, where most backgrounded work is an ad-hoc command composed at the call site. It puts the record’s production in the one process whose death is the event being detected, so a producer that dies before claiming is invisible in exactly the window that matters. And it bills a program for a property that belongs to the launch rather than to the program: the same script backgrounded is in this class and run in the foreground is not. The launcher, by contrast, knows the PID of anything it started, is the party that needs to read it back, and is the only one present at the moment the class is entered.

The liveness clause widened rather than acquiring an exception, and its own premise is what falsified it. The clause sanctioned a recorded-PID match where liveness genuinely is the condition — a producer this session did not start, and that exclusion rested on an assumption: a session that started the producer was taken to have a better instrument, namely its own artifact. The better instrument requires a live reader, and the failure being fixed is the loss of the reader. So the subject widens to cover a producer the session started and backgrounded, and the widening is on the subject alone, never on the predicate — pattern-matched liveness stays refused, and the bracket trick stays refused as the sanctioned form on the grounds below.

The record’s grammar is evidence-kit’s, and that is what makes the second reader free. There are two readers, and they are different parties at different transitions: the launching session, in-turn, as its wait condition; and whoever arrives next, which is the case the attested failure is actually about, where the launcher is gone and something else must be able to tell that a producer is still mutating shared files. Written in the one-line pid=<n> run=<key> grammar the producer-liveness lock already carries, the record is a legal subject for check-producer-liveness unchanged — that gate already takes a record path and already reports exactly is the producer named here still running, on a ruled exit contract. No new gate, no new tool, no new grammar, and a reader already specified, already fixture-covered and already wired. An inverted grammar would have bought a second implementation of a decided thing, which is the only argument this needs; it is recorded because a launch record is not a lock, so it should have its own shape is the reading a later session will reach for, and the answer is that the two records carry the same field, answer the same question, and differ only in who wrote them. How a PID’s liveness is decided stays evidence-kit’s — the predicate, the PID-reuse residual and the refused TTL are ruled in evidence-kit/SPEC.md §The producer-liveness lock and are not seconded here. delegation-kit owns when a session waits and on what.

The record’s name is a convention, and what the convention buys is a derivable set. It is written as <scratch-dir>/<key>.run, carrying the same pid=<n> run=<key> line — the filename’s <key> is that record’s own run= value, not a third field. The grammar is untouched; what was missing is not the record’s shape but its discoverability. Without a convention the record’s path is known only to the session that wrote it and to whoever it happens to tell, so the second reader named above — whoever arrives next, where the launcher is gone — can read a record only by guessing its name. A suffix makes the set a glob, and every reader that must ask is anything still running here reads the set rather than a path: check-producer-liveness’s set mode (evidence-kit/SPEC.md §check-producer-liveness), the stage-entry preflight behind it, and guard-kit rule 14.

.run rather than .lock, and the distinction is load-bearing. A lock is claimed and released by one owner and its absence means free; EVIDENCE_KIT_LOCK_FILE is one and keeps its name. A launch record is a statement of fact left behind — it answers is it still running to whoever arrives next, a question a dead session’s successor can still ask — and its absence means nothing was recorded, never nothing is running. Two meanings, two suffixes; and a consumer’s scratch reset carries no *.run on its keep-list, because a record surviving a work-unit boundary names a producer from the previous one, which is precisely the stale statement the sweep exists to remove.

Both affordances are refused, and the refusal’s cost is bounded rather than denied. A tool to write the record and a second one to read it are declined: the write is a PID captured at launch and the read is a single liveness match, each a shell builtin away and both already spelled in the template, and a script whose body is one builtin is the clearest case of the script-interpreter surface this methodology shrinks. The generic reading affordance that would genuinely earn its place already exists and is check-producer-liveness, which the grammar reuse reaches without adding anything. What the refusal costs is stated rather than hidden: an affordance would make the record’s grammar impossible to get wrong, and a hand-written one can be malformed. That failure is bounded by the consumer, which exits 2 on a record it cannot parse, so a malformed record reads as could not be established and never as nothing running.

What the naming does not claim. No gate enforces it, and the reason is structural rather than budgetary: the subject is did a session record a PID before backgrounding, and a session that skips the rule writes nothing, so there is no absence a check could have been told to expect. The artifact-side backstop stays the one §Operative residency already names — check-producer-liveness at a stage entry, covering the concurrent case, a producer still live when the next entry is stamped. Widening what that backstop may be pointed at does not make the rule itself checkable. What the naming removes is a different failure: a session that wanted to comply had no named artifact and was carved out of the only clause that fit.

The enforcement-design question is now answered, and the answer is a relocation rather than a reversal. Given that prose alone does not hold, what does stood open here while the ruling above — the write is structurally uncheckable — stayed true. It still is. What changed is the subject: the harm a missed wait causes is not the turn-end but a tracked-tree mutation under a live producer, and that arrives as an ordinary tool call at a PreToolUse chokepoint, which is precisely the class §Operative residency says the interception axis admits. It is enforced there, as guard-kit rule 14 (guard-kit/SPEC.md §The generic ruleset). The chokepoint ruling is confirmed by this, not weakened.

The claim ships with its bound, because a claim without one is the failure this rule keeps re-earning. The enforcement holds only for a session that recorded. The paragraph above rules that the record’s write is structurally uncheckable — a session that skips the rule writes nothing, so there is no absence a check could have been told to expect — so a session that backgrounds without recording is invisible to rule 14 and to the entry preflight alike. It is now advised at the launch, and still not refused. This narrows the failure to that residue rather than closing it.

The record’s write is now reached at the launch chokepoint, and the refusal that stood here is discharged. The candidate was a rule firing on the backgrounding call itself, held back on one empirical unknown: a shell & is in the command text every rule already reads, but a harness’s background-this parameter is tool input, not command text, and whether it reaches the PreToolUse payload was a question about one field. It does: .tool_input.run_in_background is a JSON boolean true on a backgrounded Bash call and absent on a foreground one (guard-kit/SPEC.md §The guard framework records the payload’s shape, so no session re-buys the probe). Both forms are therefore reachable and guard-kit rule 15 builds both arms in one rule. The judgment that held it back is kept rather than retired, because it is what made the two-arm rule the right build rather than a bigger one: building only the & arm would have been worse than not building it — every attested firing used the harness form, so a rule covering only the shell spelling would have blocked the form nobody uses and passed the one that fires, coverage in appearance and an assertion about nothing in fact. What ships is an advisory, not a block, on grounds guard-kit/SPEC.md §The generic ruleset states with the rule, and its re-opening condition is named there.

The guard interaction that produced it resolves without touching the guard. The attested sequence: a session reached for the correct artifact wait, spelled it against the harness’s own scratchpad path, and the consumer’s bash guard refused it and steered to repo-local scratch. The guard was right and needed no change. What was missing is that the doctrine never said where an awaited artifact belongs, so the refusal read as a refusal of artifact-waiting itself and pushed the session onto the process-liveness form that then broke. With the split above, a session waiting on an Agent never reaches for a path at all, and a session waiting on a shell child has a path the guard already sanctions.

The bracket trick is refused as the sanctioned form, and the grounds are here because the template defers them here. The template carries the mechanism — why a process-liveness predicate can never go false, and the two sanctioned repairs — and sends here the one question it does not answer: why the third, pgrep -f '[r]un-smoke.sh', is refused rather than passed over. It is the first thing a reader reaches for and it works, so what disqualifies it is not correctness but its failure mode: a spelling that must be remembered, whose omission is invisible at the call site and whose cost when omitted is silent and expensive. A rule whose correct form differs from its incorrect form by one character is a rule that will be got wrong, which is why the enforcement half is a guard rule over the shape (guard-kit/SPEC.md §The generic ruleset) rather than more prose.

The template’s Findings you will act on are durable before you act on them rule rests on a near-loss rather than a loss: a close stage dispatched two audit sweeps and then hit the session wall. Both had already returned, which was luck; had the wall landed mid-sweep the audit work would have been gone with no record it ran. The gap is that the journal mechanics are written for a dispatched mutating agent, so a read-only child journals nothing and its findings exist only as a return value in the parent’s context. The fix deliberately does not overturn the read-only-fan-out caveat in §Resume journal — that caveat was re-examined against this evidence and kept, with its duty relocated to the receiving end, because overturning it would put the write back exactly where the observed failures are. Recorded here because the rule and the caveat otherwise read as each other’s contradiction and the next reader re-litigates them.

templates/agent-budget-guard.sh closes the gap the template’s Budget-check before each dispatch in a fan-out rule leaves when it relies on the agent choosing to run the tool: a memory-quoted percentage acts in its place (a session quoted ~5% while the live verdict read 29%). It is a PreToolUse hook (matcher Agent) the harness fires on every dispatch once the consumer registers it in settings — per-dispatch freshness, not a start-of-session reading a mid-session window outlives. It runs usage-verdict at the decision point and routes on the exit code:

  • PAUSE (1)guard_block: the verdict line plus its corrective (wait for the window reset, or re-run with DELEGATION_KIT_PAUSE_PCT deliberately raised). This is guard-kit’s one sanctioned fail-closed deny — the hook matcher proves the tool identity and PAUSE is reachable only through a fresh, readable, over-threshold snapshot, so blocking cannot wedge a consumer with no producer.
  • STALE / unreadable (2) and OK / RESET-OK (0)guard_advise, feeding the verdict line back as additionalContext so the live reading rides in context at every dispatch and a memory-quoted percentage can never be the acting source. The advise arm relays that line and adds nothing: the verdict string carries its own decision consequence (§usage-verdict), so restating it on the arm would be one fact in two places. Routing STALE to advice rather than out of delegation is what keeps a consumer with no snapshot producer working — budget-unknown is decision-relevant, never disqualifying.

The harness is the producer (it fires the hook once the settings registration is set); the consumers are the dispatching agent, which reads the verdict line off additionalContext at the dispatch-decision transition, and the supervising user, who consumes a block by ruling — raise the knob or wait; the agent never overrides. delegation-kit owns the verdict, its thresholds, and the routing; guard-kit supplies only the framework primitives (its second consumer). Registration is the opt-in valve: a consumer wanting pure advice does not wire the hook. No new persistent state and no new key on the usage.txt contract — the verdict line is the only interface.

Covered at every depth, aggregated at none. The hook’s registration is session-wide, so it fires inside dispatched sessions too: a dispatched session’s own dispatches re-arm it, carrying that session’s identity, and the coverage reaches every depth rather than stopping at the root. What it cannot do is aggregate — each fire is independent and holds no state about the tree, so a verdict prices the one call it gates and never the subtree that call opens. That pairing is what the subtree clause of the template’s Budget-check before each dispatch in a fan-out rule exists to compensate, and stating both halves here is what stops the next reader re-filing either. Calibration history, because the silence invited the wrong turn: the hook script is stateless and carries no depth counter, and a scope survey read that as the guard does not reach below the root and filed against it. The reading of the script was exact; the inference from it was false. A guard whose output is only ever seen by the session it fires in looks absent from every other vantage point — so a supervisor counting its own hook fires is watching a strict undercount of its own tree, which is the observation the residual projection gap is stated against rather than a coverage gap to be re-armed.

A knob-raise ruling has one working transport in this harness: the settings env block (.claude/settings.local.json), which hooks re-read per fire — the raised DELEGATION_KIT_PAUSE_PCT reaches the guard on the next dispatch with no restart, and deleting the entry afterwards restores the default just as immediately. A Bash-tool inline export never reaches the hook (the hook process does not inherit the tool’s shell), so it is not an override path. Propagation is asymmetric within a live session: the hook re-reads the file per fire, but the Bash tool environment retains an injected value after the entry is deleted until the session restarts — an in-session usage-verdict run and a hook fire can judge against different thresholds in that window, so the hook’s verdict line, not an in-session re-run, is the acting reading. That this transport works is not a permission to use it for everything a harness env var reaches: the subagent-model override below rides the same block and is refused there, so the two must not read as one sanction.

The subagent-model environment override is refused, and the reason is precedence. Model resolution runs in a fixed order — the subagent-model environment override, then a dispatch’s own model parameter, then the agent definition’s model: frontmatter, then the dispatching conversation’s model — and the environment rung sits above the per-dispatch parameter. Setting it would pin every subagent to a cheap tier in one line, and would thereby silently defeat a supervisor’s deliberate per-batch tier pin, the mechanism lifecycle-kit/templates/lead.md §Economics — batch, and compact where it pays depends on to tier a batch by its work classes. A lever that overrides the deliberate choice is not a default; it is a ceiling, so it is recorded as rejected rather than left to be rediscovered as an improvement. The correct rung is the frontmatter, third in the chain: it supplies the standing default the template’s Match the dispatched model and effort to the unit’s shape rule asks for while the per-dispatch parameter still overrides it. The same rule’s affirmative-selection clause is why the field is stated rather than left out — an omitted model: resolves to the literal inherit, so omission spells the dispatcher’s tier rather than a neutral absence, and check-agent-tier-explicit is that clause’s oracle over the tracked surface.

Isolation’s second purpose, and why the cost objection does not land. The template’s A read-only claim is made by isolation, not by sentence rule states the disposition and carries none of its grounds, which are these. “It has no Edit tool” is not the safety property it reads as: every agent type available for audit-shaped work carries write tools, or at least a shell that reaches git, so a toolset is never narrowed by the brief describing it.

Confinement is the whole of it, and the “its own detector” claim is retired. The isolation was once credited with a second purpose: the harness auto-cleans a worktree left unchanged, so a surviving worktree was read as proof the agent had written, making one mechanism both the confinement and its own detector. That biconditional is false in the direction that matters. Auto-clean is best-effort: every attested surviving worktree in this repo’s record was locked at a stale stamp with an empty git status --porcelain inside, so survival does not imply a write. A detector whose positive is unsound is worse than no detector, because it trains its reader to investigate nothing — the reader who finds a surviving worktree and infers a stray write hunts a change that is not there, and stops hunting the third time. What replaces it is a weaker true statement and no new mechanism: a surviving worktree means either the child wrote or the reclamation did not fire, and telling the two apart is one git status --porcelain inside it — a read the reaping boundary now performs and reports per path, so the disjunction is resolved where it is met rather than by hand (lifecycle-kit/SPEC.md §bin/enter-stage.sh). Isolation’s case is untouched and never needed the detector — the template’s A read-only claim is made by isolation, not by sentence rule rests on write confinement, which is a property of the mechanism rather than an observation about its cleanup. Gitignoring the worktree path trades the free git status signal away, which is why the rule’s second cost sends the parent to an explicit git worktree list at the reaping boundary instead — a reap now enforced there, since the iteration boundary refuses outright on any linked worktree (lifecycle-kit/SPEC.md §bin/enter-stage.sh).

And this is the same failure as the worktree.baseRef one below, met from the other side. That paragraph rules that repeated confirmation of a behavior is not evidence about its mutability, after a false universal reached shipped doctrine on attestations taken under one default. Here it was repeated successful auto-cleans that confirmed the detector until they did not, each success making the mechanism feel more settled and so suppressing the probe that would have falsified it. The two corrections are filed one paragraph apart so a reader meets the general lesson once with two instances, rather than twice as two unrelated repairs.

A dispatch-time sweep in the guard is refused, and recorded here so the earlier-is-better instinct meets the ruling rather than the silence. The obvious earlier fix for accumulating residue is to have the dispatch guard reap it on the way past. It is ruled out on two grounds. A verdict surface may not own lifecycle: the guard’s contract is to decide — allow or block a dispatch — and a guard that removes directories and deletes refs en route to a verdict has become a lifecycle owner whose failures are invisible at the only place anyone looks, which is the verdict. And the timing is wrong in the one direction that cannot be recovered: a dispatch-time sweep runs while sibling dispatches are in flight, so its predicate would have to establish that a worktree belongs to no live child, and a sweep that guesses wrong destroys a running sibling’s checkout. The iteration-boundary refusal has the opposite property on both counts: it takes no destructive action at all, and it runs at the one moment whose whole definition is that the previous iteration’s work is finished. The guard’s contract, its assertions and its source are therefore unchanged by this ruling.

The second ground rested on a premise an experiment has since falsified, and the correction is recorded rather than left to be re-derived. That ground read “lock is not a liveness signal here”: the attested residue was locked, stale-stamped and clean, taken to be indistinguishable from a live child’s worktree. It is distinguishable. The lock reason names the holding process’s pid and its start time, so does this worktree belong to a live child is answerable from outside the child — the classification lifecycle-kit’s boundary check now performs. The same experiment supplied the residue’s cause, which nothing had established: a locked worktree is refused by git worktree prune and by an unforced git worktree remove, so a harness dying before its cleanup step strands one that is clean, commitless and unreclaimable — the exact signature the attested survivors carried and which the auto-clean-is-best-effort reading above described without explaining. Reclamation is tied to the child’s own return, not to the dispatching session’s lifetime.

The ruling stands, and stands on the ground that survived. A verdict surface may not own lifecycle is untouched by any of this, and it alone refuses the dispatch-time sweep. What the falsified premise changes is what is now designable elsewhere: a reaper at a boundary can be built against a published mechanism rather than against a guess, which is why the classification landed there and not here. The predicate also degrades to the old blind state wherever the consumer configures no lock-reason pattern, so a guard-side sweep would be resting on optional consumer config to avoid destroying a sibling’s checkout — an argument against moving it here even now that the signal exists.

worktree.baseRef is vendor configuration, and choosing it is the consumer’s job. The template’s Isolation charges four harness costs, and paying them is the parent’s job rule names the knob and both of its values rather than a choice, because a vendoring consumer inherits the template and never a settings file. A consumer that dispatches isolated agents therefore sets the knob deliberately instead of inheriting the default, and — where its harness config is gated — pins it, through the general settings-pin mechanism (context-kit/SPEC.md §check-settings-pins) rather than anything this kit ships. What is ruled is that the value be chosen, never which value: a consumer wanting a clean tree per dispatch is right to stay on the default. No knob joins delegation-kit’s own roster for it (§Layout and configuration) — this is harness configuration, not kit configuration, and a kit knob shadowing a vendor setting would be a second source for one value. Recorded so a later reader does not read the omission as an oversight and close it.

Why that cost names a knob and not a measurement. “The worktree is cut at the remote tracking ref, so staleness equals the unpushed backlog” describes the fresh value alone; stated as universal it is false, and it reached shipped, consumer-facing doctrine on seven attestations — every one taken under that default, not one of them bearing on whether the behavior was configurable. Repeated confirmation of a behavior is not evidence about its mutability, and each re-confirmation made the mechanism feel more settled and so suppressed the configuration probe that would have settled it. Hence the rule’s shape: name the knob and both values, and leave any measurement attributed to the configuration it was taken under. It follows that no parent “owes the doubt back” for a structural defect here — there is none, and the discipline below costs one line in a prompt.

The child-side rev discipline is kept as defence in depth, and the first ground alone is sufficient. The parent cannot read the child’s effective setting — baseRef resolves on the dispatching machine’s merged settings, nothing in the dispatch payload carries it and nothing returns it — so a parent assuming either value is asserting an unverified premise about a third-party tool, which is the move that produced the false doctrine above. Second, a vendoring consumer inherits the template and not a settings file, so for every consumer that has not set the knob the discipline is the whole protection. Third, even a repo that pins the knob is not covered end to end: a managed tier and a command-line override outrank a project settings file and lie outside any repo’s reach. It is also nearly free and self-verifying — one command whose output is the evidence — so under a holding pin it degrades from load-bearing to a cheap assertion rather than becoming dead weight.

The untracked half reaches no configuration at all, and its disposition is refusal. An isolated child sees only committed state, so the third cost is a ruling rather than a mitigation: no value of the knob helps and naming a rev does not either. Attested twice — a sweep sent to triage an untracked capture log read an empty file and reported its corpus absent, and a settings-overlay comparison saw no overlay because the local settings file is uncommitted. Both returned a confident “nothing there”, indistinguishable in shape from a clean result, which makes it the worse of the two failure modes. It binds against the read-only rule rather than beside it: a read-only claim is made by isolation, and isolation is what blinds the read, so a claimed-read-only sweep over an untracked corpus has no correct form at all.

The dispatch-shape rules are one mechanism, seen from several sides. The template’s Never dispatch a fork to narrow a child, A read-only claim is made by isolation, not by sentence, and A child’s only upward route is a durable artifact rules are one defect seen from three sides: each governs the shape of a dispatch call, and each failed the same way — the prose did not reach the reader it binds. One attested surface per rule: a fork dispatched as a read-only audit completed a whole stage, commits included; four forks dispatched with prose-only “read-only, no edits” briefs, one of which committed unreviewed on the shared branch; and repeated fan-outs, across stages and across iterations, messaged the top-level session instead of their dispatcher, which no prompt wording could have fixed.

Why an oracle is owed here and not for the turn-ending rule. §Operative residency closes on the finding that a restated rule is reachable, not obeyed, and owes no gate because no check can read a session’s choice to end a turn — the act leaves no tracked artifact. A dispatch leaves no tracked artifact either, so the distinguishing axis is not durability but interception:

Where a rule binds an act that leaves no tracked artifact, the enforcement question is whether the act passes a chokepoint — not whether it reaches the tree. A dispatch does. A turn-end does not.

That second sentence was a claim about the harness, and it has now been measured. A SubagentStop hook is a chokepoint at the turn-end, so it is the one candidate that could falsify the axis — and the decisive question was whether the harness already defers a subagent’s stop while a background child is live, since a harness that deferred would dissolve the class this rule exists for. The probe at §The turn-end liveness hook (template) put a live producer under a deliberate turn-end and read live=yes: the stop is not deferred. So the sentence stands as written for the PreToolUse axis it was said on, the relocation to guard-kit rule 14 stands, and the turn-end’s own event is reachable.

The second sentence of that block holds only on the axis it was said on, and the qualification is stated rather than left to be read past. A turn-end does not pass a chokepoint on the tool-call axis, which is the axis the block is about. On the harness’s own event axis it does, and the hook there refuses, by exit 2, on an operator authorization since spent (§The turn-end liveness hook (template) owns the mechanism and the authorization’s content). So the turn-end rule carries enforcement on both axes — rule 14 over the harm, this hook over the act — and neither is the other’s substitute, since rule 14 reaches acts this hook’s event never sees and this hook reaches a session that mutates nothing.

The chokepoint — what a PreToolUse hook can actually read. The payload carries session_id, prompt_id, transcript_path, cwd, permission_mode, hook_event_name, tool_name, tool_input, and tool_use_id; and, only when the hook fires inside a subagent, agent_id and agent_type. For the dispatch tool, tool_input carries the dispatch’s own parameters — subagent_type, isolation, model, description, prompt. Three consequences carry the guard below: the fork ban has an exact trigger (tool_input.subagent_type), the read-only claim has an exact trigger (tool_input.isolation), and depth has an exact, documented discriminator — agent_id is present iff the dispatching session is itself a subagent, so its presence means the call about to be made creates a grandchild. That is a discriminator on the field’s presence, never on its value, and the distinction is worth one clause because §The turn-end liveness hook (template) records a live doubt about the value: an observation of five firings in one session saw five distinct agent_id values. Nothing in that doubt touches this trigger — presence is still presence whether the value is per-agent or per-firing — so the two readings are compatible rather than in tension, and neither is the other’s drift.

Honest limit on that roster. It is sourced from the harness’s own published hook contract, fetched rather than recalled, and from nothing in this tree — the same footing as CLAUDE_CODE_CHILD_SESSION’s “verified, not trusted” treatment (lifecycle-kit/SPEC.md §bin/session-id.sh). A future harness revision reshaping the payload is drift this SPEC cannot self-detect; only re-reading the hook contract catches it.

Two depth routes ruled out, both being the obvious move. CLAUDE_CODE_CHILD_SESSION is set in top-level sessions too, and CLAUDE_CODE_SESSION_ID carries the root id for a child, so a depth-1 and a depth-2 dispatcher are indistinguishable by environment alone — that route ships a rule silently mis-firing at the top level. The meta record beside a transcript does carry an exact spawn depth, and that route works and is deliberately not taken: it couples a hook to an uncontracted harness artifact to answer a question one contracted boolean already answers. The stage-economics meter accepts that same coupling (drift-kit/SPEC.md §The stage-economics meter, the fan-out row) because it needs the whole spawn forest and no contracted field supplies it; a hook needs one bit and gets it under contract. The axis is what the contract supplies, not appetite for risk.

templates/agent-dispatch-guard.sh is that oracle — a second PreToolUse hook on matcher Agent, beside the budget guard, composed from guard-kit/lib/guard.sh primitives. It reads the payload once and runs the rules below, in order:

# rule trigger decision
D1 fork ban tool_input.subagent_type is fork block
D2 isolation claim tool_input.subagent_typeDELEGATION_KIT_READONLY_TYPES and tool_input.isolation is not worktree block
D3 depth advisory payload carries agent_id advise

Order is load-bearing only between D1/D2 and D3 — a blocked dispatch needs no advisory, so D3 runs last. D1 precedes D2 because the fork ban is unconditional and its message is the more specific one for a dispatch violating both.

Why a second guard rather than more rules in the budget guard. Four reasons, each independently sufficient: the budget guard blocks on a transient condition and is deliberately knob-overridable, while these block on a permanent doctrine violation that must not be per-dispatch overridable; the budget guard reads no stdin at all; a consumer may want either enforcement without the other, and hook registration is the only opt-in valve there is; and their test lanes take different payload grammars. The harness fires every hook registered on a matcher and any exit 2 blocks, so the two compose with no dispatcher between them.

D1 blocks unconditionally, which the template’s own boundary does not — a fork stays correct where the child does the same job at the same authority, and a hook cannot read intent. The block is ruled unconditional on the ground that no sanctioned fork use exists in this doctrine, and the valve is unwiring the hook, never a knob: a per-dispatch override is exactly the honour system these rules exist to end, and a knob would restore it under a better name. The block message names the two lawful alternatives, as guard-kit requires of every block message.

D2 is inert until configured, by construction — the default roster is empty and the kit ships no agent-type names. An inert D2 reports nothing; it is a hook, not a gate, so there is no clean line to print. Stated here so a consumer does not infer coverage it has not configured.

D3 advises rather than blocks, and both halves are deliberate. It cannot block: a nested dispatch is legitimate and common — a stage session’s own read-only fan-out is a grandchild dispatch — so blocking would remove a capability rather than confine one. It must not be silent: this is the one rule whose bound reader provably never loads the protocol template (§One template, a resident pointer), so the advisory is the only delivery of it, which is §Operative residency’s (a)–(c) shape applied at a hook instead of a doc. Ruled out for D3: keying on whether the prompt text “names a path” — that fires on the word and teaches dispatchers to game the wording, buying a green hook and no channel. The agent_id trigger is exact and unforgeable; the message carries the judgment the trigger cannot.

Degradation — fail-open, but loud, the posture guard-kit/SPEC.md §The guard framework names for a deny-guard whose matcher proves the tool but whose rule turns on a payload field:

what is missing behavior
jq absent allow; one advisory naming the unenforced rules
payload unparseable, or tool_input absent allow; same advisory
subagent_type absent from a parseable payload D1 and D2 do not fire (nothing to match); D3 unaffected
DELEGATION_KIT_READONLY_TYPES unset or empty D2 inert, and silently so; D1 and D3 unaffected
DELEGATION_KIT_CONFIG_FILE set but naming no file allow; advisory naming D2 unenforced
guard-kit/lib/guard.sh not vendored the guard exits 0 before sourcing, as both shipped guards already do

Two implementation facts those rows force, recorded because the obvious build gets each of them wrong:

  • The jq-absent arm cannot deliver through guard_advise. That primitive is itself jq-backed, and measuring it rather than assuming settles what happens: with jq off PATH it writes an empty stdout and leaves a shell “command not found” on stderr, which names none of the unenforced rules — the silent success these rules exist to end, arriving inside the mechanism meant to prevent it. The guard therefore emits the advisory envelope itself on that arm, and every advisory literal it carries is kept free of the characters JSON must escape, since that fallback carries no escaper.
  • The guard does not source delegation-kit’s validating config loader, only the consumer config file, for the one array it reads. The loader exits 2 on any malformed knob and a hook that exits 2 blocks, so routing the fork ban through a validator for DELEGATION_KIT_FAN_WIDTH would let an unrelated typo wedge every dispatch in the consumer whose primary token lever is delegation — the fail-closed posture this guard is specified against, arriving through the config door. The distinction the last two table rows draw is this kit’s standing one: a roster configured to nothing is silent, a roster that could not be read is loud.

One template, a resident pointer

templates/agent-execution.md is the single source for the protocol — a binding-shim template in lifecycle-kit’s grammar (lifecycle-kit/SPEC.md §check-skill-binding). The consumer creates .claude/commands/agent-execution.md as a shim that names the template and binds its two slots — the shared-file roster and the validate battery — the consumer-specialization discipline of guard-kit’s consumer-rules block, now carried by the slot/binding mechanism the lifecycle skills already use. check-skill-binding enforces the slot pairing on the consumer side.

A spec reference to one of those rules uses the citation grammar the template's **<name>** rule, where <name> is a bullet’s bold lead-in verbatim minus its trailing period; check-rule-citation resolves every such citation in §The delegation model forward into the template, so a lead-in rename cannot silently dangle a reference.

templates/dispatch-checklists.md is a second, load-triggered template the protocol template reaches by a single pointer line: agent-execution.md names it for a deletion, rename, or heavy cross-spec audit dispatch, and it loads only then — never resident, and not a binding shim (it binds no slots, so check-skill-binding does not scan it). It carries the mechanical pre-flight for those dispatch shapes — the importer/collision/sweep-verification checks — while the protocol proper stays whole in agent-execution.md; the pointer-line contract is that the protocol template stays complete alone and the checklists are a reach-through, so the width, journal, and validate rules live once in the protocol template and are cited from the checklists, never restated. This is the load-trigger-residency rule applied a rung deeper: even inside a load-triggered template, a subset that only a narrower trigger (a deletion or rename dispatch) needs sits behind its own pointer.

The consumer’s CLAUDE.md carries no digest of the bullets, only a resident pointer: the pre-authorization sentence (consumer judgment on what delegation needs no ask) and /agent-execution. Rationale: a rule is resident only when it has no load trigger (the load-trigger-residency doctrine), and every protocol bullet triggers at Agent dispatch for the role that dispatches — which already has a mechanical seam, the per-dispatch budget guard, whose block message names /agent-execution. The doctrine lives behind the trigger, not in the always-loaded file. Honest limit, tightened now that a second hook rides the same matcher: the guards enforce mechanically exactly the rules that pass a chokepoint — the budget check, and the dispatch-shape rules (§The delegation model) — and protocol literacy beyond those stays unenforced. A session that dispatches without invoking the skill still carries only the resident pointer for everything else.

That role qualifier is load-bearing rather than hedging. Stated without it, the rationale is role-blind: a dispatched role fires no such trigger — it is not dispatching at all, it is executing a stage, and no stage skill or stage template loads this protocol — so for that reader the pointer posture leaves the rule unreached, which is how a posture nobody disputes produced three incidents. §Operative residency governs that case, and the bounded imperative it sanctions in a consumer’s always-loaded agent definition is this same doctrine applied at a different reader, not an exception carved out of this paragraph.

Operative residency

A rule may be restated as an imperative in a surface that does not own it when all three of these hold:

  • (a) Unreachable trigger. The actor the rule binds never fires a trigger that loads the rule’s owning doc. This is a property of the reader, settled by asking which triggers that reader actually fires.
  • (b) Imperative only. What is restated is the instruction. The reasoning, the failure analysis, and the mechanism stay with the owner.
  • (c) Adjacent citation. The restatement names the owning surface beside itself, so the owner stays the single place the rule is explained and changed.

Each condition has a reader at a transition rather than being self-evident: (a) and (b) are read by whoever authors a restatement, at authoring, and again by a reviewer or an authoring stage assessing an existing one; (c) is the pointer any later reader — and any content-tiering check — follows back to the owner. The reader the sanction exists for is the third and never inspects a condition at all: the bound actor, which receives the imperative at the tier it always loads, which is the whole point.

The anti-licence clause is part of the rule, not commentary. The sanction is keyed on (a): a rule that the bound actor’s own skill or template already loads fails (a) and stays a pointer. Without that clause this reads as “duplicate when it feels important”, which is the failure content-tiering exists to prevent and would cost more than the gap it closes.

A worked case the clause decided, recorded because it decided against residency. The provenance floor (§Resume journal) binds a session that has dispatched, and such a session loads templates/agent-execution.md through its own trigger — so condition (a) fails and the rule stays template-tier rather than joining an always-loaded manifest. A resident copy would be a second source paid for by every session that never dispatches at all. It is recorded here precisely because the rule reads as important enough to promote, and reading as important is the licence (a) withholds.

This is load-trigger residency’s own logic, not an exception to it. That doctrine earns a rule a place in an always-loaded file only when no stage, skill, or tool-call trigger exists to load it, and the decisive question is exists for whom. A trigger that exists but that the bound actor never fires is, for that actor, no trigger. The doctrine’s stated justification — a rule a stage or a tool call would load anyway costs nothing to defer and everything to keep resident — does not merely fail for such an actor, it inverts: nothing loads it anyway, so deferring costs everything.

Content-tiering is satisfied by bounding what is copied, which is what (b) is for. The restatement is still a restatement, which is why it needs this sanction rather than an argument; but its drift surface is one instruction rather than a slab, and the place the rule is explained and changed stays single.

The attested subjects are the template’s Background + notification, never poll rule and its Findings you will act on are durable before you act on them rule, both of which bind a dispatched session. Three such sessions read a descriptive pointer to them at the always-loaded tier and derived no constraint from it. The derivations were easy, which is what makes the failure instructive: under load a descriptive sentence reads as background about one’s situation rather than as a constraint on the next action. The defect this rule names is therefore pointer-versus-operative-statement, never absence.

Operative is not obeyed, and a fourth firing is the attestation. A dispatched session ended its turn on work still running while the waiting rule sat in that session’s own agent definition as a bare imperative, restated under (a)–(c) — the compliant form this sanction licenses. So the diagnosis above names a cause removed, not the defect closed: residency makes a rule reachable at the point of use, and reachable is not obeyed. Every restatement this sanction licenses is a request delivered where it is read. Weigh it as one — worth its small drift surface for the failure mode it does remove, and never a substitute for an oracle where one is buildable. In the attested instance what caught the consequence was the artifact-side oracle below, with the prose already in its strengthened form.

The propagate obligation has fired, and the record names what it cost. The waiting rule changed — the primitive named by its reactivity property, the awaited artifact given a home — and the template’s when either rule changes here, propagate header sent that change to both of its restatements in this consumer’s .claude/agents/ definitions: one a stage-session type, one a read-only sweep type. They did not carry the rule alike. One cited the bullet by name and restated its wait mechanics at length; the other carried a single turn-end line with no citation, no primitive and no home. The target is symmetric even where the starting text is not, so propagation into the second was a genuine addition sized to that file’s own voice rather than a paste of the first’s paragraph — and neither absorbed the rejected-alternative reasoning, which is (b)’s line and stays with this section’s owner. Recorded because the sanction’s cost is exactly this: every rule it licenses acquires N carriers that a later change must find, and a restatement that drifted unevenly is harder to spot than one that drifted at all.

The obligation fired again, and the second firing found a carrier the roster did not hold. The waiting rule changed once more — the shell-child branch named its artifact, and the liveness clause widened past a producer this session did not start — and propagation reached both .claude/agents/ definitions as before. It also reached a third carrier sitting outside this section’s roster entirely: guard-kit’s self-matching-pgrep rule states the same clause verbatim in the corrective help text it prints when it blocks, mirrored in prose in that kit’s own SPEC. Left alone, a guard-blocked session would read advice that undersells what it may now do with its own backgrounded child — a restatement whose reader arrives at the worst possible moment, already stopped and looking for the sanctioned form. Recorded because of what found it: the clause’s own wording, grepped across the tree, and not this section’s list of known carriers. The sanction’s cost is N carriers a later change must find, and N is discovered by grepping the rule’s phrasing rather than by consulting the roster — a roster is a record of the carriers someone noticed, never a proof of the set.

The rule’s own attested counter-examples are now explained, and the explanation indicted the wording rather than the primitive. Four of one session’s backgrounded waiters had died before their conditions went true with their producers verifiably alive, against an event-stream call that succeeded first try — a standing case that the mandated primitive was the wrong one on this machine. It was measured rather than argued (§bin/wait-probe): every trial on a completion-marker predicate outlived its condition, on both harness forms and on a detached shell child, at producer durations up to and including the longest swept — so no form carries a wall-clock ceiling in that range. Every early exit instead shared one property, and it was not the form: the loop tested the producer’s liveness, and it reproduced on the harness-uninvolved control in milliseconds, which exonerates the harness and the primitive together.

The cause is that the rule’s two clauses compose into an inverted loop. The protocol mandates wrapping until <cond>; do sleep N; done and looping on the recorded PID’s liveness. until ends when its condition becomes true and kill -0 is true while the producer is alive, so the literal composition of the two clauses exits at once, cleanly, with the producer running — which is exactly the reported signature, down to the clean exit status. The wait’s condition must be its continuation condition: until takes a done predicate and while takes a still-running one, and liveness is a still-running predicate. So the ordering the template states stands unchanged and what changed is the spelling every carrier shipped, propagated to all four. That the correction is free at the guard was probed rather than assumed: rules 13 and 15 walk do … done spans without reading the loop keyword, so the while form is exempt exactly as the until form is (guard-kit/SPEC.md §The generic ruleset).

Recorded here because of what the near-miss was. A rule whose attested counter-examples stay unexplained is a rule its readers discount, and the discounting was already under way — the counter-examples had been read as evidence against the primitive, which would have inverted a correct ordering and left the actual defect, one keyword, shipped in all four carriers. The fifth firing’s own lesson generalizes past this rule: an operative restatement propagates a rule’s wording, so a defect in the wording is propagated with it, and N carriers make N copies of the bug rather than N chances to catch it.

No gate is owed over the act, and not for budget — but one is now owed over its harm, and it exists. No check can read a session’s choice to end a turn: the act leaves no tracked artifact, and no PreToolUse chokepoint sees it. The harness’s own turn-end event does see it — measured, not assumed (§The turn-end liveness hook (template)) — and that hook refuses there as well as seeing, on an authorization since given and spent. So the relocation below is not the only enforcement, and it is not the enforcement for want of an alternative: it is that on the merits, because the harm it blocks reaches a chokepoint the turn-end event never sees. What the ruling never licensed was stopping there. The harm the turn-end causes does pass a chokepoint — a tracked-tree mutation while a recorded producer is still writing arrives as an ordinary tool call — and it is blocked there by guard-kit rule 14 (guard-kit/SPEC.md §The generic ruleset, whose design §The delegation model owns). So the enforcement question this section left open is answered by relocating the subject from the act to its consequence, and the honest bound travels with the answer: it reaches only a session that recorded, since the record’s write is itself structurally uncheckable.

The anti-restatement gates were checked against a sanctioned restatement rather than assumed inert, since a sanction that required weakening a gate would be the wrong sanction: check-shim-restatement scans binding shims under the skills directory against a corpus of the consumer’s always-loaded file plus kit templates, so a consumer’s agent definition is neither its scanned surface nor its corpus, and check-surface-duplication’s predicate is a glossary term’s definition on a canonical SPEC surface, which reaches no agent definition either. The sanction therefore ships with no gate exemption — and the honest converse ships with it: nothing in the battery would catch a non-compliant restatement, so (a)–(c) are review-enforced. Both buildable oracles detect a consequence rather than the act, and they sit at different distances from it: guard-kit rule 14 prevents the consequence at the tool call that would cause it, and check-producer-liveness detects a producer still running at the next stage’s entry (evidence-kit/SPEC.md §check-producer-liveness). The second detects late by construction — the turns between the firing and that entry are already spent, and a firing at the last stage of an iteration has no entry after it — which is what makes it the backstop behind the first rather than a substitute for it.

A case where an oracle was buildable now sits beside this one, and the two are not each other’s contradiction. The dispatch-shape rules bind acts that leave no tracked artifact either, yet they are gated. The axis that separates them is interception, not durability: a dispatch passes a chokepoint the harness fires a hook on, and a turn-end passes none the dispatch-shape rules could have used (§The delegation model, which owns the generalization, and §The turn-end liveness hook (template), which measured the harness’s own turn-end event and found it reachable). So “no tracked artifact” is why neither rule gets a gate over the tree, and it is not by itself a reason to stop looking for an oracle. Recorded here for the same reason §The delegation model records the durability rule beside the read-only-fan-out caveat: otherwise the next reader re-litigates one of the two paragraphs.

That last sentence was cashed rather than left standing. The search it licensed found one: the turn-end is unreachable from the tool-call axis, but the mutation it enables is a PreToolUse call, and rule 14 fires there. It then found a second candidate on a different axis — the harness’s own SubagentStop event, which the hook above measured and found live. It began as an oracle that observed rather than refused; turning it into the second kind took a separate authorization, that authorization was given, and it now refuses. Recorded because a sentence saying keep looking is cheap to write and easy to leave un-acted, and the next reader should find the outcome beside the licence rather than have to reconstruct whether anyone ever looked.

The turn-end liveness hook (template)

templates/subagent-stop-liveness.sh is an opt-in SubagentStop hook, inert until a consumer registers it, that refuses a dispatched session’s turn end while the launch records under the scratch dir say a producer is live. It shipped as a probe first, and the probe’s own result is what made it this: the question it was built to answer — does the harness already defer a subagent’s stop while a background child is live? — came back no (§The probe is asymmetric), so the axis §The delegation model’s waiting rule exists for holds at the turn-end too and a blocking hook was the only lever left.

The authorization is recorded here because it is spent. Turning the observer into a refuser was ruled by the operator on 2026-08-24, unconditionally, and the ruling named its own discharge event — spent when the enforcing hook ships — so it is retired from TRAJECTORY.md §The closed rulings and its content lives here. Two things it settled bind any later change. First, a capped variant was offered as an explicit option and refused: refuse only on a verdict=red live=yes reading carrying at least one record, stay advisory otherwise. So every narrowing below is argued on its own stated grounds, never inherited as a safety margin, and where a boundary here coincides with the refused option’s the coincidence is disclosed rather than left to be read back in through the resemblance. Second, what needed authorizing was the class — an observer turned refuser at a turn end. A scope probe established that this repo had the hook registered already, so the change was a script edit rather than a permission-surface write under TRAJECTORY.md’s 2026-08-22 wall; that is a true finding about cost and it is not what the authorization rests on, which is why it would have been given just the same had the registration been absent.

The event is SubagentStop, never Stop. A dispatched session is a subagent, so its turn end fires SubagentStop, and every attested firing of the waiting rule was a dispatched session. Stop is deliberately not registered: the main-session turn end has no attested firing, so registering it would widen the subject past what the evidence carries. Enforcement inherits that boundary unchanged — the main session’s turn end is still unreached.

The contract: exit 2 on red, corrupt or unresolved, exit 0 on every other path, and no hook JSON on either. The hook reads its payload from stdin, asks the liveness reader whether any launch record under ${GATE_SDK_TMP_DIR:-.tmp} names a live producer, appends one line to DELEGATION_KIT_STOP_LOG, and then either exits 0 or exits 2 with its refusal written to stderr.

The refusal needs no emitter, and that is a repair to this section rather than a new claim. This section used to argue that a hook which had to speak at SubagentStop would need a primitive guard-kit does not have — all three of guard-kit’s emitters hardcode hookEventName:"PreToolUse" — and concluded that the blocking variant’s emitter cost was stacked on its own authorization. The harness’s published hook contract, fetched rather than recalled, settles it the other way for the exit-code route: SubagentStop is listed as an event a hook can block, exit 2 is the blocking route, and the hook’s stderr is shown to Claude and is itself the blocking reason when the hook emits no JSON decision. So the refusal speaks through stderr, mints no guard-kit primitive, and leaves this template’s standing property intact — it does not source guard-kit’s lib, which is what keeps delegation-kit from acquiring a dependency on guard-kit being vendored. The two payload fields it reads still cost a cat and a jq -r.

There is no advisory tier at this event, so the choice was deliver or do not. At exit 0 a hook’s stderr goes to the debug log only and Claude never sees it. A “log it more loudly” alternative therefore does not exist to be weighed: exit 2 is the only delivery, which is why the refusal is not offered in a softer form.

Honest limit on both of those, inherited deliberately. That contract is the harness’s published one and nothing in this tree. It sits on the same footing §The delegation model already declares for the PreToolUse payload roster: a future harness revision reshaping the event is drift no gate here can self-detect, and only re-reading the contract catches it. Recorded so the next reader does not mistake a fetched fact for a measured one.

The predicate is the reader’s exit class, and it refuses on three of six arms. Reader exit 2 is read through two names, chosen by the hook’s own *.run count: corrupt over a non-empty record set, unresolved over an empty one. The count picks the name, never the decision — both refuse.

reading the hook holds decision
green no live producer log, exit 0 — there is nothing to refuse
red a live producer under a launch record log, exit 2 — the whole subject
corrupt reader exit 2 over a non-empty record set: a record that does not parse log, exit 2 — see below
unresolved reader exit 2 over an empty record set: a reader that could not run at all log, exit 2 once — allows when stop_hook_active is true; see below
unavailable no reading at all (no reader named or resolvable) log, exit 0
error a configured reader that ran and did not answer log, exit 0

corrupt refuses, and it diverges from guard-kit rule 14 on purpose. Rule 14 — tracked-tree mutation under a live producer — rules that a record which does not parse declines rather than blocks, because a guard is not the place a corruption verdict is taken and check-producer-liveness already exits 2 on one. That reasoning does not transfer, and the reason is structural rather than a matter of appetite. Rule 14 reads the records one at a time, so a malformed record declines for itself while a sibling record naming a live PID still blocks. This hook reads the whole set through one exit code, and the reader’s own resolution is exit 2 wins over red wins over green (evidence-kit/SPEC.md §check-producer-liveness). Allowing on corrupt would therefore mean that one malformed record anywhere under the scratch dir suppresses every refusal in the tree — a bypass rule 14 does not have and cannot have, minted by copying rule 14’s disposition into a reader that lost the per-record view. The divergence is recorded from both sides, here and at guard-kit/SPEC.md §The generic ruleset rule 14, so neither surface reads as the other’s drift.

The corrupt arm is cheap to be wrong about and expensive to skip. A corrupt record cannot be carried far: check-producer-liveness is a battery member, so the next commit reds on it. The false-refusal window is one malformed record’s lifetime; the bypass window, had the arm been dropped, is however long a session cares to leave one in place.

unresolved is the same refusal under a different diagnosis, and the ground is a proof from the reader’s own contract rather than a judgment about appetite. check-producer-liveness in set mode derives corruption per record and aggregates exit 2 wins over red wins over green (evidence-kit/SPEC.md §check-producer-liveness). Over an empty glob there is no per-record verdict to aggregate, so the gate cannot return corrupt on an empty set. A reading of exit 2 at records=0 is therefore provably not record corruption; it is the reader failing for a reason that has nothing to do with any record. The shape that produces it in practice is a worktree-isolated dispatch: a fresh git worktree add checkout carries no build output, so the binary a .gate member dispatches to is absent and the reader fails closed before it reads a record. The hook already computes records by its own glob over the same directory, so the discriminator costs no new field, no new knob and no second reader. The arm stays reachable once a consumer’s reader takes the worktree-resolvability requirement below, and the row is not retired: a reader whose own binary is genuinely missing in a main checkout still lands here, and so does every consumer whose adapter has not taken that requirement. What it removes is the isolated dispatch as this arm’s routine producer, never the arm.

The count labels the diagnosis and decides nothing, and that is a ruling rather than a first draft. The split was first drafted with unresolved allowing, on the sound ground just given that the case cannot be record corruption. Checked against the shipped hook, that would have flipped a records=0 reading from refuse to allow — a real, mechanical edge of the “unconditionally” the authorization above records. Ruled 2026-08-24 by the operator: keep the split for its diagnostic value and map unresolved to decision=refuse, so no edge of the secured refusal set narrows. That is not a reversal of the 2026-08-24 authorization; it is that authorization holding intact against a delta that would have narrowed it. The technical ground was not found wrong — the unconditional refusal was simply worth more than the one provably-empty reading it costs. The finding is recorded here and not only the disposition, because a later reader who re-derives the cannot be corruption proof will reach the drafted conclusion again and needs to find the ruling rather than the argument alone.

The cost that ruling keeps is stated rather than left to be discovered — and it was measured, then bounded, without narrowing the ruling. A worktree-isolated dispatch is still refused at turn end on a binary-absent reading, because unresolved refuses. Priced as one wasted exit when ruled, it was measured on 2026-08-25 as a loop: a refused turn end is retried, the condition — a reader that cannot run — is invariant under anything the child does, so one read-only audit spent 142 tool uses and ~179k tokens refusing and its report was lost with them. Hotfixed 2026-08-25 on operator ruling: unresolved refuses once. The hook reads the payload’s stop_hook_active — true when the harness is already continuing because of a stop hook — and on that firing the unresolved arm allows, logging verdict=unresolved decision=allow, which is how a triage reader tells a bounded refusal from a first one without a new field. red and corrupt read nothing from the field and stay unconditional: their conditions resolve (the producer ends; the record is deleted), so the loop they hold is the rule’s own bound, and the field is read for the one arm whose condition never resolves. No edge of the 2026-08-24 refusal set narrows — the first refusal on every arm stands — and the refused alternative, allowing unresolved inside a linked worktree, is refused for exactly that reason. The root fix, a reader resolvable inside a worktree, is worktree-isolated-dispatch-cannot-reach-the-main-checkout’s and dissolves the arm’s firing rather than bounding it. Measured live the same day, after the fix: a worktree-isolated read-only dispatch was refused once and allowed on its next stop — two tool uses, ~18k child tokens, report delivered — against the 142 / ~179k / report-lost reading the hotfix answered.

records=0 is not a clause, and the resemblance to the refused option is disclosed at both places it arises. The refused capped variant read “verdict=red live=yes carrying at least one record”. The record count is not a condition here and is not imported as one: check-producer-liveness cannot return red over an empty set, so red already implies at least one record by the reader’s own contract. That half of the refused option was vacuous, and nothing here adopts or needs it. The corrupt/unresolved split is the second coincidence, and it is disclosed on the same terms: there the count is not vacuous — exit 2 genuinely occurs at zero records — but it still decides no refusal, only which of two refusing names the reading gets. Record count decides no refusal anywhere in this hook, on either arm.

unavailable and error allow on the degradation posture, not on leniency. Both mean the hook obtained no reading — the knob names no reader, or a configured reader failed. Refusing there would refuse every turn end in a tree that has not configured a reader, in a kit that ships this hook opt-in and inert. This is where unresolved parts from them and the boundary is the reader’s own contract, not a preference: unavailable is a reader that never ran and error is one whose answer this hook does not map, while unresolved is a configured, readable reader that ran and returned its own fail-closed verdict. Taking that verdict is not refusing on an absent reading; second-guessing it would be the fail-open the reader’s exit 2 exists against. It is guard-kit/SPEC.md §The guard framework’s fail-open-but-loud posture for a deny-guard whose rule turns on an external reader, the same posture §The delegation model’s dispatch guard already takes, and the verdict=error value in the grammar below is what supplies the “loud”.

jq-absence does not disable enforcement, and this is the one place this hook is strictly better off than its PreToolUse siblings. The decision reads the liveness reader over the run directory and reads no payload field at all; the payload feeds only the log’s event, session and keys columns. So an absent jq degrades the log line and leaves the refusal exact, and the advisory-envelope problem the dispatch guard had to solve by hand does not arise here.

The bounded call stays, and its meaning inverts. The reader is invoked under timeout where one is available. While this hook only logged, the bound was there because a reader that hung would have refused the turn end by accident — the blocking variant arrived at sideways. Now that the hook is the blocking variant, the bound is what keeps a hung reader from being read as a live producer: a timeout is an unmapped exit code, so it is error and it allows, and a refusal is only ever the reader’s own verdict.

The liveness reading reuses check-producer-liveness and copies no grammar. §The delegation model rules that the reading affordance which would genuinely earn its place already exists and is that gate; a hook re-implementing the pid=<n> run=<key> parse and the PID predicate would be a third copy of a grammar evidence-kit owns. So the hook invokes the reader in set mode over the scratch dir — which already quantifies the per-record verdict and already resolves exit 2 wins over red wins over green (evidence-kit/SPEC.md §check-producer-liveness) — and maps its exit class onto verdict, live and now the exit code. Nothing about how a PID’s liveness is decided is seconded here.

The prerequisite is stated, not assumed. A consumer that wires this hook with no reader resolvable at DELEGATION_KIT_LIVENESS_CMD — evidence-kit unvendored, or the knob left unset — gets verdict=unavailable decision=allow on every line: a hook that answers nothing and refuses nothing. That is honest degradation and it is preferable to a silent third parse that would work everywhere and drift from its owner.

The template ships no default reader, and enforcement makes that ruling stronger rather than negotiable. It defaulted to evidence-kit/checks/check-producer-liveness.sh while that gate was shell-declared. shell-gate-tail-port made the gate a descriptor dispatched to the binary, so that path exists in no tree — the readability test the reader sits behind fails everywhere, and the hook logged unavailable on every firing in a tree whose battery was green over it. A default naming a path nothing resolves is a fake default: it reads as a shipped capability and is none, which is worse than declaring the prerequisite. So the knob has no default and the reader is the consumer’s to name; a kit-side default would now decide whether a tree refuses. The alternative was to teach this knob to resolve a gate name, and it is refused on a recorded precedent rather than on taste: evidence-kit/SPEC.md §check-evidence-manifest met the identical break one caller over, when the same port turned a pre-flight entry’s named path into a descriptor, and discharged it with a consumer-side front end resolving the name — “deliberately not teaching lifecycle-kit to resolve a name in that knob, which would be a kit-contract change”. The same reasoning binds here, and it is the reason this repair changes no contract: the value is still a path this hook runs with the scratch dir as its only argument. This repo’s own reader is scripts/producer-liveness-reader.sh, which reaches the gate by name through the front end its whole pre-flight roster already uses.

A consumer’s reader must resolve from a worktree-isolated dispatch, and this is a requirement on the adapter rather than advice to it. The consequence of one that does not is severe and indirect, so it is named here: a fresh git worktree add checkout carries no build output, a reader that dispatches to a compiled binary therefore fails closed inside it, the hook reads unresolved over an empty record set and refuses — and because the harness returns only a dispatched session’s last assistant message, that refusal displaces the child’s report on the only channel the dispatcher reads. The child did the work; the parent receives a liveness complaint, and the failure is silent in the worse direction, since the returned text reads as a liveness complaint rather than as a dropped report. The bind can be structural rather than a rate: where a consumer’s dispatch rules require isolation for read-only agents, isolation is also what arms this, and no dispatch shape avoids it while staying in contract.

The kit states the bar and cannot clear it, which is why this is stated at all. Only a consumer knows its front end — the reason the knob has no default — so the requirement takes the same shape as the reader’s exit-code contract above: something a consumer’s adapter must satisfy. This repo’s own reader satisfies it by resolving the configured binary against the main checkout when the configured path does not exist and the cwd is a linked worktree, deriving the main checkout vendor-neutrally from git rev-parse --git-common-dir. That narrow resolution is safe where a general one would not be: check-producer-liveness reads *.run records and nothing else, so its verdict does not depend on the binary matching the worktree’s source and a main-checkout binary answers the same question a locally-built one would. That is emphatically not true of gates in general — check-gate-binary-fresh exists precisely to compare a binary against the source in its own tree — which is why this belongs in one consumer front end for one gate and not in how the binary knob resolves.

It resolves an artifact; it does not build one. The recorded refusal this might look like reversing is untouched: an isolated agent still may not build the crate, and there is still no hook that can reach a worktree which does not exist yet at dispatch time. This resolves a binary that already exists in a tree that already exists, at the moment the reader runs.

The cost is stated beside the requirement, because it is what makes the bar worth stating rather than leaving to be discovered. The ordinary case is one wasted round-trip paid while the dispatcher waits. The worst attested tail put no ceiling on it: an isolated dispatch wedged in a stop-hook loop across three resumptions, spent a full context budget and returned nothing at all, recovered only by re-dispatching fresh.

One line per firing, appended, space-delimited key=value after a leading timestamp:

<UTC ISO-8601>  event=<hook_event_name|->  session=<session_id|->  live=<yes|no>  verdict=<green|red|corrupt|unresolved|error|unavailable>  records=<n>  decision=<refuse|allow>  keys=<comma-separated top-level payload keys>

Every field has a reader at a named transition, and no field is carried that this list does not name one for:

  • timestamp — read by the close-stage reader to order firings and to tell a fresh line from a stale one.
  • event — the payload’s own hook_event_name. Read at the first firing, to confirm the event fires at all for a dispatched session and that it is spelled SubagentStop. The entry’s whole correction turned on the event’s identity, which is what earns it a field.
  • session — the payload’s session_id, - otherwise. Read to separate the firings of one top-level session from another’s in a shared log. It is not the subagent’s own id, and the first firings settled that against the field’s original reader: a dispatched agent and its dispatcher log the same session_id, and neither matches the identifier a stage stamp carries, so attributing a firing to a stage session is not a reading this field supports. Measured, and re-corroborated first-hand rather than carried forward: a worktree-isolated child’s own log and its dispatcher’s log carried the identical session= token, read out of two different files in two different checkouts. Re-derive by reading both logs, not by citing this. What would attribute a firing is not established, and the claim this bullet used to make is withdrawn. It named the payload’s agent_id as the discriminator that would, with a grammar delta as the only thing in the way. That is a forward promise the evidence does not carry. Observed, not measured: one read of five firings in a single session saw five distinct top-level agent_id values, none of them matching the stable identifier the same payloads’ background_tasks array reported for the one live dispatched agent. If that holds, the field is per-firing rather than per-agent, and would attribute nothing. The observation is carried at that tier — a single unreplicated read — and neither promoted nor dismissed. Why the doubt cannot be settled from this tree, which is a consequence of a ruling rather than of effort: the tracked record is .workflow/subagent-stop-liveness.log, and by the no-values ruling (§What background_tasks carries) it records the payload’s top-level key set and no values — so every line of it carries agent_id as a key and not one as a value. Settling the question means reading raw payloads, which that ruling holds operator-class. So nothing here says attribution is one grammar delta away: what is established is that session_id does not attribute, and what is open is whether anything in the payload does. A consumer proposing to log agent_id for attribution is proposing to settle that question, not to consume a settled answer.
  • liveyes exactly when the reader reported a live producer. It stays two-valued and stays paired with verdict; the honest limit below is what bounds how it may be read.
  • verdict, records — the reader’s exit class and the number of *.run records under the scratch dir. Read together: records=0 makes a live=no uninformative, while records=2 live=no says records existed and their producers had exited. verdict=corrupt carries live=no because the field is two-valued — the pair is the reading, never live alone. The pair is also what names the exit-2 arm: the same reader exit reads corrupt at records>0 and unresolved at records=0, so a log line’s records column is the only place a later reader can see which of the two it was, and the two want different fixes.
  • verdict=error — a configured reader that ran and did not answer: an unmapped exit code, or the timeout bound firing. Its named reader is the close-stage triage below, at the same transition, distinguishing this tree never configured enforcement (unavailable) from this tree’s enforcement is broken (error). Before enforcement both meant “no reading” and the distinction cost more than it bought.
  • verdict=unresolved — the third state that triage tells apart, and the second actionable one: a configured, readable reader that ran, exited 2, and held no record to be about. It is what earns a third name rather than a fold onto unavailable, because the fix is a different one — the reader could not run at all, where error is a reader that ran and answered off-contract and unavailable is a tree that never wired one. Its named reader is the same close-stage triage at the same transition. Unlike the other two it also refuses, so the triage reads it for a second question the others never raise: whether a refusal was diagnostic rather than about a real producer.
  • decisionrefuse exactly when the hook exits 2, allow otherwise, on every firing including the allowing ones. Its named reader is the close-stage triage at the close-surface drain, where the log is read and cleared, and it is read for a question no other field answers: whether a firing was acted on. verdict alone cannot answer it across the landing commit, because the same verdict=red line means observed before enforcement and refused after it, in one append-only file that spans both. decision is also what makes a refusal countable, so the forcing function’s own effectiveness is measurable — which is the defect §The probe is asymmetric spent a whole iteration recording about live=no. It cannot be derived from live: a corrupt refusal carries live=no decision=refuse. The honest limit on that countability, and it is a break in this contract rather than a caveat on it: the named reader cannot see a worktree-isolated agent’s firings at all. DELEGATION_KIT_STOP_LOG defaults under ${GATE_SDK_WORKFLOW_DIR:-.workflow}, resolved against the writing session’s cwd. An isolated agent’s cwd is its own worktree, so its lines land in that worktree’s .workflow/ and are destroyed with the worktree at reclamation — and that is precisely the class unresolved names, so the field’s claim that a refusal is countable is true of main-checkout firings only. Not repaired here, and the reason is a seam rather than appetite: pointing the knob at an absolute main-checkout path would bake a machine path into a consumer surface, and the vendor-neutral derivation (git rev-parse --git-common-dir) now exists in this repo’s reader without reaching this writer — a read-side bridge for one gate is not a write-side one. It is filed rather than flagged and skipped. What the reader’s resolution changes is which class goes unseen. Where a consumer’s adapter resolves inside a worktree, an isolated child’s firings read verdict=green records=0 decision=allow rather than unresolved … refuse, so the triage still cannot see them and what it cannot see is an allow. That is a smaller loss and not a closed one. The log line’s grammar, field list and order are untouched, so the space-delimited parse the triage uses does not move.
  • keys — the payload’s top-level key set, nothing more. Read once, at the first firing, to settle what a SubagentStop payload carries without asserting anything about it in advance. It returned against the advance claim, which is the whole reason the field logs the set rather than assuming it: the payload carries a top-level background_tasks, so the pre-wiring premise that it names no background task was wrong, and only the key read caught it. Values stay unlogged on the ground that survives that correction — logging them would put transcript paths and prompt ids into a file for no reader. What background_tasks carries was then settled off this field, by a one-off read that widened nothing here: §What background_tasks carries.

decision sits before keys deliberately: keys is the one free-ish field and stays last, so the space-delimited parse never has to step over it.

No field is carried that this list does not name a reader for, and the two the authorization might have invited and that are not carried are recorded so the omission does not read as an oversight: no session-attribution field (no payload value is established to attribute — session_id is established not to, and agent_id’s ability to is in doubt and unsettleable here; see session above and the shared-scratch-dir paragraph below) and no refusal counter (the bound on repeated refusals is the producer’s own life, not a count). The session-attribution omission rests on those two facts and not on a negative over the whole payload: asserting that nothing in it attributes would be a claim nothing here has checked, and one that would be quietly falsified — on a surface nobody re-reads — if agent_id turned out to be per-agent after all. The omission’s conclusion is unchanged either way, because the two reasons for not wanting such a field are the ones just given rather than the payload’s contents.

records is counted by the hook’s own *.run glob, not parsed back out of the reader, and the reason is the reader’s output contract rather than convenience: check-producer-liveness publishes a count only on its green line, printing one finding per blocking record on red and no total. Same directory, same glob, same number — and the field stays meaningful when the reader is unavailable, which is the case the prerequisite above makes reachable. For the same reason the reader’s first output line is not carried verbatim: over verdict, records and live it adds only the blocking record paths, and a free-text field would break the space-delimited parse the grammar above is for.

The glob is taken after the reader has run, and the order is load-bearing now that the count names an arm. A record created between the two would, in the other order, produce records=0 beside a reading that legitimately saw that record — the one window in which the discriminator could name a genuine corruption unresolved. Globbing after does not close the window (nothing but a lock could) but it inverts which way an in-flight record errs: a record appearing during the reader’s run is counted, so the reading is named corrupt and the message points at a record that exists. The residual window is one malformed record’s lifetime against a battery member that reds on the next commit, which is the bound the corrupt arm is already priced at above.

The refusal message names the finding and the lawful exits, as guard-kit requires of every block message, and its branch is three-way — one arm per refusing verdict, never two. On red it states that a launch record under the scratch dir names a live producer; on corrupt, that a record does not parse so no reading says whether one is live; on unresolved, that the reader produced no reading at all and held no record to have been about. Each arm names the turn may not end on it, and each carries its own way forward. red and corrupt share the two rule 14 already names — wait for the producer on its own artifact, in a loop that ends when the condition goes true, or delete the record once the producer has exited — and unresolved takes neither, because there is no producer to wait for and no record to delete: what it names instead is the reader as the thing to fix, plus the isolated-dispatch limit (templates/agent-execution.md) and the lawful response to it, reporting the gate unavailable and returning. Every arm names the reader command with the run directory: on red to see the record set, on corrupt to find which record is malformed, on unresolved to read the reader’s own reason for failing. It carries no session identity, because the hook has none to carry.

Folding unresolved onto the corrupt arm is the specific mistake this branch exists against. A two-way branch would print “a launch record does not parse” over a case that holds no record to parse — a false diagnosis pointing at a remedy (find the malformed record) that cannot be carried out. That is the same message-axis conflation scripts/gate-exec.sh was repaired for one caller down (gate-sdk/SPEC.md §lib/gate.sh), and a refusal whose message names no reachable remedy is exactly what invites a read-only agent to invent a mutating one.

There is no knob, and unwiring the hook is the valve. §The delegation model rules exactly this for the dispatch guard’s D1 unconditional block: “the valve is unwiring the hook, never a knob: a per-dispatch override is exactly the honour system these rules exist to end, and a knob would restore it under a better name.” The same reasoning binds here and for the same reason, so this hook adds no knob.

The ordinary escape is not the operator’s, and that matters under the permission wall. Unwiring means editing the consumer’s settings, which is operator-class work no stage session may do (TRAJECTORY.md §The closed rulings, 2026-08-22). If unwiring were the only escape, this hook would be a mechanism whose every recovery path needed the operator. It is not: the record set is the session’s own artifact, check-producer-liveness names the blocking record, and deleting a record whose producer has exited is — in rule 14’s own words — “not a workaround, it is the statement of fact becoming false and being retracted”. The operator-class valve is the last resort, not the first.

Loop protection reads the producer on the arms whose condition resolves, and the contracted stop_hook_active on the one whose condition does not. An earlier revision of this paragraph refused the field as an uncontracted harness artifact; probed 2026-08-25 against the published hooks reference, that premise was false — the field is listed in the SubagentStop input schema, defined as true when the harness is already continuing because of a stop hook, with the reference’s own guidance to check it “to avoid blocking on a condition that will never resolve”, and an 8-consecutive-block harness cap behind it. The correction is recorded rather than silently made because the refusal’s other ground still holds and still decides the shape: for red and corrupt the refusal’s trigger is a real-world condition that ends when the producer ends or the record is retracted, so the loop is bounded by the thing the rule is about and the field is not read. For unresolved no such bound exists — see §The cost that ruling keeps — so the field is read there and nowhere else. The harness cap did not bound the measured loop, presumably because a child’s tool calls between refusals break the consecutive count; recorded as observed, not explained, so a reader does not mistake the cap for a guard this tree relies on.

The shared scratch dir is the subject, stated rather than discovered. The reading is over ${GATE_SDK_TMP_DIR:-.tmp}, which concurrent sessions in one checkout share, so a record written by one session can refuse another’s turn end. That is the mechanism’s subject and not a defect filed against it. The project already binds every session to that shared set: guard-kit rule 14 blocks index-writing git in any session while any *.run record under a scratch dir names a live PID — it is not narrowed to the record’s writer and never was. Extending the same binding from “you may not commit under a live producer” to “you may not end your turn under one” widens the act set, not the subject. A design that narrowed the turn-end rule to the writer would leave rule 14 and this hook disagreeing about whose producers bind whom, on one record set, with no surface owning the difference.

Attribution was weighed and is not available. The payload’s session_id is shared by a dispatched agent and its dispatcher, and the pid=<n> run=<key> grammar carries no writer identity. Adding one is a grammar change across evidence-kit and guard-kit for a narrowing the paragraph above argues against wanting.

The residue is unchanged and is not closed here. Two firings this hook cannot reach: an unrecorded launch (guard-kit rule 15’s advisory residue), and the harness’s own background_tasks view, which §What background_tasks carries established enumerates what was launched rather than what is running and so cannot substitute for the record set.

A refusal fires at intermediate steps too, and that cost is accepted rather than narrowed. SubagentStop is not the session-end event: it fired seventeen times inside one dispatched session that had ended no turn at all, spaced by assistant steps (§The probe is asymmetric). There is no contracted discriminator for a real turn end in the payload, and both uncontracted candidates are refused on the precedent the loop-protection paragraph cites. What is left is a judgment about the cost and it comes out in favour: at an intermediate step the refusal reaches the agent with the obligation it is about to breach before it breaches it, which is earlier and cheaper than at the end, and it is the only channel that reaches the agent at all. The frequency is bounded by the producer’s own life, and a session that is correctly waiting in-turn is making few assistant steps by construction.

What exit 2 does at a firing that is not a stop was observed, not assumed. The harness contract says exit 2 “prevents the subagent from stopping”; it does not say what that means at a firing where the subagent was not stopping, and both readings — the reason is injected and the session continues, or nothing happens and the line is still logged — were survivable, so the design branches on neither. The build bought the firing rather than reasoning about it: a dispatched session backgrounded a bounded producer, wrote its <key>.run record, and read its own log against its own transcript. The second reading holds. Two firings took live=yes verdict=red records=1 decision=refuse and exited 2, and nothing was delivered: no blocking reason reached the session, no tool call was interrupted or re-run, and the session continued normally. Both lines were still logged. When the producer exited the next firing read verdict=green decision=allow, so the refusal window closed with the producer’s life — the bound the loop-protection paragraph above relies on, observed rather than assumed.

One half of the cost argument above is measured down by that, and is corrected rather than restated. The claim that a refusal at an intermediate step reaches the agent with the obligation before it breaches it does not hold in this harness revision: at an intermediate firing the reach is nil. The judgment still comes out the same way, because the cost it was weighed against is nil for the same reason — an intermediate firing costs the session nothing at all, and what survives is a logged decision=refuse line that the close-stage triage can count. The honest limit is that this measured the non-stop firing only. Delivery at a real turn end under a red reading rests on the published contract and on nothing measured here, and it sits under the same limit as the rest of that contract: only re-reading it, or buying that firing, catches a revision.

The log is capture-tier — gitignored, advisory, drained by a named reclaim path (gate-sdk/SPEC.md §The workflow directory), which is what keeps check-workflow-tiering green on a member that is neither tracked nor ignored, and it declares itself on the close-surface roster (lifecycle-kit/SPEC.md §The close-surface roster) naming its own clear as the reclaim path:

close-surface: .workflow/subagent-stop-liveness.log advisory reclaim=: > .workflow/subagent-stop-liveness.log

advisory rather than forced, on the reasoning guard-kit’s friction log takes: nothing refuses a close that skips it, and a visible skip is the honest mode for a log whose enforcement lives in the exit code rather than in the file.

No gate observes the wiring, and that is recorded rather than assumed. check-settings-pins asserts only the pinned paths in the consumer’s pins file (none under hooks), check-settings-paths’s subject is permissions.allow[] alone and never hooks[].hooks[].command, and check-memory-off scans the memory surface. So the registration reds nothing — and neither would a registration naming a script that does not exist. A session decides this wiring by reading this section, never by predicting a verdict. Under enforcement the consequence sharpens: an unwired hook is a tree where the turn-end rule has no enforcement at all, and nothing in the battery says so.

One half of that is now observed, and the half that is not is the same half. The registration is still unwatched, for the reasons above. What is watched is the configured reader, and both sides of the copy seam are covered because enforcement lands exactly on that seam. This kit’s own gate-tests/subagent-stop-liveness.test.sh drives a stub reader per exit class — which is what lets it hold every verdict arm hermetically, and a stub is by construction not the configured one — and asserts the exit code per arm (2 on red, corrupt and unresolved, 0 on green, unavailable and error), the decision column that goes with it, and a non-empty stderr on each refusing arm, because that stderr is the blocking reason. Because corrupt and unresolved are one reader exit read through two record counts, the stub lane drives that exit twice — once over a run dir holding a record and once over an empty one — and asserts the message wording apart as well as the verdict, so a two-way branch regrown behind a three-way spec reds. The consumer’s own scripts/gate-tests/subagent-stop-reader.test.sh fires this repo’s hook copy against its own configured reader over a scratch run dir it constructs, asserting green and an allowed exit on an empty dir, red, decision=refuse and exit 2 with a reason on a record naming a PID that is always alive, and unresolved decision=refuse when the reader is real and resolvable but the binary it dispatches to is absent over an empty run dir — the isolated-dispatch shape, and the one arm that must not come back unavailable, which would misreport a wired reader as one that was never configured. unavailable fails that lane by name. The gap between those two lanes is exactly where the dead default lived for a whole iteration under a green battery, so both lanes move together or the seam re-opens.

What background_tasks carries

The keys field settled that the payload has this top-level key and left what is in it open. A deliberate one-off read then took it, out of band and without touching this template: the consumer’s hook copy dumped each firing’s raw payload to gitignored scratch across five firings and was restored byte-for-byte, so the grammar above still logs keys and never values.

The ruling the read was bought to inform: the probe keeps logging key names only. No field is added and the grammar takes no delta. Ruled 2026-08-23 by the iteration lead under an explicit operator delegation — the question was relayed to the operator as operator-class and the operator returned it to the lead (“your call”), so the authorization is delegated rather than exercised, and the ruling is neither the operator’s own nor one the lead took unilaterally. The channel is cited here because a later reader cannot otherwise tell those three apart. What decided it is the read’s own result, and the direction is the unusual part: the read STRENGTHENED the ruling rather than merely failing to overturn it. The only named reader a value log would ever have had was the harness-view substitution below, and the read falsified that reader outright. A ruling with nothing on the other side of it has stopped being a restriction. A derived non-value — a count, a type tally — was weighed and refused on the same ground: it serves no reader either, and a grammar delta plus a template and a consumer edit are not spent on a field nothing reads. Should a reader appear, it reopens as a new question and not as this one.

It is a live-children enumeration, and it is populated. An array of objects in two shapes, both carrying id, type, status and description: a type of subagent adds agent_type, a type of shell adds command. It spans the emitting agent’s own tree rather than its direct children only — the emitting session, its backgrounded shell task and its dispatched grandchild all appeared with status running, the count rising as the grandchild started. Alongside it the payload carries transcript_path, cwd, prompt_id, permission_mode, agent_id, agent_type, stop_hook_active, agent_transcript_path, last_assistant_message and session_crons, beside the three fields the grammar above reads. agent_id appears in that list as a key and nothing more. The same five firings that enumerated these keys carried five distinct top-level agent_id values, none matching the stable id this array reported for the one live dispatched agent — so listing it here asserts nothing about its being able to tell one agent from another. §The turn-end liveness hook (template)’s session bullet owns that doubt and the reason this tree cannot settle it.

It enumerates what the harness launched, not what is running — and that is the decisive finding. A producer detached from a foreground tool call, live throughout and carrying a *.run record the reader was concurrently reporting red, appeared in no firing. That is precisely the residue §The probe is asymmetric names as one of live=no’s three readings and guard-kit rule 15 advises against without closing. So the blocking hook cannot substitute the harness’s view for the *.run record set: the two disagree exactly on the class the waiting rule exists for. Supplementing is the most it could do — and now that the hook refuses on the record set alone (§The turn-end liveness hook (template)), this is a constraint on a shipped mechanism rather than on a hypothetical one.

No entry carries a pid. Those six field names are the whole schema, and a shell entry’s id is the harness’s own opaque task id. Joining this view to a record’s pid=<n> therefore means parsing the command string — the value the ruling withholds — even over the children the view does see.

The privacy ground came back confirmed and wider, not weakened. The value is not benign: command is a verbatim shell command line and description is free-text prose. Logging it would put command lines and task prose into a capture-tier file, which is more exposure than the transcript paths and prompt ids the ruling was originally written against, and it would do so in whatever tree vendors this kit.

The probe is asymmetric, and no reading may treat it otherwise

A live=yes line proves the harness does not defer the stop. One firing with a live producer settles the unknown in that direction and the class stands.

No accumulation of live=no lines proves that it does. live=no is equally consistent with the harness deferred the stop, the session waited correctly, and the session recorded nothing — the last being the residue guard-kit rule 15 advises against and does not close. Passive accumulation therefore cannot return the finding that would dissolve the class, and anyone waiting for it waits forever and then reports the wrong thing.

So the reading is bought by a deliberate firing, never by a wait. A dispatched session backgrounds a producer that runs long, writes its <key>.run record, and then ends its turn. Both outcomes are results: the hook fires immediately with live=yes, so the harness does not defer and §Operative residency’s axis holds at the turn-end too; or the hook does not fire until the child exits, so the harness does defer, the class dissolves, and this template’s remaining value is the evidence for retiring it. The log is what makes that firing, and every later accidental one, legible.

The firing was bought, and it returned the first branch. A dispatched agent backgrounded a producer with a recorded PID and ended its turn without waiting; the log took live=yes verdict=red records=1 four seconds later, and again at that agent’s own stop, while the producer went on writing for another two and a half minutes. The harness does not defer the stop, so the class the waiting rule exists for does not dissolve, the relocation to guard-kit rule 14 stands, and a blocking hook was the only lever left. That was a second authorization this result did not grant; it was asked for and given, and the hook blocks (§The turn-end liveness hook (template)). This subsection is kept as the evidence the authorization was argued from, not as a standing limit on it.

The same firings settled a second thing nothing could have settled before wiring: SubagentStop is not the session-end event. It fired seventeen times inside one dispatched session that had ended no turn at all, spaced by assistant steps rather than by the clock — a stretch spanning three long tool calls and no assistant step produced no firing at all. The blocking variant therefore fires at every intermediate step of a session rather than once at its return, which is what a bounded run of blocks is actually spent on. Recorded here because it was a cost the authorization was weighed against, and because it is exactly the class of fact a probe exists to buy. The cost came in smaller than the estimate, and the measurement is at §The turn-end liveness hook (template): an intermediate firing that exits 2 delivers nothing to the session, so the per-step frequency costs the session nothing and buys a countable decision=refuse line.

Resume journal — agent writes, scratch reset sweeps

The contract, in four clauses. A mutating agent journals to a repo-local gitignored scratch directory in the main checkout; the supervisor grants that path absolute in the dispatch prompt; each finding lands inline as it is confirmed; a DONE marker is appended on success. The operative text — what each clause requires of an agent, and why a temporary worktree and a system temp dir both fail the survivability test — is templates/agent-execution.md’s Resume journal — agent writes, scratch reset sweeps bullet, which is the surface an agent loads. This section owns what that bullet cannot carry: the lifetime rule’s reasoning, the attestation it was priced against, and the two readings the marker carries.

The grant clause stands and its status changed: it is now a restatement, not a source. Where the consumer runs a stage machine, the journal’s path is a derivation from the stage that the machine owns and can compute (lifecycle-kit/SPEC.md §The state machine); the supervisor still spells it out absolutely in the prompt, because an agent cannot read a knob it has no reason to look for. What that buys is that the grant becomes checkable against the derivation instead of being the only record of what was granted — the gap that left this whole channel unoracled. This kit owns the journal contract and states no path convention; that kit owns the path and states no contract.

Lifetime — why retention rather than an eager deletion chore. Cleanup is the consumer’s own scratch reset at its next work-unit boundary, a mechanism the consumer already owns (here, enter-stage.sh’s boundary wipe of .tmp/ at the next scope entry — lifecycle-kit/SPEC.md §bin/enter-stage.sh). Two things break when the supervisor deletes on its own schedule instead, and both appear only once a dispatched agent is resumable rather than one-shot: a resumed agent is still writing to the file, and a supervisor treating the journal as its pull channel (lifecycle-kit/templates/lead.md §Channel design) destroys at the first verified commit the channel it is supposed to keep reading. A one-shot sweep hides both, which is why an eager deletion chore read as correct for as long as it did.

Attested. A supervisor deleted a resumable session’s journal after validating that session’s first commits; the session resumed minutes later, found the file gone mid-write, rebuilt it from context, and reported the disappearance as a suspected scratch-directory reliability defect. The lost working state was the cheaper half — the false defect signal, raised against the scratch dir and against an unrelated sibling unit, is what retention is priced against. The same supervisor then retained two later sessions’ journals against the then-governed text; that deviation is what this section now states.

Reading DONE under retention. Retention costs the presence of a journal its meaning: every journal now survives its session, so finding one signals nothing by itself. The marker still carries the completion claim, with one sharpening a resumable session forces — DONE is meaningful only as the file’s last line, since a session resumed past its own marker appends after it, and a mid-file DONE claims a completion the later content contradicts. Whether a missing marker signals interruption still turns on whether the supervisor consumed the agent’s return. On the ordinary completion path it did, and that return plus its post-commit verification (§Verify after every agent commit) is the recovery contract, so the marker is redundant there. Only in a cold read — a journal found with no return ever consumed, the agent’s session having died before returning (a background sandbox died, a crash, a timeout) — is the marker the sole signal, and there the original reading holds: no DONE = interrupted, resume from it. A live journal is told from a spent one by the consumer’s own work cursor and by git log, which together say more than presence ever did, and the boundary sweep bounds the ambiguity to the current work unit. Per-session journal naming discriminates only where the path is the dispatcher’s to choose — under a stage machine the journal is named for the stage, so several sessions of one stage share the file and append to it. Nothing is lost at the granularity a reader actually asks about: the marker rule already says DONE counts only as the last line, so a shared file answers did the session that wrote last finish, which is the question, and an earlier session’s mid-file marker is not a claim the later content contradicts. The inline-findings rule above is that cold arm’s insurance — with no surviving return, a pointer-only journal would make a would-be DONE lie about recoverability.

Caveat — a journal write has been observed to fail, and the cause is unexplained. A background agent was once unable to Write to the granted path, silently falling back to returning findings in the final message — which defeats the journal exactly when it matters (a long, interruptible run). The template’s read-only-fan-out carve-out and its grant-the-path-explicitly clause both follow from this observation.

Narrowed from a property of isolation to a single unexplained observation, because the isolation reading was probed and did not hold. A worktree-isolated agent dispatched with an absolute path in the main checkout wrote it successfully, from inside its own worktree — so isolation does not block the write, and the caveat’s own prescribed remedy works under the strongest isolation this tree dispatches. That matters rather than being trivia: it is what makes an entry-time assertion on a journal’s existence fair, since a missing journal is a session that did not write rather than one that could not. The caveat is narrowed and not deleted — one observation of capability does not falsify an observation of failure, and the failure that prompted this text remains unexplained, so the remedy stands and only the generalisation goes. That contract has two ends. The caveat’s evidence is about the child’s write failing silently, and its true content is do not make recoverability depend on a backgrounded child’s write — which is why it is kept rather than overturned by the template’s Findings you will act on are durable before you act on them rule. That rule’s duty falls on the parent, which has already demonstrated it can write, being the session that granted the path. The two are disjoint readings of one contract, not each other’s contradiction.

Journalling is a discharge path, not a dispatched session’s privilege. The template’s durability rule turns on capability, never role — a session journals when it cannot commit and commits when it can — and this section owns why the earlier role-keyed reading failed. That reading granted the journal to a dispatched session and denied it to a top-level one, on the premise that a top-level session is by construction able to commit. The premise is false whenever another session holds the shared index, which is the normal condition while a work unit is in flight rather than an edge case: the supervising role improvised scratch notes on three separate occasions in one iteration, each time because a live dispatched session held the index. Improvised scratch is the journal mechanism with none of its contract — no granted path inside the sweep’s reach, no inline landing, no marker — so the failure was silent and the reconstruction cost was paid per occurrence. Widening by capability rather than carving out a second surface per role is what keeps the reclaim story unchanged: every discharge path still lands in the same scratch directory the consumer’s own work-unit boundary already sweeps, so no new lifetime, keep-list entry, or inbound-triage surface is created by the widening.

The provenance floor sits here because this section already owns the receiving side. A dispatching session can narrate a finding from a subagent whose output it never received, and nothing reds — five attested instances, every one self-caught, four of them failures in two distinct shapes and the fifth a save. The operative text is the template’s A return you did not receive is not a finding, and an identifier you did not read is not a citation bullet; what belongs here is the argument for why one of its two halves gets a rule instead of a gate.

No gate reaches the relayed-return half, and that is a proof rather than an appetite. The tree cannot observe what a session did or did not receive. A return lives in the parent’s context and leaves no artifact, so tree state is byte-identical whether the return was held or invented — the prose reads the same either way, which is what makes this a gap rather than a lapse. Arrival is also unobservable to the parent by construction in at least one attested mode: a child that could not reach its dispatcher by name delivered its synthesis to the top-level session instead, so not even the parent’s own self-inquiry discharges it. A gate would therefore have to assert a fact about a conversation, and no scanner over a repository reaches one. Recorded so the next session weighing an oracle for this half reads the argument rather than re-deriving it. The minted-identifier half is a different matter and is not conceded: it leaves a token behind, so it is gated one record at a time at lifecycle-kit/SPEC.md §check-survey-record.

What stands in for the missing oracle is a mitigation, and it is this section’s own mechanism. The resume journal makes a child’s output land on disk rather than only in a message, so a parent’s claim becomes checkable against an artifact wherever the child wrote one. That is a reduction of the class’s reach and not a closure of it, because the journal is the child’s write and the sandbox caveat above says that write can fail silently. Stated as a bound, never banked as a fix.

Attested first-person, and this instance is recorded because it is one where the floor HELD. An isolated agent dispatched from the authoring session returned a message citing an audit roster “from my earlier turns”. That roster was not in the return the dispatcher received — the return carried one paragraph about an environment defect and nothing else. It was not narrated, not relayed onward and not used; every update target in that iteration’s amendments was derived first-hand instead. The same return also carried a claim the dispatcher could not verify, that the child had built the native binary to escape a refusal loop, and it is carried at that tier wherever it appears rather than promoted to a finding — what was verified is that the main checkout’s binary mtime was unchanged and git status stayed clean. Two instances from two consecutive stage sessions of one iteration, a mint and a relay, is what makes the class structural rather than anecdotal.

Verify after every agent commit

Re-run the relevant gates plus the consumer’s validate battery (for this repo and any gate-sdk consumer: bash gate-sdk/bin/run-gates.sh and the kit fixture runners; a toolchain consumer adds its compile/lint/test set in the template’s marked section, including a zero-byte-file sweep after renames — silent edit corruption is real). Diff every gate change in an agent commit before accepting: an agent blocked by a gate will weaken it (a false exemption) rather than fix the code — “the gate is in my way” almost always means the code doesn’t fit the convention.

The act is verify; the artifact stays the validate battery. The discipline is an act a supervisor performs; the battery is the command set that act runs. One word for both made completing the act read as completing whatever else the consumer names with that word — attested where a consumer’s stage roster carries it, and the near-miss was a skipped stage. Renaming the act rather than the artifact is the cheap side: the battery is a binding-slot name every consumer’s shim carries (§One template, a resident pointer, read by check-skill-binding), while the act is named only in prose. The result reads coherently — you verify an agent’s commit by re-running the relevant gates and the consumer’s validate battery — and the act and the artifact having different names is the distinction that was missing.

A verify step never runs the producer of what it is verifying. Re-running is safe and idempotent exactly when the dispatched unit’s output is work and the check’s output is a verdict about that work; it stops being safe the moment those coincide. Where a unit’s deliverable is an evidence artifact, the verify is a read of the committed artifact, never a re-execution of what wrote it: re-running the producer mutates or duplicates the very record the check exists to confirm.

The two wrong re-runs are not equally harmful, and a supervisor needs the split. A check that writes nothing the artifact depends on is inert on it — misrouted and wasted work, with nothing lost. Re-running the artifact’s sole writer is destructive. The routing defect is the same in both; only the second leaves evidence to restore, and the supervisor that has already run the wrong one reads this split to learn which one it is holding. Collapsing them would either overstate the harm of a wasteful re-run or understate the harm of the destructive one.

Honest limit — this rule installs no oracle, and recurrence is what establishes it matters rather than a projection: the misrouting fired in two consecutive iterations and an operator caught it both times. The check class that would catch it is a gate over a supervisor’s choice of command, and no scanner is buildable — the choice leaves no tracked artifact to read, the same structural reason §Operative residency owes no gate. A consumer whose evidence producer claims a liveness lock has a partial artifact-side proxy; its coverage boundary is that consumer’s own mechanism to state, not this section’s. So detection stays human, and it is stated here rather than left implicit because an unstated limit reads as an oversight for a later session to close with a gate that cannot exist.

check-gate-tamper is the mechanical floor under the diff-every-gate-change duty. Two assertions, blocking the two attested tamper shapes:

  • A — gate-edit isolation. A commit that touches a gate file (the DELEGATION_KIT_GATE_FILES globs) may touch only meta-layer paths (DELEGATION_KIT_META_PATHS prefixes + root *.md); co-staging product code with a gate edit is blocked. Split the gate change into its own commit.
  • B — no self-serving exemption. A newly added path/glob entry in any gate’s # exception-list: array must not match a file staged in the same commit — an exemption never excuses the very change it lands with.

Honest limit: the gate blocks by commit shape; it does not catch semantic weakening inside a legitimate scripts-only commit — the by-eye diff review of agent gate edits remains a supervisor duty. --fixture <dir> injects staged-files / added-exemptions lists (fixture-pair test capability); live mode reads git diff --cached.

checks/check-gate-tamper.gate (precommit, binary-dispatched). --fixture survives the port: it selects the rule’s own input corpus, which no knob resolves, so it is not the arrives-too-late shape gate-sdk/SPEC.md §The non-gate arm deletes — the invocation delegation-kit/README.md documents stays true.

Criterion 4 binds on this gate as a property of the consumer’s configured globs, not of the gate. Under the kit-shipped DELEGATION_KIT_GATE_FILES default the corpus is the consumer’s own gates directory, which holds no kit declaration, and the criterion clears. Under a config widening the globs to every kit’s check dir — this repo’s — the gate’s own declaration falls inside them, and staging it makes the gate read its own bytes. Reading the kit default, which is the natural first stop, therefore gives the wrong answer for the tree the port actually runs against; gate-sdk/SPEC.md §The port-candidate criteria carries the instance class.

Two behaviours changed with the port, and both are rulings rather than drift. Assertion B’s report lines came out of the shell form in no reproducible order once a commit added two matching exemptions; the compiled form emits the same set byte-sorted, on the kit-roots cohort’s ruling that a compiled form implements set semantics rather than a shell’s incidental ordering. And a trailing --fixture with no directory after it hung the shell form in its argument-parsing loop; the compiled form reads the missing value as the empty directory name and refuses. A hang is not a verdict, so ending it costs no assertion.

check-agent-tier-explicit

Every agent definition under DELEGATION_KIT_AGENT_DIR declares a model: field in its frontmatter. This is the oracle over the tracked half of the template’s Match the dispatched model and effort to the unit’s shape rule: the per-dispatch habit leaves no artifact, but a standing choice does, and the gate reads it.

It polices silence, not the choice. An explicit inherit-valued model: passes — a dispatched session that should ride its dispatcher’s tier is a legitimate answer and the gate has no business overruling it. What reds is omission, the one state indistinguishable from not having thought about it, and the one state that is not the neutral absence it looks like — the refused environment override and the precedence chain behind that reading are in §The delegation model. A reader arriving at this gate expecting it to enforce cheapness has the wrong model of it.

Counted inertness. A consumer with no such directory — or one holding no definitions — scans zero and reports a clean counted line, the derived-scan-set shape the kit’s other gates use. There is no roster and no registration list to maintain: the scan set is the directory’s contents.

Honest limit. It holds the tracked surface only. A dispatch naming no agent type and no model parameter inherits and leaves no artifact for any gate to read, so the gate converts the standing-choice class from unenforceable to enforced and leaves the per-dispatch class to the rule’s prose. That is strictly better than nothing and not a claim to have closed the tier question mechanically.

usage-verdict

Emits a trustworthy budget verdict from a usage snapshot file, closing the three failure modes a raw percentage reading leaves open:

  1. Stale readingnow - updated_at beyond DELEGATION_KIT_STALE_AGE → STALE (exit 2): budget-unknown, never blocking delegation — re-read or refresh before trusting the number.
  2. Dead windowresets_at <= now → RESET-OK (exit 0): the percentage is from the dead window and must not be read as a pause signal. The weekly axis carries the same rule per-axis (seven_day_resets_at <= now disarms the weekly pause without forcing the whole verdict to RESET-OK — the axes are judged independently).
  3. Post-login lag — a fresh login starts a new window but the server-fed percentage lags it, and the file-write age check cannot see that. The gate reads the auth event from the credentials file’s mtime (DELEGATION_KIT_LOGIN_WINDOW) and, within that window, routes a would-be OK to STALE (re-read). Letting the lag print OK would emit a fresh-looking chimera, since the producer stamps the new account id while the percentage and resets_at still carry the dead login’s window. Self-limiting: once the mtime ages out, the reroute stops firing.

    The reroute is asymmetric — it may suppress an OK, never a PAUSE. It therefore follows the axis compares rather than preceding them. The two directions of a lagging reading have opposite costs: a lagging low percentage printing OK invites a dispatch the budget may not afford, which is what the reroute exists to prevent; a lagging high percentage suppressed into STALE waves that dispatch through, because STALE is advisory and never blocks (§usage-verdict). An unconditional reroute closes the first and opens the second, so it must fire on the non-blocking outcome only: inside the window an at-or-over reading still exits 1 on whichever axis fired, and an under-threshold one still exits 2. An account switch swaps both windows, so neither axis escapes the suppression when the outcome is a would-be OK.

    The mtime is a proxy, and the roll witnesses refute it. A credentials file’s mtime cannot distinguish an actual login from a routine OAuth token rotation, which rewrites the same file — so every rotation mints a fake auth event and blinds the verdict for the window’s duration, and rotations cluster around long-session resume, exactly when a verdict is being asked for. The reroute’s premise is that a low percentage inside the window is a lagging reading; when the five-hour window has demonstrably rolled, a low percentage is the expected reading and the premise is false. Two witnesses of a roll are already in hand at that point, and the verdict consults both — conjunctively, since either alone is ambiguous:

    • the snapshot’s five_hour_resets_at differs from the newest DELEGATION_KIT_USAGE_HISTORY sample’s resets_at (the boundary moved);
    • the snapshot’s updated_at is past that previous boundary (time actually crossed it, rather than the boundary being restamped by an account switch).

    Both firing disarms the reroute and the reading verdicts on its own merits. This makes the verdict path a reader of the trend log it already appends to; the read is taken before this run’s own append, so the witness is the previous reading and never this one. It falls open to the unrefuted reroute whenever the log cannot answer — knob unset, file absent or unreadable, or a tail carrying no numeric resets_at — which is the pre-existing behavior, so an unconfigured consumer is unchanged. The residual limit is honest and named: a rotation with no roll still blinds the window, because no witness in hand contradicts it; closing that needs an account-keyed check against the credential’s own identity, not a sharper time window.

Check order: parse → RESET-OK → age-STALE → pause axes → login-STALE → OK. The RESET-OK branch is the same decision one step earlier and is ordering precedent for the witnesses: the file already accepts that a demonstrated roll outranks the reroute. It covers only a snapshot from an already-dead window; the witnesses cover a window that rolled between samples, where the re-polled snapshot carries a live resets_at.

Exit codes: 0 OK / RESET-OK, 1 PAUSE, 2 STALE or unreadable. Exit 2 is fail-closed on trusting the number and deliberately fail-open on refusing work — budget-unknown never blocks delegation — so it is never the code a reading that should refuse an expensive dispatch may land on. The bin’s # exit: header declares that mapping, stating exit 2’s consequence rather than labelling it a bare “fail-closed” (which named only the trust axis and read as a claim about the blocking one). That declaration is machine-read by check-assertion-strength (gate-sdk/SPEC.md), which derives the verdict vocabulary from it rather than carrying a literal — so the header is a surface with a consumer, not loose documentation, and rewording it reshapes what that gate can discriminate. Fail-closed throughout: missing keys and a non-numeric percentage route to STALE; each threshold compare uses awk, not integer-only bash arithmetic, so a fractional percentage cannot silently skip PAUSE; and both pause compares are at-or-over (>=), so a reading exactly at DELEGATION_KIT_PAUSE_PCT / DELEGATION_KIT_PAUSE_PCT_7D pauses — the boundary reading is judged at the limit, not under it.

Demand-driven refresh. The decision point triggers the poll: when DELEGATION_KIT_REFRESH_CMD is non-empty, usage-verdict runs it before reading the snapshot, so the budget guard and any verdict caller read live data instead of whatever the last statusline render left behind. This closes the push producer’s blind spot at the point of use — a lead that delegates stops rendering exactly when it goes static, and the first live poll proved the gap: the snapshot said 2-5% while the endpoint said 28%. Empty (the default) keeps the read-only behavior, so an unconfigured consumer’s push and timer producers are unchanged.

The refresh is short-circuited by DELEGATION_KIT_REFRESH_MIN_AGE: the command runs only when the snapshot is missing, unreadable, or its updated_at age is at least that value. The statusline calls usage-verdict on every render for trend sampling; without the floor a configured refresh would hammer the source on the render path. At dispatch-decision time a stale-enough snapshot still polls.

It is fail-soft: a non-zero refresh exit leaves the snapshot untouched and the verdict proceeds on the cached file — the staleness machinery above judges its trust, turning a dead producer into a STALE verdict rather than a silent green. Refresh diagnostics are suppressed rather than mixed into the verdict output, which callers relay verbatim; the snapshot’s age is the signal. usage.txt survives as last-known-good cache, source-agnostic seam, and test seam. A refresh inside the DELEGATION_KIT_LOGIN_WINDOW rewrites updated_at while the server-fed percentage may still lag the login by about a minute — the reroute correctly keeps those readings STALE for the window’s duration, save the at-or-over ones it is forbidden to suppress.

The verdict-string contract. Every line usage-verdict emits carries three parts: the reading (the measured fields), the epistemic status of that reading (how far to trust the number), and its decision consequence for delegation (what the reading gates). A verdict string stating a status without its consequence is incomplete by contract — the reader completes it by inference, and the inference is unconstrained: a session reading a bare STALE infers “do not trust the number” into “do not dispatch”. That read is wrong (STALE never blocks) and is structurally uncorrectable downstream, because the budget guard fires on dispatch, the act a hesitating session has not yet performed. STALE’s status half is site-specific — each emission site names why it is stale — while its consequence half is uniform, because the consequence is uniform: STALE is budget-unknown at every site and never blocks delegation. The consequence clause appends after the -> <verdict> arrow, leaving it disjoint from the width=<n> invariant below.

The width=<n> field. Every emitted verdict line — OK, PAUSE, STALE (the fail-closed diagnostics included), RESET-OK — carries a width=<n> field immediately before the -> <verdict> arrow, read from DELEGATION_KIT_FAN_WIDTH (§Layout and configuration). It is the knob’s mechanical reader: the budget check already runs before every dispatch, so the read-only fan-out bound surfaces at exactly the wave-sizing decision point, and agent-budget-guard.sh relays the verdict line verbatim (block on PAUSE, advise otherwise) so the width rides into context with the budget verdict. Verbatim governs the verdict line’s own text, not the surrounding frame: the guard prefixes the relayed line and the PAUSE arm appends its corrective, but no arm restates the payload’s content — there is exactly one advise arm and it adds nothing. The field is a config constant, not derived from the snapshot, so it is present even on an unreadable-snapshot STALE. No exit-code or other-field change: existing callers that key off the exit code or the -> <verdict> arrow are unaffected.

Two pause axes. The five-hour window is the always-on axis; the weekly (7-day) window is a second axis, armed only when both seven_day_used_pct and seven_day_resets_at are present (a three-line snapshot keeps today’s behavior — no retroactive contract break). The weekly limit can deplete while the 5h window sits comfortable, and a weekly PAUSE costs days, not hours, so it must gate delegation planning, not merely appear in a log — delegation is the discretionary spend, the first thing to stop near the weekly ceiling so the remaining week stays with the supervisor. The axes are judged independently against DELEGATION_KIT_PAUSE_PCT and DELEGATION_KIT_PAUSE_PCT_7D; either firing is a PAUSE (exit 1), and the message names the axis that fired (PAUSE (7-day window) vs PAUSE (5h window), the weekly named when both fire) because the operator’s remediation differs by days. No caller changes: the session-context hook and the per-dispatch Agent budget guard already consume the exit code, so the weekly axis flows into every dispatch decision the moment a producer supplies the keys.

Usage-history sampling. When DELEGATION_KIT_USAGE_HISTORY is non-empty, usage-verdict appends one sample line (§The usage.txt contract) to that log after every successfully parsed snapshot, whatever the verdict — the raw harness-reported values verbatim; write-time smoothing or correction is forbidden (a later corrective push is evidence about the earlier sample, and only the reader has both). A STALE exit from an unreadable or unparseable snapshot appends nothing — a sample the gate would not trust is not history. usage-trend (§Trend reporter) reads the log, and so does usage-verdict itself: the newest sample’s resets_at is one of the two roll witnesses (§usage-verdict). That read is the log’s only in-verdict consumer and is strictly fall-open, so the log stays an advisory trend surface rather than becoming a correctness dependency — a consumer with sampling off gets the same verdicts it always did.

The usage.txt contract

The snapshot is the wire contract between any producer and the gate — three key=value lines:

five_hour_used_pct=<float>
five_hour_resets_at=<epoch-seconds>
updated_at=<epoch-seconds>

Beyond the three mandatory lines a producer may write optional keys when its source exposes them; usage-verdict reads the ones it interprets and passes the rest through: seven_day_used_pct / seven_day_resets_at (the weekly window — read at the verdict transition to arm the second pause axis), account (the logged-in account identity — login_at detects a switch, account says to whom, which lets usage-trend group a multi-account operator’s segments per account), tier (subscription tier — the denominator behind the percentages), and tokens_in / tokens_out (cumulative token counts, the axis that binds API-billed consumers for whom no subscription percentage applies). Optional keys are omitted when their source has no value, never written empty; keys the verdict does not read pass through unchanged.

The kit ships two reference producers — one push, one poll — and any producer honoring the contract works beside them. templates/statusline-usage.sh is the push producer: a statusline hook that parses the harness’s rate-limit JSON and atomically writes the snapshot (tmp + mv) to ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/usage.txt. It also produces the weekly pair from the payload and account / tier from the local account config (.credentials.json / ~/.claude.json, overridable via DELEGATION_KIT_CRED_FILE / DELEGATION_KIT_ACCOUNT_CONFIG). It ships no tokens_in / tokens_out producer: this harness’s payload carries no cumulative token count, so under the dead-producer rule the keys stay defined here for third-party producers but no dead producer is shipped. Beyond the snapshot write it renders a status bar (§The statusline template). The snapshot write is the contract; the bar is reference UX a consumer may restyle or discard. The source is pluggable (DELEGATION_KIT_USAGE_FILE), so no single-operator CLAUDE_CONFIG_DIR assumption is baked in.

templates/usage-poller.sh is the poll producer, closing the push producer’s blind spot: the statusline fires on the supervising session’s own message flow, so a lead that delegates stops producing exactly when it goes static — a delegated build can run for hours with every per-dispatch budget verdict reading a stale file. The poller is one poll cycle per invocation: read the harness OAuth token from the credentials file the kit already knows (DELEGATION_KIT_CRED_FILE; the token goes into the request header and nowhere else — never logged, never echoed, never written to the snapshot), query the account usage source, map the payload onto the contract (the three mandatory lines plus whichever optional keys the source exposes; no new key), and atomically rewrite the snapshot (tmp + mv, the same discipline). No daemon, no loop: scheduling belongs to the consumer, and two modes are sanctioned. Demand-driven (§usage-verdict): point DELEGATION_KIT_REFRESH_CMD at the poller and the same one-cycle producer runs at verdict time, which puts the freshest reading exactly at the decision point. Timer-driven: a cron line such as */5 * * * * bash usage-poller.sh or a systemd timer, for consumers wanting continuous trend density independent of verdict calls. Either entry is the enabling config; with neither, the producer is dead. The usage endpoint is harness-account plumbing, not a published contract, so the poller is fail-soft: a missing/unreadable credentials file, a fetch failure, or an unparseable payload exits non-zero with a help: line without touching the snapshot — a stale snapshot is already the detected condition downstream (usage-verdict’s updated_at staleness check turns it into a STALE verdict, never a silent green), and the non-zero exit lands in the invoking timer’s logging, deliberately nowhere else. The endpoint URL is the DELEGATION_KIT_USAGE_ENDPOINT knob — both the test seam (the smoke points it at a local file:// stub) and the valve when the source moves.

Both producers write the whole snapshot atomically from the same account source; last-writer-wins is correct because the freshest write is the truest and updated_at arbitrates downstream. Nothing serializes them and nothing needs to. The trend log is unaffected: usage-verdict stays the single append author (§usage-verdict); the poller writes the snapshot only.

Because the statusline fires far more often than the per-session / per-dispatch verdict calls, a consumer wanting a denser trend history can drive sampling from the render path — usage-verdict stays the single append author (§usage-verdict), so the statusline calls it (with DELEGATION_KIT_USAGE_HISTORY set) rather than appending the log itself.

The sample line. With sampling enabled (§usage-verdict), usage-verdict appends one line per parsed snapshot — the trend log’s wire contract between it and usage-trend:

updated_at=<epoch> pct=<float> resets_at=<epoch> verdict=<word> login_at=<epoch>[ account=<word>][ tier=<word>][ pct_7d=<float> resets_7d=<epoch>][ tokens_in=<n> tokens_out=<n>]

Space-separated key=value, order-insensitive; optional groups are omitted (never written empty) when the snapshot lacks them. login_at is the credentials-file mtime the post-login-lag check already reads, stamped per sample so an account switch becomes data; pct / resets_at are the 5h values verbatim and pct_7d / resets_7d the weekly ones. The log is append-only (the tmp-prune / boundary-truncate conventions own cleanup), and carries operator-local account identifiers, so it lives under the gitignored measurement dir and never reaches a tracked file.

The statusline template

templates/statusline-usage.sh renders model/effort, a context gauge, the 5h and 7d windows with reset countdowns, an iteration@stage readout, and a queue counter group. Self-contained ANSI, no external asset (§Out of scope).

The iteration@stage readout’s two halves come from two surfaces: the iteration from the queue header, the stage from the lifecycle evidence file’s last data line. Both are repo-root-relative reads in the template’s existing hardcode style — a consumer-owned template adds no knob. With no cursor to read (no evidence file, or one truncated to its preamble at an iteration boundary) the readout degrades to the iteration alone rather than printing a dangling separator or a partial parse.

The counter group is a compact N12 T3 D48 I7 tally of the queue’s task sections, appended last. The template does not know how many sections exist, what they are called, or which tiers the set contains: it runs queue-kit’s bin/queue-counts.sh and renders whatever <section-name><TAB><count> lines come back, labelling each with the initial of the name it was handed (queue-kit/SPEC.md §bin/queue-counts.sh). The section vocabulary therefore stays entirely inside the kit that owns it — there is nothing here for a later editor to hardcode. As soon as two returned names share an initial, every label widens to two characters, so a colliding pair stays distinguishable and the group stays aligned rather than only the collision changing width.

It is a subprocess call, and that is the contract, not an implementation detail. The counter’s library exits 2 at source time on a malformed queue config; sourced into this shell it would take the whole status bar down — model, context gauge, both rate-limit gauges — over a component worth four characters. As a subprocess the same fault is a non-zero exit the render absorbs. An absent or non-executable counter, a non-zero exit, and empty output all drop the group and change nothing else about the bar, matching the two degradations the template already performs (an absent queue file drops the iteration; an absent evidence file drops the @stage suffix). The existence guard is kept even though every installer profile carrying this template also carries queue-kit, because a future profile pairing differently would otherwise turn a roster fact into a broken status bar.

The render cost is one subprocess per statusline fire, which is the kit’s most frequent trigger by a wide margin (§The usage.txt contract). A full scan of a ~3,600-line queue measures in single-digit milliseconds, so the group needs no cache — recorded so a later session does not add one on suspicion.

Trend reporter

bin/usage-trend.sh reads the history log and reports how the footprint evolves — advisory tooling, never a gate: exit 0 report emitted, 2 knob unset or history missing/unreadable (fail-closed, mirroring the verdict’s STALE discipline), never 1 (it renders no verdict; the verdict stays the sole pause authority). The source signal is known-noisy — a rolling-window reading spikes and reverts-down when a harness over-report is corrected by the next push — so the design separates signal from noise by the window’s one physical constraint: within a segment, true usage never decreases.

  1. Segment samples per axis by that axis’s reset epoch, login_at, account, and tier: the 5h axis keys on resets_at, the weekly axis on resets_7d (the windows roll independently — a weekly segment spans many 5h segments). A timer reset, a /login, or an account or tier change each starts a segment; only within-segment comparisons are meaningful, so an account switch is a boundary, not a flagged anomaly.
  2. Flag any sample whose pct is below an earlier one in the same segment as a monotonicity violation: the downward correction indicts the elevated sample(s) before it as reader noise, and both sides are excluded from rate math, never averaged in. Median-of-3 smoothing resolves single-sample spikes; segment endpoints keep their own value (no 2-window averaging).
  3. Report per segment and axis — first/last smoothed pct, pct-per-hour rate, token deltas when token keys ride, tier, sample count, and suspect-sample count (a high suspect ratio means the producer is unreliable and no number from that segment is trusted). The weekly axis additionally reports headroom against DELEGATION_KIT_PAUSE_PCT_7D at the current rate — the planning number for how much delegation the week still affords — and, when account is present, segments group under an account heading so a rotating operator reads one weekly trajectory per account rather than an interleaved stream.

bin/wait-probe

bin/wait-probe.sh is the instrument behind §Operative residency’s wait-primitive measurement: it stands a known-duration producer up and exercises candidate wait forms against it, recording one line per trial. Hand-invoked, no trigger, wired into no tier — it launches processes and sleeps for its declared durations, so a gate tier would put a multi-second sleep on every commit for a measurement bought once. Its precedent for existing at all is demo/run-demo.sh and gate-sdk/bin/port-blockers.sh, runnable artifacts a session invokes by hand. It writes nothing tracked: producers, markers and launch records go to GATE_SDK_TMP_DIR, trials to the capture-tier evidence file below, and it mints no knob of its own for either (§Layout and configuration owns why).

Exit 0 on a completed subcommand, 2 on misuse; -h/--help prints the subcommand roster on stdout and exits 0, the bin/-tool contract (gate-sdk/SPEC.md §The bin/-tool contract).

One trial is: launch a producer that sleeps a declared duration and then writes a completion marker; record its PID at launch in a <key>.run file exactly as templates/agent-execution.md mandates — the rule applies to the instrument that measures it, so the probe’s own producers are visible to check-producer-liveness and to guard rule 14 like any other; arm the wait form under test; and record whether the waiter exited before the marker appeared. That before/after relation is the whole measurement: a waiter that outlives its condition is working, and one that exits early is the observed failure.

The producer’s duration is swept, not fixed, because a single duration cannot distinguish the candidate causes. The probe reads no harness internals and asserts no ceiling; it varies the input and reports where the behavior changes.

Two axes, and the second is what the instrument earns its keep on. The form axis varies how the wait is armed. The predicate axis varies what the loop tests — a completion marker, or the producer’s liveness. A form-only instrument can report that a form failed and can never report why, and the attested failures turned out to sit on the predicate axis, which a form-only sweep would have recorded as unexplained. Both axes are swept in the same run on identical producers, so no cell’s result rests on a comparison with a differently-shaped trial, and every form arms the same wait body.

The per-trial line. Every field has a named reader and the field set is closed at these:

  • form — which wait form the trial armed. Read at the comparison; the only field that distinguishes the candidates.
  • predicate — what the loop tested. Read at the classification, and it is what separates a failing form from a failing condition.
  • producer_ms — the producer’s declared duration. Read at the sweep analysis: a ceiling shows as a threshold in this field.
  • waiter_exit — the waiter’s exit status. Read at the classification, to separate a signal status from a clean zero with the marker absent.
  • marker_at_ms, waiter_at_ms — elapsed wall-clock at the marker’s appearance and at the waiter’s exit. Read together and only together: their order is the measurement and neither alone says anything.
  • producer_alive_at_exit — whether the recorded PID still answered kill -0 when the waiter exited, establishing the producer verifiably alive condition — on the recorded PID and never a process-table pattern, which is the protocol’s own rule.
  • classok on a clean trial; otherwise the cause, from the closed list below.

The producer’s own PID is not logged: the <key>.run record already holds it for the lifetime the wait needs, and a second copy in an advisory log would be a PID with no reader once the trial ends.

The candidate causes are enumerated before the run, not after it, so a run returns a cause rather than an anecdote:

  • (i) reaped — the harness or the session boundary killed the backgrounded shell. Tell: a signal exit status with the producer still alive.
  • (ii) ceiling — a wall-clock deadline the form carries. Tell: early exits cluster above one duration and short trials never fail.
  • (iii) predicate — the condition went true, or could not go false, for a reason unrelated to the producer. Tell: a clean zero exit with the marker absent, reproducing on a form the harness never touched.
  • (iv) unexplained — recorded as such and reported as such. A closed list that cannot say I do not know would force a wrong attribution, which is the failure the enumeration exists to avoid.

The honest limit, and it is a division of labour rather than a defect. Cause (ii)’s tell is a threshold across the sweep, so no single line can carry it: record classifies from the tells a trial can see on its own and the report arm is the only reader that sees the sweep whole and can name (ii). A reader taking a line’s class for the run’s verdict has read the wrong surface. The second limit is scope: the probe arms the forms a shell can arm and the session arms the harness’s own, calling record for each — the tool cannot invoke a harness tool parameter, so a run that measures the harness forms is a session’s run and sweep is the harness-uninvolved reproducer a second machine executes.

The trials land in .workflow/wait-primitive-evidence.txt — capture-tier, gitignored, advisory (gate-sdk/SPEC.md §The workflow directory), declared on the close-surface roster (lifecycle-kit/SPEC.md §The close-surface roster) naming its own clear as the reclaim path:

close-surface: .workflow/wait-primitive-evidence.txt advisory reclaim=: > .workflow/wait-primitive-evidence.txt

advisory rather than forced, on the reasoning §The turn-end liveness hook’s log already takes: nothing refuses a close that skips it, and a visible skip is the honest mode for a probe. The finding — the branch the trials select and its grounds — lands in this SPEC, never in the log; the log is evidence, and evidence is not the record.

Layout and configuration

delegation-kit/
  bin/usage-verdict.sh
  bin/usage-trend.sh              # footprint trend reporter over the history log
  bin/run-usage-tests.sh          # verdict decision-table runner
  bin/run-budget-guard-tests.sh   # budget-guard decision-table runner
  bin/run-dispatch-guard-tests.sh # dispatch-guard decision-table runner (D1/D2/D3)
  bin/run-trend-tests.sh          # trend-reporter assertion runner
  bin/wait-probe.sh               # wait-primitive probe: hand-invoked, no trigger, wired into no tier
  lib/delegation.sh               # shared helpers for the usage tools and the kit's gates
  usage-tests/cases.tsv           # expected-verdict <TAB> scenario knobs
  usage-tests/budget-guard-cases.tsv  # expected-action <TAB> scenario knobs
  usage-tests/dispatch-guard-cases.tsv  # expected-outcome <TAB> scenario knobs
  usage-tests/trend-history.log   # fixture history for the trend runner
  checks/check-gate-tamper.gate  # binary-dispatched; live arm reads the index through git
  checks/check-rule-citation.gate  # hermetic, binary-dispatched: every SPEC rule citation resolves to a template lead-in
  checks/check-agent-tier-explicit.gate  # hermetic, binary-dispatched: every agent definition declares an explicit model:
  gate-tests/check-gate-tamper/{good,bad}/
  gate-tests/check-gate-tamper.test.sh  # the live arm, over throwaway git repos
  gate-tests/check-rule-citation/{good,bad}/
  gate-tests/check-agent-tier-explicit/{good,bad}/
  templates/agent-execution.md            # full protocol, bound as a skill shim
  templates/dispatch-checklists.md        # deletion/rename/audit pre-flight, reached by a pointer
  templates/agent-budget-guard.sh         # PreToolUse(Agent) budget guard
  templates/agent-dispatch-guard.sh       # PreToolUse(Agent) dispatch-shape guard (D1/D2/D3)
  templates/statusline-usage.sh           # push usage.txt producer (statusline hook) + status bar incl. the queue counter group
  templates/usage-poller.sh               # poll usage.txt producer (timer-driven, fail-soft)
  templates/subagent-stop-liveness.sh     # SubagentStop turn-end liveness hook: logs, and refuses by exit 2 on a red or corrupt reading
  templates/delegation-config.sh          # knob overrides (arrays live here)
  smoke/install.sh
  smoke/violation.sh

Config follows the established kit pattern: copy templates/delegation-config.sh into the gates dir (or point DELEGATION_KIT_CONFIG_FILE elsewhere) and override any knob; defaults fill what the consumer left unset. The loader is fail-closed: a DELEGATION_KIT_CONFIG_FILE named but absent, or a config leaving any knob malformed, exits 2 (a broken machine gates nothing). Knobs (this repo’s layout as defaults):

  • DELEGATION_KIT_USAGE_FILE — default ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/usage.txt; positional $1 overrides (test injection).
  • DELEGATION_KIT_CRED_FILE — default the usage file’s sibling .credentials.json; positional $2 overrides.
  • DELEGATION_KIT_USAGE_ENDPOINT — the poll producer’s usage source; default https://api.anthropic.com/api/oauth/usage. The test seam (a file:// stub) and the stability valve when the unpublished source moves (§The usage.txt contract).
  • DELEGATION_KIT_PAUSE_PCT — default 80.
  • DELEGATION_KIT_PAUSE_PCT_7D — weekly-axis pause threshold; default 95, deliberately looser than the 5h axis: a weekly PAUSE is remediated in days, not hours, so pausing at the 5h conservatism would strand a fifth of the week’s budget — only the true red zone stops delegation on this axis.
  • DELEGATION_KIT_STALE_AGE — default 600 (seconds).
  • DELEGATION_KIT_LOGIN_WINDOW — default 600 (seconds).
  • DELEGATION_KIT_REFRESH_CMD — the command usage-verdict runs before reading the snapshot (§usage-verdict); default empty, which keeps the read-only behavior. This repo points it at the poll producer, run in place from its template path.
  • DELEGATION_KIT_REFRESH_MIN_AGE — the refresh short-circuit (§usage-verdict); default 60 (seconds), validated a non-negative integer by the loader. The floor is set by the render path, not the dispatch path: the statusline calls the verdict on every render, so the default bounds source traffic to roughly one poll a minute while leaving any dispatch-time reading fresh enough to act on.
  • DELEGATION_KIT_USAGE_HISTORY — sample-log path; default empty (sampling off). This repo sets .metric/usage-history.log, a gitignored persistent measurement trend (drift-kit/SPEC.md §Layout and configuration owns the metric-dir retention contract).
  • DELEGATION_KIT_FAN_WIDTH — read-only-fan-out width bound; default 2, validated a positive integer by the loader. It bounds read-only fan-outs only: a committing fan-out serializes or takes its own worktree regardless (the template’s Serialize on shared files; ≤DELEGATION_KIT_FAN_WIDTH-wide otherwise rule), so this knob is never a licence to widen concurrent committers. The default is the loss-bounding invariant — bound the in-flight loss to what the window can absorb when the wall fires mid-flight — at a Pro-class subscription window; a Max-class window absorbs more, and an API-billed operator has no mid-flight window wall at all, so there spend rate and the provider’s rate limits replace the rationale. A consumer retunes from the invariant, not the number. Supervision ceiling: operator attention over N concurrent reports does not scale with the budget window — a bigger window raises the affordable width, not the reviewable one, so budget is never the sole input to widening. usage-verdict surfaces the live value as the width= field (§usage-verdict), the knob’s mechanical reader.
  • DELEGATION_KIT_AGENT_DIR — the directory check-agent-tier-explicit walks for agent definitions; default .claude/agents, this harness’s conventional agent-definition location, validated non-empty by the loader. Positional $1 overrides (fixture injection). A consumer whose harness keeps definitions elsewhere repoints it; one with no such directory leaves it and the gate reports its counted-inert clean line (§check-agent-tier-explicit).
  • DELEGATION_KIT_GATE_FILES — globs naming gate files for tamper assertion A; default ("${GATE_SDK_GATES_DIR:-scripts}/check-*.sh" "${GATE_SDK_GATES_DIR:-scripts}/check-*.gate") plus the gate-sdk lib and runners. Both declaration spellings are on the default, because a gate’s declaration path is <name>.sh or <name>.gate (gate-sdk/SPEC.md §The # graph: manifest) and a consumer on the default would otherwise receive a ported gate whose edits escape the isolation rule entirely — the widening the check-gate-tamper row of gate-sdk/SPEC.md §Meta-gate conservation for the binary substrate mandates, come due at the first live descriptor. A consumer declaration replaces this default outright rather than extending it — the loader guards it with declare -p … ||, so the default is the no-declaration fallback, not a base to append to. A consumer adding kit-shipped globs must therefore restate any default glob it still wants covered (this repo’s config names */checks/*.sh and */checks/*.gate and their scripts/check-* counterparts, because it has gates in both places). Contrast DELEGATION_KIT_META_PATHS below, whose kit-root union is additive — the two knobs do not behave alike.
  • DELEGATION_KIT_META_PATHS — prefixes counted as meta-layer for assertion A; default ("${GATE_SDK_GATES_DIR:-scripts}/" "${GATE_SDK_WORKFLOW_DIR:-.workflow}/" ".claude/"); root-level *.md is always meta. When gate.sh is resolvable (GATE_SDK_LIB, else the vendored sibling), the loader unions every gate_kit_roots member into this array as a root-relative dir/ prefix — a vendored kit’s edits are meta-layer by definition, so the consumer’s config need not name kit dirs at all (this repo’s keeps only the non-kit prefixes). The union is additive, never a filter: a prefix the consumer declared cannot be lost, and without gate.sh the config is used exactly as written.
  • DELEGATION_KIT_STOP_LOG — the turn-end probe’s log (§The turn-end liveness probe (template)); default ${GATE_SDK_WORKFLOW_DIR:-.workflow}/subagent-stop-liveness.log, the same deferral guard-kit’s two logs already take. No scratch-dir knob sits beside it: the launch record’s home is ${GATE_SDK_TMP_DIR:-.tmp}, the cross-kit deferral every kit reaching that directory already resolves it through, and a second name for it would be the duplication rather than the config. The rule has since governed a second artifact and held: bin/wait-probe (§bin/wait-probe) writes producers, markers and <key>.run records into that same directory and mints no knob of its own for it either. It mints none for its evidence log either, and there the reasoning is the narrower one this bullet’s own knob does not meet: a knob needs a reader, and DELEGATION_KIT_STOP_LOG has one — a second process, the consumer-side hook, which must be told where to write. The probe both writes and reads its own log in one tool, so a knob there would be a name nothing reads, and the close stage reaches the file through the roster’s literal rather than through a knob.
  • DELEGATION_KIT_LIVENESS_CMD — the liveness reader the probe invokes in set mode, a path run with the scratch dir as its only argument; no default. A knob rather than a literal because a consumer’s evidence-kit may sit elsewhere, and defaultless because the gate behind it is reached by name rather than by path and only a consumer knows its front end (§The turn-end liveness hook (template) owns why the knob is not taught to resolve a name). Unset and empty are therefore the same value here, and both are the supported way to run the hook with no reader at all — the honest-degradation case the section states, in which every firing logs verdict=unavailable decision=allow and refuses nothing.
  • DELEGATION_KIT_READONLY_TYPES — agent-type names the consumer dispatches for read-only work; D2’s only trigger (§The delegation model). Default empty, in which case D2 is inert by construction. Every entry is the consumer’s own agent roster, never the kit’s (CLAUDE.md §The provenance seam) — the same reasoning that ships GUARD_KIT_BREADTH_PROBES with no default probes. Deliberately not knobbed alongside it, each ruled out for a stated reason: a fork-type-name knob and an accepted-isolation-values knob — fork and worktree are the harness’s dispatch vocabulary, and a guard template carries its harness’s tool vocabulary as literals the same way wakeup-guard.sh matches ScheduleWakeup/CronCreate literally, so knobbing them would imply a portability the mechanism does not have; a warn-only or per-rule opt-out — a doctrine downgradable per session is the honour system these rules exist to end, so the valve stays the whole guard’s registration, never a finer one; and accepting isolation: remote as satisfying D2 — plausibly correct (a remote agent cannot touch the local index) but unverified from here, and remote availability is gated, so it is excluded with its reason recorded rather than admitted on plausibility.

check-gate-tamper registers in the consumer’s gates.list (tier: precommit) — in this repo’s too; dogfooding is day-one, and agents commit here.

agent-budget-guard.sh is not a gate — it is a hook, so it registers not in gates.list but under PreToolUse matcher Agent in the consumer’s .claude/settings.json (beside the guard-kit Bash guard). Copy the template into the gates dir and wire bash scripts/agent-budget-guard.sh; it resolves guard-kit/lib/guard.sh and bin/usage-verdict.sh at their vendored paths, overridable with GUARD_KIT_LIB / DELEGATION_KIT_VERDICT_BIN. Registration is the whole opt-in: unwired, the guard is inert. This repo registers it, and its consumer session brief (scripts/session-context.sh) additionally prints the verdict line at SessionStart for planning-time visibility — a consumer-side edit; the context-kit template stays uncoupled from delegation-kit.

subagent-stop-liveness.sh is a third hook, and the only one on a non-PreToolUse event. It registers under SubagentStop in the consumer’s .claude/settings.json — an event that takes no matcher, so the entry carries a hooks array alone and is not tool-scoped, the shape a SessionStart entry already has. Copy the template into the gates dir and wire bash scripts/subagent-stop-liveness.sh; it sources no kit lib at all (§The turn-end liveness hook (template) owns why) and resolves its reader through DELEGATION_KIT_LIVENESS_CMD. Registration is again the whole opt-in, and here the opt-in is also the consent: the wiring is a permission-surface write, so it is the consumer’s own act and never an agent session’s.

agent-dispatch-guard.sh is a second hook on that same matcher, registered the same way and independently: the harness fires every hook a matcher carries and any exit 2 blocks, so neither guard knows about the other and a consumer may wire either one alone (§The delegation model owns why they are two scripts rather than one). It resolves guard-kit/lib/guard.sh at its vendored path, overridable with GUARD_KIT_LIB, and reads the consumer’s delegation config for D2’s roster and nothing else. Registration is again the whole opt-in: unwired, it is inert — and an unwired dispatch guard is the exact failure shape it exists to catch, so wiring it is not optional dogfooding for a consumer that delegates.

An OK verdict is a floor, not a recommendation. The guard answers one question — is there headroom in the window right now — and blocks only on PAUSE. Everything else a dispatch decision turns on sits outside its reach: how long the dispatched work runs, what a fan-out beneath the checkpoint will cost in total (the guard re-arms on those children but prices each of them alone: covered at every depth, aggregated at none — §The delegation model), and whether anything forces the dispatch now rather than after the window resets. OK is therefore one input to that decision and never the decision itself; a dispatcher reading OK as “go” has substituted the guard’s question for its own. The asymmetry is deliberate: a wrong PAUSE costs a delayed dispatch, while an OK that is right about headroom and silent about duration costs the work in flight.

Testing

check-gate-tamper speaks the full gate contract (GATE-TAMPER: clean (…) / findings + help: lines / exit 0-1-2) and ships the standard good/+bad/ fixture pair driven through --fixture by gate-sdk’s run-gate-tests.sh. The pair reaches both assertions — the bad case carries a co-staged non-meta path and a newly added path exemption matching one of its own staged files, beside a bare token assertion B must pass over.

The pair cannot reach the live arm, and that is a property of the injection rather than of the cases. --fixture supplies both lists directly, so the function that derives them — reading each staged gate file’s bytes out of the object store and diffing its exemption set against HEAD’s — runs in no fixture case. gate-tests/check-gate-tamper.test.sh is where it is exercised: each case builds a throwaway git repo, stages a commit shape into a real index, and runs the gate inside it. It holds the four verdicts the pair cannot — a HEAD-resident exemption is not “added” even when a co-staged file matches it, a newly appended one is, an added one matching nothing staged stays silent, and a staged deletion of a gate file reads as an empty exemption set rather than a failure to read.

check-rule-citation speaks the same gate contract and ships the standard good/+bad/ fixture pair (a citation resolving to a template lead-in vs one naming an absent lead-in), driven by run-gate-tests.sh over fixture-local spec + template files passed as its two positional arguments.

check-agent-tier-explicit speaks the same gate contract and ships the standard good/+bad/ fixture pair over a fixture-local agent directory passed as its one positional argument. The good/ side carries both passing shapes — a definition naming a cheaper class outright and one declaring inherit — so the pair proves the gate discriminates omission from choice rather than merely finding a model: string; the bad/ side omits the field.

usage-verdict does not fit the gate contract (a three-state verdict, not a clean/violation pair), so — like guard-kit’s guard-tests — the kit ships its own decision-table runner: usage-tests/cases.tsv pairs an expected verdict token (OK/PAUSE/STALE/RESET-OK) and exit code with scenario knobs (percentage, snapshot age offset, reset offset, credential age); bin/run-usage-tests.sh materializes each case as a generated snapshot file (timestamps must be computed relative to now — static fixtures would age into permanent STALE) and asserts verdict and exit code. Each case runs in a throwaway sandbox with no consumer config on the lookup path and with ambient DELEGATION_KIT_* exports stripped at each gate invocation, so the gate exercises its own defaults hermetic to the host repo — the decision table encodes those defaults, and a host override (a raised pause threshold, say) must not reshape it. The trend and budget-guard runners below share the strip, each runner under a deliberate poison export that fails its own table loudly if the strip ever breaks; cases.tsv columns are verdict exit pct age_off reset_off cred_age pct_7d reset7d_off append axis desc (the offsets seconds from now; pct_7d - omits the weekly keys, append is the expected sample-line count, axis asserts which window a PAUSE names). Every verdict branch and both pause axes carry a firing and a non-firing case — the fixture-pair discipline, transplanted — covering a weekly PAUSE while 5h is comfortable, the axis named in the output, absent keys disarming the weekly axis, a dead weekly window not pausing, each axis’s at-or-over boundary (a reading exactly at the threshold pauses; just under does not), both directions of the login reroute (an under-threshold percentage with a fresh login is STALE, not OK; an at-or-over one on either axis is still PAUSE, not STALE — the asymmetry is what keeps the reroute from waving an unaffordable dispatch through), and the sample-append discipline (a parsed snapshot appends one line whatever the verdict; a non-numeric snapshot appends none; pct_7d present-vs-omitted in the passed-through line).

The demand-driven refresh needs a command seam the table’s columns do not carry, so it is asserted beside the table through a stub DELEGATION_KIT_REFRESH_CMD (a real poll would need the network): armed — a stale snapshot invokes the command and the verdict reads the values the refresh wrote, not the cached ones; fail-soft — a non-zero stub leaves the snapshot byte-identical, the verdict proceeds and the cached reading is judged STALE by the existing staleness machinery, and no refresh diagnostic leaks into the relayed verdict line; short-circuit — a snapshot under DELEGATION_KIT_REFRESH_MIN_AGE never invokes the command. Armed and short-circuit are the firing/non-firing pair over the same stub, so the absence proves the floor rather than a broken stub.

The roll witnesses need a pre-seeded history tail the table’s columns do not carry, so they are asserted beside it too, over one fixed shape — a low percentage with a credentials mtime inside the login window, which without a witness is STALE. Both witnesses present verdicts OK; each of the four ways the log can fail to answer holds the STALE — the boundary unmoved, the previous boundary uncrossed, a tail with a non-numeric resets_at, and no history at all. The two single-witness cases are the discriminating pair: they prove the conjunction is load-bearing rather than either witness carrying it alone. A sixth case asserts the read is not self-witnessing — a run with an empty log still STALEs and leaves exactly its own sample behind, so the witness can only ever be a previous reading.

usage-trend is likewise not a gate (it renders no clean/violation verdict), so it ships an assertion runner, bin/run-trend-tests.sh, over a static fixture history usage-tests/trend-history.log (static epochs are safe — the reporter measures within-segment deltas, never against now). It asserts per-axis segmentation at a reset boundary, a login_at change, and an account change; per-account grouping reuniting a weekly trajectory across a switch-back; a spike-then-correction flagged and excluded rather than averaged; token deltas and weekly headroom on the report; and the fail-closed exits (knob unset / history missing → 2). No fixture pair owed — neither script is a gate.

agent-budget-guard.sh is a hook, not a gate, so it speaks exit-2 + hook JSON rather than the gate output contract — and like the verdict it ships a decision-table runner beside the verdict tests: usage-tests/budget-guard-cases.tsv pairs an expected action (block/advise) with the same snapshot knobs (action pct age_off reset_off cred_age desc, pct=UNREADABLE for the no-snapshot case), and bin/run-budget-guard-tests.sh drives the template with each injected snapshot — feeding an Agent hook JSON on stdin, pointing DELEGATION_KIT_USAGE_FILE at the generated fixture — and classifies the result: PAUSE → block (exit 2, verdict on stderr), OK/RESET-OK/STALE/unreadable → advise (exit 0, verdict in additionalContext). The block branch carries its firing (PAUSE) and non-firing (the four advise cases) — the fixture-pair discipline again — and every case asserts the live verdict text rides the output so a memory-quoted percentage cannot be the acting source.

agent-dispatch-guard.sh takes its own decision-table lane rather than guard-kit’s guard-tests/ (keyed on a command, or a to+message — neither can express a dispatch’s parameters, the same reason the budget guard has its own runner) or the budget guard’s table (a different payload grammar again): usage-tests/dispatch-guard-cases.tsv pairs an expected outcome (block/advise/fallthrough) with subagent_type isolation nested desc, and bin/run-dispatch-guard-tests.sh drives the template with a built Agent hook payload — tool_input.subagent_type/.isolation, and, for a nested case, the top-level agent_id/agent_type the harness only sets inside a subagent — classifying block (exit 2) / advise (exit 0, additionalContext present) / fallthrough (exit 0, empty stdout). The table fixes one roster for its whole run, DELEGATION_KIT_READONLY_TYPES=(ro-type), supplied through a generated config file and DELEGATION_KIT_CONFIG_FILE; a subagent_type prefixed noroster: runs instead with no config file at all (an empty declare), stripped before it reaches the guard — the row that proves D2 is inert on the roster’s absence, not merely on this dispatch’s type missing from a populated one. Every rule carries a firing and a non-firing case (the fixture-pair discipline, transplanted): D1 — a fork blocks, a typed dispatch under an empty roster does not; D2 — a rostered type without isolation blocks, the same type with isolation: worktree does not, an unrostered type under a populated roster does not, and the empty-roster case does not; D3 — a payload carrying agent_id advises, one without it falls through. The degradation posture gets its own case rather than being asserted only in prose: an unparseable payload (subagent_type UNPARSEABLE, sent as invalid JSON on stdin) advises rather than blocks — the assertion that makes delta 6’s fail-open-but-loud posture testable, not merely stated, and the one case this lane must not drop for brevity. The runner strips ambient DELEGATION_KIT_* at every invocation, the discipline run-budget-guard-tests.sh already uses, so a consumer’s live roster or config-file pointer cannot leak into the fixture.

smoke/install.sh copies the templates and bin/ tools into the scratch consumer, registers the tamper gate, and drives one crafted snapshot through usage-verdict asserting a verdict — self-verifying install. It opens with one hermetic env prelude covering the whole file: it strips the whole DELEGATION_KIT_* namespace and then sets the knobs with no per-call home — sampling off, and both pause thresholds at the defaults the 95% reading is judged against. A prelude rather than per-instance pins because instance-pinning has already missed once by construction: a pass that pinned the two knobs then biting left the third, and the survivor was the one whose failure is silent — an unpinned history path makes every smoke verdict append a synthetic sample into whatever trend log the ambient config names, indistinguishable in shape from a real operator reading. That this repo’s runs happened not to pollute was accidental, not designed: the harness runs the file with cwd = the scratch consumer root and the configured history path is relative, so the appends died with the scratch tree — hermeticity that evaporates the moment a consumer configures an absolute path or the file is invoked from a real tree. Stripping the whole namespace is the shape that cannot miss a third knob. The credentials pin stays at each invocation rather than moving into the prelude, because gate-sdk/SPEC.md §check-test-hermetic reads it there — a line-local pin is evidence a reader and a gate can both see without tracing file-level state — and the poller’s own snapshot, credentials and endpoint paths stay inline too, being genuine per-call arguments the prelude has no business pre-empting. That assertion is code-specific: the 95% guard captures the exit status and compares it to the PAUSE code, reporting the status it observed rather than claiming an outcome it did not establish, so a STALE regression cannot pass under a “did not PAUSE” message (gate-sdk/SPEC.md §check-assertion-strength, the gate for the class). It also drives the poll producer through its file:// stub seam: the happy path writes a contract-valid snapshot (asserted by driving it through usage-verdict), and a fetch failure exits non-zero leaving a pre-seeded snapshot byte-identical — the fail-soft contract, exercised end to end. smoke/violation.sh stages a gate edit co-staged with a product file in the scratch consumer and asserts the battery reds (assertion A) — the violation is craftable, so the file is mandatory (gate-sdk/SPEC.md §Consumer smoke).

Out of scope

A consumer’s validate battery (its compile/lint/test command set and rename corruption sweeps), its shared-file roster, and its width/burn anecdotes tied to specific sweeps are rule content, referenced only as marked-section examples. A task-output tailer is not shipped: such a tool hardcodes local harness paths and exists to violate the template’s Background + notification, never poll rule for debugging. The usage.txt write contract and a reference producer rendering the gauge bars and iteration@stage readout do ship (the producer’s ANSI is self-contained, pulling no external asset); a consumer’s particular statusline styling and any asset its own gauges pull are not the rendering mechanism and stay with it. The split is protocol and mechanisms here, a consumer’s rosters, batteries, and bespoke UX in its own copies.