From fcf22312d23c655cfb1ec47e33e0779a38ba74ca Mon Sep 17 00:00:00 2001 From: "vanillagreen-fleet-lanes[bot]" <328550061+vanillagreen-fleet-lanes[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:03:49 +0000 Subject: [PATCH 1/3] fix(KEN-2403): guard registered-session wake processes Address review --- .../skills/orch/references/watch-delivery.md | 2 + .claude/hooks/lane-mail-check.sh | 114 +++++++++- .codex/hooks/lane-mail-check.sh | 114 +++++++++- .pi/kendex/hooks/lane-mail-check.sh | 114 +++++++++- .../added/registered-session-wake-check.md | 1 + hooks/README.md | 2 +- hooks/lane-mail-check.sh | 114 +++++++++- hooks/tests/lane-mail-check-wake.test.sh | 199 ++++++++++++++++++ hooks/tests/lane-mail-check.test.sh | 16 +- hooks/tests/lib/lane-mail-world.sh | 138 +++++++++++- skills/orch/references/watch-delivery.md | 2 + 11 files changed, 796 insertions(+), 20 deletions(-) create mode 100644 changelog.d/added/registered-session-wake-check.md create mode 100755 hooks/tests/lane-mail-check-wake.test.sh diff --git a/.agents/skills/orch/references/watch-delivery.md b/.agents/skills/orch/references/watch-delivery.md index 7f27f99cc0..a566c492f4 100644 --- a/.agents/skills/orch/references/watch-delivery.md +++ b/.agents/skills/orch/references/watch-delivery.md @@ -8,6 +8,8 @@ The harness picks the path before any launch. A harness that delivers a detached ## Repeat watch +The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. A continued turn or an unavailable check reports model context and passes. + Launch the repeat command once from the overseer's own pane by [Waiter launch](waiter-launch.md) § Launch, run path `[RUN_DIR]/watch`: output in `[RUN_DIR]/watch.log`, status in `[RUN_DIR]/watch.exit`. `[NEXT_LINE]` starts at 1 in each fresh `[RUN_DIR]`. Save the numbered follow below as `[RUN_DIR]/follow.sh` with the harness file-write tool. Every harness follows the log with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]`, one simple command that prefixes each line with its number. Arm every follow from the line after the last number handled. The launch starts the watch through the orch job runner ([job-units.md](job-units.md)), which records how it runs in `[RUN_DIR]/watch.runner`; line 1 of `[RUN_DIR]/watch.log` repeats its runner line. The watch is the launch shell, whose argv carries the run path as its own word, and that shell leads the process group of everything the watch started. One read finds it, `pgrep -f 'waiter[.][RUN_ID]/watc[h] '`, `[RUN_ID]` being the letters and digits `mktemp` put after `waiter.` in `[RUN_DIR]`, whatever spelling launched the command: the name holds no regex character where the checkout path may, the bracket keeps the read from matching the shell that runs it, the trailing space keeps it off `watch.log`, and a pid it prints is that group. Exit 0 is a live watch and exit 1 is no watch. Any other status is a failed read: report it with pgrep's stderr, and launch, stop or relaunch nothing on it. The watch's own `oversee-watch.pid`, beside the fleet state, is its claim record for the refusals `oversee-watch --help` states, not this liveness read. After a self-succession `oversee-succeed` restarts the watch from the successor pane, and the successor's own launch here takes that watch over. After each delivery and expiry, run that read first, then `test -s "[RUN_DIR]/watch.exit"`. A printed pid is a live watch. With no pid, a nonempty file ends the watch: `stopped` is the mark the stop below writes and ends oversight with no restart, and any other value follows the stop and restart rules of [oversee.md § 4](../workflows/oversee.md#4-watch-and-advance). With no pid and an empty file, the watch died without writing its status, killed together with its launch shell: report it and launch a new one in a fresh `[RUN_DIR]` under the same restart rule, then end the current follow and arm a new one on the new `[RUN_DIR]/watch.log` from line 1 (on Pi, stop the kept pid's task and keep the new pid). A follow is re-armed only while these checks read a live watch. diff --git a/.claude/hooks/lane-mail-check.sh b/.claude/hooks/lane-mail-check.sh index 8eb225acb4..ffd3fefc95 100755 --- a/.claude/hooks/lane-mail-check.sh +++ b/.claude/hooks/lane-mail-check.sh @@ -3,8 +3,8 @@ # name: lane-mail-check # event: Stop # matcher: -# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A payload carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands a lane the messages its overseer sent before the turn can end, so a directive is acted on instead of waiting for the next launch, and the same way hands the notes another repository's overseer sent to the one session the checkout's fleet record names. It also holds the turn end once the lane is near the end of its context window or its account's limit, until the lane records where it got to and exits, so the work resumes in a fresh session instead of stopping mid-round. The session running the fleet is held the same way, and hands itself over to a fresh one. A launched lane's turn end with no lane-mail ask or notice sent is held or reported to its overseer, and never judged from the lane's words; this hook's description states when each happens. +# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -13,6 +13,19 @@ set -euo pipefail +# The registered session's wake check runs at its lead's turn end, after a +# standing handoff is allowed to exit. A live repeat watch claim requires the +# numbered follow on the claim's cwd, not just the watch. Without that claim, +# a running single pass names this fleet state; a master opts in through +# ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. +# A missing wake refuses under wake=unarmed with watch-delivery's re-arm line +# or the master's start command. Continued turns and unavailable probes pass +# with keyed model context. This hook starts, restarts and kills none of them. +# Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop +# holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one +# further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. +# Gemini, Antigravity, OpenCode and Cursor provide no held wake check. + # The session-start-row, session-end-row and stop-failure-row hooks require # this one, and it names none of them back: two run on Claude Code alone, and a # companion that does not run on a harness withholds the hook requiring it @@ -65,6 +78,7 @@ READ_GAP="" # the line tool_notice_told records once that is handed over, empty where the # notice is one told at every call. TOOL_NOTICE="" +WAKE_NOTICE="" TOLD_PENDING="" HANDOFF_INSTRUCTION="" # The two commands a lane asks its overseer through, and the read of its own @@ -193,6 +207,12 @@ message() { # KEY VALUE [CAUSE] payload=invalid-json) echo "the hook payload is not valid JSON, or a field it reads is not a string; refusing rather than skipping the check" ;; + wake=unarmed) + echo "the registered session has no running wake process; arm it before ending the turn:" + ;; + wake-tools=* | wake-state=* | wake-record=* | wake-setting=* | wake-process=*) + echo "the wake check is unjudged; independent turn-end checks still run" + ;; item=invalid) echo "LANE_MAIL_ITEM is not spelled in the alphabet a work item is spelled in, ASCII letters, digits, dot, underscore and hyphen, and is never . or ..; refusing rather than reading a mailbox it does not name" ;; @@ -562,6 +582,7 @@ refuse() { # KEY VALUE [CAUSE] local text text=$(message "$@" 2>&1) [ "$ARM" != deliver ] || text="$TOOL_NOTICE$text" + [ -z "$WAKE_NOTICE" ] || text="$text$NL$WAKE_NOTICE" printf '%s\n' "$text" >&2 case "$ARM" in start | prompt) exit 0 ;; @@ -635,7 +656,7 @@ READ=$(printf '%s' "$INPUT" | jq -r --arg arm "$ARM" ' then (f | tostring) else error("not a whole number") end; def either(a; b): if a != null then a else b end; [(.stop_hook_active == true | tostring), - (if str(.agent_id) + str(.agent_type) == "" then "lead" else "subagent" end), + (if .hook_event_name == "SubagentStop" or str(.agent_id) + str(.agent_type) != "" then "subagent" else "lead" end), str(either(.transcript_path; .transcriptPath)), str(either(.session_id; .sessionId)), str(.tool_name), @@ -1131,6 +1152,87 @@ watch_live() { esac } +# session_gate owns identity. The repeat claim names no follow, so pgrep is +# the fallback for watch-delivery's follow on its cwd. Single passes wake on +# exit; a master without a claim opts in through orch-env. No harness setting +# holds a turn on process liveness. Pending context must not skip other marks. +wake_check() { + local state record mode cwd pattern start rc answer escaped cause key + [ "$ROLE" = overseer ] || return 0 + state=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$OVERSEER_ITEM" 2>&1) || + { wake_report wake-state "$SCRIPTS/workflow-state" "$state"; return 0; } + [ -n "$state" ] || { wake_report wake-state "$SCRIPTS/workflow-state" "no fleet state path was printed"; return 0; } + record="${state%/*}/oversee-watch.pid" + rc=0 + answer=$("$BASH" -euo pipefail -c ' + . "$1" || exit 3 + if [ -e "$2" ] || [ -L "$2" ]; then + [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } + fi + if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? + if [ "$rc" -ne 0 ]; then + cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } + wake_report wake-record "$record" "$cause" + return 0 + fi + mode=${answer%%"$TAB"*} + cwd=${answer#*"$TAB"} + case "$mode" in + repeat) + [ -n "$cwd" ] || { wake_report wake-record "$record" "the live watch claim names no cwd"; return 0; } + pattern=$cwd + start='sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]' + ;; + single) + [ -x "$SCRIPTS/orch-env" ] || { wake_report wake-setting "$SCRIPTS/orch-env" "orch-env is unavailable"; return 0; } + for key in ORCH_WAKE_PROCESS ORCH_WAKE_START; do + answer=$("$SCRIPTS/orch-env" "$key" "" 2>&1) || { wake_report wake-setting "$key" "$answer"; return 0; } + case "$key" in + ORCH_WAKE_PROCESS) pattern=$answer ;; + ORCH_WAKE_START) start=$answer ;; + esac + done + if [ -n "$pattern" ]; then + # No command means no remedy that could clear a refusal. + [ -n "$start" ] || { wake_report wake-setting ORCH_WAKE_START "ORCH_WAKE_PROCESS is set but no start command is set"; return 0; } + mode=master + else + pattern=$state + fi + ;; + *) wake_report wake-record "$record" "watch claim reader returned an unknown mode"; return 0 ;; + esac + command -v pgrep >/dev/null 2>&1 || { wake_report wake-tools pgrep; return 0; } + if [ "$mode" != master ]; then + command -v sed >/dev/null 2>&1 || { wake_report wake-tools sed; return 0; } + escaped=$(printf '%s' "$pattern" | sed 's/[][\\.*^$+?(){}|]/\\&/g' 2>&1) || + { wake_report wake-process "$pattern" "$escaped"; return 0; } + case "$mode" in + repeat) pattern="follow[.]sh $escaped/tmp/waiter[.][^/]*/watch[.]log" ;; + single) pattern="oversee-watc[h].*--state[ =]$escaped([[:space:]]|$)" ;; + esac + fi + rc=0 + answer=$(pgrep -f -- "$pattern" 2>&1) || rc=$? + case "$rc" in + 0) return 0 ;; + 1) + # Empty master keys with no claim leave a hand-opened session unchecked. + [ "$mode" != single ] || return 0 + [ "$CONTINUED" != true ] || { wake_report wake unarmed "$start"; return 0; } + refuse wake unarmed "$start" + ;; + *) wake_report wake-process "$pattern" "$answer" ;; + esac +} + +# Pass only the wake check. Deliver its context with any later refusal, or +# through hand_over after the independent turn-end checks, as one JSON answer. +wake_report() { # KEY VALUE [CAUSE] + WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" +} + # The mailbox this session reads, into MAILBOX_ITEM: its own where it is a # launched lane, and otherwise the checkout's overseer mailbox, where nothing # names this session a lane, the fleet record names this session's pane @@ -2564,6 +2666,7 @@ handoff_check() { # context mark, and when: the orch skill's Oversee events § # Judgement rules, the overseer's own case. if [ "$ROLE" = overseer ]; then + wake_check # A turn that ended lifts a wall its StopFailure row recorded, and dates # the turn end oversee-watch holds the context record against. session_row @@ -2574,6 +2677,11 @@ handoff_check() { overseer_context_read || message "$FAIL_KEY" "$FAIL_VALUE" "$FAIL_CAUSE" [ "$COMPACTED" != true ] || refuse_handoff compacted auto overseer_marks + if [ -n "$WAKE_NOTICE" ]; then + printf '%s\n' "$WAKE_NOTICE" >&2 + CONTEXT_EVENT=Stop + hand_over "$WAKE_NOTICE" + fi return 0 fi diff --git a/.codex/hooks/lane-mail-check.sh b/.codex/hooks/lane-mail-check.sh index 8eb225acb4..ffd3fefc95 100755 --- a/.codex/hooks/lane-mail-check.sh +++ b/.codex/hooks/lane-mail-check.sh @@ -3,8 +3,8 @@ # name: lane-mail-check # event: Stop # matcher: -# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A payload carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands a lane the messages its overseer sent before the turn can end, so a directive is acted on instead of waiting for the next launch, and the same way hands the notes another repository's overseer sent to the one session the checkout's fleet record names. It also holds the turn end once the lane is near the end of its context window or its account's limit, until the lane records where it got to and exits, so the work resumes in a fresh session instead of stopping mid-round. The session running the fleet is held the same way, and hands itself over to a fresh one. A launched lane's turn end with no lane-mail ask or notice sent is held or reported to its overseer, and never judged from the lane's words; this hook's description states when each happens. +# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -13,6 +13,19 @@ set -euo pipefail +# The registered session's wake check runs at its lead's turn end, after a +# standing handoff is allowed to exit. A live repeat watch claim requires the +# numbered follow on the claim's cwd, not just the watch. Without that claim, +# a running single pass names this fleet state; a master opts in through +# ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. +# A missing wake refuses under wake=unarmed with watch-delivery's re-arm line +# or the master's start command. Continued turns and unavailable probes pass +# with keyed model context. This hook starts, restarts and kills none of them. +# Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop +# holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one +# further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. +# Gemini, Antigravity, OpenCode and Cursor provide no held wake check. + # The session-start-row, session-end-row and stop-failure-row hooks require # this one, and it names none of them back: two run on Claude Code alone, and a # companion that does not run on a harness withholds the hook requiring it @@ -65,6 +78,7 @@ READ_GAP="" # the line tool_notice_told records once that is handed over, empty where the # notice is one told at every call. TOOL_NOTICE="" +WAKE_NOTICE="" TOLD_PENDING="" HANDOFF_INSTRUCTION="" # The two commands a lane asks its overseer through, and the read of its own @@ -193,6 +207,12 @@ message() { # KEY VALUE [CAUSE] payload=invalid-json) echo "the hook payload is not valid JSON, or a field it reads is not a string; refusing rather than skipping the check" ;; + wake=unarmed) + echo "the registered session has no running wake process; arm it before ending the turn:" + ;; + wake-tools=* | wake-state=* | wake-record=* | wake-setting=* | wake-process=*) + echo "the wake check is unjudged; independent turn-end checks still run" + ;; item=invalid) echo "LANE_MAIL_ITEM is not spelled in the alphabet a work item is spelled in, ASCII letters, digits, dot, underscore and hyphen, and is never . or ..; refusing rather than reading a mailbox it does not name" ;; @@ -562,6 +582,7 @@ refuse() { # KEY VALUE [CAUSE] local text text=$(message "$@" 2>&1) [ "$ARM" != deliver ] || text="$TOOL_NOTICE$text" + [ -z "$WAKE_NOTICE" ] || text="$text$NL$WAKE_NOTICE" printf '%s\n' "$text" >&2 case "$ARM" in start | prompt) exit 0 ;; @@ -635,7 +656,7 @@ READ=$(printf '%s' "$INPUT" | jq -r --arg arm "$ARM" ' then (f | tostring) else error("not a whole number") end; def either(a; b): if a != null then a else b end; [(.stop_hook_active == true | tostring), - (if str(.agent_id) + str(.agent_type) == "" then "lead" else "subagent" end), + (if .hook_event_name == "SubagentStop" or str(.agent_id) + str(.agent_type) != "" then "subagent" else "lead" end), str(either(.transcript_path; .transcriptPath)), str(either(.session_id; .sessionId)), str(.tool_name), @@ -1131,6 +1152,87 @@ watch_live() { esac } +# session_gate owns identity. The repeat claim names no follow, so pgrep is +# the fallback for watch-delivery's follow on its cwd. Single passes wake on +# exit; a master without a claim opts in through orch-env. No harness setting +# holds a turn on process liveness. Pending context must not skip other marks. +wake_check() { + local state record mode cwd pattern start rc answer escaped cause key + [ "$ROLE" = overseer ] || return 0 + state=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$OVERSEER_ITEM" 2>&1) || + { wake_report wake-state "$SCRIPTS/workflow-state" "$state"; return 0; } + [ -n "$state" ] || { wake_report wake-state "$SCRIPTS/workflow-state" "no fleet state path was printed"; return 0; } + record="${state%/*}/oversee-watch.pid" + rc=0 + answer=$("$BASH" -euo pipefail -c ' + . "$1" || exit 3 + if [ -e "$2" ] || [ -L "$2" ]; then + [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } + fi + if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? + if [ "$rc" -ne 0 ]; then + cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } + wake_report wake-record "$record" "$cause" + return 0 + fi + mode=${answer%%"$TAB"*} + cwd=${answer#*"$TAB"} + case "$mode" in + repeat) + [ -n "$cwd" ] || { wake_report wake-record "$record" "the live watch claim names no cwd"; return 0; } + pattern=$cwd + start='sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]' + ;; + single) + [ -x "$SCRIPTS/orch-env" ] || { wake_report wake-setting "$SCRIPTS/orch-env" "orch-env is unavailable"; return 0; } + for key in ORCH_WAKE_PROCESS ORCH_WAKE_START; do + answer=$("$SCRIPTS/orch-env" "$key" "" 2>&1) || { wake_report wake-setting "$key" "$answer"; return 0; } + case "$key" in + ORCH_WAKE_PROCESS) pattern=$answer ;; + ORCH_WAKE_START) start=$answer ;; + esac + done + if [ -n "$pattern" ]; then + # No command means no remedy that could clear a refusal. + [ -n "$start" ] || { wake_report wake-setting ORCH_WAKE_START "ORCH_WAKE_PROCESS is set but no start command is set"; return 0; } + mode=master + else + pattern=$state + fi + ;; + *) wake_report wake-record "$record" "watch claim reader returned an unknown mode"; return 0 ;; + esac + command -v pgrep >/dev/null 2>&1 || { wake_report wake-tools pgrep; return 0; } + if [ "$mode" != master ]; then + command -v sed >/dev/null 2>&1 || { wake_report wake-tools sed; return 0; } + escaped=$(printf '%s' "$pattern" | sed 's/[][\\.*^$+?(){}|]/\\&/g' 2>&1) || + { wake_report wake-process "$pattern" "$escaped"; return 0; } + case "$mode" in + repeat) pattern="follow[.]sh $escaped/tmp/waiter[.][^/]*/watch[.]log" ;; + single) pattern="oversee-watc[h].*--state[ =]$escaped([[:space:]]|$)" ;; + esac + fi + rc=0 + answer=$(pgrep -f -- "$pattern" 2>&1) || rc=$? + case "$rc" in + 0) return 0 ;; + 1) + # Empty master keys with no claim leave a hand-opened session unchecked. + [ "$mode" != single ] || return 0 + [ "$CONTINUED" != true ] || { wake_report wake unarmed "$start"; return 0; } + refuse wake unarmed "$start" + ;; + *) wake_report wake-process "$pattern" "$answer" ;; + esac +} + +# Pass only the wake check. Deliver its context with any later refusal, or +# through hand_over after the independent turn-end checks, as one JSON answer. +wake_report() { # KEY VALUE [CAUSE] + WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" +} + # The mailbox this session reads, into MAILBOX_ITEM: its own where it is a # launched lane, and otherwise the checkout's overseer mailbox, where nothing # names this session a lane, the fleet record names this session's pane @@ -2564,6 +2666,7 @@ handoff_check() { # context mark, and when: the orch skill's Oversee events § # Judgement rules, the overseer's own case. if [ "$ROLE" = overseer ]; then + wake_check # A turn that ended lifts a wall its StopFailure row recorded, and dates # the turn end oversee-watch holds the context record against. session_row @@ -2574,6 +2677,11 @@ handoff_check() { overseer_context_read || message "$FAIL_KEY" "$FAIL_VALUE" "$FAIL_CAUSE" [ "$COMPACTED" != true ] || refuse_handoff compacted auto overseer_marks + if [ -n "$WAKE_NOTICE" ]; then + printf '%s\n' "$WAKE_NOTICE" >&2 + CONTEXT_EVENT=Stop + hand_over "$WAKE_NOTICE" + fi return 0 fi diff --git a/.pi/kendex/hooks/lane-mail-check.sh b/.pi/kendex/hooks/lane-mail-check.sh index 8eb225acb4..ffd3fefc95 100755 --- a/.pi/kendex/hooks/lane-mail-check.sh +++ b/.pi/kendex/hooks/lane-mail-check.sh @@ -3,8 +3,8 @@ # name: lane-mail-check # event: Stop # matcher: -# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A payload carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands a lane the messages its overseer sent before the turn can end, so a directive is acted on instead of waiting for the next launch, and the same way hands the notes another repository's overseer sent to the one session the checkout's fleet record names. It also holds the turn end once the lane is near the end of its context window or its account's limit, until the lane records where it got to and exits, so the work resumes in a fresh session instead of stopping mid-round. The session running the fleet is held the same way, and hands itself over to a fresh one. A launched lane's turn end with no lane-mail ask or notice sent is held or reported to its overseer, and never judged from the lane's words; this hook's description states when each happens. +# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -13,6 +13,19 @@ set -euo pipefail +# The registered session's wake check runs at its lead's turn end, after a +# standing handoff is allowed to exit. A live repeat watch claim requires the +# numbered follow on the claim's cwd, not just the watch. Without that claim, +# a running single pass names this fleet state; a master opts in through +# ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. +# A missing wake refuses under wake=unarmed with watch-delivery's re-arm line +# or the master's start command. Continued turns and unavailable probes pass +# with keyed model context. This hook starts, restarts and kills none of them. +# Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop +# holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one +# further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. +# Gemini, Antigravity, OpenCode and Cursor provide no held wake check. + # The session-start-row, session-end-row and stop-failure-row hooks require # this one, and it names none of them back: two run on Claude Code alone, and a # companion that does not run on a harness withholds the hook requiring it @@ -65,6 +78,7 @@ READ_GAP="" # the line tool_notice_told records once that is handed over, empty where the # notice is one told at every call. TOOL_NOTICE="" +WAKE_NOTICE="" TOLD_PENDING="" HANDOFF_INSTRUCTION="" # The two commands a lane asks its overseer through, and the read of its own @@ -193,6 +207,12 @@ message() { # KEY VALUE [CAUSE] payload=invalid-json) echo "the hook payload is not valid JSON, or a field it reads is not a string; refusing rather than skipping the check" ;; + wake=unarmed) + echo "the registered session has no running wake process; arm it before ending the turn:" + ;; + wake-tools=* | wake-state=* | wake-record=* | wake-setting=* | wake-process=*) + echo "the wake check is unjudged; independent turn-end checks still run" + ;; item=invalid) echo "LANE_MAIL_ITEM is not spelled in the alphabet a work item is spelled in, ASCII letters, digits, dot, underscore and hyphen, and is never . or ..; refusing rather than reading a mailbox it does not name" ;; @@ -562,6 +582,7 @@ refuse() { # KEY VALUE [CAUSE] local text text=$(message "$@" 2>&1) [ "$ARM" != deliver ] || text="$TOOL_NOTICE$text" + [ -z "$WAKE_NOTICE" ] || text="$text$NL$WAKE_NOTICE" printf '%s\n' "$text" >&2 case "$ARM" in start | prompt) exit 0 ;; @@ -635,7 +656,7 @@ READ=$(printf '%s' "$INPUT" | jq -r --arg arm "$ARM" ' then (f | tostring) else error("not a whole number") end; def either(a; b): if a != null then a else b end; [(.stop_hook_active == true | tostring), - (if str(.agent_id) + str(.agent_type) == "" then "lead" else "subagent" end), + (if .hook_event_name == "SubagentStop" or str(.agent_id) + str(.agent_type) != "" then "subagent" else "lead" end), str(either(.transcript_path; .transcriptPath)), str(either(.session_id; .sessionId)), str(.tool_name), @@ -1131,6 +1152,87 @@ watch_live() { esac } +# session_gate owns identity. The repeat claim names no follow, so pgrep is +# the fallback for watch-delivery's follow on its cwd. Single passes wake on +# exit; a master without a claim opts in through orch-env. No harness setting +# holds a turn on process liveness. Pending context must not skip other marks. +wake_check() { + local state record mode cwd pattern start rc answer escaped cause key + [ "$ROLE" = overseer ] || return 0 + state=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$OVERSEER_ITEM" 2>&1) || + { wake_report wake-state "$SCRIPTS/workflow-state" "$state"; return 0; } + [ -n "$state" ] || { wake_report wake-state "$SCRIPTS/workflow-state" "no fleet state path was printed"; return 0; } + record="${state%/*}/oversee-watch.pid" + rc=0 + answer=$("$BASH" -euo pipefail -c ' + . "$1" || exit 3 + if [ -e "$2" ] || [ -L "$2" ]; then + [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } + fi + if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? + if [ "$rc" -ne 0 ]; then + cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } + wake_report wake-record "$record" "$cause" + return 0 + fi + mode=${answer%%"$TAB"*} + cwd=${answer#*"$TAB"} + case "$mode" in + repeat) + [ -n "$cwd" ] || { wake_report wake-record "$record" "the live watch claim names no cwd"; return 0; } + pattern=$cwd + start='sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]' + ;; + single) + [ -x "$SCRIPTS/orch-env" ] || { wake_report wake-setting "$SCRIPTS/orch-env" "orch-env is unavailable"; return 0; } + for key in ORCH_WAKE_PROCESS ORCH_WAKE_START; do + answer=$("$SCRIPTS/orch-env" "$key" "" 2>&1) || { wake_report wake-setting "$key" "$answer"; return 0; } + case "$key" in + ORCH_WAKE_PROCESS) pattern=$answer ;; + ORCH_WAKE_START) start=$answer ;; + esac + done + if [ -n "$pattern" ]; then + # No command means no remedy that could clear a refusal. + [ -n "$start" ] || { wake_report wake-setting ORCH_WAKE_START "ORCH_WAKE_PROCESS is set but no start command is set"; return 0; } + mode=master + else + pattern=$state + fi + ;; + *) wake_report wake-record "$record" "watch claim reader returned an unknown mode"; return 0 ;; + esac + command -v pgrep >/dev/null 2>&1 || { wake_report wake-tools pgrep; return 0; } + if [ "$mode" != master ]; then + command -v sed >/dev/null 2>&1 || { wake_report wake-tools sed; return 0; } + escaped=$(printf '%s' "$pattern" | sed 's/[][\\.*^$+?(){}|]/\\&/g' 2>&1) || + { wake_report wake-process "$pattern" "$escaped"; return 0; } + case "$mode" in + repeat) pattern="follow[.]sh $escaped/tmp/waiter[.][^/]*/watch[.]log" ;; + single) pattern="oversee-watc[h].*--state[ =]$escaped([[:space:]]|$)" ;; + esac + fi + rc=0 + answer=$(pgrep -f -- "$pattern" 2>&1) || rc=$? + case "$rc" in + 0) return 0 ;; + 1) + # Empty master keys with no claim leave a hand-opened session unchecked. + [ "$mode" != single ] || return 0 + [ "$CONTINUED" != true ] || { wake_report wake unarmed "$start"; return 0; } + refuse wake unarmed "$start" + ;; + *) wake_report wake-process "$pattern" "$answer" ;; + esac +} + +# Pass only the wake check. Deliver its context with any later refusal, or +# through hand_over after the independent turn-end checks, as one JSON answer. +wake_report() { # KEY VALUE [CAUSE] + WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" +} + # The mailbox this session reads, into MAILBOX_ITEM: its own where it is a # launched lane, and otherwise the checkout's overseer mailbox, where nothing # names this session a lane, the fleet record names this session's pane @@ -2564,6 +2666,7 @@ handoff_check() { # context mark, and when: the orch skill's Oversee events § # Judgement rules, the overseer's own case. if [ "$ROLE" = overseer ]; then + wake_check # A turn that ended lifts a wall its StopFailure row recorded, and dates # the turn end oversee-watch holds the context record against. session_row @@ -2574,6 +2677,11 @@ handoff_check() { overseer_context_read || message "$FAIL_KEY" "$FAIL_VALUE" "$FAIL_CAUSE" [ "$COMPACTED" != true ] || refuse_handoff compacted auto overseer_marks + if [ -n "$WAKE_NOTICE" ]; then + printf '%s\n' "$WAKE_NOTICE" >&2 + CONTEXT_EVENT=Stop + hand_over "$WAKE_NOTICE" + fi return 0 fi diff --git a/changelog.d/added/registered-session-wake-check.md b/changelog.d/added/registered-session-wake-check.md new file mode 100644 index 0000000000..68c53e29bb --- /dev/null +++ b/changelog.d/added/registered-session-wake-check.md @@ -0,0 +1 @@ +- Registered overseers and configured masters must arm their wake process before ending a fresh lead turn. The refusal prints the start command. diff --git a/hooks/README.md b/hooks/README.md index 27f4ac40c9..0d1ef8a974 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -11,7 +11,7 @@ The catalog's hooks, one script each. `crates/core/tests/hooks_readme.rs` render - `block-worktree-refresh`: Stops a kendex command that writes a project from inside a linked git worktree, where the write would land somewhere the command does not name. - `command-safety`: Refuses shell commands matching a project's declared deny pattern, and every command while its settings file cannot be read. - `doc-drift-check`: Stops an agent at the end of its turn when documents covering the code it changed did not change or an architecture topic names a path that does not exist, and hands it the list. Where some topic declares a Covers entry, changed code with no covering document is named too. -- `lane-mail-check`: Hands a lane the messages its overseer sent before the turn can end, so a directive is acted on instead of waiting for the next launch, and the same way hands the notes another repository's overseer sent to the one session the checkout's fleet record names. It also holds the turn end once the lane is near the end of its context window or its account's limit, until the lane records where it got to and exits, so the work resumes in a fresh session instead of stopping mid-round. The session running the fleet is held the same way, and hands itself over to a fresh one. A launched lane's turn end with no lane-mail ask or notice sent is held or reported to its overseer, and never judged from the lane's words; this hook's description states when each happens. +- `lane-mail-check`: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. - `lane-mail-compact`: Marks a Copilot lane or overseer for handoff when Copilot starts compacting it automatically before its context reading could hand it off, so its next turn end holds it until it hands the work to a fresh session. - `lane-mail-deliver`: Hands a working lane the messages its overseer sent as soon as a tool call finishes, and the notes another repository's overseer sent to the one session the checkout's fleet record names. It also tells the session running the fleet, at its next tool call, that it has used enough of its context to hand over. - `lane-mail-halt`: Stops a lane at its next tool call when its overseer sends a halt, until the lane reads it, and refuses the harness question tool in a lane, naming lane mail as the route. diff --git a/hooks/lane-mail-check.sh b/hooks/lane-mail-check.sh index 8eb225acb4..ffd3fefc95 100755 --- a/hooks/lane-mail-check.sh +++ b/hooks/lane-mail-check.sh @@ -3,8 +3,8 @@ # name: lane-mail-check # event: Stop # matcher: -# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A payload carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands a lane the messages its overseer sent before the turn can end, so a directive is acted on instead of waiting for the next launch, and the same way hands the notes another repository's overseer sent to the one session the checkout's fleet record names. It also holds the turn end once the lane is near the end of its context window or its account's limit, until the lane records where it got to and exits, so the work resumes in a fresh session instead of stopping mid-round. The session running the fleet is held the same way, and hands itself over to a fresh one. A launched lane's turn end with no lane-mail ask or notice sent is held or reported to its overseer, and never judged from the lane's words; this hook's description states when each happens. +# description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -13,6 +13,19 @@ set -euo pipefail +# The registered session's wake check runs at its lead's turn end, after a +# standing handoff is allowed to exit. A live repeat watch claim requires the +# numbered follow on the claim's cwd, not just the watch. Without that claim, +# a running single pass names this fleet state; a master opts in through +# ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. +# A missing wake refuses under wake=unarmed with watch-delivery's re-arm line +# or the master's start command. Continued turns and unavailable probes pass +# with keyed model context. This hook starts, restarts and kills none of them. +# Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop +# holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one +# further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. +# Gemini, Antigravity, OpenCode and Cursor provide no held wake check. + # The session-start-row, session-end-row and stop-failure-row hooks require # this one, and it names none of them back: two run on Claude Code alone, and a # companion that does not run on a harness withholds the hook requiring it @@ -65,6 +78,7 @@ READ_GAP="" # the line tool_notice_told records once that is handed over, empty where the # notice is one told at every call. TOOL_NOTICE="" +WAKE_NOTICE="" TOLD_PENDING="" HANDOFF_INSTRUCTION="" # The two commands a lane asks its overseer through, and the read of its own @@ -193,6 +207,12 @@ message() { # KEY VALUE [CAUSE] payload=invalid-json) echo "the hook payload is not valid JSON, or a field it reads is not a string; refusing rather than skipping the check" ;; + wake=unarmed) + echo "the registered session has no running wake process; arm it before ending the turn:" + ;; + wake-tools=* | wake-state=* | wake-record=* | wake-setting=* | wake-process=*) + echo "the wake check is unjudged; independent turn-end checks still run" + ;; item=invalid) echo "LANE_MAIL_ITEM is not spelled in the alphabet a work item is spelled in, ASCII letters, digits, dot, underscore and hyphen, and is never . or ..; refusing rather than reading a mailbox it does not name" ;; @@ -562,6 +582,7 @@ refuse() { # KEY VALUE [CAUSE] local text text=$(message "$@" 2>&1) [ "$ARM" != deliver ] || text="$TOOL_NOTICE$text" + [ -z "$WAKE_NOTICE" ] || text="$text$NL$WAKE_NOTICE" printf '%s\n' "$text" >&2 case "$ARM" in start | prompt) exit 0 ;; @@ -635,7 +656,7 @@ READ=$(printf '%s' "$INPUT" | jq -r --arg arm "$ARM" ' then (f | tostring) else error("not a whole number") end; def either(a; b): if a != null then a else b end; [(.stop_hook_active == true | tostring), - (if str(.agent_id) + str(.agent_type) == "" then "lead" else "subagent" end), + (if .hook_event_name == "SubagentStop" or str(.agent_id) + str(.agent_type) != "" then "subagent" else "lead" end), str(either(.transcript_path; .transcriptPath)), str(either(.session_id; .sessionId)), str(.tool_name), @@ -1131,6 +1152,87 @@ watch_live() { esac } +# session_gate owns identity. The repeat claim names no follow, so pgrep is +# the fallback for watch-delivery's follow on its cwd. Single passes wake on +# exit; a master without a claim opts in through orch-env. No harness setting +# holds a turn on process liveness. Pending context must not skip other marks. +wake_check() { + local state record mode cwd pattern start rc answer escaped cause key + [ "$ROLE" = overseer ] || return 0 + state=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$OVERSEER_ITEM" 2>&1) || + { wake_report wake-state "$SCRIPTS/workflow-state" "$state"; return 0; } + [ -n "$state" ] || { wake_report wake-state "$SCRIPTS/workflow-state" "no fleet state path was printed"; return 0; } + record="${state%/*}/oversee-watch.pid" + rc=0 + answer=$("$BASH" -euo pipefail -c ' + . "$1" || exit 3 + if [ -e "$2" ] || [ -L "$2" ]; then + [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } + fi + if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? + if [ "$rc" -ne 0 ]; then + cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } + wake_report wake-record "$record" "$cause" + return 0 + fi + mode=${answer%%"$TAB"*} + cwd=${answer#*"$TAB"} + case "$mode" in + repeat) + [ -n "$cwd" ] || { wake_report wake-record "$record" "the live watch claim names no cwd"; return 0; } + pattern=$cwd + start='sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]' + ;; + single) + [ -x "$SCRIPTS/orch-env" ] || { wake_report wake-setting "$SCRIPTS/orch-env" "orch-env is unavailable"; return 0; } + for key in ORCH_WAKE_PROCESS ORCH_WAKE_START; do + answer=$("$SCRIPTS/orch-env" "$key" "" 2>&1) || { wake_report wake-setting "$key" "$answer"; return 0; } + case "$key" in + ORCH_WAKE_PROCESS) pattern=$answer ;; + ORCH_WAKE_START) start=$answer ;; + esac + done + if [ -n "$pattern" ]; then + # No command means no remedy that could clear a refusal. + [ -n "$start" ] || { wake_report wake-setting ORCH_WAKE_START "ORCH_WAKE_PROCESS is set but no start command is set"; return 0; } + mode=master + else + pattern=$state + fi + ;; + *) wake_report wake-record "$record" "watch claim reader returned an unknown mode"; return 0 ;; + esac + command -v pgrep >/dev/null 2>&1 || { wake_report wake-tools pgrep; return 0; } + if [ "$mode" != master ]; then + command -v sed >/dev/null 2>&1 || { wake_report wake-tools sed; return 0; } + escaped=$(printf '%s' "$pattern" | sed 's/[][\\.*^$+?(){}|]/\\&/g' 2>&1) || + { wake_report wake-process "$pattern" "$escaped"; return 0; } + case "$mode" in + repeat) pattern="follow[.]sh $escaped/tmp/waiter[.][^/]*/watch[.]log" ;; + single) pattern="oversee-watc[h].*--state[ =]$escaped([[:space:]]|$)" ;; + esac + fi + rc=0 + answer=$(pgrep -f -- "$pattern" 2>&1) || rc=$? + case "$rc" in + 0) return 0 ;; + 1) + # Empty master keys with no claim leave a hand-opened session unchecked. + [ "$mode" != single ] || return 0 + [ "$CONTINUED" != true ] || { wake_report wake unarmed "$start"; return 0; } + refuse wake unarmed "$start" + ;; + *) wake_report wake-process "$pattern" "$answer" ;; + esac +} + +# Pass only the wake check. Deliver its context with any later refusal, or +# through hand_over after the independent turn-end checks, as one JSON answer. +wake_report() { # KEY VALUE [CAUSE] + WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" +} + # The mailbox this session reads, into MAILBOX_ITEM: its own where it is a # launched lane, and otherwise the checkout's overseer mailbox, where nothing # names this session a lane, the fleet record names this session's pane @@ -2564,6 +2666,7 @@ handoff_check() { # context mark, and when: the orch skill's Oversee events § # Judgement rules, the overseer's own case. if [ "$ROLE" = overseer ]; then + wake_check # A turn that ended lifts a wall its StopFailure row recorded, and dates # the turn end oversee-watch holds the context record against. session_row @@ -2574,6 +2677,11 @@ handoff_check() { overseer_context_read || message "$FAIL_KEY" "$FAIL_VALUE" "$FAIL_CAUSE" [ "$COMPACTED" != true ] || refuse_handoff compacted auto overseer_marks + if [ -n "$WAKE_NOTICE" ]; then + printf '%s\n' "$WAKE_NOTICE" >&2 + CONTEXT_EVENT=Stop + hand_over "$WAKE_NOTICE" + fi return 0 fi diff --git a/hooks/tests/lane-mail-check-wake.test.sh b/hooks/tests/lane-mail-check-wake.test.sh new file mode 100755 index 0000000000..8f3deeb9f5 --- /dev/null +++ b/hooks/tests/lane-mail-check-wake.test.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# The registered session's wake, over real processes with the follow and +# single-pass argv watch-delivery produces. The harness rows drive the hook's +# existing lead identity and refusal formats, not a second wake implementation. +# HOOK_UNDER_TEST selects the planted copy for each must-fail control. +set -euo pipefail + +# shellcheck source=lib/lane-mail-world.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/lane-mail-world.sh" + +SELECTED="" +if [ "${1:-}" = --wake-row ]; then SELECTED=$2; fi +REARM='sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]' +MASTER_START='run-master-wake --resume' + +# CASE|HARNESS|EXIT|STDERR-KEY|DECISION|CONTEXT-KEY|COMMAND-COUNT[|WAKE-NOTICE] +# Stop, Codex Stop and Copilot agentStop are held. Pi's carrier can make only +# one further model request, so these are hook-format tests, not proof of an +# indefinitely held Pi turn. Unsupported harnesses dispatch no hook at all. +while IFS='|' read -r scenario harness want_rc first decision context commands wake_notice; do + [ -z "$SELECTED" ] || [ "$scenario:$harness" = "$SELECTED" ] || continue + stop_wake_processes + wake_overseer "wake-$scenario-$harness" "$harness" + command=$REARM + payload='{"session_id":"s1","stop_hook_active":false}' + if [ "$harness" = copilot ]; then + payload=$(jq -nc --arg p "$COP_HOME/session-state/s1/events.jsonl" '{sessionId:"s1",transcriptPath:$p,timestamp:1}') || exit 1 + fi + case "$scenario" in + armed) start_wake_watch repeat; start_follow ;; + no-follow | continuation | continuation-mark | agent-id | agent-type | subagent-stop | other-session | no-pgrep | probe-error | probe-error-mark | record-cwd | record-armed | missing-cwd | unreadable-record) + start_wake_watch repeat + ;; + single) start_wake_watch single ;; + master-live | master-dead | master-continuation | incomplete-master) + command=$MASTER_START + if [ "$scenario" = master-live ]; then + wake_process "$TMP_ROOT/master-wake" --fleet "$LANE" + CALL_ENV+=("ORCH_WAKE_PROCESS=master-wake --fleet $LANE") + else + CALL_ENV+=("ORCH_WAKE_PROCESS=master-wake --fleet $LANE") + fi + CALL_ENV+=("ORCH_WAKE_START=$MASTER_START") + ;; + empty-keys | absent-keys) command="" ;; + no-record) + command="" + (cd -- "$LANE" && "$REPO_ROOT/skills/orch/scripts/workflow-state" set oversee overseer null >/dev/null) + CALL_ENV+=(ORCH_WAKE_PROCESS=never-matched "ORCH_WAKE_START=$MASTER_START") + ;; + *) echo "wake-suite: case=unknown value=$scenario" >&2; exit 1 ;; + esac + case "$scenario" in + no-follow) + # A matched master setting cannot replace a repeat follow. The live + # watch is also deliberately left running throughout the refusal. + CALL_ENV+=("ORCH_WAKE_PROCESS=oversee-watc[h]" "ORCH_WAKE_START=$MASTER_START") + ;; + continuation | continuation-mark | master-continuation) payload=$(jq -c '. + {stop_hook_active:true}' <<< "$payload") || exit 1 ;; + agent-id) payload=$(jq -c '. + {agent_id:"child"}' <<< "$payload") || exit 1 ;; + agent-type) payload=$(jq -c '. + {agent_type:"worker"}' <<< "$payload") || exit 1 ;; + subagent-stop) payload=$(jq -c '. + {hook_event_name:"SubagentStop"}' <<< "$payload") || exit 1 ;; + other-session) CALL_ENV+=(TMUX_PANE=%3) ;; + no-pgrep) wake_without_pgrep ;; + probe-error | probe-error-mark) + printf '#!/bin/sh\necho "probe failed" >&2\nexit 2\n' > "$TMUX_BIN/pgrep" + chmod +x "$TMUX_BIN/pgrep" + ;; + record-cwd | record-armed) + # A follow on the session root does not arm a watch whose claim names + # another cwd. The path includes regex characters and a space. + stop_wake_processes + start_wake_watch repeat "$TMP_ROOT/watch [cwd].root" + if [ "$scenario" = record-armed ]; then start_follow "$TMP_ROOT/watch [cwd].root"; else start_follow; fi + ;; + missing-cwd) + sed '/^cwd=/d' "${WAKE_STATE%/*}/oversee-watch.pid" > "$TMP_ROOT/claim" + mv -- "$TMP_ROOT/claim" "${WAKE_STATE%/*}/oversee-watch.pid" + ;; + unreadable-record) + rm -- "${WAKE_STATE%/*}/oversee-watch.pid" + mkdir -- "${WAKE_STATE%/*}/oversee-watch.pid" + ;; + empty-keys) CALL_ENV+=(ORCH_WAKE_PROCESS= ORCH_WAKE_START=) ;; + incomplete-master) CALL_ENV+=(ORCH_WAKE_START=); command="" ;; + esac + if [ -n "$wake_notice" ]; then + # An unarmed continuation or failed wake probe coincides with a handoff + # mark. Its Stop row and context record must still land. + judge_says "$CONTEXT_MARK_LINE" + wake_row_process_table + mkdir -p "$LANE/tmp/lane-mail/overseer" + fi + case "$context" in + record) context="wake-record=${WAKE_STATE%/*}/oversee-watch.pid" ;; + process) + # The process error reports the actual query under its own key. Do + # not pin the prose pgrep wrote below it. + context="wake-process=follow[.]sh ${LANE//./\\.}/tmp/waiter[.][^/]*/watch[.]log" + ;; + esac + case "$first" in record | process) first=${context#lane-mail-check: } ;; esac + [ "$first" = - ] || first="lane-mail-check: $first" + [ "$context" = - ] || context="lane-mail-check: $context" + run_payload "$payload" + got=$(wake_observation "$command") || exit 1 + want="RC=$want_rc first=$first decision=$decision context=$context command=$commands" + if [ -n "$wake_notice" ]; then + row=$(jq -rs 'map([.event, .harness] | join(":")) | join(",")' \ + "$LANE/tmp/lane-mail/overseer/session-$OVERSEER_SERVER-${OVERSEER_PANE#%}.jsonl") || exit 1 + record=$(jq -r '[.harness, .session_id, .pane_key, (.tokens | tostring)] | join(":")' \ + "$LANE/tmp/lane-mail/overseer/context.json") || exit 1 + notices=$(grep -c "^lane-mail-check: $wake_notice" "$ERR_FILE") || exit 1 + objects=$(jq -s 'length' "$TMP_ROOT/stdout") || exit 1 + case "$harness" in + copilot) + # The wake warning joins the block reason, never a second JSON reply. + reason=$(jq -r '.reason | split("\n") | map(select(startswith("lane-mail-check: wake"))) | length' "$TMP_ROOT/stdout") || exit 1 + output="objects=1 reason=1" + tokens=100000 + ;; + *) reason=-; output="objects=0 reason=-"; tokens=null ;; + esac + got="$got row=$row record=$record notices=$notices objects=$objects reason=$reason" + want="$want row=Stop:$harness record=$harness:s1:$OVERSEER_SERVER $OVERSEER_PANE:$tokens notices=1 $output" + fi + assert_eq "$got" "$want" "$scenario:$harness" + rm -f -- "$TMUX_BIN/pgrep" "$TMUX_BIN/ps" +done <<'ROWS' +armed|claude|0|-|-|-|0 +armed|codex|0|-|-|-|0 +armed|copilot|0|-|-|-|0 +armed|pi|0|-|-|-|0 +no-follow|claude|2|wake=unarmed|-|-|1 +no-follow|codex|2|wake=unarmed|-|-|1 +no-follow|copilot|0|wake=unarmed|block|-|1 +no-follow|pi|2|wake=unarmed|-|-|1 +continuation|claude|0|wake=unarmed|-|wake=unarmed|1 +continuation|codex|0|wake=unarmed|-|wake=unarmed|1 +continuation|copilot|0|wake=unarmed|-|wake=unarmed|1 +continuation|pi|0|wake=unarmed|-|wake=unarmed|1 +continuation-mark|claude|2|context=612000|-|-|1|wake=unarmed +continuation-mark|codex|2|context=612000|-|-|1|wake=unarmed +continuation-mark|copilot|0|context=612000|block|-|1|wake=unarmed +continuation-mark|pi|2|context=612000|-|-|1|wake=unarmed +probe-error-mark|claude|2|context=612000|-|-|0|wake-process= +probe-error-mark|codex|2|context=612000|-|-|0|wake-process= +probe-error-mark|copilot|0|context=612000|block|-|0|wake-process= +probe-error-mark|pi|2|context=612000|-|-|0|wake-process= +agent-id|claude|0|-|-|-|0 +agent-type|codex|0|-|-|-|0 +subagent-stop|claude|0|-|-|-|0 +other-session|claude|0|-|-|-|0 +single|claude|0|-|-|-|0 +master-live|claude|0|-|-|-|0 +master-dead|claude|2|wake=unarmed|-|-|1 +master-continuation|claude|0|wake=unarmed|-|wake=unarmed|1 +empty-keys|claude|0|-|-|-|0 +absent-keys|claude|0|-|-|-|0 +no-record|claude|0|-|-|-|0 +no-pgrep|claude|0|wake-tools=pgrep|-|wake-tools=pgrep|0 +probe-error|claude|0|process|-|process|0 +record-cwd|claude|2|wake=unarmed|-|-|1 +record-armed|claude|0|-|-|-|0 +missing-cwd|claude|0|record|-|record|0 +unreadable-record|claude|0|record|-|record|0 +incomplete-master|claude|0|wake-setting=ORCH_WAKE_START|-|wake-setting=ORCH_WAKE_START|0 +ROWS + +# Copilot's custom subagent names its own session and the lead's transcript. +# This is the measured agentStop producer, distinct from agent_id/type. +if [ -z "$SELECTED" ]; then + stop_wake_processes + wake_overseer wake-copilot-child copilot + start_wake_watch repeat + payload=$(jq -nc --arg p "$COP_HOME/session-state/s1/events.jsonl" '{sessionId:"child",transcriptPath:$p,timestamp:1}') || exit 1 + run_payload "$payload" + got=$(wake_observation "$REARM") || exit 1 + assert_eq "$got" 'RC=0 first=- decision=- context=- command=0' 'Copilot custom subagent exclusion' + + wake_control disabled no-follow:claude \ + ' refuse wake unarmed "$start"' \ + ' message wake unarmed "$start"; return 0' + wake_control continued-held continuation:claude \ + ' [ "$CONTINUED" != true ] || { wake_report wake unarmed "$start"; return 0; }' \ + ' [ "$CONTINUED" != true ] || refuse wake unarmed "$start"' + wake_control empty-held absent-keys:claude \ + ' [ "$mode" != single ] || return 0' \ + ' [ "$mode" != single ] || refuse wake unarmed "$start"' + wake_control unavailable-held no-pgrep:claude \ + ' command -v pgrep >/dev/null 2>&1 || { wake_report wake-tools pgrep; return 0; }' \ + ' command -v pgrep >/dev/null 2>&1 || refuse wake-tools pgrep' + wake_control warning-exits continuation-mark:claude \ + ' WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE"' \ + ' WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE"; exit 0' +fi + +printf '\n=== %s passed, %s failed ===\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hooks/tests/lane-mail-check.test.sh b/hooks/tests/lane-mail-check.test.sh index 9b23496dd5..d442e036b0 100755 --- a/hooks/tests/lane-mail-check.test.sh +++ b/hooks/tests/lane-mail-check.test.sh @@ -2459,7 +2459,9 @@ expect 0 - "control: without the halt check a subagent's call carrying the ackno # The agent_type read dropped, agent_id still read: a subagent the pi-hooks # carrier marks is judged as the lead. -mutant no-agent-type -e 's@str(\.agent_id) + str(\.agent_type) == ""@str(.agent_id) == ""@' +wake_mutant no-agent-type \ + ' (if .hook_event_name == "SubagentStop" or str(.agent_id) + str(.agent_type) != "" then "subagent" else "lead" end),' \ + ' (if .hook_event_name == "SubagentStop" or str(.agent_id) != "" then "subagent" else "lead" end),' new_lane control_sub_type ken-38 install_arms "$MUTANT_PATH" send KEN-38 'For the lead.' @@ -3090,13 +3092,15 @@ start_watch() { local state state="$(cd "$LANE" && "$REPO_ROOT/skills/orch/scripts/workflow-state" path oversee)" mkdir -p "${state%/*}" - bash -c 'exec -a oversee-watch sleep 600' & + env -i "PATH=$PATH" bash -c 'exec -a oversee-watch sleep 600' & FAKE_WATCH=$! - printf 'pid=%s\nstate=%s\npane=none\norigin=hand\n' "$FAKE_WATCH" "$state" > "${state%/*}/oversee-watch.pid" + printf 'pid=%s\nstate=%s\npane=none\norigin=hand\ncwd=%s\n' "$FAKE_WATCH" "$state" "$LANE" > "${state%/*}/oversee-watch.pid" + start_follow } stop_watch() { kill "$FAKE_WATCH" 2>/dev/null || : wait "$FAKE_WATCH" 2>/dev/null || : + stop_wake_processes } # Every orch library but the watch record's, linked one by one, since the # reader sources its own from the install's directory. @@ -3169,7 +3173,8 @@ expect 0 - "nor a session the fleet record does not name" named_session peer_no_mailbox state_stub path-fails stop "${SESSION_ENV[@]}" -expect 0 - "the named session with no overseer mailbox never judges the fleet state" +assert_eq "RC=$RC fleet-state=$(grep -c '^lane-mail-check: fleet-state=' "$ERR_FILE" || :)" "RC=0 fleet-state=0" \ + "the named session with no overseer mailbox never judges mail protection, independent of the wake probe" named_session peer_library_absent peer_send 'No library.' @@ -3261,7 +3266,8 @@ named_session control_peer_state "$MUTANT_PATH" peer_send 'Path unread.' state_stub path-fails stop "${SESSION_ENV[@]}" -expect 0 - "control: without the path refusal a failed state read passes the turn with the note unread" +assert_eq "RC=$RC unread=$(overseer_unread 'Path unread.')" "RC=0 unread=1" \ + "control: without the path refusal a failed state read passes mail protection and leaves the note unread" mutant peer-library-passes -e 's@^ \*) refuse fleet-state "\$SCRIPTS/lib/watch-pid.sh" .*$@ *) return 1 ;;@' named_session control_peer_library "$MUTANT_PATH" diff --git a/hooks/tests/lib/lane-mail-world.sh b/hooks/tests/lib/lane-mail-world.sh index efcab70621..82e19031f1 100644 --- a/hooks/tests/lib/lane-mail-world.sh +++ b/hooks/tests/lib/lane-mail-world.sh @@ -26,7 +26,7 @@ TMP_ROOT="$(mktemp -d)" || { echo "lane-mail-world: scratch=mktemp-failed" >&2; [[ -d $TMP_ROOT && ! -L $TMP_ROOT ]] || { echo "lane-mail-world: scratch=not-a-directory value=[$TMP_ROOT]" >&2; exit 1; } TMP_ROOT="$(cd -- "$TMP_ROOT" && pwd -P)" || { echo "lane-mail-world: scratch=resolve-failed" >&2; exit 1; } # FAKE_WATCH is the stand-in watch process the overseer mailbox rows start. -trap '[ -z "${FAKE_WATCH:-}" ] || kill "$FAKE_WATCH" 2>/dev/null || :; chmod -R u+rwx -- "${TMP_ROOT:?}" 2>/dev/null || :; rm -rf -- "${TMP_ROOT:?}"' EXIT +trap 'stop_wake_processes; [ -z "${FAKE_WATCH:-}" ] || kill "$FAKE_WATCH" 2>/dev/null || :; chmod -R u+rwx -- "${TMP_ROOT:?}" 2>/dev/null || :; rm -rf -- "${TMP_ROOT:?}"' EXIT ERR_FILE="$TMP_ROOT/stderr" # Whether this world can hold two names differing only in case. On a # case-insensitive filesystem, every macOS default one, the second name is the @@ -204,7 +204,7 @@ run_payload() { # RAW-JSON [ENV=VAL...] printf '%s' "$payload" | (cd "${CALL_DIR:-$LANE}" && env -u CLAUDE_CONFIG_DIR -u CLAUDE_PROJECT_DIR -u CODEX_HOME -u COPILOT_HOME -u LANE_MAIL_ITEM \ -u ORCH_HANDOFF_CONTEXT_PCT -u ORCH_HANDOFF_HEADROOM_PCT -u ORCH_STATE_DIR \ - -u ORCH_OVERSEER_HEADROOM_PCT -u ORCH_OVERSEER_SUCCESSION -u TMUX -u TMUX_PANE \ + -u ORCH_OVERSEER_HEADROOM_PCT -u ORCH_OVERSEER_SUCCESSION -u ORCH_WAKE_PROCESS -u ORCH_WAKE_START -u TMUX -u TMUX_PANE \ "LANES_HOME=$OFFLINE_HOME" "PI_CODING_AGENT_DIR=$OFFLINE_HOME/.pi/agent" \ DISABLE_AUTO_COMPACT=1 DISABLE_COMPACT=0 "ORCH_COMPACTION_OVERRIDES=$CODEX_COMPACTION" "ORCH_LANES_FETCH_CMD=$NO_FETCH" ${CONTEXT_PCT_ENV:+"$CONTEXT_PCT_ENV"} \ ${CALL_ENV[@]+"${CALL_ENV[@]}"} "$@" bash "$CASE_HOOK" ${ARM_ARGS[@]+"${ARM_ARGS[@]}"}) >"$TMP_ROOT/stdout" 2>"$ERR_FILE" || RC=$? @@ -496,6 +496,140 @@ new_overseer() { # NAME [PANE] [SERVER] record_overseer "${2:-$OVERSEER_PANE}" "${3:-$OVERSEER_SERVER}" } +# Real processes with the argv the wake check reads, not a pgrep answer stub. +# A FIFO holds each shell without a child process; the parent reads its ready +# handshake before the hook can inspect it. The environment is only this +# fixture's, and the owner stops and reaps every process before removal. +WAKE_PIDS=() +WAKE_SEQUENCE=0 +WAKE_PID="" +WAKE_STATE="" +wake_process() { # SCRIPT ARGS... + local script="$1" dir + shift + WAKE_SEQUENCE=$((WAKE_SEQUENCE + 1)) + dir="$TMP_ROOT/wake-process-$WAKE_SEQUENCE" + mkdir -p -- "$dir" "${script%/*}" + mkfifo "$dir/ready" "$dir/wait" + printf '%s\n' '#!/bin/sh' 'printf "ready\n" > "$WAKE_READY"' 'read -r line < "$WAKE_WAIT"' > "$script" + env -i "PATH=$PATH" "WAKE_READY=$dir/ready" "WAKE_WAIT=$dir/wait" sh "$script" "$@" & + WAKE_PID=$! + WAKE_PIDS+=("$WAKE_PID") + read -r _ < "$dir/ready" +} +stop_wake_processes() { + local pid + for pid in ${WAKE_PIDS[@]+"${WAKE_PIDS[@]}"}; do + kill "$pid" 2>/dev/null || : + wait "$pid" 2>/dev/null || : + done + WAKE_PIDS=() +} +start_follow() { # [CWD] + local cwd="${1:-$LANE}" + wake_process "$cwd/tmp/waiter.fixture/follow.sh" "$cwd/tmp/waiter.fixture/watch.log" 1 +} +start_wake_watch() { # repeat|single [CWD] + WAKE_STATE=$(cd -- "$LANE" && "$REPO_ROOT/skills/orch/scripts/workflow-state" path oversee) || return 1 + wake_process "$TMP_ROOT/watch-$WAKE_SEQUENCE/oversee-watch" --state "$WAKE_STATE" + [ "$1" = repeat ] || return 0 + printf 'pid=%s\nstate=%s\npane=none\norigin=hand\ncwd=%s\n' "$WAKE_PID" "$WAKE_STATE" "${2:-$LANE}" \ + > "${WAKE_STATE%/*}/oversee-watch.pid" +} + +# A literal one-line substitution for wake controls. It retains the message +# and changes the action, so a control cannot pass by deleting its subject. +wake_mutant() { # NAME OLD NEW + local count line + [ ! -L "$HOOK" ] || { echo "wake-mutant: source=symlink" >&2; exit 1; } + count=$(grep -Fxc -- "$2" "$HOOK") || { echo "wake-mutant: match=missing" >&2; exit 1; } + [ "$count" -eq 1 ] || { echo "wake-mutant: match=$count" >&2; exit 1; } + MUTANT_PATH="$TMP_ROOT/$1.sh" + while IFS= read -r line; do + if [ "$line" = "$2" ]; then printf '%s\n' "$3"; else printf '%s\n' "$line"; fi + done < "$HOOK" > "$MUTANT_PATH" + cmp -s -- "$HOOK" "$MUTANT_PATH" && { echo "wake-mutant: changed=no" >&2; exit 1; } + return 0 +} + +wake_overseer() { # NAME HARNESS [HOOK] + local dir + new_overseer "$1" + judge_says "$BELOW_MARK_LINE" + case "$2" in + claude) dir=.claude ;; + codex) dir=.codex ;; + copilot) + dir=.github + record_overseer "$OVERSEER_PANE" "$OVERSEER_SERVER" "$COP_HOME" + mkdir -p "$COP_HOME/session-state/s1" "$LANE/tmp/lane-mail/overseer" + : > "$COP_HOME/session-state/s1/events.jsonl" + bash -c 'set -euo pipefail; . "$1/lib/lane-context.sh" + lane_context_record "$2" copilot 100000 800000 "" s1 "$3" "" "$LANE_CONTEXT_COPILOT_CAPACITY_SOURCE"' \ + _ "$REPO_ROOT/skills/orch/scripts" "$LANE/tmp/lane-mail/overseer" "$OVERSEER_SERVER $OVERSEER_PANE" + ;; + pi) dir=.pi ;; + *) echo "wake-world: harness=unknown value=$2" >&2; exit 1 ;; + esac + if [ "$dir" != .claude ]; then + mkdir -p "$LANE/$dir/skills" + ln -s "$LANE/.claude/skills/orch" "$LANE/$dir/skills/orch" + fi + if [ "$2" = pi ]; then dir="$dir/kendex"; fi + install_hook "${3:-$HOOK}" "$LANE/$dir/hooks/lane-mail-check.sh" + CALL_ENV=("PATH=$TMUX_BIN:$PATH" TMUX=fake "TMUX_PANE=$OVERSEER_PANE" + "TMUX_SERVER_ID=$OVERSEER_SERVER" "TMUX_SERVER_START=$OVERSEER_SERVER_START" "HOME=$COP_HOME") +} + +# A refusal's key is first on stderr. A passed gap is model context instead. +# The Start value is protocol data: the command the caller must run. +wake_observation() { # START + local decision=- context=- command=0 text first answer + first=$(first_line) || return 1 + text=$(cat -- "$ERR_FILE") || return 1 + if [ -s "$TMP_ROOT/stdout" ]; then + decision=$(jq -r '.decision // "-"' "$TMP_ROOT/stdout") || return 1 + context=$(jq -r '(.additionalContext // .hookSpecificOutput.additionalContext // "-") | split("\n")[0]' "$TMP_ROOT/stdout") || return 1 + answer=$(jq -r '.additionalContext // .hookSpecificOutput.additionalContext // ""' "$TMP_ROOT/stdout") || return 1 + [ -z "$answer" ] || text=$answer + fi + [ -z "$1" ] || command=$(grep -Fc -- "$1" <<< "$text" || :) + printf 'RC=%s first=%s decision=%s context=%s command=%s' "$RC" "$first" "$decision" "$context" "$command" +} + +# A hook install with its ordinary command dependencies but no pgrep. This +# reaches the wake probe's availability branch, not a payload dependency gap. +wake_without_pgrep() { + local cmd executable bin="$TMP_ROOT/no-pgrep" + mkdir -p "$bin" + for cmd in bash jq cat git tr awk mktemp tail dirname sed ps flock date rm mv sleep basename grep realpath readlink mkdir chmod touch find head wc stat; do + if executable=$(command -v "$cmd"); then ln -s -f -- "$executable" "$bin/$cmd"; fi + done + ln -s -f -- "$TMUX_BIN/tmux" "$bin/tmux" + CALL_ENV+=("PATH=$bin") +} + +# session-rows reads ancestry through ps. This table names one top-level +# harness under the fixture's pane, and delegates all other process queries. +wake_row_process_table() { + local executable + executable=$(command -v ps) || return 1 + { + printf '#!/bin/sh\n' + printf 'if [ "$1" = -o ] && [ "$2" = ppid= ]; then printf "%%s claude\\n" %s; exit 0; fi\n' "$OVERSEER_SERVER" + printf 'exec %q "$@"\n' "$executable" + } > "$TMUX_BIN/ps" + chmod +x "$TMUX_BIN/ps" +} + +wake_control() { # NAME ROW OLD NEW + local status=0 + wake_mutant "$1" "$3" "$4" + env -i "PATH=$PATH" "HOME=$COP_HOME" "HOOK_UNDER_TEST=$MUTANT_PATH" \ + bash "$TEST_DIR/lane-mail-check-wake.test.sh" --wake-row "$2" > "$TMP_ROOT/$1.control.log" 2>&1 || status=$? + assert_eq "$status" 1 "control: $1 makes the $2 assertion red" +} + # The record that ends an overseer's refusal, written on the fleet's own item. # It names the session that wrote it in both of the two names this hook reads, # the payload's id and the pane key, and a row supplies another value for diff --git a/skills/orch/references/watch-delivery.md b/skills/orch/references/watch-delivery.md index 7f27f99cc0..a566c492f4 100644 --- a/skills/orch/references/watch-delivery.md +++ b/skills/orch/references/watch-delivery.md @@ -8,6 +8,8 @@ The harness picks the path before any launch. A harness that delivers a detached ## Repeat watch +The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. A continued turn or an unavailable check reports model context and passes. + Launch the repeat command once from the overseer's own pane by [Waiter launch](waiter-launch.md) § Launch, run path `[RUN_DIR]/watch`: output in `[RUN_DIR]/watch.log`, status in `[RUN_DIR]/watch.exit`. `[NEXT_LINE]` starts at 1 in each fresh `[RUN_DIR]`. Save the numbered follow below as `[RUN_DIR]/follow.sh` with the harness file-write tool. Every harness follows the log with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]`, one simple command that prefixes each line with its number. Arm every follow from the line after the last number handled. The launch starts the watch through the orch job runner ([job-units.md](job-units.md)), which records how it runs in `[RUN_DIR]/watch.runner`; line 1 of `[RUN_DIR]/watch.log` repeats its runner line. The watch is the launch shell, whose argv carries the run path as its own word, and that shell leads the process group of everything the watch started. One read finds it, `pgrep -f 'waiter[.][RUN_ID]/watc[h] '`, `[RUN_ID]` being the letters and digits `mktemp` put after `waiter.` in `[RUN_DIR]`, whatever spelling launched the command: the name holds no regex character where the checkout path may, the bracket keeps the read from matching the shell that runs it, the trailing space keeps it off `watch.log`, and a pid it prints is that group. Exit 0 is a live watch and exit 1 is no watch. Any other status is a failed read: report it with pgrep's stderr, and launch, stop or relaunch nothing on it. The watch's own `oversee-watch.pid`, beside the fleet state, is its claim record for the refusals `oversee-watch --help` states, not this liveness read. After a self-succession `oversee-succeed` restarts the watch from the successor pane, and the successor's own launch here takes that watch over. After each delivery and expiry, run that read first, then `test -s "[RUN_DIR]/watch.exit"`. A printed pid is a live watch. With no pid, a nonempty file ends the watch: `stopped` is the mark the stop below writes and ends oversight with no restart, and any other value follows the stop and restart rules of [oversee.md § 4](../workflows/oversee.md#4-watch-and-advance). With no pid and an empty file, the watch died without writing its status, killed together with its launch shell: report it and launch a new one in a fresh `[RUN_DIR]` under the same restart rule, then end the current follow and arm a new one on the new `[RUN_DIR]/watch.log` from line 1 (on Pi, stop the kept pid's task and keep the new pid). A follow is re-armed only while these checks read a live watch. From f1f33474681ee859af7481756907a21d2913cd35 Mon Sep 17 00:00:00 2001 From: "vanillagreen-fleet-lanes[bot]" <328550061+vanillagreen-fleet-lanes[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:42:45 +0000 Subject: [PATCH 2/3] fix(KEN-2403): avoid continuing turns for wake warnings Address PR review --- .agents/skills/orch/scripts/lib/watch-pid.sh | 13 +++- .claude/hooks/lane-mail-check.sh | 21 ++++--- .codex/hooks/lane-mail-check.sh | 21 ++++--- .pi/kendex/hooks/lane-mail-check.sh | 21 ++++--- .../added/registered-session-wake-check.md | 2 +- hooks/README.md | 2 +- hooks/lane-mail-check.sh | 21 ++++--- hooks/tests/lane-mail-check-wake.test.sh | 59 ++++++++++++------- hooks/tests/lib/lane-mail-world.sh | 3 +- skills/orch/scripts/lib/watch-pid.sh | 13 +++- skills/orch/tests/oversee_watch_lifecycle.sh | 32 ++++++++++ 11 files changed, 147 insertions(+), 61 deletions(-) diff --git a/.agents/skills/orch/scripts/lib/watch-pid.sh b/.agents/skills/orch/scripts/lib/watch-pid.sh index 24f6e4cd68..9c291f5da2 100644 --- a/.agents/skills/orch/scripts/lib/watch-pid.sh +++ b/.agents/skills/orch/scripts/lib/watch-pid.sh @@ -57,7 +57,8 @@ watch_pid_paths() { # STATE # Whether a live watch holds the record for STATE, with its WATCH_PID, # WATCH_PANE, WATCH_ORIGIN, WATCH_SCRIPT and WATCH_CWD set. Returns 1 where # there is no record, its state is another file, or its pid runs no -# oversee-watch. +# oversee-watch. Returns 2 when the process query is unknown, with its cause +# on stderr; callers must not treat that answer as an absent claim. watch_pid_live() { # STATE local line state="" WATCH_PID="" WATCH_PANE="" WATCH_ORIGIN="" WATCH_SCRIPT="" WATCH_CWD="" @@ -78,11 +79,17 @@ watch_pid_live() { # STATE } # Whether PID runs an oversee-watch: it is running, is no zombie nobody has -# reaped, and its command line names oversee-watch. +# reaped, and its command line names oversee-watch. Returns 2 if ps fails +# while PID still exists, rather than reporting that process as dead. watch_pid_runs() { # PID local line kill -0 "$1" 2>/dev/null || return 1 - line="$(ps -o stat= -o args= -p "$1" 2>/dev/null)" || return 1 + if ! line="$(ps -o stat= -o args= -p "$1" 2>&1)"; then + # The process can exit between kill and ps. Only that race is absence. + kill -0 "$1" 2>/dev/null || return 1 + printf 'watch-pid: process=unknown pid=%s\n%s\n' "$1" "$line" >&2 + return 2 + fi [[ "${line# }" != Z* && "$line" == *oversee-watch* ]] } diff --git a/.claude/hooks/lane-mail-check.sh b/.claude/hooks/lane-mail-check.sh index ffd3fefc95..15059f0a65 100755 --- a/.claude/hooks/lane-mail-check.sh +++ b/.claude/hooks/lane-mail-check.sh @@ -4,7 +4,7 @@ # event: Stop # matcher: # description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check warns without continuing the turn. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -19,8 +19,9 @@ set -euo pipefail # a running single pass names this fleet state; a master opts in through # ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. # A missing wake refuses under wake=unarmed with watch-delivery's re-arm line -# or the master's start command. Continued turns and unavailable probes pass -# with keyed model context. This hook starts, restarts and kills none of them. +# or the master's start command. Continued turns and unavailable probes warn +# on stderr and pass. +# This hook starts, restarts and kills none of them. # Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop # holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one # further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. @@ -1169,7 +1170,13 @@ wake_check() { if [ -e "$2" ] || [ -L "$2" ]; then [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } fi - if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + rc=0 + watch_pid_live "$3" || rc=$? + case "$rc" in + 0) printf "repeat\t%s" "$WATCH_CWD" ;; + 1) printf "single\t" ;; + *) exit "$rc" ;; + esac' \ _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? if [ "$rc" -ne 0 ]; then cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } @@ -1227,8 +1234,8 @@ wake_check() { esac } -# Pass only the wake check. Deliver its context with any later refusal, or -# through hand_over after the independent turn-end checks, as one JSON answer. +# Pass only the wake check. A later refusal carries the warning; otherwise +# stderr leaves it visible without Stop.additionalContext continuing the turn. wake_report() { # KEY VALUE [CAUSE] WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" } @@ -2679,8 +2686,6 @@ handoff_check() { overseer_marks if [ -n "$WAKE_NOTICE" ]; then printf '%s\n' "$WAKE_NOTICE" >&2 - CONTEXT_EVENT=Stop - hand_over "$WAKE_NOTICE" fi return 0 fi diff --git a/.codex/hooks/lane-mail-check.sh b/.codex/hooks/lane-mail-check.sh index ffd3fefc95..15059f0a65 100755 --- a/.codex/hooks/lane-mail-check.sh +++ b/.codex/hooks/lane-mail-check.sh @@ -4,7 +4,7 @@ # event: Stop # matcher: # description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check warns without continuing the turn. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -19,8 +19,9 @@ set -euo pipefail # a running single pass names this fleet state; a master opts in through # ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. # A missing wake refuses under wake=unarmed with watch-delivery's re-arm line -# or the master's start command. Continued turns and unavailable probes pass -# with keyed model context. This hook starts, restarts and kills none of them. +# or the master's start command. Continued turns and unavailable probes warn +# on stderr and pass. +# This hook starts, restarts and kills none of them. # Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop # holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one # further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. @@ -1169,7 +1170,13 @@ wake_check() { if [ -e "$2" ] || [ -L "$2" ]; then [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } fi - if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + rc=0 + watch_pid_live "$3" || rc=$? + case "$rc" in + 0) printf "repeat\t%s" "$WATCH_CWD" ;; + 1) printf "single\t" ;; + *) exit "$rc" ;; + esac' \ _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? if [ "$rc" -ne 0 ]; then cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } @@ -1227,8 +1234,8 @@ wake_check() { esac } -# Pass only the wake check. Deliver its context with any later refusal, or -# through hand_over after the independent turn-end checks, as one JSON answer. +# Pass only the wake check. A later refusal carries the warning; otherwise +# stderr leaves it visible without Stop.additionalContext continuing the turn. wake_report() { # KEY VALUE [CAUSE] WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" } @@ -2679,8 +2686,6 @@ handoff_check() { overseer_marks if [ -n "$WAKE_NOTICE" ]; then printf '%s\n' "$WAKE_NOTICE" >&2 - CONTEXT_EVENT=Stop - hand_over "$WAKE_NOTICE" fi return 0 fi diff --git a/.pi/kendex/hooks/lane-mail-check.sh b/.pi/kendex/hooks/lane-mail-check.sh index ffd3fefc95..15059f0a65 100755 --- a/.pi/kendex/hooks/lane-mail-check.sh +++ b/.pi/kendex/hooks/lane-mail-check.sh @@ -4,7 +4,7 @@ # event: Stop # matcher: # description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check warns without continuing the turn. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -19,8 +19,9 @@ set -euo pipefail # a running single pass names this fleet state; a master opts in through # ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. # A missing wake refuses under wake=unarmed with watch-delivery's re-arm line -# or the master's start command. Continued turns and unavailable probes pass -# with keyed model context. This hook starts, restarts and kills none of them. +# or the master's start command. Continued turns and unavailable probes warn +# on stderr and pass. +# This hook starts, restarts and kills none of them. # Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop # holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one # further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. @@ -1169,7 +1170,13 @@ wake_check() { if [ -e "$2" ] || [ -L "$2" ]; then [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } fi - if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + rc=0 + watch_pid_live "$3" || rc=$? + case "$rc" in + 0) printf "repeat\t%s" "$WATCH_CWD" ;; + 1) printf "single\t" ;; + *) exit "$rc" ;; + esac' \ _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? if [ "$rc" -ne 0 ]; then cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } @@ -1227,8 +1234,8 @@ wake_check() { esac } -# Pass only the wake check. Deliver its context with any later refusal, or -# through hand_over after the independent turn-end checks, as one JSON answer. +# Pass only the wake check. A later refusal carries the warning; otherwise +# stderr leaves it visible without Stop.additionalContext continuing the turn. wake_report() { # KEY VALUE [CAUSE] WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" } @@ -2679,8 +2686,6 @@ handoff_check() { overseer_marks if [ -n "$WAKE_NOTICE" ]; then printf '%s\n' "$WAKE_NOTICE" >&2 - CONTEXT_EVENT=Stop - hand_over "$WAKE_NOTICE" fi return 0 fi diff --git a/changelog.d/added/registered-session-wake-check.md b/changelog.d/added/registered-session-wake-check.md index 68c53e29bb..8ab25d2c7c 100644 --- a/changelog.d/added/registered-session-wake-check.md +++ b/changelog.d/added/registered-session-wake-check.md @@ -1 +1 @@ -- Registered overseers and configured masters must arm their wake process before ending a fresh lead turn. The refusal prints the start command. +- Repeat-watch overseers and configured masters must arm their wake process before ending a fresh lead turn. The refusal prints the start command. diff --git a/hooks/README.md b/hooks/README.md index 0d1ef8a974..7afdc5a534 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -11,7 +11,7 @@ The catalog's hooks, one script each. `crates/core/tests/hooks_readme.rs` render - `block-worktree-refresh`: Stops a kendex command that writes a project from inside a linked git worktree, where the write would land somewhere the command does not name. - `command-safety`: Refuses shell commands matching a project's declared deny pattern, and every command while its settings file cannot be read. - `doc-drift-check`: Stops an agent at the end of its turn when documents covering the code it changed did not change or an architecture topic names a path that does not exist, and hands it the list. Where some topic declares a Covers entry, changed code with no covering document is named too. -- `lane-mail-check`: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. +- `lane-mail-check`: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check warns without continuing the turn. - `lane-mail-compact`: Marks a Copilot lane or overseer for handoff when Copilot starts compacting it automatically before its context reading could hand it off, so its next turn end holds it until it hands the work to a fresh session. - `lane-mail-deliver`: Hands a working lane the messages its overseer sent as soon as a tool call finishes, and the notes another repository's overseer sent to the one session the checkout's fleet record names. It also tells the session running the fleet, at its next tool call, that it has used enough of its context to hand over. - `lane-mail-halt`: Stops a lane at its next tool call when its overseer sends a halt, until the lane reads it, and refuses the harness question tool in a lane, naming lane mail as the route. diff --git a/hooks/lane-mail-check.sh b/hooks/lane-mail-check.sh index ffd3fefc95..15059f0a65 100755 --- a/hooks/lane-mail-check.sh +++ b/hooks/lane-mail-check.sh @@ -4,7 +4,7 @@ # event: Stop # matcher: # description: Blocks a lane's turn end while its overseer mailbox holds unread lines, so a directive or a ruling reaches the lane without a keystroke, a pane or a question tool. The lane is the work item `LANE_MAIL_ITEM` names, or the one directory under `/tmp/lane-mail/` whose name lowercases to the current branch; a session with neither is not a lane; a lane whose mailbox holds no unread line passes silently, as does a directory git reports no repository for and that holds no mailbox of its own. The checkout's overseer mailbox, `/tmp/lane-mail/overseer`, has one reader: the session whose tmux server and pane the checkout's oversee workflow state names under `.overseer`, the pair every writer the orch `schemas/workflow-state.md` `overseer` row names writes, `oversee register` among them for a session opened by hand, and the one the fleet's OVERSEER is established by below, so a record naming a dead pane names no reader. The pair names a session only on the tmux server started at the record's `server_start`, so a server a tmux restart handed the recorded pid and pane id names no reader either; a record carrying no start at all names the session in that pane, which writes its server's start into it. Where a file stands there, that session is handed the notes `lane-mail peer send --repo` wrote there at its turn end and after its tool calls, under the same `unread=` line, unless a live watch holds the fleet state: the `oversee-watch.pid` record a repeat `oversee-watch` writes beside the fleet state on every surface and removes on exit, judged by the orch `lib/watch-pid.sh` library's `watch_pid_live`, which reads the mailbox itself, so the named session is handed nothing from it beside that watch. A fleet watched in single passes writes no watch record, so its named overseer is handed the notes as `unread=` lines and the next pass does not report them. Every other session in the checkout, one the owner opened there for other work included, is handed nothing from that mailbox, and so is a session whose install has no reader beside this hook. For the named session, where the install's `workflow-state path oversee` prints no path, or the library cannot be sourced or answers neither way, the hook refuses, opening `lane-mail-check: fleet-state=` with that file's own words under it; the halt arm never reads that mailbox, a subagent's turn end is handed nothing from it, and a session in a checkout with no overseer mailbox pays two file tests for the question. `` is the lane's root, resolved in this order: `CLAUDE_PROJECT_DIR`, the directory Claude Code started the session in; else the root the launch marker for `LANE_MAIL_ITEM` binds, where that root exists; else the directory the hook runs in, which on Codex and Pi is the session's start directory and on Claude Code without the variable is the call's own. A Claude Code lane working from the main clone is therefore still judged on its own mailbox. A root its launch marker binds that has no `tmp/lane-mail` directory is refused at a turn end, before any tool call and after the lead's finished one, opening `lane-mail-check: mailbox-missing=` with the marker and the one `mkdir` command that restores the directory, and that command alone passes a tool call. Before a tool call a subagent is refused whatever it runs, and a call whose caller is unknown may run that command; after one a subagent is handed nothing and refused nothing, as with any mail, and its next call meets the refusal. The marker is looked up for the item `LANE_MAIL_ITEM` names or else the branch names, so that check costs one stat in a repository whose common git directory holds no `lane-mail` directory, and one branch read and one marker stat in one that does. A mailbox belongs to a lane only where a launch recorded one: `open-terminal` and `lane-host create` write the lane's root to `lane-mail/` under the repository's common git directory and create the lane's own `tmp/lane-mail/`, and a mailbox with no marker bound to this root passes silently. Unread lines are peeked through the orch skill's own `lane-mail inbox --peek`, the one reader of the mailbox and its cursor, and acknowledged with `inbox --ack` only once the refusal is written, so a hook killed at its budget leaves them unread and a line acknowledged here is never handed over twice. That reader is resolved from this hook's own install, walking up to the home directory for `skills/orch/scripts/lane-mail` or the shared `.agents/skills/orch/scripts/lane-mail` beside it, then the home's own shared tree for a harness root relocated out of it; the open repository's `.agents/skills/orch/scripts/lane-mail` is used only where this hook is installed in that repository, and a reader outside that containment is refused rather than run. The refusal opens with `lane-mail-check: unread=` and carries one JSON envelope per line under it, and for the lead, where a halt is among them, the one `lane-mail inbox` command that reads it; the turn then continues with them. Run with the argument `deliver` by the lane-mail-deliver hook after a tool call, it exits 0 with the harness's JSON on stdout, whose `additionalContext` carries the same lines. That run first judges the context mark of the fleet's OVERSEER, below, for a lead's call in tmux in a checkout whose `tmp/lane-mail/overseer` directory stands, so a mark crossed mid-turn is read at the next tool call: the context is read and recorded as the overseer's turn end reads it and judged at `ORCH_HANDOFF_CONTEXT_PCT` by the rule the orch `references/oversee-events.md` § Judgement rules states for the overseer's context mark. On Pi the window is the payload's own `context_window`, which the pi-hooks carrier puts on the PostToolUse payload of every tool call; a Pi payload naming none, the lane mail wake's run or a tool call from an older carrier, takes no reading, writes no record and hands nothing over, so the turn end's reading and judgement stand. A reached mark, or a setting it cannot be judged on, is handed over as that `additionalContext`, opening `lane-mail-check: context=` with the succession and the handoff record as the route, at every tool call until that record stands. Every other line that judgement writes, a judgement that could not run, a transcript it could not read or an `orch-env` this install has not got, is handed over the same way once per session and first line, the last told kept in `tmp/lane-mail/overseer/context-told`, written only once the run's output carrying it is written and cleared by a clean reading; only the session the fleet record names, or the overseer the record lost, writes or clears that file. Either opens the same context as any mail the call hands over, above its `unread=` line, and a refusal the mailbox check makes on the same call, above its keyed line, whether that refusal is Copilot's `additionalContext` or stderr at exit 2. The judgement itself refuses no call, and no account is read there. Run with `start` by the lane-mail-start hook at a session start, or with `prompt` by the lane-mail-prompt hook at a prompt, both on Copilot alone, it hands the same lines over the same way and acknowledges them only once that is written, and refuses nothing: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Run with `halt` by the lane-mail-halt hook before one, it acknowledges nothing and refuses the call while an unread directive sent with `lane-mail send --halt` stands, opening `lane-mail-check: halt=` with the directive and the one `lane-mail inbox` command that reads it, which names the lane's root with `--root` so it reads the same mailbox from any checkout; that command alone passes. Run with `row` by the session-start-row, session-end-row and stop-failure-row hooks, it writes the event row of a session with no lane of its own, and only for the pane's top-level harness, through the orch skill's `lib/session-rows.sh`, the file those hooks describe, and refuses nothing: every gap below its payload read is reported and passed; the overseer's own turn end writes a Stop row there through the same library at every turn end, compact, its time, event, harness and session alone, except over a standing StopFailure row, which it lifts and where it keeps the turn's last message. On a Pi install a launched lane's lead writes its own turn rows through that library into its mailbox directory, `session-rows.jsonl`: a Stop row at each turn end, carrying the `stopReason` of the turn's last assistant message in the transcript and Pi's `errorMessage` where that reason is `error`, and a PreToolUse row at the first tool call of each turn, after a turn end or on an empty file, written ahead of the mailbox check; oversee-watch and `lanes state` judge a Pi lane idle, working or walled from those rows in place of its pane, and a gap in the write is reported under `lane-rows-skipped` or `lane-rows-unwritten` and passed. The Pi install is the hook directory under `.pi` or `.pi/agent`, or under the user root `PI_CODING_AGENT_DIR` names; an install naming no harness writes no row, which a launched lane's turn end reports under `harness-unlisted`. Who made a call is answered once for every arm: the lead, a subagent, or unknown. A SubagentStop payload or one carrying a non-empty `agent_id` or `agent_type` is a subagent's on every harness, handed no mail and acknowledging none, and while a halt stands refused without that command. On Copilot, whose payloads name no agent, a preToolUse or postToolUse call, a preCompact and a context reading the usage arm is handed are the lead's where the session id they carry is a lead session this install recorded, the one rule for all four: one empty file per session under `~/.cache/lane-mail/copilot-leads`, written at the session's sessionStart, which Copilot fires for the lead alone, and at each turn end the transcript rule below proves the lead's, and pruned at a start once untouched for 30 days. A custom subagent's call was measured on Copilot CLI 1.0.88 carrying its own session id, which nothing recorded; whether a built-in task-tool subagent's calls carry their own session id or the lead's is a pending live-lane proof, and one whose calls carry the lead's is read as the lead, handed the lead's mail after its calls, which marks it read, and under a halt shown the read that clears it. A call whose session is no recorded lead, a custom subagent's among them, is unknown: the deliver arm hands it nothing and acknowledges nothing, writing `lane-mail-check: session-unrecorded=` to stderr, the compact arm flags nothing and writes the same line at exit 0, the usage arm records nothing and refuses under that key at exit 2, so the lead's turn end hands the lines over and acknowledges them, and the halt arm refuses it without showing that command yet passes the command, because the reader's `--ack` stops short of an unread halt and a lead whose record could not be written would otherwise be refused for good; the lead is shown the command by every notice and refusal that hands it the halt, its turn end's included. A record that cannot be written is reported on stderr under `lane-mail-check: lead-unrecorded=`, or `=none` for a session start naming no session id, and never refused. A Copilot sessionStart or userPromptSubmitted is the lead's, and an agentStop is ruled by its transcript, below; a prompt records no lead, since that a subagent's prompt fires no userPromptSubmitted is unmeasured. `stop_hook_active` true skips the mailbox check whole on the turn-end run. The flag is in the payload, so nothing above the payload read knows a turn was continued, and `arm`, the `missing-tools` refusal for `jq` or `cat`, `payload=unreadable` and `payload=invalid-json` are refused on every turn, the continued one included. Below that read one rule holds and every refusal is on one side of it: a refusal the lane itself can clear is still made, and a refusal it cannot is reported on stderr and passed. The lane clears the two handoff marks, and `script`, `setting`, `setting-range` and both `transcript` refusals, by writing its handoff record, and `mailbox-missing` by running the command it names, so those are refused on a continued turn as on any other. It clears `idle` by sending a lane-mail ask or notice, so `idle` is refused on a turn another stop hook continued, except on Pi, as the idle rule below states. It clears none of `workdir`, `git`, `item`, `marker` or the `missing-tools` refusal for `git`, `tr`, `awk`, `mktemp` or `tail`, so each of those is reported and the turn ends, where a fresh turn refuses it. The halt arm reports and passes that same set at every tool call: a lane whose tool calls are refused can clear nothing, since clearing it takes a tool call, so there only the payload refusals and the mailbox's own, `reader`, `inbox`, `halt` and `mailbox-missing`, are refused, the last two each passing the one command that clears it. The same turn-end run hands the lane off before it runs out, so the handoff never waits on an overseer reading a pane. It reads this session's context use from the `transcript_path` the payload names, through the orch adapter for the harness this hook's install directory names, `.claude/hooks`, `.codex/hooks` or Pi's `kendex/hooks`: the tokens the last response left in context and effective capacity, with Pi reading the payload's own `context_window`. It records that reading as `context.json` in the lane's mailbox directory, where `lanes context` reads it, and refuses the turn end when `lane_context_handoff_due` requires handoff under the shared context rule in orch `references/oversee-events.md`, Judgement rules. It reads the account the credential this session runs on still has through the orch skill's own `lanes pick --lane`, and refuses at or below `ORCH_HANDOFF_HEADROOM_PCT` (default 3). A Pi session's account is the one its model's provider bills, by the orch `lane_pick_harness` rule on the `/` its transcript names: a `pi-claude/` model spends the Claude seat `CLAUDE_CONFIG_DIR` names and is judged there on that model, a `github-copilot/` model spends the Copilot pool on Pi's own root, and any other provider, or a reading naming none, is left unjudged under `account=unmeasured`. Either refusal opens `lane-mail-check: context=` or `lane-mail-check: headroom=` and carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The instruction opens with the `workflow-state init ` that `set` needs where the item has no state file yet, so it is enough on its own. It repeats at every turn end, `stop_hook_active` included, until the item's workflow state carries a `.handoff` object no relaunch has resumed; only the lane can write that record, so a single refusal it declines to act on would end the session with nothing recorded. That record is judged before every mark, before every read they rest on and before `orch-env` and `lanes` are looked for, so no failure but the record's own writer can hold a lane that has already done what it was asked. `orch-env` or `lanes` missing from this hook's install, a mark setting that is not a whole number in range, and a transcript the payload names and nothing can read are refusals too, each on the lane path alone and each carrying the same instruction. What the marks cannot judge is reported and passed, never refused: a payload naming no transcript leaves the context unread, and so does a transcript whose last usage line is an object carrying none of the field names its adapter reads, which is reported under `usage-unread=` rather than summed to a figure of zero and read as room, a reading below the independent token limit whose capacity the adapter could not name, reported under `window-unread=`, and an install directory naming no harness, reported under `harness-unlisted=`; a reading that could not be recorded is reported under `context-unrecorded=` and still judged; an account `lanes` keeps no inventory for, one it could not measure and a read that passed this hook's own ceiling each leave the account unjudged under `account=unlisted`, `account=unmeasured` or `account=timeout`, never read as room, so a setup with no usage endpoint still ends its turns; and a lane whose handoff record cannot be judged leaves both marks unjudged under one of four keys, `handoff-skipped=` for a reader or a script this install has not got, or `handoff-skipped=unlocatable` where this hook's own directory could not be resolved and none of them could be looked for, `handoff-outside=` for one only the open repository supplies, `handoff-unanswered=` for one that is there and answered nothing this hook can read, and `handoff-unreadable=` for a state file the install's own `workflow-state` could not read. Passing the turn is the answer for all four. For the first three it is because an install whose orch scripts cannot answer cannot run `workflow-state set` either, so a lane told to record a handoff with them could never end a turn again; for `handoff-unreadable` the install answers and the fault is the item's own state file, which is the file the record would be written into, so that write could not land either and the refusal would be as uncloseable. A subagent's turn end is judged on neither mark. The fleet's OVERSEER meets four triggers of its own on its turn end: a session with no lane of its own whose tmux server and pane are the pair the oversee workflow state records under `.overseer` is that overseer, and nothing weaker establishes one, so an ordinary session in a fleet checkout is judged on nothing. A session the record does not name whose pane the overseer's `context.json` names, the overseer the record lost, is judged on nothing either, and reports `pane-unrecorded=` with the recorded key under it and writes that record with no reading and the gap `pane-unrecorded`; any other session the record does not name writes nothing, so the real overseer's reading stands. A fleet record naming this session's pane that lacks its server start, its harness or its launch home is healed first, at a turn end and at a tool call alike: the start of this session's own tmux server, the harness this install names and the launch home the harness variable of this session's own environment carries, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` or `COPILOT_HOME` or that harness's default where it is unset, are written into it through the orch `lib/overseer-launch.sh` `ol_record_heal`, which writes only a fact the record lacks, and the transcript is bound to that home on the same run; a write that fails is reported under `record-unhealed=` with the writer's words, and the run still binds to that home. Before it reads that transcript for the overseer it binds it to the current session: the `transcript_path` the payload names is read only where it is this session's own native file, held to the `session_id` the payload carries and the launch home the fleet record names under `.overseer.home`, through the orch adapter for a claude, codex or copilot install; a Pi install states no transcript shape and reads the payload's own window as a Pi lane does. A file that is not this session's under that home, a newer unrelated transcript, a predecessor's in the same pane, or one under an account the fleet never picked, and a binding that cannot be made, a payload naming no session or a record naming no home, are each reported once under `transcript-unowned=` with the reason the gate gave. Nothing is read then, and the context record is written with a null token count and that reason as its `gap` (orch `workflow-state.md`, `context.json` row), so it still advances at every turn end and oversee-watch reports the gap, and the judge is handed this install's harness and no context figure, so it judges the account triggers alone, the `transcript-unowned` line is this hook's one report that the context went unmeasured, and a session is never judged on a file it does not own. Every overseer turn end whose orch context library loads writes that record, a reading or a null token count with a `gap` naming why none was taken: the gate's reason, `binding-missing` for a payload naming no session, `home-unnamed` for a fleet record naming no launch home, `session-mismatch` for a file another session wrote and `home-mismatch` for one under another launch home, `transcript-unnamed` for a payload naming no transcript, `usage-absent` for a transcript holding no usage line, `usage-unread` for a usage object its adapter does not read, which is also reported under `usage-unread=`, `session-record` for a Copilot session with no extension reading whose statusLine session record does not answer either, which is also reported under `session-record=`, or `pane-unrecorded` below. A Copilot statusLine record reporting allow_all_enabled false, read whether or not an extension reading stands, in a session whose `COPILOT_ALLOW_ALL` is true (a launch line granting `--allow-all` or `--yolo`), is reported under `stop-cause=allow-all-blocked-by-policy`, and the turn end judged as usual. An install naming no harness an adapter reads writes no gap, since its context is never read, and leaves the record as it was, and a Copilot one names none where the reading its usage arm wrote for this session stands, its turn end reading that record rather than taking a reading of its own; a gap record that cannot be written is reported under `context-gap-unrecorded=`, the record keeping its earlier entry. This hook records the overseer's own context reading as `context.json` in the overseer mailbox directory at the main checkout, naming the session and its pane, and hands that reading to `oversee-succeed --check-marks --context :` from this hook's own install, which judges those marks on it and on no stored figure, and nothing at the turn end here judges them, under the same ceiling the account read runs under, so the turn-end refusal and the `overseer-mark` watch event cannot describe one overseer differently. Its refusal opens with the `context=`, `headroom=`, `rate=` or `qualifying=` key, carrying the figure that judgement read, and names `oversee-succeed`, handed the same reading, as the route, with `workflow-state set oversee handoff` under it for a succession that refuses, so the refusal always has an escape the overseer can reach. That record names the session that wrote it, in a `session_id` the payload gives and a `pane_key` for a harness that sends none. One other refusal stands on that path, `script` for an `oversee-succeed` this install has not got, and the record clears it as it clears the marks. `ORCH_OVERSEER_SUCCESSION=off`, read off the judgement's own line and never here, turns the account-mark refusals off with the succession they name, the watch's `overseer-mark` event still reporting those marks; the context mark is refused whatever the setting, since no watch event reports it, and the handoff record ends that refusal. An answer this hook cannot act on, one the ceiling abandoned, and one whose own reading was unmeasured are reported under `marks=unjudged`, `marks=timeout` and `marks=unmeasured` and the turn ends, never held. A lane asks its overseer only through lane mail. Before a tool call in a launched lane, the halt arm refuses the harness question tool by its name, whichever of the four names the payload's `tool_name` carries, Claude Code's `AskUserQuestion` and `EnterPlanMode`, Codex's `request_user_input` and Pi's `question`, opening `lane-mail-check: question-tool=` and naming the `lane-mail ask --item --file ` send and the `lane-mail wait` on its printed id as the route, because a dialog on the pane reaches no overseer; an unread halt is refused ahead of it, a subagent's call is refused and told to report its question to the lead, and a session that is no launched lane passes the call silently, a committed mailbox and status file included. At a launched lane lead's turn end, once the handoff marks pass, the lane is judged idle on lane-mail facts alone, never on words the lane wrote, on Claude Code, Codex, Pi and Copilot; OpenCode and Cursor run no kendex hooks, and Gemini and Antigravity do not run this one, so a lane there is never judged idle. The reader's own `lane-mail events` listing, which moves no cursor, counts the asks and notices in the lane's outbound file against the count this hook recorded at the lane's last judged turn end, `sent-count` in the lane's mailbox directory: a line holding the count and, where this judge refused that turn end, the word `held`; no record is a count of zero. A count other than the recorded one is a send this turn and passes, a standing handoff record ends the run before this judge, the newest directive the overseer sent carrying `halt` passes, and a lane whose launch marker no longer binds its root, the close-out's state, is no lane. With none of these the turn is refused and the hold recorded, opening `lane-mail-check: idle=` with one continuation, the lane's `lane-mail inbox` read and then its workflow, beside the ask route and the `lane-mail notice` send, which is also the route for a lane that ends its turn to wait on a dispatched agent or an armed wake, the notice naming what it waits on. On a turn the harness continued for this judge's own recorded hold it refuses nothing and sends the overseer a lane notice stating what it checked, opening `lane-mail-check: idle-notice=`, or `idle-notice-unsent=` with the sender's words where the send fails, so an idle lane is reported and never looped. A turn another stop hook's refusal continued is held as a fresh one, except on Pi, whose pi-hooks carrier runs no further request after a continued turn, so there the notice is sent with no hold before it. The record counts that notice, so the lane's next turn is judged on what the lane itself sends. An `events` listing that fails, or whose envelopes jq cannot read, is reported under `events=` or `events=envelope`, and a record that is not a plain file holding a whole number and at most the word `held` under `idle-record=` and rewritten. A record that cannot be written is reported under `idle-unrecorded=`; under `idle-hold-unrecorded=` where it was to take a hold, which is then not made, since one with no record would be made again on every continued turn; and under `idle-notice-unrecorded=` where it was to count the notice, which the next turn end then reads as the lane's own send. Each of these ends the turn, because nothing a lane does at its turn end repairs its mailbox; an install with no reader leaves the turn unjudged under the marks' own `handoff-skipped` line. On Copilot the event is `agentStop`, registered in Copilot's own hook file under its camelCase name, so the payload spells its fields `sessionId`, `transcriptPath` and `toolArgs`, which this hook reads beside the snake_case spellings; its reference spells `stop_hook_active` so on both, and `toolName` goes unread, since the question-tool rule names no Copilot tool. Copilot also runs the hooks `.claude/settings.json` registers, under their PascalCase names, but the registration kendex writes there exits 0 before its script in a Copilot hook process, so a Copilot call reaches an install that is not Copilot's, the claude copy of this hook among them, only through a registration made there by hand or one kendex wrote before that skip and no refresh has rewritten. Whether Copilot made the call is read from the call, never from the install: a payload carrying `timestamp`, which both of Copilot's payload formats carry and no Claude, Codex or Pi payload does, or naming the session `sessionId`, is Copilot's, and any install but Copilot's own passes it silently, exit 0 with nothing written and nothing acknowledged, so the Copilot install is the one reader of the mailbox for a Copilot call. A Copilot agentStop names no agent: the stop whose `transcriptPath` sits in the directory named for the `sessionId`, Copilot's `session-state//events.jsonl`, is the lead's and records it, a stop naming a transcript under another directory is read as a subagent's, handed nothing and judged on nothing, and a payload naming no transcript, or no session, is the lead's and records nothing. Copilot CLI 1.0.88 was measured firing agentStop at a custom subagent's end too, with the subagent's own `sessionId` and the lead's `transcriptPath`, which this rule reads as a subagent's. A refusal on Copilot is the documented answer for its event, on stdout beside the keyed stderr line: at a turn end `{"decision":"block","reason":}` with exit 0, which Copilot stops honouring after 8 consecutive blocks, ending that turn unheld, so a handoff refusal holds a Copilot session for 8 continued turns and then returns at its next turn end, before a tool call `{"permissionDecision":"deny","permissionDecisionReason":}` with exit 2, since any non-zero exit denies the call and the JSON carries the words, and after one `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. That halt decision is local, the mailbox peek and nothing else, so it lands inside the hook's deadline: a preToolUse hook that times out on Copilot lets the call through. A Copilot session's context is read through Copilot's own SDK event `session.usage_info`: the orch `copilot-lane-context` extension, which `open-terminal` installs in a Copilot fleet lane's `COPILOT_HOME` with the EXTENSIONS feature on, runs this hook with the argument `usage` at each reading of the session's root agent, handing it `{session_id, cwd, current_tokens, token_limit}` on stdin, and for a recorded lead session that is a launched lane's lead or the fleet's overseer, named by the same gate every arm that judges the marks asks, it records `current_tokens` as the tokens and, as the capacity, the limit Copilot compacts at: 80 percent of `token_limit`, the SDK's documented `backgroundCompactionThreshold` default, named in the record's `capacity_source`. It writes nothing on stdout; a gap is refused on stderr at exit 2, which the extension writes to the session timeline: `payload=invalid-json`, an orch install it cannot use under the key its turn end reports that gap under, `handoff-skipped=`, `handoff-outside=` or `handoff-unanswered=`, or `context-unrecorded=` for a reading that could not be written, where the session's earlier reading is removed too so its turn end reports the context unmeasured rather than judging an older figure as room. Any other session passes silently. The Copilot turn end judges this session's record under the shared context rule exactly as it judges a transcript reading, the capacity putting the handoff before Copilot's own compaction; no reading of this session, no record or a gap record, is read from the fallback below; where the pending marker the extension leaves at `~/.cache/lane-mail/copilot-usage/` for a reading it handed on and no run has recorded yet still stands after a wait of 5 seconds, the turn end reports `reading-pending=` and passes with the context unmeasured, reading no fallback, since the extension runs for that session, and never judging the earlier record as room, since nothing orders the extension's run before the turn end; and a record that stands and cannot be read is refused under `record=`. Where no pending marker stands and no reading of the extension's stands for this session, no record, one naming another session, a gap record or one the fallback below wrote, which its `capacity_source` tells apart, the turn end reads the fallback: the session record the orch `copilot-statusline` command writes as the account's `statusLine`, under `/lane-status/.json`, through the orch adapter for copilot, which holds the record to the payload's `sessionId`, its `transcriptPath` where one is named, the account directory and a freshness bound, and hands the shared judge the same compaction point of the window as the capacity; that reading is judged and recorded as any other harness's, and a record that does not answer leaves the context unmeasured under `reading-unrecorded=` and `session-record=`, reported and passed, never read as room. An extension reading below the mark is never overridden by a statusLine record past it. Run with `compact` by the lane-mail-compact hook at a preCompact whose `trigger` is `auto`, from a recorded lead session that gate names, it flags that compaction as `compaction.json` beside the reading, the backstop for a turn that crossed into the compaction before a reading past the mark reached a turn end, and the turn end then refuses under `compacted=auto` until the handoff record stands, the overseer's whatever its succession setting; a gap there is refused under `compaction-unrecorded=` on stderr at exit 2, which Copilot shows the operator without holding the compaction. A Copilot account is judged through `lanes pick --lane` like any other. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. -# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check reports model context and passes without starting or stopping a process. +# summary: Hands lanes their overseer's mail and holds turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or a master whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty master keys with no watch claim disable the check, and a running single pass is its own wake. The wake check holds Claude Code Stop, Codex Stop and Copilot CLI agentStop, is partial on Pi, and is unsupported on Gemini, Antigravity, OpenCode and Cursor; a continuation or unavailable check warns without continuing the turn. # safety: Reads the payload, the repository's branch, the lane's launch marker, read with the shell's own `read`, and the lane mailbox directory, and for a session that is no lane the checkout's overseer mailbox and, where a file stands there and the fleet record names this session's pane, the fleet state's path from one `workflow-state path oversee` and the watch record beside it, read by the orch watch record library `lib/watch-pid.sh` sourced in a child shell from the same install as the mailbox reader, which checks the record's pid with `kill -0` and `ps`; the writes are the mailbox cursor the orch reader advances, the session's context reading, `context.json`, and a Copilot session's compaction flag, `compaction.json`, each renamed into its mailbox directory, and on a Pi lane its turn rows, `session-rows.jsonl`, appended there under that file's own lock at a turn end and at the first tool call of each turn, and for the overseer one row appended to its rows file, `session--.jsonl` in the overseer mailbox directory, under that file's own lock, a compact Stop at every overseer turn end, the overseer's directory made where the fleet has not made it yet, the fleet record's `.overseer` server start, harness and launch home where a record naming this session's pane lacks one, through one `workflow-state update oversee` in a child shell sourcing the orch `lib/lane-context.sh` and `lib/overseer-launch.sh`, and after each overseer tool call its `context.json` and, where a gap is told, `tmp/lane-mail/overseer/context-told`, and on Copilot a lead session's empty record under `~/.cache/lane-mail/copilot-leads`, touched at its session start and at each turn end its transcript proves the lead's, with records untouched for 30 days removed by one `find` at each start, as are the extension's pending markers under `~/.cache/lane-mail/copilot-usage`, which a Copilot turn end reads and polls for 5 seconds. Exit 2 names the unread count and the messages, and asks for them to be acted on, never bypassed. The reader it runs comes from its own install, never from the repository a session has open, so a repository that tracks a mailbox and an executable at that path cannot have it run. jq and cat read the payload; a payload it cannot read is refused on every turn, the continued one included, because the flag that marks a continued turn is in the payload none of those refusals reached. A mailbox whose reader is missing or fails is refused on the turns the mailbox check runs, which is every turn end but a continued one; on a continued turn that check is skipped whole, and the same missing reader is reported under `handoff-skipped` or `handoff-outside` and the turn is passed. An item name outside the alphabet a work item is spelled in, a branch that matches more than one mailbox, and a repository state git cannot report where a mailbox sits under the working directory are refused on a fresh turn and reported on a continued one and before a tool call, by the one rule the description states; none of them is ever passed in silence. For a lane's handoff marks it also reads the transcript the payload names and runs `orch-env`, `lanes` and `workflow-state` and sources the orch context library from the same install as the mailbox reader, never the open repository's, and on Pi one child shell sourcing that install's `lib/lane-launch.sh` to name the account the session's provider bills; the context reading is the one thing it writes for them. A session that names no lane costs two file tests of the checkout's overseer mailbox; where a file stands there it adds that install's resolution and, inside tmux, the overseer test below, and for the session the record names one `workflow-state path oversee`, one child shell sourcing the watch record library, and the reader's peek where no live watch stands. Outside tmux it costs nothing more, where the overseer test stops at its first condition. After a tool call a lead session with no lane of its own, in tmux, in a checkout with no `tmp/lane-mail/overseer` directory costs one stat more; where that directory stands it pays the overseer test below, and for the overseer one transcript read, one `workflow-state handoff-standing`, one `orch-env` read and one record write, the reads the turn end makes but for the account judgement; the overseer test runs once per call, and the mailbox check takes its answer. Inside tmux the overseer test costs that install's resolution, one child shell sourcing the orch context library, one tmux read and one `workflow-state` read, which together ask the oversee state whether this pane is the overseer's, plus, for a session whose pane the state names beside a server start, one child shell sourcing the orch `lib/tmux-server.sh` library for one more tmux read of its server's start; a session the state does not name stops there, after one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json` where the state names another pane or none, and where that record names this session's pane, one `pane-unrecorded` report and one rewrite of that record. The overseer's own marks are then judged by `oversee-succeed --check-marks` from that same install, under the same 20 second ceiling the lane's account read runs under. That judgement measures the one account the overseer session runs on, through the same `lanes pick --lane` this hook asks about a lane, renewing an expired token in that account's credential file and refreshing that account's usage cache, the writes `lanes` states in its own contract; it opens no window and launches nothing. Only the last 1 MiB of the transcript is parsed, and the whole file only where that window carries no usage line, so the cost does not grow with the session. `lanes pick --lane` measures one account and renews that account's expired token, the write its own contract states; it can wait on a credentials lock and two network calls, and on a cache miss on the host-wide usage refresh lock for up to 10 seconds and, after a 429 with nothing cached, one sleep of up to 5 seconds and one more usage request, so it runs under a 20 second ceiling that leaves the rest of the run inside this hook's 30 second budget, and a read that reaches the ceiling is reported as a gap rather than refused. Where `timeout` is not installed that read runs unbounded, and a hook the harness then kills at its budget leaves the account unjudged, the same outcome the reported gap gives without the line. For the question-tool rule it reads the payload's `tool_name` before a tool call. For the idle judge at a lane lead's turn end it runs the reader's `events` listing, which moves no cursor, and reads the kind, the box and the halt flag of each envelope, never its text and never the transcript; it writes the lane's `sent-count` record, renamed into its mailbox directory, and the one idle notice the description names, through the reader's `notice`. Every refusal opens with `lane-mail-check: =`; what a command this hook runs writes is captured at the site and replayed under that line, so nothing precedes the key. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] @@ -19,8 +19,9 @@ set -euo pipefail # a running single pass names this fleet state; a master opts in through # ORCH_WAKE_PROCESS and ORCH_WAKE_START, read with empty orch-env defaults. # A missing wake refuses under wake=unarmed with watch-delivery's re-arm line -# or the master's start command. Continued turns and unavailable probes pass -# with keyed model context. This hook starts, restarts and kills none of them. +# or the master's start command. Continued turns and unavailable probes warn +# on stderr and pass. +# This hook starts, restarts and kills none of them. # Claude Code Stop and Codex Stop use the caller rule below. Copilot agentStop # holds at most 8 blocks. Pi's turn_end at agent_before_settle gives only one # further model request, and PI_SUBAGENT_CHILD_AGENT dispatches no turn_end. @@ -1169,7 +1170,13 @@ wake_check() { if [ -e "$2" ] || [ -L "$2" ]; then [ -f "$2" ] && [ -r "$2" ] || { echo "watch claim is not a readable file" >&2; exit 3; } fi - if watch_pid_live "$3"; then printf "repeat\t%s" "$WATCH_CWD"; else printf "single\t"; fi' \ + rc=0 + watch_pid_live "$3" || rc=$? + case "$rc" in + 0) printf "repeat\t%s" "$WATCH_CWD" ;; + 1) printf "single\t" ;; + *) exit "$rc" ;; + esac' \ _ "$SCRIPTS/lib/watch-pid.sh" "$record" "$state" 2>"$WORK_DIR/wake.err") || rc=$? if [ "$rc" -ne 0 ]; then cause=$(cat -- "$WORK_DIR/wake.err" 2>&1) || { wake_report wake-state "$WORK_DIR/wake.err" "$cause"; return 0; } @@ -1227,8 +1234,8 @@ wake_check() { esac } -# Pass only the wake check. Deliver its context with any later refusal, or -# through hand_over after the independent turn-end checks, as one JSON answer. +# Pass only the wake check. A later refusal carries the warning; otherwise +# stderr leaves it visible without Stop.additionalContext continuing the turn. wake_report() { # KEY VALUE [CAUSE] WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE" } @@ -2679,8 +2686,6 @@ handoff_check() { overseer_marks if [ -n "$WAKE_NOTICE" ]; then printf '%s\n' "$WAKE_NOTICE" >&2 - CONTEXT_EVENT=Stop - hand_over "$WAKE_NOTICE" fi return 0 fi diff --git a/hooks/tests/lane-mail-check-wake.test.sh b/hooks/tests/lane-mail-check-wake.test.sh index 8f3deeb9f5..6eac308953 100755 --- a/hooks/tests/lane-mail-check-wake.test.sh +++ b/hooks/tests/lane-mail-check-wake.test.sh @@ -28,7 +28,7 @@ while IFS='|' read -r scenario harness want_rc first decision context commands w fi case "$scenario" in armed) start_wake_watch repeat; start_follow ;; - no-follow | continuation | continuation-mark | agent-id | agent-type | subagent-stop | other-session | no-pgrep | probe-error | probe-error-mark | record-cwd | record-armed | missing-cwd | unreadable-record) + no-follow | continuation | continuation-mark | agent-id | agent-type | subagent-stop | other-session | no-pgrep | probe-error | probe-error-mark | watch-probe-error | record-cwd | record-armed | missing-cwd | unreadable-record) start_wake_watch repeat ;; single) start_wake_watch single ;; @@ -66,6 +66,15 @@ while IFS='|' read -r scenario harness want_rc first decision context commands w printf '#!/bin/sh\necho "probe failed" >&2\nexit 2\n' > "$TMUX_BIN/pgrep" chmod +x "$TMUX_BIN/pgrep" ;; + watch-probe-error) + executable=$(command -v ps) || exit 1 + { + printf '#!/bin/sh\n' + printf 'if [ "$2" = stat= ]; then echo "watch-query-failed" >&2; exit 1; fi\n' + printf 'exec %q "$@"\n' "$executable" + } > "$TMUX_BIN/ps" + chmod +x "$TMUX_BIN/ps" + ;; record-cwd | record-armed) # A follow on the session root does not arm a watch whose claim names # another cwd. The path includes regex characters and a space. @@ -91,20 +100,23 @@ while IFS='|' read -r scenario harness want_rc first decision context commands w wake_row_process_table mkdir -p "$LANE/tmp/lane-mail/overseer" fi - case "$context" in - record) context="wake-record=${WAKE_STATE%/*}/oversee-watch.pid" ;; - process) - # The process error reports the actual query under its own key. Do - # not pin the prose pgrep wrote below it. - context="wake-process=follow[.]sh ${LANE//./\\.}/tmp/waiter[.][^/]*/watch[.]log" - ;; - esac - case "$first" in record | process) first=${context#lane-mail-check: } ;; esac + record_key="wake-record=${WAKE_STATE%/*}/oversee-watch.pid" + process_key="wake-process=follow[.]sh ${LANE//./\\.}/tmp/waiter[.][^/]*/watch[.]log" + case "$context" in record) context=$record_key ;; process) context=$process_key ;; esac + case "$first" in record) first=$record_key ;; process) first=$process_key ;; esac [ "$first" = - ] || first="lane-mail-check: $first" [ "$context" = - ] || context="lane-mail-check: $context" run_payload "$payload" got=$(wake_observation "$command") || exit 1 want="RC=$want_rc first=$first decision=$decision context=$context command=$commands" + # Exit 0 alone does not end Claude Stop: additionalContext requests a turn. + # Pin the whole output count. + objects=$(jq -s 'length' "$TMP_ROOT/stdout") || exit 1 + event=$(jq -rs '.[0].hookSpecificOutput.hookEventName // "-"' "$TMP_ROOT/stdout") || exit 1 + want_objects=0 want_event=- + if [ "$decision" = block ]; then want_objects=1; fi + got="$got replies=$objects event=$event" + want="$want replies=$want_objects event=$want_event" if [ -n "$wake_notice" ]; then row=$(jq -rs 'map([.event, .harness] | join(":")) | join(",")' \ "$LANE/tmp/lane-mail/overseer/session-$OVERSEER_SERVER-${OVERSEER_PANE#%}.jsonl") || exit 1 @@ -135,10 +147,10 @@ no-follow|claude|2|wake=unarmed|-|-|1 no-follow|codex|2|wake=unarmed|-|-|1 no-follow|copilot|0|wake=unarmed|block|-|1 no-follow|pi|2|wake=unarmed|-|-|1 -continuation|claude|0|wake=unarmed|-|wake=unarmed|1 -continuation|codex|0|wake=unarmed|-|wake=unarmed|1 -continuation|copilot|0|wake=unarmed|-|wake=unarmed|1 -continuation|pi|0|wake=unarmed|-|wake=unarmed|1 +continuation|claude|0|wake=unarmed|-|-|1 +continuation|codex|0|wake=unarmed|-|-|1 +continuation|copilot|0|wake=unarmed|-|-|1 +continuation|pi|0|wake=unarmed|-|-|1 continuation-mark|claude|2|context=612000|-|-|1|wake=unarmed continuation-mark|codex|2|context=612000|-|-|1|wake=unarmed continuation-mark|copilot|0|context=612000|block|-|1|wake=unarmed @@ -154,17 +166,18 @@ other-session|claude|0|-|-|-|0 single|claude|0|-|-|-|0 master-live|claude|0|-|-|-|0 master-dead|claude|2|wake=unarmed|-|-|1 -master-continuation|claude|0|wake=unarmed|-|wake=unarmed|1 +master-continuation|claude|0|wake=unarmed|-|-|1 empty-keys|claude|0|-|-|-|0 absent-keys|claude|0|-|-|-|0 no-record|claude|0|-|-|-|0 -no-pgrep|claude|0|wake-tools=pgrep|-|wake-tools=pgrep|0 -probe-error|claude|0|process|-|process|0 +no-pgrep|claude|0|wake-tools=pgrep|-|-|0 +probe-error|claude|0|process|-|-|0 +watch-probe-error|claude|0|record|-|-|0 record-cwd|claude|2|wake=unarmed|-|-|1 record-armed|claude|0|-|-|-|0 -missing-cwd|claude|0|record|-|record|0 -unreadable-record|claude|0|record|-|record|0 -incomplete-master|claude|0|wake-setting=ORCH_WAKE_START|-|wake-setting=ORCH_WAKE_START|0 +missing-cwd|claude|0|record|-|-|0 +unreadable-record|claude|0|record|-|-|0 +incomplete-master|claude|0|wake-setting=ORCH_WAKE_START|-|-|0 ROWS # Copilot's custom subagent names its own session and the lead's transcript. @@ -193,6 +206,12 @@ if [ -z "$SELECTED" ]; then wake_control warning-exits continuation-mark:claude \ ' WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE"' \ ' WAKE_NOTICE=$(message "$@" 2>&1) || refuse notice unwritten "$WAKE_NOTICE"; exit 0' + wake_control warning-continues continuation:claude \ + ' printf '\''%s\n'\'' "$WAKE_NOTICE" >&2' \ + ' printf '\''%s\n'\'' "$WAKE_NOTICE" >&2; CONTEXT_EVENT=Stop; hand_over "$WAKE_NOTICE"' + wake_control watch-unknown-skips watch-probe-error:claude \ + ' *) exit "$rc" ;;' \ + ' *) printf "single\t" ;;' fi printf '\n=== %s passed, %s failed ===\n' "$PASS" "$FAIL" diff --git a/hooks/tests/lib/lane-mail-world.sh b/hooks/tests/lib/lane-mail-world.sh index 82e19031f1..32a8f1f0bb 100644 --- a/hooks/tests/lib/lane-mail-world.sh +++ b/hooks/tests/lib/lane-mail-world.sh @@ -581,7 +581,8 @@ wake_overseer() { # NAME HARNESS [HOOK] "TMUX_SERVER_ID=$OVERSEER_SERVER" "TMUX_SERVER_START=$OVERSEER_SERVER_START" "HOME=$COP_HOME") } -# A refusal's key is first on stderr. A passed gap is model context instead. +# A refusal or passed wake warning has its key first on stderr. +# Stop context would continue the turn. # The Start value is protocol data: the command the caller must run. wake_observation() { # START local decision=- context=- command=0 text first answer diff --git a/skills/orch/scripts/lib/watch-pid.sh b/skills/orch/scripts/lib/watch-pid.sh index 24f6e4cd68..9c291f5da2 100644 --- a/skills/orch/scripts/lib/watch-pid.sh +++ b/skills/orch/scripts/lib/watch-pid.sh @@ -57,7 +57,8 @@ watch_pid_paths() { # STATE # Whether a live watch holds the record for STATE, with its WATCH_PID, # WATCH_PANE, WATCH_ORIGIN, WATCH_SCRIPT and WATCH_CWD set. Returns 1 where # there is no record, its state is another file, or its pid runs no -# oversee-watch. +# oversee-watch. Returns 2 when the process query is unknown, with its cause +# on stderr; callers must not treat that answer as an absent claim. watch_pid_live() { # STATE local line state="" WATCH_PID="" WATCH_PANE="" WATCH_ORIGIN="" WATCH_SCRIPT="" WATCH_CWD="" @@ -78,11 +79,17 @@ watch_pid_live() { # STATE } # Whether PID runs an oversee-watch: it is running, is no zombie nobody has -# reaped, and its command line names oversee-watch. +# reaped, and its command line names oversee-watch. Returns 2 if ps fails +# while PID still exists, rather than reporting that process as dead. watch_pid_runs() { # PID local line kill -0 "$1" 2>/dev/null || return 1 - line="$(ps -o stat= -o args= -p "$1" 2>/dev/null)" || return 1 + if ! line="$(ps -o stat= -o args= -p "$1" 2>&1)"; then + # The process can exit between kill and ps. Only that race is absence. + kill -0 "$1" 2>/dev/null || return 1 + printf 'watch-pid: process=unknown pid=%s\n%s\n' "$1" "$line" >&2 + return 2 + fi [[ "${line# }" != Z* && "$line" == *oversee-watch* ]] } diff --git a/skills/orch/tests/oversee_watch_lifecycle.sh b/skills/orch/tests/oversee_watch_lifecycle.sh index fe85d9294b..53fedca492 100755 --- a/skills/orch/tests/oversee_watch_lifecycle.sh +++ b/skills/orch/tests/oversee_watch_lifecycle.sh @@ -310,6 +310,38 @@ assert_eq "$RECORDED_ARGV|$(sed -n 's/^cwd=//p' "$STUB_DIR/oversee-watch.pid")" "--interval 0 --max-loops 2 --repo owner/repo --max-loops 1 --repeat 0 --state $STUB_DIR/state.json |$TMP_ROOT/repo" \ "the record keeps the loop's own words and directory, and none of the overseer's flags" +# A failed ps read of that live claim is unknown, not an absent claim. +# The control retains the cause but makes the reader return absence instead. +mutant probe_absent lib/watch-pid.sh ' return 2' ' return 1' +probe_mutant="$(dirname "$MUTANT")/lib/watch-pid.sh" +mkdir -p "$TMP_ROOT/probe-bin" +printf '#!/bin/sh\necho "watch-query-failed" >&2\nexit 1\n' > "$TMP_ROOT/probe-bin/ps" +chmod +x "$TMP_ROOT/probe-bin/ps" +while IFS='|' read -r reader want_rc; do + case "$reader" in + source) reader="$REPO_ROOT/skills/orch/scripts/lib/watch-pid.sh" ;; + mutant) reader=$probe_mutant ;; + esac + rc=0 + env -i "PATH=$TMP_ROOT/probe-bin:$PATH" bash -euo pipefail -c ' + source "$1" + source "$2" + rc=0 + watch_pid_live "$3" 2>"$4" || rc=$? + first="" cause="" + { IFS= read -r first; IFS= read -r cause; } < "$4" + assert_eq "rc=$rc first=$first cause=$cause" \ + "rc=2 first=watch-pid: process=unknown pid=$5 cause=watch-query-failed" \ + "a failed query of a live watch claim is unknown" + [[ "$FAIL" == 0 ]]' \ + _ "$REPO_ROOT/skills/orch/tests/lib/assertions.sh" "$reader" \ + "$STUB_DIR/state.json" "$TMP_ROOT/probe.err" "$LOOP" > "$TMP_ROOT/probe.log" 2>&1 || rc=$? + assert_eq "$rc" "$want_rc" "watch reader probe/control verdict=$want_rc" "$TMP_ROOT/probe.log" +done <<'READERS' +source|0 +mutant|1 +READERS + # A second start on the same state is refused, naming the live loop, and # leaves the record and the loop as they were. err="$TMP_ROOT/e-record_second" From 21d2fbe5d4449be11cf303345d35a355195cbd99 Mon Sep 17 00:00:00 2001 From: "vanillagreen-fleet-lanes[bot]" <328550061+vanillagreen-fleet-lanes[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:53:37 +0000 Subject: [PATCH 3/3] docs(KEN-2403): remove stale wake delivery claim Address PR review --- .agents/skills/orch/references/watch-delivery.md | 2 +- skills/orch/references/watch-delivery.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/orch/references/watch-delivery.md b/.agents/skills/orch/references/watch-delivery.md index a566c492f4..98b01230d6 100644 --- a/.agents/skills/orch/references/watch-delivery.md +++ b/.agents/skills/orch/references/watch-delivery.md @@ -8,7 +8,7 @@ The harness picks the path before any launch. A harness that delivers a detached ## Repeat watch -The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. A continued turn or an unavailable check reports model context and passes. +The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. Launch the repeat command once from the overseer's own pane by [Waiter launch](waiter-launch.md) § Launch, run path `[RUN_DIR]/watch`: output in `[RUN_DIR]/watch.log`, status in `[RUN_DIR]/watch.exit`. `[NEXT_LINE]` starts at 1 in each fresh `[RUN_DIR]`. Save the numbered follow below as `[RUN_DIR]/follow.sh` with the harness file-write tool. Every harness follows the log with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]`, one simple command that prefixes each line with its number. Arm every follow from the line after the last number handled. The launch starts the watch through the orch job runner ([job-units.md](job-units.md)), which records how it runs in `[RUN_DIR]/watch.runner`; line 1 of `[RUN_DIR]/watch.log` repeats its runner line. The watch is the launch shell, whose argv carries the run path as its own word, and that shell leads the process group of everything the watch started. One read finds it, `pgrep -f 'waiter[.][RUN_ID]/watc[h] '`, `[RUN_ID]` being the letters and digits `mktemp` put after `waiter.` in `[RUN_DIR]`, whatever spelling launched the command: the name holds no regex character where the checkout path may, the bracket keeps the read from matching the shell that runs it, the trailing space keeps it off `watch.log`, and a pid it prints is that group. Exit 0 is a live watch and exit 1 is no watch. Any other status is a failed read: report it with pgrep's stderr, and launch, stop or relaunch nothing on it. The watch's own `oversee-watch.pid`, beside the fleet state, is its claim record for the refusals `oversee-watch --help` states, not this liveness read. After a self-succession `oversee-succeed` restarts the watch from the successor pane, and the successor's own launch here takes that watch over. diff --git a/skills/orch/references/watch-delivery.md b/skills/orch/references/watch-delivery.md index a566c492f4..98b01230d6 100644 --- a/skills/orch/references/watch-delivery.md +++ b/skills/orch/references/watch-delivery.md @@ -8,7 +8,7 @@ The harness picks the path before any launch. A harness that delivers a detached ## Repeat watch -The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. A continued turn or an unavailable check reports model context and passes. +The registered overseer's lead turn end refuses with `lane-mail-check: wake=unarmed` when the repeat watch is live but its follow is not running on the watch claim's cwd. Re-arm with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]` from the next unhandled line. The hook starts no process. Launch the repeat command once from the overseer's own pane by [Waiter launch](waiter-launch.md) § Launch, run path `[RUN_DIR]/watch`: output in `[RUN_DIR]/watch.log`, status in `[RUN_DIR]/watch.exit`. `[NEXT_LINE]` starts at 1 in each fresh `[RUN_DIR]`. Save the numbered follow below as `[RUN_DIR]/follow.sh` with the harness file-write tool. Every harness follows the log with `sh "[RUN_DIR]/follow.sh" "[RUN_DIR]/watch.log" [NEXT_LINE]`, one simple command that prefixes each line with its number. Arm every follow from the line after the last number handled. The launch starts the watch through the orch job runner ([job-units.md](job-units.md)), which records how it runs in `[RUN_DIR]/watch.runner`; line 1 of `[RUN_DIR]/watch.log` repeats its runner line. The watch is the launch shell, whose argv carries the run path as its own word, and that shell leads the process group of everything the watch started. One read finds it, `pgrep -f 'waiter[.][RUN_ID]/watc[h] '`, `[RUN_ID]` being the letters and digits `mktemp` put after `waiter.` in `[RUN_DIR]`, whatever spelling launched the command: the name holds no regex character where the checkout path may, the bracket keeps the read from matching the shell that runs it, the trailing space keeps it off `watch.log`, and a pid it prints is that group. Exit 0 is a live watch and exit 1 is no watch. Any other status is a failed read: report it with pgrep's stderr, and launch, stop or relaunch nothing on it. The watch's own `oversee-watch.pid`, beside the fleet state, is its claim record for the refusals `oversee-watch --help` states, not this liveness read. After a self-succession `oversee-succeed` restarts the watch from the successor pane, and the successor's own launch here takes that watch over.