diff --git a/.agents/skills/harness-ci/scripts/change-class b/.agents/skills/harness-ci/scripts/change-class index ad8fc482..8b84f4cc 100755 --- a/.agents/skills/harness-ci/scripts/change-class +++ b/.agents/skills/harness-ci/scripts/change-class @@ -1280,24 +1280,9 @@ load_measurement() { # the measurement's render roots, or roots this script cannot load, keep the # inventory on the diff, where the list below refuses it. Any other change to # it, a changed entry or an entry for a path that stays, keeps it there too. -names_only_roots_rendered() { # ROOTS - local root listed - [ "$1" != - ] || return 0 - load_measurement || return 1 - set -f - for root in ${1//,/ }; do - for listed in $BRANCH_GROWTH_RENDER_ROOTS; do - [ "$root" = "$listed" ] && continue 2 - done - set +f - echo "inventory-kept: cause=root-not-rendered root=$root" >&2 - return 1 - done - set +f -} case "$inventory_change" in names-only\ *\ roots=*) - if names_only_roots_rendered "${inventory_change##* roots=}"; then + if names_only_roots_rendered "${inventory_change##* roots=}" load_measurement; then changed="$(awk '$0 != ".kendex-generated.json"' <<<"$changed")" || answer standard "cause=path-set-unreadable" fi ;; diff --git a/.agents/skills/harness-ci/scripts/lib/change-class.sh b/.agents/skills/harness-ci/scripts/lib/change-class.sh index 5e53381e..e9a328b6 100755 --- a/.agents/skills/harness-ci/scripts/lib/change-class.sh +++ b/.agents/skills/harness-ci/scripts/lib/change-class.sh @@ -18,6 +18,26 @@ # configuration. Candidacy permits proof setup; release comparison owns it. HARNESS_CI_REFRESH_CALLER=.github/workflows/kendex-refresh.yml +# harness-only owns the names-only verdict. Its roots must also belong to +# the caller's measurement roots before the inventory can leave the path set. +# LOAD_ROOTS sets BRANCH_GROWTH_RENDER_ROOTS from the caller's trusted settings. +names_only_roots_rendered() { # ROOTS LOAD_ROOTS [ARG...] + local roots="$1" root listed + shift + [ "$roots" != - ] || return 0 + "$@" || return 1 + set -f + for root in ${roots//,/ }; do + for listed in $BRANCH_GROWTH_RENDER_ROOTS; do + [ "$root" = "$listed" ] && continue 2 + done + set +f + echo "inventory-kept: cause=root-not-rendered root=$root" >&2 + return 1 + done + set +f +} + change_class_path_match() { # PATH GLOBS local glob PATH_MATCH_HIT="" diff --git a/.agents/skills/orch/references/communication-modes.md b/.agents/skills/orch/references/communication-modes.md index 98ecef33..09528278 100644 --- a/.agents/skills/orch/references/communication-modes.md +++ b/.agents/skills/orch/references/communication-modes.md @@ -188,7 +188,7 @@ In flight: [EACH TRACKER ID WITH ONE NEXT ACTION] [CURRENT SESSION'S OPEN PROGRESS ONLY: ONE LINE PER ACTION WITH ITS TIME, OPERATION, OBSERVED RESULT AND DECISION REASON; EXCLUDE RULINGS AND FILING HISTORY (FLEET LOG), AND VERIFYING READINGS (THEIR TRACKER ITEMS)] ``` -The handoff is a current snapshot. Prepare the complete body below Start here in a separate ignored scratch file. At Stop and succession, write it with `oversee-succeed --write-only --handoff-body [BODY]` under [oversee.md § 5](../workflows/oversee.md#5-stop). Add `--handoff [PATH]` when the fleet uses another destination. Require a successful write before completing preparation, attempting plain succession, claiming a current installed snapshot or recording it for manual recovery. A failed write stops preparation; report its cause. The writer replaces the complete snapshot and supplies its current fleet generation and UTC write time. It refuses the installed handoff as body input, links to it and any body with a Start here line. The body is replaceable session scratch with no archive role. The caller keeps ownership of it. The successor and recovery read only the installed handoff. +The handoff is a current snapshot. Prepare the complete body below Start here in a separate ignored scratch file. At Stop and succession, write it with `oversee-succeed --write-only --handoff-body [BODY]` under [oversee.md § 5](../workflows/oversee.md#5-stop). Add `--handoff [PATH]` when the fleet uses another destination. Require a successful write before completing preparation, attempting plain succession, claiming a current installed snapshot or recording it for manual recovery. A failed write stops preparation; report its cause. The writer replaces the complete snapshot and supplies its current fleet generation and UTC write time. It refuses the installed handoff as body input, links to it and any body with a Start here line. In the body's Progress log, it refuses labeled rulings, filing history and Verifying readings with the first matching line number and record home; `oversee-succeed --help` names the refusal. It checks labeled forms only; paraphrases pass. A bare Verifying state stays legal. The body is replaceable session scratch with no archive role. The caller keeps ownership of it. The successor and recovery read only the installed handoff. For live self-succession, `oversee-succeed` refuses `handoff-stale` when the Start here generation is absent or differs from the fleet record; it does not judge the time. Plain succession never rewrites retained content. Dead and walled recovery use the existing handoff plus bounded live records, even when the handoff is stale or absent: their callers cannot write a live snapshot. At succession, promote open progress into the sections above and cut the log. Keep no archive copy: commits, the tracker, mailboxes and Slack hold history. diff --git a/.agents/skills/orch/references/narrow-change.conf b/.agents/skills/orch/references/narrow-change.conf index baf8d292..bd37236f 100644 --- a/.agents/skills/orch/references/narrow-change.conf +++ b/.agents/skills/orch/references/narrow-change.conf @@ -156,7 +156,7 @@ path .github/allowed_models.txt # reach this file for the wider `small` class, and micro.md § Escape # condition 3 takes them too: every `path` line here escapes the micro tier, # except one a `superseded` line names, as the header states. The inventory -# exception is stated in ../../harness-ci/scripts/change-class beside +# exception is stated in ../../harness-ci/scripts/lib/change-class.sh beside # names_only_roots_rendered. path .kendex-lock.json path .kendex-generated.json diff --git a/.agents/skills/orch/references/oversee-events.md b/.agents/skills/orch/references/oversee-events.md index b5d9dd5f..33fbd4f5 100644 --- a/.agents/skills/orch/references/oversee-events.md +++ b/.agents/skills/orch/references/oversee-events.md @@ -82,7 +82,7 @@ To wake a lane below is to send it the directive and wake it per `oversee-lanes. - `parked-closed [ITEM] pr=[N] repo=[OWNER/REPO]` → the dependency PR closed without merging. The watch reports it once. Resolve the hold or resume the lane through the same Recovery relaunch route. - `lane-exited` → the window is alive but the harness under it is gone (the lane's closing lines follow the event and say why; `payload-noise-only lines=N` in their place means the lane deleted its own worktree and the shell has drawn nothing since). A lane stopped by a harness session limit ("You've hit your session limit · resets 9:50am") is not dead: resume it under another auth lane (`lanes pick --harness [HARNESS] --model [MODEL]` for the model the replacement will run, then [lane-directive.md § Recovery relaunch](lane-directive.md#recovery-relaunch)), or wait for the shown reset and use Recovery relaunch on the original account. The relaunch carries its own continuation line, except on a hosted codex lane, which resumes without one and takes it by Pane paste ([lane-directive.md § Recovery relaunch](lane-directive.md#recovery-relaunch)). A lane that reported `handoff` on an earlier run is relaunched per that event, not resumed or nudged. Any other exit is the `window-gone` rule below. - `overseer-dead`, `overseer-walled`, `overseer-reserve`, `overseer-context-unmeasured`, `overseer-context-stale`, `overseer-mark` → the overseer's own session: [overseer-session-events.md](overseer-session-events.md). -- `handoff` → the item's lane recorded its handoff and exited: relaunch it per § Judgement rules, Hand off a lane; the record follows the event line. +- `handoff` → the item's lane recorded its handoff and exited: relaunch it per § Judgement rules, Hand off a lane; the record follows the event line. When `cause` is `model-switched`, pin the recorded `model` in the recovery launch flags. A native remote lane dispatches its predecessor stop through the launch identity owner. Report `stop_failure` cause and process identity when that field stands on the handoff. The existing controller recovery route rechecks the stop before relaunch. A handoff alone does not confirm that its predecessor stopped. - `lane-closed` → the watch ran `lane-close` under the [Lane close contract](lane-reach.md#lane-close). Record the line below in the fleet log, the provider's `kept=PATH`, `kept=none` for no archive, or `closed=absent item=ITEM` for a host that no longer held the item and ran no archive pass ([lane-host.md](../schemas/lane-host.md) `close`). - `lane-close-refused` → `lane-close` relayed exit 3 from the provider and the sandbox stays. Surface the `path=` line that follows to the user; its value is the checkout the close stopped on, never the path that stopped it. Render-only drift is saved as a binary patch under the hosted clone's `tmp`, restored, and refreshed before close, and the provider refuses on any of several conditions. An untraced change in the checkout is the common one; a saved patch that does not carry a path the close was about to restore or remove is another. Several of them leave every changed path inside the generated inventory, so a hunt for an untraced path can find none. Inspect the directory and never force the close. - `lane-close` refusing `stop-failed` or `exit-timeout` → the idle lane's harness did not end by signal, and the window and record stay. `stop-failed` names the failed step as `cause=`: `cause=timeout` is a harness that ignored its signal, which on a hosted lane arrives as `cause=provider` with the provider's own lines above the refusal, `cause=identity-unread` is a local record naming no launch identity over a pane with no harness process under it, `cause=identity-stale` one naming a harness that has exited, or whose `pid=` a later process holds, over a pane with none under it either, and `cause=process-read-failed` a process read under the pane, or the process table read at the identity the stop signals by, that failed. `target=wake` is the turn a wake started, stopped by the record's `wake` before the record reads done or stopped. A hosted stop by `lane-host-ssh` refusing `stop-identity-invalid` is a clone whose `.git/lane-host-pid` holds no pid and start: remove that file and close again, which stops the lane by its worktree. `cause=provider status=4` under `--keep-sandbox` is a hosted item whose worktree its own close-out removed, under a stop that finds the harness by that directory: the stop has no directory to find the harness by, and only a full close, which prints `stop-skipped cause=worktree-removed`, closes it: its host close ends the harness where the provider's `close` ends the sandbox, and otherwise the window kill hangs up the lane's ssh session. That reading needs the provider's `stop-worktree-removed item=[ITEM]` line above the refusal; `cause=provider status=4` without it is the provider's own `4`, such as a sandbox it stopped, whose line above names it: start the sandbox, then close again. `exit-timeout` on a local lane names `identity=`, the identity the stop signalled by: `recorded`, the record's `launch`, or `pane`, read off the pane because the record named none or a stale one. `processes=0` there is a harness that identity named which exited before its signal, or under `recorded` no process named for the harness at or below the recorded pid. `processes=` above 0 means that many harness processes were signalled and confirmed exited, yet the pane still does not read exited: inspect what the pane now runs. Report the refusal lines to the user. Never type into the pane to end the harness instead. diff --git a/.agents/skills/orch/schemas/workflow-state.md b/.agents/skills/orch/schemas/workflow-state.md index f997a5c2..547ca907 100644 --- a/.agents/skills/orch/schemas/workflow-state.md +++ b/.agents/skills/orch/schemas/workflow-state.md @@ -185,7 +185,9 @@ The same locked update appends one `fleet_log` entry with `kind` `model-switched The lane recovery dispatcher marks the switch row's `recovery` as `starting` before launch, `replaced` after success, or `failed` after failure. A later launch's mismatch also proves a pending `starting` replacement reached a live harness. The dispatcher counts successful replacements since this item's last `model-recovery-reset` row. [lanes help](../scripts/lanes) defines `ORCH_MODEL_RECOVERY_LIMIT` and its default. A refusal adds one `model-recovery-limit` row with `recoveries`, `limit` and the keyed refusal in `text`. The first Stop of the bound replacement resets the count when that launch has no mismatch. It writes `model_check.outcome` as `passed` with `session_id`, `launch_identity` and `at`, and appends one `model-recovery-reset` row. Repeated Stops retain that check. A Stop from a stale session cannot reset the current launch. -A lane that cannot read its fleet launch record stores the unavailable reading under `model_checks[session_id]` in its existing item workflow state. The hook uses the state writer's lock when that item state needs initialization. Repeated events retain the first unavailable reading and deliver no second notice. The session continues with its model identity unjudged. +A native remote lane with no local fleet record reads its launch model from the separate JSON launch record owned by `lane-marker` (`--launch-path`). Its `identity_file` names the predecessor record written by the SSH launch prefix. The hook verifies that the callback descends from that live predecessor. It stores the model check under `model_checks[session_id]` in the item's existing workflow state. A mismatch preserves the durable handoff, adds `cause:"model-switched"` and the recorded `model`, then dispatches the existing stop owner for the verified native predecessor. A failed stop stores `handoff.stop_failure` with `cause`, the affected `pid` (null when the failure names no process), the predecessor `launch` identity, `exit`, and `at`. The detached child also writes a keyed failure to the existing recovery log. A new model handoff clears the prior stop failure. A dispatch failure reaches the callback as unavailable context. The controller reads the handoff over the existing host transport, reports a stop failure, and rechecks the existing stop before relaunching with the recorded model pinned. No fleet watcher runs on the lane's host. + +A lane without readable launch evidence stores the unavailable reading under `model_checks[session_id]` in its existing item workflow state. The hook uses the state writer's lock when that item state needs initialization. Repeated events for the same launch retain the reading and deliver no second notice. The session continues with its model identity unjudged. `workflow-state-oversee.json`, under the key `oversee`, is the fleet's record, at the one address every launch passes `open-terminal --state-dir` and the watch reads as `--state` ([oversee.md § 3 Lane record](../workflows/oversee.md#3-launch)). `open-terminal` creates it on the first launch of a tmux fleet; the other surfaces create it as oversee.md § 3 Lane record directs. It carries the fields above unused and these: diff --git a/.agents/skills/orch/scripts/item-tier b/.agents/skills/orch/scripts/item-tier index bc2613df..14e70ec1 100755 --- a/.agents/skills/orch/scripts/item-tier +++ b/.agents/skills/orch/scripts/item-tier @@ -195,6 +195,10 @@ if [ -n "$base" ]; then consider micro branch-paths while IFS= read -r -d '' path; do change_class_path "$path" launch + if [ "$path" = .kendex-generated.json ] && [[ "$CHANGE_CLASS_PATH_CAUSE" = excluded-path\ * ]] && + change_class_inventory_rendered "$base" "$head" "$repo" "$scratch/inventory-err"; then + continue + fi [ -z "$CHANGE_CLASS_PATH" ] || consider "$CHANGE_CLASS_PATH" "$CHANGE_CLASS_PATH_CAUSE" done <"$range_paths" fi diff --git a/.agents/skills/orch/scripts/lane-host-ssh b/.agents/skills/orch/scripts/lane-host-ssh index 9bd45d4e..171e00fb 100755 --- a/.agents/skills/orch/scripts/lane-host-ssh +++ b/.agents/skills/orch/scripts/lane-host-ssh @@ -801,7 +801,8 @@ if [ ! -x "$marker" ]; then printf 'the host clone predates the launch record writer; refresh it to a build carrying that script\\n' >&2 exit 1 fi -exec "$marker" "$2" "$3"''', row["clone"], path, args.item) +exec "$marker" --launch-record "$4" "$2" "$3"''', row["clone"], path, args.item, + json.dumps({"identity_file": row["clone"] + IDENTITY_FILE})) pi_root = f"\tpi-root={remote_account}" if args.harness == "pi" else "" lifecycle_output(row, f"ssh-target={row['target']}\tpath={path}\tremote-prefix={prefix}{pi_root}\n".encode()) diff --git a/.agents/skills/orch/scripts/lane-marker b/.agents/skills/orch/scripts/lane-marker index 891cd827..cd54c0a4 100755 --- a/.agents/skills/orch/scripts/lane-marker +++ b/.agents/skills/orch/scripts/lane-marker @@ -21,7 +21,7 @@ message() { # KEY VALUE [CAUSE] printf 'lane-marker: %s=%s\n' "$1" "$2" case "$1" in args) - echo "lane-marker takes WORKTREE ITEM with an optional resumed HARNESS SESSION pair, either form after an optional --lane-refresh, or --marker-path COMMON_GIT_DIR ITEM, or --refresh-path GIT_DIR" + echo "lane-marker takes WORKTREE ITEM with an optional resumed HARNESS SESSION pair, after optional --launch-record JSON and --lane-refresh, or --launch-contents JSON MODEL, --marker-path COMMON_GIT_DIR ITEM, --launch-path COMMON_GIT_DIR ITEM, or --refresh-path GIT_DIR" ;; item) echo "the 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 composing a path it does not name" @@ -52,8 +52,11 @@ usage() { cat << 'EOF' Usage: lane-marker WORKTREE ITEM lane-marker WORKTREE ITEM HARNESS SESSION + lane-marker --launch-record JSON WORKTREE ITEM [HARNESS SESSION] + lane-marker --launch-contents JSON MODEL lane-marker --lane-refresh WORKTREE ITEM [HARNESS SESSION] lane-marker --marker-path COMMON_GIT_DIR ITEM + lane-marker --launch-path COMMON_GIT_DIR ITEM lane-marker --refresh-path GIT_DIR Write a lane's launch record, both halves of it: @@ -65,6 +68,19 @@ and the refresh record beside them: /lane-refresh holds the lane's root +The path marker holds only the root and a final newline. A separate record +holds optional launch fields: + + /lane-launch/.json + +This record is one JSON object. `model` is the recorded launch model. +`identity_file` names the native predecessor record the SSH prefix writes +before the harness starts. `--launch-contents` prints these fields for a +launcher that writes through its host transport. MODEL adds +the controller's recorded model while retaining the native launch fields. +`--launch-path` prints the record's path without writing it. A launch without +`--launch-record` removes a previous launch record. + `--lane-refresh` writes the refresh record, which says the launch asked for a refresh lane, one whose brief runs `kendex refresh` or `kendex apply` with the CLI's own `--lane-refresh`. Every launch without it removes the record, so a @@ -95,17 +111,18 @@ itself would be a second spelling of the one this script writes. directory it is given; that launcher writes the root there for a refresh lane and empties the file otherwise, an empty record reading as no refresh lane. -Containment: five components must be plain, never a symlink and never another +Containment: the components must be plain, never a symlink and never another kind of file. Under the common git directory, `lane-mail` and the marker in it; under the worktree, `tmp/lane-mail` and the item's own directory in it; the -refresh record in the worktree git directory. `tmp` itself is left alone, +refresh record in the worktree git directory; `lane-launch` and the launch +record under the common git directory. `tmp` itself is left alone, because a worktree setup may link it. A component outside that rule would put the marker, the mailbox or the refresh record somewhere the lane never reads, and the refusal names the one that broke it. Exit status: 0 both halves stand (and the refresh record, with `--lane-refresh`), or - `--marker-path` or `--refresh-path` printed the path + `--launch-contents` printed the launch record, or a path option printed its path 2 a component is not what it names, the item is outside the alphabet a work item is spelled in, a git or context read failed, or a write failed; the cause is one `lane-marker: =` line on stderr with the English under it @@ -139,8 +156,28 @@ refresh_path() { # GIT_DIR printf '%s/lane-refresh\n' "$1" } +# The launch fields live apart from path markers read by released CLIs. +launch_path() { # COMMON_GIT_DIR ITEM + local marker + marker=$(marker_path "$1" "$2") + printf '%s/lane-launch/%s.json\n' "$1" "${marker##*/}" +} + +launch_contents() { # LAUNCH_RECORD MODEL + local record + record=$(jq -nc --argjson record "$1" --arg model "$2" '$record + {model:$model}' 2>&1) || refuse args launch-record "$record" + printf '%s\n' "$record" +} + [ "${1:-}" != --help ] || { usage; exit 0; } +if [ "${1:-}" = --launch-contents ]; then + [ "$#" -eq 3 ] || refuse args "$#" + shift + launch_contents "$@" + exit 0 +fi + if [ "${1:-}" = --marker-path ]; then [ "$#" -eq 3 ] || refuse args "$#" check_item "$3" @@ -148,6 +185,13 @@ if [ "${1:-}" = --marker-path ]; then exit 0 fi +if [ "${1:-}" = --launch-path ]; then + [ "$#" -eq 3 ] || refuse args "$#" + check_item "$3" + launch_path "$2" "$3" + exit 0 +fi + if [ "${1:-}" = --refresh-path ]; then [ "$#" -eq 2 ] || refuse args "$#" refresh_path "$2" @@ -155,6 +199,12 @@ if [ "${1:-}" = --refresh-path ]; then fi REFRESH=false +LAUNCH_RECORD="" +if [ "${1:-}" = --launch-record ]; then + [ "$#" -ge 2 ] || refuse args "$#" + LAUNCH_RECORD=$(jq -ce 'select(type == "object")' <<<"$2" 2>&1) || refuse args launch-record "$LAUNCH_RECORD" + shift 2 +fi if [ "${1:-}" = --lane-refresh ]; then REFRESH=true shift @@ -177,17 +227,18 @@ GIT_DIR=$(git -C "$WT" rev-parse --absolute-git-dir 2>&1) || GIT_DIR_RC=$? [ "$GIT_DIR_RC" -eq 0 ] || refuse git 'rev-parse --absolute-git-dir' "$GIT_DIR" MARKER=$(marker_path "$COMMON" "$ITEM") REFRESH_RECORD=$(refresh_path "$GIT_DIR") +LAUNCH_PATH=$(launch_path "$COMMON" "$ITEM") BOX="$ROOT/tmp/lane-mail/$ITEM" # Judged before anything is created, so a refusal leaves the tree as it was. -for path in "$COMMON/lane-mail" "$ROOT/tmp/lane-mail" "$BOX"; do +for path in "$COMMON/lane-mail" "$COMMON/lane-launch" "$ROOT/tmp/lane-mail" "$BOX"; do { [ ! -L "$path" ] && { [ ! -e "$path" ] || [ -d "$path" ]; }; } || refuse unsafe "$path" done -for path in "$MARKER" "$REFRESH_RECORD"; do +for path in "$MARKER" "$REFRESH_RECORD" "$LAUNCH_PATH"; do { [ ! -L "$path" ] && { [ ! -e "$path" ] || [ -f "$path" ]; }; } || refuse unsafe "$path" done -MADE=$(mkdir -p -- "$COMMON/lane-mail" "$BOX" 2>&1) || refuse write "$BOX" "$MADE" +MADE=$(mkdir -p -- "$COMMON/lane-mail" "$COMMON/lane-launch" "$BOX" 2>&1) || refuse write "$BOX" "$MADE" # A resumed session keeps its context. The launcher supplies the selected # identity; a fresh or different session cannot inherit this reading. @@ -217,16 +268,21 @@ fi # The rename consumes it, so this removes nothing on the path that worked. STAGED="" trap '[ -z "$STAGED" ] || rm -f -- "${STAGED:?}"' EXIT -write_root() { # RECORD +write_record() { # RECORD CONTENTS local staged wrote staged=$(mktemp "${1%/*}/.${1##*/}.XXXXXX" 2>&1) || refuse write "$1" "$staged" STAGED=$staged - wrote=$({ printf '%s\n' "$ROOT" > "$STAGED" && mv -f -- "$STAGED" "$1"; } 2>&1) || + wrote=$({ printf '%s\n' "$2" > "$STAGED" && mv -f -- "$STAGED" "$1"; } 2>&1) || refuse write "$1" "$wrote" } -write_root "$MARKER" +write_record "$MARKER" "$ROOT" +if [ -n "$LAUNCH_RECORD" ]; then + write_record "$LAUNCH_PATH" "$LAUNCH_RECORD" +else + CLEARED=$(rm -f -- "${LAUNCH_PATH:?}" 2>&1) || refuse write "$LAUNCH_PATH" "$CLEARED" +fi if [ "$REFRESH" = true ]; then - write_root "$REFRESH_RECORD" + write_record "$REFRESH_RECORD" "$ROOT" else CLEARED=$(rm -f -- "${REFRESH_RECORD:?}" 2>&1) || refuse write "$REFRESH_RECORD" "$CLEARED" fi diff --git a/.agents/skills/orch/scripts/lib/change-class.sh b/.agents/skills/orch/scripts/lib/change-class.sh index 5a576842..12f4cf52 100755 --- a/.agents/skills/orch/scripts/lib/change-class.sh +++ b/.agents/skills/orch/scripts/lib/change-class.sh @@ -58,6 +58,7 @@ change_class_location_load() { # REPO fi CHANGE_CLASS_CAUSE=base-settings-unreadable root="$("$CHANGE_CLASS_LIB_DIR/../git-context" common-root "$1")" || return 1 + CHANGE_CLASS_SETTINGS_ROOT="$root" # The lane and its inherited settings cannot choose their own review depth. CHANGE_CLASS_STANDARD_PATHS="$( unset HARNESS_CI_STANDARD_PATHS @@ -72,6 +73,21 @@ change_class_location_load() { # REPO CHANGE_CLASS_CAUSE="" } +# An unreadable or absent verdict keeps the inventory under the path rules. +change_class_inventory_rendered() { # BASE HEAD REPO STDERR_FILE + local reader="$CHANGE_CLASS_LIB_DIR/../../../harness-ci/scripts/harness-only" inventory_change + [ -x "$reader" ] || return 1 + "$reader" --mode harness --event pull_request --base "$1" --head "$2" --repo "$3" \ + --output /dev/null > /dev/null 2>"$4" || return 1 + inventory_change="$(sed -n 's/^inventory-change: //p' "$4" | tail -1)" || return 1 + case "$inventory_change" in + names-only\ *\ roots=*) + names_only_roots_rendered "${inventory_change##* roots=}" \ + branch_growth_render_roots "$CHANGE_CLASS_SETTINGS_ROOT" ;; + *) return 1 ;; + esac +} + change_class_read() { # BASE HEAD REPO STDERR_FILE local classifier answer status=0 CHANGE_CLASS="" diff --git a/.agents/skills/orch/scripts/lib/lane-relaunch.sh b/.agents/skills/orch/scripts/lib/lane-relaunch.sh index 0cffc95e..e14e2648 100644 --- a/.agents/skills/orch/scripts/lib/lane-relaunch.sh +++ b/.agents/skills/orch/scripts/lib/lane-relaunch.sh @@ -88,6 +88,30 @@ lane_model_recovery_result() { # RECORD STATE_DIR SCRIPTS SESSION RESULT ' --argjson record "$1" --arg session "$4" --arg result "$5" >/dev/null } +# The detached native stop cannot return its failure to the callback. Keep it +# on the same handoff the controller already reads, bound to this launch. +lane_native_model_stop() { # RECORD STATE_DIR SCRIPTS ITEM SESSION + local record="$1" state_dir="$2" scripts="$3" item="$4" session="$5" pid start detail rc=0 + pid="$(jq -er '.launch.pid' <<<"$record")" || return 1 + start="$(jq -er '.launch.start' <<<"$record")" || return 1 + lane_stop_identity "$pid" "$start" claude || rc=$? + [[ "$rc" -ne 0 ]] || return 0 + printf 'lane-relaunch: native-stop=failed item=%s cause=%s pid=%s launch_pid=%s exit=%s\n' \ + "$item" "$LANE_STOP_CAUSE" "${LANE_STOP_PID:-none}" "$pid" "$rc" >&2 + detail="$("$scripts/workflow-state" --state-dir "$state_dir" update "$item" ' + if .handoff.session_id == $session + and .model_checks[$session].launch_identity == ($record | {launch,launched_at}) + then .handoff.stop_failure = {cause:$cause,pid:(if $pid == "" then null else $pid end), + launch:$record.launch,exit:$exit,at:(now|strftime("%Y-%m-%dT%H:%M:%SZ"))} + else . end' --arg session "$session" --argjson record "$record" \ + --arg cause "$LANE_STOP_CAUSE" --arg pid "$LANE_STOP_PID" --argjson exit "$rc" 2>&1)" || { + printf 'lane-relaunch: native-stop=record-failed item=%s\n' "$item" >&2 + printf '%s\n' "$detail" >&2 + return 1 + } + return "$rc" +} + # The hook acquires one session's recovery under workflow-state's lock before # starting this outside the predecessor's process group. Check the recovery # arguments before stopping it; account selection and launch follow the stop. diff --git a/.agents/skills/orch/scripts/open-terminal b/.agents/skills/orch/scripts/open-terminal index 5fe6fbf9..23d6653e 100755 --- a/.agents/skills/orch/scripts/open-terminal +++ b/.agents/skills/orch/scripts/open-terminal @@ -2292,7 +2292,7 @@ host_marker_busy() { # STATUS # first `lane-mail append` that reaches it creates it under the lock that host # holds; nothing here can take that lock. host_lane_marker() { # ITEM ROOT - local gitfile common marker read_back git_dir refresh + local gitfile common marker read_back git_dir refresh launch launch_record contents model read_rc=0 LANE_MARKER_REASON=host-gitfile-unread LANE_MARKER_FIELDS=("path=$2/.git") gitfile="$(host_transport cat --item "$1" "$2/.git")" || { host_marker_busy $?; return 1; } @@ -2306,6 +2306,15 @@ host_lane_marker() { # ITEM ROOT printf '%s\n' "$2" | host_transport put --item "$1" "$marker" || { host_marker_busy $?; return 1; } read_back="$(host_transport cat --item "$1" "$marker")" || { host_marker_busy $?; return 1; } [[ "$read_back" == "$2" ]] || return 1 + launch="$("$SCRIPT_DIR/lane-marker" --launch-path "$common" "$1")" || return 1 + LANE_MARKER_FIELDS=("path=$launch") + launch_record="$(host_transport cat --item "$1" "$launch")" || read_rc=$? + case "$read_rc" in 0) ;; 2) launch_record='{}' ;; *) host_marker_busy "$read_rc"; return 1 ;; esac + model="$(launch_choice_model_id "$LAUNCH_HARNESS" "$LAUNCH_MODEL" launch)" || return 1 + contents="$("$SCRIPT_DIR/lane-marker" --launch-contents "$launch_record" "$model")" || return 1 + printf '%s\n' "$contents" | host_transport put --item "$1" "$launch" || { host_marker_busy $?; return 1; } + read_back="$(host_transport cat --item "$1" "$launch")" || { host_marker_busy $?; return 1; } + [[ "$read_back" == "$contents" ]] || return 1 # The refresh record, which the host has no verb to delete: a launch that is # no refresh lane empties it, and the hook reads an empty record as none. LANE_MARKER_REASON=marker-failed diff --git a/.agents/skills/orch/scripts/oversee-succeed b/.agents/skills/orch/scripts/oversee-succeed index 7aae1e2f..158dd9f2 100755 --- a/.agents/skills/orch/scripts/oversee-succeed +++ b/.agents/skills/orch/scripts/oversee-succeed @@ -25,6 +25,8 @@ # headerless file. The writer adds Start here with the current fleet generation # and UTC write time. `--write-only` does this without a pane or launch choice, # even when succession is off. The source is caller-owned session scratch. +# Progress log refuses labeled rulings and filing history (fleet log), and +# Verifying readings (tracker item); paraphrases are not detected. # Plain succession only checks the installed handoff and never stamps it. # # The launch modes share ONE launch path. The first is an overseer succeeding @@ -513,7 +515,7 @@ message() { # KEY FIELD=VALUE... invalid-wait-secs) text='--wait-secs takes a positive whole number of seconds.' ;; invalid-handoff) text='The handoff path takes letters, digits and ./_- only.' ;; handoff-stale) text='The handoff must carry one Start here line with the current fleet generation. found= names what the reader took from the handoff. Rewrite it before succession; no successor was launched.' ;; - handoff-body-invalid) text='Supply a separate nonempty regular body file with no Start here line.' ;; + handoff-body-invalid) text='Supply a separate nonempty regular body file with no Start here line. Move labeled Progress log records to the named home: rulings and filing history to the fleet log, Verifying readings to their tracker items.' ;; handoff-write-failed) text='The snapshot could not replace the handoff. The previous handoff remains in place.' ;; model-recovery-refused) text='The acquired model recovery could not complete for its recorded predecessor. Read the reason and recover that bound session or register its executable launch command before requesting recovery again.' ;; invalid-dead-pane) text='--dead-pane takes a tmux pane id, %N.' ;; @@ -741,6 +743,7 @@ handoff_record_read() { # staged files and ends before succession can create the successor. handoff_write() ( local source_parent destination_parent source_path destination_path stage now header_rc + local line line_number=0 progress_log=0 kind home HANDOFF="$HANDOFF_FILE" [[ ! -L "$HANDOFF_BODY" && -f "$HANDOFF_BODY" && -s "$HANDOFF_BODY" ]] \ || die handoff-body-invalid "path=$HANDOFF_BODY" reason=not-nonempty-regular @@ -775,6 +778,30 @@ handoff_write() ( 1) ;; *) die handoff-write-failed "path=$HANDOFF" step=body-check ;; esac + # Overseers supply these labeled records in their body payload. Recovery's + # retained predecessor snapshot is not current input and stays untouched. + while IFS= read -r line || [[ -n "$line" ]]; do + line_number=$((line_number + 1)) + case "$line" in + '## Progress log') progress_log=1; continue ;; + '## '*) progress_log=0 ;; + esac + (( progress_log )) || continue + kind="" + home=fleet-log + if matches "$line" '^[-* ]*Rulings?:'; then + kind=ruling + elif matches "$line" '(^|[^[:alnum:]_])Filed:' \ + || matches "$line" '([A-Z]+-[0-9]+|#[0-9]+) filed([^[:alnum:]_]|$)'; then + kind=filing + elif matches "$line" '^[-* ]*Verifying reads?:' \ + || matches "$line" 'box [0-9]+ (failed|passed)([^[:alnum:]_]|$)'; then + kind=verifying + home=tracker-item + fi + [[ -z "$kind" ]] || die handoff-body-invalid "path=$HANDOFF_BODY" \ + "reason=progress-log-$kind" "line=$line_number" "home=$home" + done < "$stage/payload" now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ || die handoff-write-failed "path=$HANDOFF" step=date printf 'Start here: %s generation=%s\n\n' "$now" "$generation" > "$stage/complete" \ diff --git a/.claude/hooks/lane-mail-check.sh b/.claude/hooks/lane-mail-check.sh index 710faefb..3c3b96dd 100644 --- a/.claude/hooks/lane-mail-check.sh +++ b/.claude/hooks/lane-mail-check.sh @@ -5,7 +5,7 @@ # 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 native session must also match the recorded harness, readable caller home and payload session id; `ol_record_heal` binds the first matching native id from the recorded pane, and `oversee register` clears that binding after a resume. Pi and any harness whose `lane_context_caller_home` reports no home compare the harness and native session id only. 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. A Jarvis session's wake, the process `ORCH_WAKE_PROCESS` matches, is no such watch: it moves no cursor, so that session is handed each note in full here once and acknowledges it here, and its wake carries no note's body, at most the count and read command `lane-mail watch` prints. Where the payload's `kendex_ack` is `caller`, which the pi-hooks lane mail wake sends, the context carries the reader's `--ack` command as `kendex_ack` and nothing is acknowledged here: the wake runs it once its send returns, so a send that throws leaves the lines unread. 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/mailbox/scripts/lane-mail` or the shared `.agents/skills/mailbox/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/mailbox/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 bound native session the fleet record names, or the overseer it lost whose native identity still matches a nonempty recorded binding, 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. A session start acknowledges them only once that context is written. A prompt leaves the lines unread because Copilot can discard its output, so a later inbox read or hook delivery can repeat them. Neither arm refuses anything: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Before an attached agent launch, the halt arm also judges the lead of a launched lane or the identified fleet overseer at the context mark, refusing missing context with the orch job runner or a separate session as the route. While an attached agent of the session may still be out, the mark is less ORCH_HANDOFF_AGENT_RESERVE_TOKENS, default 150000 tokens, equality refused: Claude Code reads that roster from its task registry where the payload carries one, else from the transcript's Agent-call results, task notifications, teammate idle notifications and sends, and reads an unreadable roster or an entry neither read classifies as unknown; Codex reads ordered collaboration snapshots, launches and followups from its rollout. Idle, completed, errored, shutdown and not-found children release the reserve; initializing, running and interrupted children keep it. A full unfiltered snapshot replaces earlier unknown observations only when its call does not overlap a launch or followup; pending calls and unreadable current observations keep the roster unknown. After waiting, Codex must call list_agents with no path filter to observe returns. Other harnesses read none, so their rosters are unknown. With none out the launch meets the plain mark. A launch the reserve alone refuses with an agent out is keyed `agent-reserve` and names waiting for those agents, the job runner or a separate session, never a handoff, since the turn end and every succession judge the plain mark. With the roster unknown no wait can be observed, so the refusal is keyed `agent-unobserved` and names the job runner or a separate session, and for a lane its handoff record. On Pi the lead launches through subagent; delegate_subagent carries child identity and is outside this lead-only reserve rule. At a context hold, a live session waits for attached agent returns in this turn; Claude Stop background_tasks is preferred, with the transcript's measured Agent-call results, async launch and teammate spawn records, task notifications, teammate idle notifications and sends as the fallback when absent. Other harnesses name known unfinished children and known output paths, or unknown paths, in the emergency handoff. A standing handoff retains its escape, and the successor reads existing output before repeating work. 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` and the event 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, only for the pane's top-level harness and, on Copilot, only for a session the caller rule names the lead, 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. Run with `lane` by the critical-path-deny hook, it prints `lane` where the launch marker for the session's item binds the session's root and its mailbox directory stands, else `none`, records and judges nothing else, and exits 0; a gap past its payload read is reported on stderr and passed with nothing on stdout. 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 handoff refusals, 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, or by updating `tmp/lane-status-.md` under its launch root, 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`, judging a Claude lane on its own model. The picker owns the account handoff decision. For an ordinary account wall, the hook refuses the turn end and names the binding bucket's headroom against `ORCH_HANDOFF_HEADROOM_PCT` (default 3). When that picker reports `seat-reserve` for an overseer seat, the hook refuses the turn end. That refusal names the binding bucket's headroom and the configured `ORCH_OVERSEER_SEAT_RESERVE_PCT` (default 50) as independent facts. 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`. A lane's context and account mark refusals open `lane-mail-check: context=`, `lane-mail-check: headroom= bucket= mark=` or `lane-mail-check: seat-reserve= bucket= reserve=`. Each carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The record is read where `workflow-state --help` § Handoff record says, a launched lane naming the root its launch marker binds as the worktree, and the instruction names the state directory the read found, so a record written as told ends both refusals. 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 whose native identity matches a nonempty binding in the fleet record but whose pane only 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, its launch home or its native session id is healed from an authorized native session 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 caller remains unbound until a later binding write succeeds. 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. If `lane_context_transcript_owned` refuses an admitted overseer's transcript binding, the hook reports `transcript-unowned=` with the reason the library returns. 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 binding library's reason, `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 the native `session_id` the payload gives. A payload with no native session id is refused and cannot read mail or write overseer context or gaps. `overseer_identify` owns its native refusal diagnostics. 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 sends and its lane status file update state, never on words in its pane, 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, the word `held` where this judge refused that turn end, and an optional encoded status file update state; no record is a count of zero with no status state. A status file update since that record, including a rewrite of the same text, passes without a notice. 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 follows the orch Coordination notice rule: changed work sends a notice, and a continuing wait on a dispatched agent or an armed wake updates the lane status file only. 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, an optional `held` mark and an optional encoded status state 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` identifies an attached launch. The `lane-mail-halt` description owns the Copilot preToolUse decision and deadline contract. 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, and after a tool call `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. 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. A session the Jarvis runtime launched, which carries `JARVIS_RECORD`, has its lead's tool calls, on Copilot any call under the session id the record's start bound, and on Claude Code its turn ends, stamp that runtime record's launch row once as its Start brief taken, the fact the Jarvis launch worker reads beside the pane; another harness's turn end is no stamp, since Pi's carrier also dispatches it on a run that ended on an error and Codex and Copilot have no turn-failure event to tell one apart; a stamp that cannot be written is reported under `jarvis-rows-unwritten=` and passed, and the next such call writes it. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. # summary: Uses orch and mailbox to hand lanes their overseer's mail and hold turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or the Jarvis session whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty Jarvis session keys with no watch claim disable the check, and a running single pass is its own wake. Pi's wake check is partial; a continued turn or an unavailable check only warns. -# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. +# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. A native remote lane without a local fleet record reads the separate launch record's named fields and the native identity file its prefix wrote. It checks that the callback descends from that live predecessor, preserves a handoff with the recorded model, and dispatches the existing identity stop. A failed native stop records its cause and process identity on that handoff and emits a keyed recovery-log failure. Its controller reads that handoff over the existing host transport and relaunches with that model pinned. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] # requires: [lane-mail-deliver, lane-mail-halt, session-start-row, session-end-row, stop-failure-row, critical-path-deny, lane-mail-model-switch] @@ -1937,9 +1937,10 @@ handoff_instruction() { # Stop's task registry is the documented input. Older Claude versions, an # unreachable registry and every tool call omit it; only then use the measured # Agent-call results, task-notification status lines, teammate idle -# notifications, SendMessage calls and TaskStop calls. A tool result is read -# for those notification markers alone; no other part of a child result body -# is interpreted. +# notifications, SendMessage calls, TaskStop calls and ListAgents observations. +# The hook cannot call ListAgents itself; its recorded structured teammate +# rows supply the idle state when no notification arrived. SendMessage results +# prove a stopped agent resumed. Child reports supply notification markers. # AGENT_ROWS names each child still out, and AGENT_STATE says whether any may # be: none, out, or unknown where the roster could not be read or holds an # entry neither read classifies, which callers never read as none. @@ -1994,11 +1995,26 @@ claude_attached_agents() { # [emergency STATE_FILE] scan_bound=("${BOUND_BY[0]}" --kill-after=1 "$scan_seconds") # Each measured record moves one child: an async launch or a teammate # spawn puts it out; a task-notification ends an async child, and a - # teammate's idle_notification, which follows each of its reports, ends a - # teammate; a plain-text SendMessage to either puts it out again, and a + # teammate's terminal idle notification ends a teammate; a plain-text + # SendMessage to either puts it out again, and a # sent shutdown_request ends it, since the lead's transcript never # receives the approval and the lead is owed no further result. A # TaskStop call that did not fail ends the child it names by id or name. + # SendMessage can resume that stopped child as an in-process teammate; + # only its measured resume result proves the stopped run started again. + # ListAgents (2.1.302) records teammate states in toolUseResult.sections; + # its idle rows end the named turn even when no idle notification arrived. + # Every roster transition invalidates an earlier pending list. Claude + # Code 2.1.302 captures list state before its awaited peer reads, so even + # a completion appended to that result is newer than the snapshot. + # SendMessage writes the inbox before the teammate consumes it. No list + # snapshot proves consumption. A send from an ended turn has no earlier + # turn whose available notice could arrive afterward. A send to an + # active turn does: even its available notice can follow an empty drain + # that preceded the send. Keep that work unknown until the task ends. + # In 2.1.302, interrupted notices precede the inbox read, missing reasons + # continue the next prompt, and failed reasons can also continue. Only + # completedStatus=failed proves the latter ended the teammate task. # A foreground Agent call (no run_in_background, no name) returns its # child's finished output, so its result puts nothing out. Any other # result of an async or named call is unclassified. Claude Code appends a @@ -2022,43 +2038,96 @@ claude_attached_agents() { # [emergency STATE_FILE] (select(startswith("Spawned successfully")) | capture("\nagent_id: (?[A-Za-z0-9_-]+)\nname: (?[^\n]+)")?); def results: .content | if type == "array" then .[] else empty end | select(.type == "tool_result"); + def changed($id; $status): .generation += 1 | .agents[$id] = $status; + def awaiting: . == "queued-active" or . == "unconfirmed-active"; + def queued: awaiting or . == "queued" or . == "unconfirmed"; + def terminal: . == "completed" or . == "failed" or . == "killed"; + def uncertain: if terminal then . elif awaiting then "unconfirmed-active" elif queued then "unconfirmed" else "unknown" end; + def settled($prior; $notice): + if $prior | terminal then $prior + elif $notice.completedStatus == "failed" then "failed" + elif $notice.idleReason == "available" then + if $prior | awaiting then ($prior | uncertain) else "completed" end + elif $notice.idleReason == "interrupted" and (($prior | queued) == false) then "completed" + else $prior | uncertain end; def noted($m): reduce ($m.content | said | scan("[\\s\\S]*?([A-Za-z0-9_-]+)[\\s\\S]*?([a-z_]+)[\\s\\S]*?")) as $n - (. ; if .agents | has($n[0]) then .agents[$n[0]] = $n[1] else . end) + (. ; if .agents | has($n[0]) then + changed($n[0]; if $n[1] | terminal then $n[1] else (.agents[$n[0]] | uncertain) end) + else . end) | reduce ($m.content | said - | scan("]*>\\s*\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"")) as $n - (. ; if .names | has($n[0]) then .agents[.names[$n[0]]] = "completed" else . end); + | scan("]*>\\s*(\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"[\\s\\S]*?)\\s*")) as $n + (. ; if .names | has($n[0]) then .names[$n[0]] as $t + | ([$n[1] | fromjson? | objects][0]) as $notice + | changed($t; if $notice == null then (.agents[$t] | uncertain) else settled(.agents[$t]; $notice) end) + else . end); reduce (inputs | fromjson | objects + | select(.type == "assistant" or .type == "user" or .type == "queue-operation") | . as $record | if .type == "queue-operation" then select(.operation == "enqueue") | {role: "user", content: (.content | strings)} - else .message? | objects end) as $m ({calls: {}, agents: {}, names: {}, unclassified: false}; + else .message? | objects | . + {result: $record.toolUseResult} end) as $m + ({calls: {}, agents: {}, names: {}, generation: 0, unclassified: false}; if $m.role == "assistant" then reduce ($m.content | if type == "array" then .[] else empty end | select(.type == "tool_use" and (.id | type) == "string")) as $c (. ; if $c.name == "Agent" or $c.name == "Task" then - .calls[$c.id] = {sync: (($c.input.run_in_background | tostring) != "true" + .generation += 1 | .calls[$c.id] = {pending: true, sync: (($c.input.run_in_background | tostring) != "true" and (($c.input.name | type) != "string" or $c.input.name == ""))} elif $c.name == "SendMessage" and ($c.input.to | type) == "string" then - if ($c.input.message | type) == "string" then .calls[$c.id] = {to: $c.input.to, status: "running"} + .generation += 1 + | if ($c.input.message | type) == "string" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "running"} elif ($c.input.message | type) == "object" - and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {to: $c.input.to, status: "completed"} + and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "completed"} else . end elif $c.name == "TaskStop" and ($c.input.task_id | type) == "string" then - .calls[$c.id] = {to: $c.input.task_id, status: "killed"} + .generation += 1 | .calls[$c.id] = {pending: true, to: $c.input.task_id, status: "killed"} + elif $c.name == "ListAgents" then + .calls[$c.id] = {list: .generation, overlap: any(.calls[]; .pending == true)} else . end) elif $m.role == "user" then reduce ($m | results | [.content | text | launched][0] | objects) as $l - (. ; .agents[$l.id] = "running" + (. ; changed($l.id; "running") | if $l.name != null then .names[$l.name] = $l.id else . end) | noted($m) | reduce ($m | results) as $r (. ; .calls[$r.tool_use_id | if type == "string" then . else "" end] as $c + | if $c != null then .calls[$r.tool_use_id].pending = false + | .generation += (if $c.pending then 1 else 0 end) else . end | if [$r.content | text | launched][0] != null or $c == null or $r.is_error == true then . + elif $c | has("list") then + if $c.overlap or $c.list != .generation then . + elif ([$m | results] | length) != 1 or ($m.result.sections | type) != "array" then + reduce (.agents | to_entries[] | select(.value == "running" or (.value | queued))) as $a + (. ; changed($a.key; ($a.value | uncertain))) + else reduce ($m.result.sections[] | select(.kind == "teammates") | .rows[]) as $a + (. ; if .names | has($a.name) then .names[$a.name] as $t + | if .agents[$t] | queued then + if $a.status == "idle" or $a.status == "running" then . + else changed($t; (.agents[$t] | uncertain)) end + elif $a.status == "idle" then changed($t; "completed") + elif $a.status == "running" then changed($t; "running") + else changed($t; (.agents[$t] | uncertain)) end + else . end) + end elif $c | has("to") then (.names[$c.to] // $c.to) as $t - | if .agents | has($t) then .agents[$t] = $c.status else . end + | if (.agents | has($t)) == false then . + elif $c.status == "running" and .agents[$t] == "killed" then + (if ([$m | results] | length) == 1 and ($m.result | type) == "object" then $m.result + else [$r.content | text | fromjson?][0] end + | if type == "object" then . else {} end) as $sent + | if $sent.success == true + and ((($sent.message | strings) // "") | test("^Teammate \"[^\"]+\" was not running; resumed it as an in-process teammate with [0-9]+ prior messages and your message as its next prompt\\.$")) + then changed($t; "queued") + else changed($t; "unconfirmed") end + else changed($t; if $c.status != "running" then $c.status + elif .agents[$t] | terminal then "queued" + elif .agents[$t] == "unknown" or .agents[$t] == "unconfirmed" or .agents[$t] == "unconfirmed-active" then "unconfirmed-active" + else "queued-active" end) end elif $c.sync then . else .unclassified = true end) else . end) - | (.agents | to_entries[] | [.key, .value] | @tsv), + | (.agents | to_entries[] | [.key, + (if .value == "queued" or .value == "queued-active" then "running" + elif .value == "unconfirmed" or .value == "unconfirmed-active" then "unknown" else .value end)] | @tsv), (select(.unclassified) | ["?", "agent-result"] | @tsv) ' <"$TRANSCRIPT" 2>"$WORK_DIR/agents.err") || scan_rc=$? if [ "$scan_rc" -ne 0 ]; then @@ -2075,7 +2144,7 @@ claude_attached_agents() { # [emergency STATE_FILE] while IFS="$TAB" read -r id status; do [ -n "$id" ] || continue if [ "$id" = '?' ]; then unclassified=$status; continue; fi - case "$status" in completed | failed | killed) continue ;; esac + case "$status" in completed | failed | killed) continue ;; unknown) unclassified=agent-status ;; esac AGENT_STATE=out output=unknown # https://code.claude.com/docs/en/hooks#subagentstop @@ -2904,6 +2973,18 @@ pi_account() { # Called plainly, so errexit is live throughout this body: a status left to it # would exit the hook with neither 0 nor 2 and end the turn with no mark # judged. Every status is therefore tested where it is taken. +model_item_state() { # FILE STATE_DIR + # Reuse the state writer's lock when a launch predates its item state. + # Initialize only under that lock so concurrent callbacks cannot erase work. + "$BASH" -euo pipefail -c ' + . "$1/lib/file-lock.sh" + mkdir -p -- "$2" + (orch_take_lock 200 "$3.lock" 10 || exit 1 + [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null + ) 200>"$3.lock" + ' _ "$SCRIPTS" "$2" "$1" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err" +} + model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] local cause="$1" observed="$2" detail="${3:-}" file sd report notice notice="lane-mail-check: model-unavailable=$cause" @@ -2912,15 +2993,7 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice"; return 0; } sd=${file%/*} - # Reuse the state writer's lock when a launch predates its item state. - # Initialize only under that lock so concurrent callbacks cannot erase work. - if ! "$BASH" -euo pipefail -c ' - . "$1/lib/file-lock.sh" - mkdir -p -- "$2" - (orch_take_lock 200 "$3.lock" 10 || exit 1 - [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null - ) 200>"$3.lock" - ' _ "$SCRIPTS" "$sd" "$file" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err"; then + if ! model_item_state "$file" "$sd"; then hand_over "$notice$NL$(cat -- "$WORK_DIR/model.err")" return 0 fi @@ -2936,6 +3009,28 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice" } +# The SSH prefix owns the native PID record. A callback must still descend +# from that live predecessor; a delayed callback must not stop a replacement. +model_native_record() { + local file record launch table descendants hops pid current=false + launch=$("$SCRIPTS/lane-marker" --launch-path "$COMMON" "$ITEM") || return 1 + [ -f "$launch" ] && [ ! -L "$launch" ] || return 1 + record=$(cat -- "$launch") || return 1 + "$BASH" -euo pipefail -c '. "$1/lib/lane-state.sh"' _ "$SCRIPTS" || return 1 + . "$SCRIPTS/lib/lane-state.sh" + file=$(jq -er '.identity_file | strings | select(startswith("/"))' <<<"$record") || return 1 + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + lane_identity_read "$file" && lane_identity_status "$LANE_IDENTITY_PID" "$LANE_IDENTITY_START" || return 1 + table=$(lane_process_table) || return 1 + descendants=$(lane_process_below "$table" "$LANE_IDENTITY_PID" '.*' 1 "" pids) || return 1 + while read -r hops pid; do + [ "$pid" != "$$" ] || current=true + done <<<"$descendants" + [ "$current" = true ] || return 1 + jq -c --arg session "$SESSION" --arg pid "$LANE_IDENTITY_PID" --arg start "$LANE_IDENTITY_START" \ + '. + {session_id:$session,launch:{pid:$pid,start:$start}}' <<<"$record" +} + model_lane_association() { # RECORD SERVER jq -r --arg session "$SESSION" --arg pane "${TMUX_PANE:-}" --arg server "$2" ' if $session == "" then "unbound" @@ -3015,24 +3110,38 @@ handoff_check() { if [ "$ARM" = model-switch ]; then [ "$HARNESS" = claude ] || return 0 - local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery + local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery native=false file observed=$(jq -er '.to_model | strings | select(length > 0)' <<<"$INPUT" 2>/dev/null) || observed="" fleet=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path oversee 2>"$WORK_DIR/model.err") || { model_unavailable_notice state-unread "$observed"; return 0; } sd=${fleet%/*} - record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { - model_unavailable_notice launch-unread "$observed"; return 0; - } - if [ "$ROLE" = overseer ]; then - record_path='["overseer"]' + if [ "$ROLE" != overseer ] && [ ! -e "$fleet" ]; then + native=true + record=$(model_native_record 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + file=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$ITEM" 2>"$WORK_DIR/model.err") || { + model_unavailable_notice state-unread "$observed"; return 0; + } + sd=${file%/*} + model_item_state "$file" "$sd" || { + model_unavailable_notice state-unread "$observed"; return 0; + } else - record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 - fi - record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 - if [ "$record" = null ] || [ -z "$record" ]; then - model_unavailable_notice launch-missing "$observed" - return 0 + record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed"; return 0; + } + if [ "$ROLE" = overseer ]; then + record_path='["overseer"]' + else + record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 + fi + record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 + if [ "$record" = null ] || [ -z "$record" ]; then + model_unavailable_notice launch-missing "$observed" + return 0 + fi fi # A delayed event from a replaced session never acts on the replacement. if [ -n "${TMUX_PANE:-}" ]; then @@ -3084,27 +3193,42 @@ handoff_check() { fi outcome=switched [ -z "$cause" ] || outcome=unavailable - report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' - getpath($path) as $current - | if ($role == "overseer" and ($current | ol_model_launch) != $identity) - or $current.session_id != $record.session_id or $current.model != $record.model - or $current.launch != $record.launch or $current.launched_at != $record.launched_at - then {state: ., report: {result:"stale"}} - elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity - and ($current.model_check.outcome == "switched" - or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) - then {state: ., report: {result:"duplicate"}} - else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at - | {outcome:$outcome, session_id:$session, launch_identity:$identity, - launched:$launched, observed:$observed, at:$at, cause:$cause} as $check - | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) - | setpath($path + ["model_check"]; $check) - | {state: ., report: {result:"acquired", record:getpath($path)}} - end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ - --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + if [ "$native" = true ]; then + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report "$ITEM" ' + if .model_checks[$session].launch_identity == $identity + and (.model_checks[$session].outcome == "switched" + or ($outcome == "unavailable" and .model_checks[$session].outcome == "unavailable")) + then {state:.,report:{result:"duplicate"}} + else .model_checks[$session] = {outcome:$outcome,session_id:$session,launch_identity:$identity, + launched:$launched,observed:$observed,cause:$cause,at:(now|strftime("%Y-%m-%dT%H:%M:%SZ"))} + | {state:.,report:{result:"acquired"}} end' --arg session "$SESSION" --argjson identity "$identity" \ + --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" \ 2>"$WORK_DIR/model.err") || { - model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; - } + model_unavailable_notice record-unwritten "$observed"; return 0; + } + else + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' + getpath($path) as $current + | if ($role == "overseer" and ($current | ol_model_launch) != $identity) + or $current.session_id != $record.session_id or $current.model != $record.model + or $current.launch != $record.launch or $current.launched_at != $record.launched_at + then {state: ., report: {result:"stale"}} + elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity + and ($current.model_check.outcome == "switched" + or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) + then {state: ., report: {result:"duplicate"}} + else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at + | {outcome:$outcome, session_id:$session, launch_identity:$identity, + launched:$launched, observed:$observed, at:$at, cause:$cause} as $check + | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) + | setpath($path + ["model_check"]; $check) + | {state: ., report: {result:"acquired", record:getpath($path)}} + end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ + --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + 2>"$WORK_DIR/model.err") || { + model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + fi result=$(jq -r '.result' <<<"$report") || return 0 [ "$result" = acquired ] || return 0 if [ "$outcome" = unavailable ]; then @@ -3144,17 +3268,41 @@ handoff_check() { [ -z "$HANDOFF_FILE" ] || handoff_sd=${HANDOFF_FILE%/*} claude_attached_agents emergency "$handoff_sd/workflow-state-$ITEM.json" handoff=$(jq -nc --arg branch "$BRANCH" --arg worktree "$ROOT" --arg agents "$AGENT_ROWS" \ - --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --argjson prior "${HANDOFF_RECORD:-null}" \ + --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --arg model "$launched" --argjson prior "${HANDOFF_RECORD:-null}" \ '($prior // {merged:[],remaining:["Resume durable workflow state after model recovery."], branch:$branch,worktree:$worktree,open_pr:null}) | .traps = (((.traps // []) + [$agents]) | unique) - | .session_id=$session | .pane_key=$pane | .cause="model-switched"') || return 0 + | del(.stop_failure) + | .session_id=$session | .pane_key=$pane | .cause="model-switched" | .model=$model') || return 0 # Preserve existing workflow state, including unpushed round work. { [ -f "$handoff_sd/workflow-state-$ITEM.json" ] || "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" init "$ITEM" --worktree "$ROOT" >/dev/null 2>"$WORK_DIR/model.err"; } && "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" set "$ITEM" handoff "$handoff" >/dev/null 2>>"$WORK_DIR/model.err" || - model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + { + if [ "$native" = true ]; then + # The acquired switch check already occupies the notice slot. + hand_over "lane-mail-check: model-unavailable=handoff-unwritten$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + } # Recovery owns the stop and runs outside this hook's process group. + if [ "$native" = true ]; then + # The controller reads this handoff through its existing host state + # transport and relaunches there. This host only stops its predecessor. + if ! lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ + "$BASH" -euo pipefail -c ' + . "$1/lib/lane-state.sh" + . "$1/lib/lane-relaunch.sh" + lane_native_model_stop "$2" "$3" "$1" "$4" "$5" + ' _ "$SCRIPTS" "$record" "$handoff_sd" "$ITEM" "$SESSION" 2>"$WORK_DIR/model.err"; then + # The switched check already occupies the once-only unavailable slot. + hand_over "lane-mail-check: model-unavailable=recovery-unstarted$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + message model-switched "$launched observed=$observed" + return 0 + fi lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ "$BASH" "$SCRIPTS/lib/lane-relaunch.sh" model-recovery "$record" "$sd" "$SESSION" message model-switched "$launched observed=$observed" diff --git a/.codex/hooks/lane-mail-check.sh b/.codex/hooks/lane-mail-check.sh index 710faefb..3c3b96dd 100644 --- a/.codex/hooks/lane-mail-check.sh +++ b/.codex/hooks/lane-mail-check.sh @@ -5,7 +5,7 @@ # 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 native session must also match the recorded harness, readable caller home and payload session id; `ol_record_heal` binds the first matching native id from the recorded pane, and `oversee register` clears that binding after a resume. Pi and any harness whose `lane_context_caller_home` reports no home compare the harness and native session id only. 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. A Jarvis session's wake, the process `ORCH_WAKE_PROCESS` matches, is no such watch: it moves no cursor, so that session is handed each note in full here once and acknowledges it here, and its wake carries no note's body, at most the count and read command `lane-mail watch` prints. Where the payload's `kendex_ack` is `caller`, which the pi-hooks lane mail wake sends, the context carries the reader's `--ack` command as `kendex_ack` and nothing is acknowledged here: the wake runs it once its send returns, so a send that throws leaves the lines unread. 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/mailbox/scripts/lane-mail` or the shared `.agents/skills/mailbox/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/mailbox/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 bound native session the fleet record names, or the overseer it lost whose native identity still matches a nonempty recorded binding, 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. A session start acknowledges them only once that context is written. A prompt leaves the lines unread because Copilot can discard its output, so a later inbox read or hook delivery can repeat them. Neither arm refuses anything: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Before an attached agent launch, the halt arm also judges the lead of a launched lane or the identified fleet overseer at the context mark, refusing missing context with the orch job runner or a separate session as the route. While an attached agent of the session may still be out, the mark is less ORCH_HANDOFF_AGENT_RESERVE_TOKENS, default 150000 tokens, equality refused: Claude Code reads that roster from its task registry where the payload carries one, else from the transcript's Agent-call results, task notifications, teammate idle notifications and sends, and reads an unreadable roster or an entry neither read classifies as unknown; Codex reads ordered collaboration snapshots, launches and followups from its rollout. Idle, completed, errored, shutdown and not-found children release the reserve; initializing, running and interrupted children keep it. A full unfiltered snapshot replaces earlier unknown observations only when its call does not overlap a launch or followup; pending calls and unreadable current observations keep the roster unknown. After waiting, Codex must call list_agents with no path filter to observe returns. Other harnesses read none, so their rosters are unknown. With none out the launch meets the plain mark. A launch the reserve alone refuses with an agent out is keyed `agent-reserve` and names waiting for those agents, the job runner or a separate session, never a handoff, since the turn end and every succession judge the plain mark. With the roster unknown no wait can be observed, so the refusal is keyed `agent-unobserved` and names the job runner or a separate session, and for a lane its handoff record. On Pi the lead launches through subagent; delegate_subagent carries child identity and is outside this lead-only reserve rule. At a context hold, a live session waits for attached agent returns in this turn; Claude Stop background_tasks is preferred, with the transcript's measured Agent-call results, async launch and teammate spawn records, task notifications, teammate idle notifications and sends as the fallback when absent. Other harnesses name known unfinished children and known output paths, or unknown paths, in the emergency handoff. A standing handoff retains its escape, and the successor reads existing output before repeating work. 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` and the event 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, only for the pane's top-level harness and, on Copilot, only for a session the caller rule names the lead, 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. Run with `lane` by the critical-path-deny hook, it prints `lane` where the launch marker for the session's item binds the session's root and its mailbox directory stands, else `none`, records and judges nothing else, and exits 0; a gap past its payload read is reported on stderr and passed with nothing on stdout. 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 handoff refusals, 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, or by updating `tmp/lane-status-.md` under its launch root, 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`, judging a Claude lane on its own model. The picker owns the account handoff decision. For an ordinary account wall, the hook refuses the turn end and names the binding bucket's headroom against `ORCH_HANDOFF_HEADROOM_PCT` (default 3). When that picker reports `seat-reserve` for an overseer seat, the hook refuses the turn end. That refusal names the binding bucket's headroom and the configured `ORCH_OVERSEER_SEAT_RESERVE_PCT` (default 50) as independent facts. 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`. A lane's context and account mark refusals open `lane-mail-check: context=`, `lane-mail-check: headroom= bucket= mark=` or `lane-mail-check: seat-reserve= bucket= reserve=`. Each carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The record is read where `workflow-state --help` § Handoff record says, a launched lane naming the root its launch marker binds as the worktree, and the instruction names the state directory the read found, so a record written as told ends both refusals. 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 whose native identity matches a nonempty binding in the fleet record but whose pane only 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, its launch home or its native session id is healed from an authorized native session 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 caller remains unbound until a later binding write succeeds. 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. If `lane_context_transcript_owned` refuses an admitted overseer's transcript binding, the hook reports `transcript-unowned=` with the reason the library returns. 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 binding library's reason, `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 the native `session_id` the payload gives. A payload with no native session id is refused and cannot read mail or write overseer context or gaps. `overseer_identify` owns its native refusal diagnostics. 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 sends and its lane status file update state, never on words in its pane, 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, the word `held` where this judge refused that turn end, and an optional encoded status file update state; no record is a count of zero with no status state. A status file update since that record, including a rewrite of the same text, passes without a notice. 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 follows the orch Coordination notice rule: changed work sends a notice, and a continuing wait on a dispatched agent or an armed wake updates the lane status file only. 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, an optional `held` mark and an optional encoded status state 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` identifies an attached launch. The `lane-mail-halt` description owns the Copilot preToolUse decision and deadline contract. 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, and after a tool call `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. 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. A session the Jarvis runtime launched, which carries `JARVIS_RECORD`, has its lead's tool calls, on Copilot any call under the session id the record's start bound, and on Claude Code its turn ends, stamp that runtime record's launch row once as its Start brief taken, the fact the Jarvis launch worker reads beside the pane; another harness's turn end is no stamp, since Pi's carrier also dispatches it on a run that ended on an error and Codex and Copilot have no turn-failure event to tell one apart; a stamp that cannot be written is reported under `jarvis-rows-unwritten=` and passed, and the next such call writes it. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. # summary: Uses orch and mailbox to hand lanes their overseer's mail and hold turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or the Jarvis session whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty Jarvis session keys with no watch claim disable the check, and a running single pass is its own wake. Pi's wake check is partial; a continued turn or an unavailable check only warns. -# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. +# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. A native remote lane without a local fleet record reads the separate launch record's named fields and the native identity file its prefix wrote. It checks that the callback descends from that live predecessor, preserves a handoff with the recorded model, and dispatches the existing identity stop. A failed native stop records its cause and process identity on that handoff and emits a keyed recovery-log failure. Its controller reads that handoff over the existing host transport and relaunches with that model pinned. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] # requires: [lane-mail-deliver, lane-mail-halt, session-start-row, session-end-row, stop-failure-row, critical-path-deny, lane-mail-model-switch] @@ -1937,9 +1937,10 @@ handoff_instruction() { # Stop's task registry is the documented input. Older Claude versions, an # unreachable registry and every tool call omit it; only then use the measured # Agent-call results, task-notification status lines, teammate idle -# notifications, SendMessage calls and TaskStop calls. A tool result is read -# for those notification markers alone; no other part of a child result body -# is interpreted. +# notifications, SendMessage calls, TaskStop calls and ListAgents observations. +# The hook cannot call ListAgents itself; its recorded structured teammate +# rows supply the idle state when no notification arrived. SendMessage results +# prove a stopped agent resumed. Child reports supply notification markers. # AGENT_ROWS names each child still out, and AGENT_STATE says whether any may # be: none, out, or unknown where the roster could not be read or holds an # entry neither read classifies, which callers never read as none. @@ -1994,11 +1995,26 @@ claude_attached_agents() { # [emergency STATE_FILE] scan_bound=("${BOUND_BY[0]}" --kill-after=1 "$scan_seconds") # Each measured record moves one child: an async launch or a teammate # spawn puts it out; a task-notification ends an async child, and a - # teammate's idle_notification, which follows each of its reports, ends a - # teammate; a plain-text SendMessage to either puts it out again, and a + # teammate's terminal idle notification ends a teammate; a plain-text + # SendMessage to either puts it out again, and a # sent shutdown_request ends it, since the lead's transcript never # receives the approval and the lead is owed no further result. A # TaskStop call that did not fail ends the child it names by id or name. + # SendMessage can resume that stopped child as an in-process teammate; + # only its measured resume result proves the stopped run started again. + # ListAgents (2.1.302) records teammate states in toolUseResult.sections; + # its idle rows end the named turn even when no idle notification arrived. + # Every roster transition invalidates an earlier pending list. Claude + # Code 2.1.302 captures list state before its awaited peer reads, so even + # a completion appended to that result is newer than the snapshot. + # SendMessage writes the inbox before the teammate consumes it. No list + # snapshot proves consumption. A send from an ended turn has no earlier + # turn whose available notice could arrive afterward. A send to an + # active turn does: even its available notice can follow an empty drain + # that preceded the send. Keep that work unknown until the task ends. + # In 2.1.302, interrupted notices precede the inbox read, missing reasons + # continue the next prompt, and failed reasons can also continue. Only + # completedStatus=failed proves the latter ended the teammate task. # A foreground Agent call (no run_in_background, no name) returns its # child's finished output, so its result puts nothing out. Any other # result of an async or named call is unclassified. Claude Code appends a @@ -2022,43 +2038,96 @@ claude_attached_agents() { # [emergency STATE_FILE] (select(startswith("Spawned successfully")) | capture("\nagent_id: (?[A-Za-z0-9_-]+)\nname: (?[^\n]+)")?); def results: .content | if type == "array" then .[] else empty end | select(.type == "tool_result"); + def changed($id; $status): .generation += 1 | .agents[$id] = $status; + def awaiting: . == "queued-active" or . == "unconfirmed-active"; + def queued: awaiting or . == "queued" or . == "unconfirmed"; + def terminal: . == "completed" or . == "failed" or . == "killed"; + def uncertain: if terminal then . elif awaiting then "unconfirmed-active" elif queued then "unconfirmed" else "unknown" end; + def settled($prior; $notice): + if $prior | terminal then $prior + elif $notice.completedStatus == "failed" then "failed" + elif $notice.idleReason == "available" then + if $prior | awaiting then ($prior | uncertain) else "completed" end + elif $notice.idleReason == "interrupted" and (($prior | queued) == false) then "completed" + else $prior | uncertain end; def noted($m): reduce ($m.content | said | scan("[\\s\\S]*?([A-Za-z0-9_-]+)[\\s\\S]*?([a-z_]+)[\\s\\S]*?")) as $n - (. ; if .agents | has($n[0]) then .agents[$n[0]] = $n[1] else . end) + (. ; if .agents | has($n[0]) then + changed($n[0]; if $n[1] | terminal then $n[1] else (.agents[$n[0]] | uncertain) end) + else . end) | reduce ($m.content | said - | scan("]*>\\s*\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"")) as $n - (. ; if .names | has($n[0]) then .agents[.names[$n[0]]] = "completed" else . end); + | scan("]*>\\s*(\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"[\\s\\S]*?)\\s*")) as $n + (. ; if .names | has($n[0]) then .names[$n[0]] as $t + | ([$n[1] | fromjson? | objects][0]) as $notice + | changed($t; if $notice == null then (.agents[$t] | uncertain) else settled(.agents[$t]; $notice) end) + else . end); reduce (inputs | fromjson | objects + | select(.type == "assistant" or .type == "user" or .type == "queue-operation") | . as $record | if .type == "queue-operation" then select(.operation == "enqueue") | {role: "user", content: (.content | strings)} - else .message? | objects end) as $m ({calls: {}, agents: {}, names: {}, unclassified: false}; + else .message? | objects | . + {result: $record.toolUseResult} end) as $m + ({calls: {}, agents: {}, names: {}, generation: 0, unclassified: false}; if $m.role == "assistant" then reduce ($m.content | if type == "array" then .[] else empty end | select(.type == "tool_use" and (.id | type) == "string")) as $c (. ; if $c.name == "Agent" or $c.name == "Task" then - .calls[$c.id] = {sync: (($c.input.run_in_background | tostring) != "true" + .generation += 1 | .calls[$c.id] = {pending: true, sync: (($c.input.run_in_background | tostring) != "true" and (($c.input.name | type) != "string" or $c.input.name == ""))} elif $c.name == "SendMessage" and ($c.input.to | type) == "string" then - if ($c.input.message | type) == "string" then .calls[$c.id] = {to: $c.input.to, status: "running"} + .generation += 1 + | if ($c.input.message | type) == "string" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "running"} elif ($c.input.message | type) == "object" - and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {to: $c.input.to, status: "completed"} + and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "completed"} else . end elif $c.name == "TaskStop" and ($c.input.task_id | type) == "string" then - .calls[$c.id] = {to: $c.input.task_id, status: "killed"} + .generation += 1 | .calls[$c.id] = {pending: true, to: $c.input.task_id, status: "killed"} + elif $c.name == "ListAgents" then + .calls[$c.id] = {list: .generation, overlap: any(.calls[]; .pending == true)} else . end) elif $m.role == "user" then reduce ($m | results | [.content | text | launched][0] | objects) as $l - (. ; .agents[$l.id] = "running" + (. ; changed($l.id; "running") | if $l.name != null then .names[$l.name] = $l.id else . end) | noted($m) | reduce ($m | results) as $r (. ; .calls[$r.tool_use_id | if type == "string" then . else "" end] as $c + | if $c != null then .calls[$r.tool_use_id].pending = false + | .generation += (if $c.pending then 1 else 0 end) else . end | if [$r.content | text | launched][0] != null or $c == null or $r.is_error == true then . + elif $c | has("list") then + if $c.overlap or $c.list != .generation then . + elif ([$m | results] | length) != 1 or ($m.result.sections | type) != "array" then + reduce (.agents | to_entries[] | select(.value == "running" or (.value | queued))) as $a + (. ; changed($a.key; ($a.value | uncertain))) + else reduce ($m.result.sections[] | select(.kind == "teammates") | .rows[]) as $a + (. ; if .names | has($a.name) then .names[$a.name] as $t + | if .agents[$t] | queued then + if $a.status == "idle" or $a.status == "running" then . + else changed($t; (.agents[$t] | uncertain)) end + elif $a.status == "idle" then changed($t; "completed") + elif $a.status == "running" then changed($t; "running") + else changed($t; (.agents[$t] | uncertain)) end + else . end) + end elif $c | has("to") then (.names[$c.to] // $c.to) as $t - | if .agents | has($t) then .agents[$t] = $c.status else . end + | if (.agents | has($t)) == false then . + elif $c.status == "running" and .agents[$t] == "killed" then + (if ([$m | results] | length) == 1 and ($m.result | type) == "object" then $m.result + else [$r.content | text | fromjson?][0] end + | if type == "object" then . else {} end) as $sent + | if $sent.success == true + and ((($sent.message | strings) // "") | test("^Teammate \"[^\"]+\" was not running; resumed it as an in-process teammate with [0-9]+ prior messages and your message as its next prompt\\.$")) + then changed($t; "queued") + else changed($t; "unconfirmed") end + else changed($t; if $c.status != "running" then $c.status + elif .agents[$t] | terminal then "queued" + elif .agents[$t] == "unknown" or .agents[$t] == "unconfirmed" or .agents[$t] == "unconfirmed-active" then "unconfirmed-active" + else "queued-active" end) end elif $c.sync then . else .unclassified = true end) else . end) - | (.agents | to_entries[] | [.key, .value] | @tsv), + | (.agents | to_entries[] | [.key, + (if .value == "queued" or .value == "queued-active" then "running" + elif .value == "unconfirmed" or .value == "unconfirmed-active" then "unknown" else .value end)] | @tsv), (select(.unclassified) | ["?", "agent-result"] | @tsv) ' <"$TRANSCRIPT" 2>"$WORK_DIR/agents.err") || scan_rc=$? if [ "$scan_rc" -ne 0 ]; then @@ -2075,7 +2144,7 @@ claude_attached_agents() { # [emergency STATE_FILE] while IFS="$TAB" read -r id status; do [ -n "$id" ] || continue if [ "$id" = '?' ]; then unclassified=$status; continue; fi - case "$status" in completed | failed | killed) continue ;; esac + case "$status" in completed | failed | killed) continue ;; unknown) unclassified=agent-status ;; esac AGENT_STATE=out output=unknown # https://code.claude.com/docs/en/hooks#subagentstop @@ -2904,6 +2973,18 @@ pi_account() { # Called plainly, so errexit is live throughout this body: a status left to it # would exit the hook with neither 0 nor 2 and end the turn with no mark # judged. Every status is therefore tested where it is taken. +model_item_state() { # FILE STATE_DIR + # Reuse the state writer's lock when a launch predates its item state. + # Initialize only under that lock so concurrent callbacks cannot erase work. + "$BASH" -euo pipefail -c ' + . "$1/lib/file-lock.sh" + mkdir -p -- "$2" + (orch_take_lock 200 "$3.lock" 10 || exit 1 + [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null + ) 200>"$3.lock" + ' _ "$SCRIPTS" "$2" "$1" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err" +} + model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] local cause="$1" observed="$2" detail="${3:-}" file sd report notice notice="lane-mail-check: model-unavailable=$cause" @@ -2912,15 +2993,7 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice"; return 0; } sd=${file%/*} - # Reuse the state writer's lock when a launch predates its item state. - # Initialize only under that lock so concurrent callbacks cannot erase work. - if ! "$BASH" -euo pipefail -c ' - . "$1/lib/file-lock.sh" - mkdir -p -- "$2" - (orch_take_lock 200 "$3.lock" 10 || exit 1 - [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null - ) 200>"$3.lock" - ' _ "$SCRIPTS" "$sd" "$file" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err"; then + if ! model_item_state "$file" "$sd"; then hand_over "$notice$NL$(cat -- "$WORK_DIR/model.err")" return 0 fi @@ -2936,6 +3009,28 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice" } +# The SSH prefix owns the native PID record. A callback must still descend +# from that live predecessor; a delayed callback must not stop a replacement. +model_native_record() { + local file record launch table descendants hops pid current=false + launch=$("$SCRIPTS/lane-marker" --launch-path "$COMMON" "$ITEM") || return 1 + [ -f "$launch" ] && [ ! -L "$launch" ] || return 1 + record=$(cat -- "$launch") || return 1 + "$BASH" -euo pipefail -c '. "$1/lib/lane-state.sh"' _ "$SCRIPTS" || return 1 + . "$SCRIPTS/lib/lane-state.sh" + file=$(jq -er '.identity_file | strings | select(startswith("/"))' <<<"$record") || return 1 + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + lane_identity_read "$file" && lane_identity_status "$LANE_IDENTITY_PID" "$LANE_IDENTITY_START" || return 1 + table=$(lane_process_table) || return 1 + descendants=$(lane_process_below "$table" "$LANE_IDENTITY_PID" '.*' 1 "" pids) || return 1 + while read -r hops pid; do + [ "$pid" != "$$" ] || current=true + done <<<"$descendants" + [ "$current" = true ] || return 1 + jq -c --arg session "$SESSION" --arg pid "$LANE_IDENTITY_PID" --arg start "$LANE_IDENTITY_START" \ + '. + {session_id:$session,launch:{pid:$pid,start:$start}}' <<<"$record" +} + model_lane_association() { # RECORD SERVER jq -r --arg session "$SESSION" --arg pane "${TMUX_PANE:-}" --arg server "$2" ' if $session == "" then "unbound" @@ -3015,24 +3110,38 @@ handoff_check() { if [ "$ARM" = model-switch ]; then [ "$HARNESS" = claude ] || return 0 - local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery + local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery native=false file observed=$(jq -er '.to_model | strings | select(length > 0)' <<<"$INPUT" 2>/dev/null) || observed="" fleet=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path oversee 2>"$WORK_DIR/model.err") || { model_unavailable_notice state-unread "$observed"; return 0; } sd=${fleet%/*} - record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { - model_unavailable_notice launch-unread "$observed"; return 0; - } - if [ "$ROLE" = overseer ]; then - record_path='["overseer"]' + if [ "$ROLE" != overseer ] && [ ! -e "$fleet" ]; then + native=true + record=$(model_native_record 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + file=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$ITEM" 2>"$WORK_DIR/model.err") || { + model_unavailable_notice state-unread "$observed"; return 0; + } + sd=${file%/*} + model_item_state "$file" "$sd" || { + model_unavailable_notice state-unread "$observed"; return 0; + } else - record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 - fi - record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 - if [ "$record" = null ] || [ -z "$record" ]; then - model_unavailable_notice launch-missing "$observed" - return 0 + record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed"; return 0; + } + if [ "$ROLE" = overseer ]; then + record_path='["overseer"]' + else + record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 + fi + record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 + if [ "$record" = null ] || [ -z "$record" ]; then + model_unavailable_notice launch-missing "$observed" + return 0 + fi fi # A delayed event from a replaced session never acts on the replacement. if [ -n "${TMUX_PANE:-}" ]; then @@ -3084,27 +3193,42 @@ handoff_check() { fi outcome=switched [ -z "$cause" ] || outcome=unavailable - report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' - getpath($path) as $current - | if ($role == "overseer" and ($current | ol_model_launch) != $identity) - or $current.session_id != $record.session_id or $current.model != $record.model - or $current.launch != $record.launch or $current.launched_at != $record.launched_at - then {state: ., report: {result:"stale"}} - elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity - and ($current.model_check.outcome == "switched" - or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) - then {state: ., report: {result:"duplicate"}} - else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at - | {outcome:$outcome, session_id:$session, launch_identity:$identity, - launched:$launched, observed:$observed, at:$at, cause:$cause} as $check - | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) - | setpath($path + ["model_check"]; $check) - | {state: ., report: {result:"acquired", record:getpath($path)}} - end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ - --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + if [ "$native" = true ]; then + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report "$ITEM" ' + if .model_checks[$session].launch_identity == $identity + and (.model_checks[$session].outcome == "switched" + or ($outcome == "unavailable" and .model_checks[$session].outcome == "unavailable")) + then {state:.,report:{result:"duplicate"}} + else .model_checks[$session] = {outcome:$outcome,session_id:$session,launch_identity:$identity, + launched:$launched,observed:$observed,cause:$cause,at:(now|strftime("%Y-%m-%dT%H:%M:%SZ"))} + | {state:.,report:{result:"acquired"}} end' --arg session "$SESSION" --argjson identity "$identity" \ + --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" \ 2>"$WORK_DIR/model.err") || { - model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; - } + model_unavailable_notice record-unwritten "$observed"; return 0; + } + else + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' + getpath($path) as $current + | if ($role == "overseer" and ($current | ol_model_launch) != $identity) + or $current.session_id != $record.session_id or $current.model != $record.model + or $current.launch != $record.launch or $current.launched_at != $record.launched_at + then {state: ., report: {result:"stale"}} + elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity + and ($current.model_check.outcome == "switched" + or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) + then {state: ., report: {result:"duplicate"}} + else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at + | {outcome:$outcome, session_id:$session, launch_identity:$identity, + launched:$launched, observed:$observed, at:$at, cause:$cause} as $check + | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) + | setpath($path + ["model_check"]; $check) + | {state: ., report: {result:"acquired", record:getpath($path)}} + end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ + --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + 2>"$WORK_DIR/model.err") || { + model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + fi result=$(jq -r '.result' <<<"$report") || return 0 [ "$result" = acquired ] || return 0 if [ "$outcome" = unavailable ]; then @@ -3144,17 +3268,41 @@ handoff_check() { [ -z "$HANDOFF_FILE" ] || handoff_sd=${HANDOFF_FILE%/*} claude_attached_agents emergency "$handoff_sd/workflow-state-$ITEM.json" handoff=$(jq -nc --arg branch "$BRANCH" --arg worktree "$ROOT" --arg agents "$AGENT_ROWS" \ - --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --argjson prior "${HANDOFF_RECORD:-null}" \ + --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --arg model "$launched" --argjson prior "${HANDOFF_RECORD:-null}" \ '($prior // {merged:[],remaining:["Resume durable workflow state after model recovery."], branch:$branch,worktree:$worktree,open_pr:null}) | .traps = (((.traps // []) + [$agents]) | unique) - | .session_id=$session | .pane_key=$pane | .cause="model-switched"') || return 0 + | del(.stop_failure) + | .session_id=$session | .pane_key=$pane | .cause="model-switched" | .model=$model') || return 0 # Preserve existing workflow state, including unpushed round work. { [ -f "$handoff_sd/workflow-state-$ITEM.json" ] || "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" init "$ITEM" --worktree "$ROOT" >/dev/null 2>"$WORK_DIR/model.err"; } && "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" set "$ITEM" handoff "$handoff" >/dev/null 2>>"$WORK_DIR/model.err" || - model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + { + if [ "$native" = true ]; then + # The acquired switch check already occupies the notice slot. + hand_over "lane-mail-check: model-unavailable=handoff-unwritten$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + } # Recovery owns the stop and runs outside this hook's process group. + if [ "$native" = true ]; then + # The controller reads this handoff through its existing host state + # transport and relaunches there. This host only stops its predecessor. + if ! lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ + "$BASH" -euo pipefail -c ' + . "$1/lib/lane-state.sh" + . "$1/lib/lane-relaunch.sh" + lane_native_model_stop "$2" "$3" "$1" "$4" "$5" + ' _ "$SCRIPTS" "$record" "$handoff_sd" "$ITEM" "$SESSION" 2>"$WORK_DIR/model.err"; then + # The switched check already occupies the once-only unavailable slot. + hand_over "lane-mail-check: model-unavailable=recovery-unstarted$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + message model-switched "$launched observed=$observed" + return 0 + fi lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ "$BASH" "$SCRIPTS/lib/lane-relaunch.sh" model-recovery "$record" "$sd" "$SESSION" message model-switched "$launched observed=$observed" diff --git a/.github/hooks/lane-mail-check.sh b/.github/hooks/lane-mail-check.sh index 710faefb..3c3b96dd 100644 --- a/.github/hooks/lane-mail-check.sh +++ b/.github/hooks/lane-mail-check.sh @@ -5,7 +5,7 @@ # 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 native session must also match the recorded harness, readable caller home and payload session id; `ol_record_heal` binds the first matching native id from the recorded pane, and `oversee register` clears that binding after a resume. Pi and any harness whose `lane_context_caller_home` reports no home compare the harness and native session id only. 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. A Jarvis session's wake, the process `ORCH_WAKE_PROCESS` matches, is no such watch: it moves no cursor, so that session is handed each note in full here once and acknowledges it here, and its wake carries no note's body, at most the count and read command `lane-mail watch` prints. Where the payload's `kendex_ack` is `caller`, which the pi-hooks lane mail wake sends, the context carries the reader's `--ack` command as `kendex_ack` and nothing is acknowledged here: the wake runs it once its send returns, so a send that throws leaves the lines unread. 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/mailbox/scripts/lane-mail` or the shared `.agents/skills/mailbox/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/mailbox/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 bound native session the fleet record names, or the overseer it lost whose native identity still matches a nonempty recorded binding, 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. A session start acknowledges them only once that context is written. A prompt leaves the lines unread because Copilot can discard its output, so a later inbox read or hook delivery can repeat them. Neither arm refuses anything: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Before an attached agent launch, the halt arm also judges the lead of a launched lane or the identified fleet overseer at the context mark, refusing missing context with the orch job runner or a separate session as the route. While an attached agent of the session may still be out, the mark is less ORCH_HANDOFF_AGENT_RESERVE_TOKENS, default 150000 tokens, equality refused: Claude Code reads that roster from its task registry where the payload carries one, else from the transcript's Agent-call results, task notifications, teammate idle notifications and sends, and reads an unreadable roster or an entry neither read classifies as unknown; Codex reads ordered collaboration snapshots, launches and followups from its rollout. Idle, completed, errored, shutdown and not-found children release the reserve; initializing, running and interrupted children keep it. A full unfiltered snapshot replaces earlier unknown observations only when its call does not overlap a launch or followup; pending calls and unreadable current observations keep the roster unknown. After waiting, Codex must call list_agents with no path filter to observe returns. Other harnesses read none, so their rosters are unknown. With none out the launch meets the plain mark. A launch the reserve alone refuses with an agent out is keyed `agent-reserve` and names waiting for those agents, the job runner or a separate session, never a handoff, since the turn end and every succession judge the plain mark. With the roster unknown no wait can be observed, so the refusal is keyed `agent-unobserved` and names the job runner or a separate session, and for a lane its handoff record. On Pi the lead launches through subagent; delegate_subagent carries child identity and is outside this lead-only reserve rule. At a context hold, a live session waits for attached agent returns in this turn; Claude Stop background_tasks is preferred, with the transcript's measured Agent-call results, async launch and teammate spawn records, task notifications, teammate idle notifications and sends as the fallback when absent. Other harnesses name known unfinished children and known output paths, or unknown paths, in the emergency handoff. A standing handoff retains its escape, and the successor reads existing output before repeating work. 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` and the event 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, only for the pane's top-level harness and, on Copilot, only for a session the caller rule names the lead, 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. Run with `lane` by the critical-path-deny hook, it prints `lane` where the launch marker for the session's item binds the session's root and its mailbox directory stands, else `none`, records and judges nothing else, and exits 0; a gap past its payload read is reported on stderr and passed with nothing on stdout. 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 handoff refusals, 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, or by updating `tmp/lane-status-.md` under its launch root, 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`, judging a Claude lane on its own model. The picker owns the account handoff decision. For an ordinary account wall, the hook refuses the turn end and names the binding bucket's headroom against `ORCH_HANDOFF_HEADROOM_PCT` (default 3). When that picker reports `seat-reserve` for an overseer seat, the hook refuses the turn end. That refusal names the binding bucket's headroom and the configured `ORCH_OVERSEER_SEAT_RESERVE_PCT` (default 50) as independent facts. 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`. A lane's context and account mark refusals open `lane-mail-check: context=`, `lane-mail-check: headroom= bucket= mark=` or `lane-mail-check: seat-reserve= bucket= reserve=`. Each carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The record is read where `workflow-state --help` § Handoff record says, a launched lane naming the root its launch marker binds as the worktree, and the instruction names the state directory the read found, so a record written as told ends both refusals. 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 whose native identity matches a nonempty binding in the fleet record but whose pane only 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, its launch home or its native session id is healed from an authorized native session 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 caller remains unbound until a later binding write succeeds. 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. If `lane_context_transcript_owned` refuses an admitted overseer's transcript binding, the hook reports `transcript-unowned=` with the reason the library returns. 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 binding library's reason, `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 the native `session_id` the payload gives. A payload with no native session id is refused and cannot read mail or write overseer context or gaps. `overseer_identify` owns its native refusal diagnostics. 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 sends and its lane status file update state, never on words in its pane, 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, the word `held` where this judge refused that turn end, and an optional encoded status file update state; no record is a count of zero with no status state. A status file update since that record, including a rewrite of the same text, passes without a notice. 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 follows the orch Coordination notice rule: changed work sends a notice, and a continuing wait on a dispatched agent or an armed wake updates the lane status file only. 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, an optional `held` mark and an optional encoded status state 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` identifies an attached launch. The `lane-mail-halt` description owns the Copilot preToolUse decision and deadline contract. 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, and after a tool call `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. 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. A session the Jarvis runtime launched, which carries `JARVIS_RECORD`, has its lead's tool calls, on Copilot any call under the session id the record's start bound, and on Claude Code its turn ends, stamp that runtime record's launch row once as its Start brief taken, the fact the Jarvis launch worker reads beside the pane; another harness's turn end is no stamp, since Pi's carrier also dispatches it on a run that ended on an error and Codex and Copilot have no turn-failure event to tell one apart; a stamp that cannot be written is reported under `jarvis-rows-unwritten=` and passed, and the next such call writes it. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. # summary: Uses orch and mailbox to hand lanes their overseer's mail and hold turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or the Jarvis session whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty Jarvis session keys with no watch claim disable the check, and a running single pass is its own wake. Pi's wake check is partial; a continued turn or an unavailable check only warns. -# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. +# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. A native remote lane without a local fleet record reads the separate launch record's named fields and the native identity file its prefix wrote. It checks that the callback descends from that live predecessor, preserves a handoff with the recorded model, and dispatches the existing identity stop. A failed native stop records its cause and process identity on that handoff and emits a keyed recovery-log failure. Its controller reads that handoff over the existing host transport and relaunches with that model pinned. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] # requires: [lane-mail-deliver, lane-mail-halt, session-start-row, session-end-row, stop-failure-row, critical-path-deny, lane-mail-model-switch] @@ -1937,9 +1937,10 @@ handoff_instruction() { # Stop's task registry is the documented input. Older Claude versions, an # unreachable registry and every tool call omit it; only then use the measured # Agent-call results, task-notification status lines, teammate idle -# notifications, SendMessage calls and TaskStop calls. A tool result is read -# for those notification markers alone; no other part of a child result body -# is interpreted. +# notifications, SendMessage calls, TaskStop calls and ListAgents observations. +# The hook cannot call ListAgents itself; its recorded structured teammate +# rows supply the idle state when no notification arrived. SendMessage results +# prove a stopped agent resumed. Child reports supply notification markers. # AGENT_ROWS names each child still out, and AGENT_STATE says whether any may # be: none, out, or unknown where the roster could not be read or holds an # entry neither read classifies, which callers never read as none. @@ -1994,11 +1995,26 @@ claude_attached_agents() { # [emergency STATE_FILE] scan_bound=("${BOUND_BY[0]}" --kill-after=1 "$scan_seconds") # Each measured record moves one child: an async launch or a teammate # spawn puts it out; a task-notification ends an async child, and a - # teammate's idle_notification, which follows each of its reports, ends a - # teammate; a plain-text SendMessage to either puts it out again, and a + # teammate's terminal idle notification ends a teammate; a plain-text + # SendMessage to either puts it out again, and a # sent shutdown_request ends it, since the lead's transcript never # receives the approval and the lead is owed no further result. A # TaskStop call that did not fail ends the child it names by id or name. + # SendMessage can resume that stopped child as an in-process teammate; + # only its measured resume result proves the stopped run started again. + # ListAgents (2.1.302) records teammate states in toolUseResult.sections; + # its idle rows end the named turn even when no idle notification arrived. + # Every roster transition invalidates an earlier pending list. Claude + # Code 2.1.302 captures list state before its awaited peer reads, so even + # a completion appended to that result is newer than the snapshot. + # SendMessage writes the inbox before the teammate consumes it. No list + # snapshot proves consumption. A send from an ended turn has no earlier + # turn whose available notice could arrive afterward. A send to an + # active turn does: even its available notice can follow an empty drain + # that preceded the send. Keep that work unknown until the task ends. + # In 2.1.302, interrupted notices precede the inbox read, missing reasons + # continue the next prompt, and failed reasons can also continue. Only + # completedStatus=failed proves the latter ended the teammate task. # A foreground Agent call (no run_in_background, no name) returns its # child's finished output, so its result puts nothing out. Any other # result of an async or named call is unclassified. Claude Code appends a @@ -2022,43 +2038,96 @@ claude_attached_agents() { # [emergency STATE_FILE] (select(startswith("Spawned successfully")) | capture("\nagent_id: (?[A-Za-z0-9_-]+)\nname: (?[^\n]+)")?); def results: .content | if type == "array" then .[] else empty end | select(.type == "tool_result"); + def changed($id; $status): .generation += 1 | .agents[$id] = $status; + def awaiting: . == "queued-active" or . == "unconfirmed-active"; + def queued: awaiting or . == "queued" or . == "unconfirmed"; + def terminal: . == "completed" or . == "failed" or . == "killed"; + def uncertain: if terminal then . elif awaiting then "unconfirmed-active" elif queued then "unconfirmed" else "unknown" end; + def settled($prior; $notice): + if $prior | terminal then $prior + elif $notice.completedStatus == "failed" then "failed" + elif $notice.idleReason == "available" then + if $prior | awaiting then ($prior | uncertain) else "completed" end + elif $notice.idleReason == "interrupted" and (($prior | queued) == false) then "completed" + else $prior | uncertain end; def noted($m): reduce ($m.content | said | scan("[\\s\\S]*?([A-Za-z0-9_-]+)[\\s\\S]*?([a-z_]+)[\\s\\S]*?")) as $n - (. ; if .agents | has($n[0]) then .agents[$n[0]] = $n[1] else . end) + (. ; if .agents | has($n[0]) then + changed($n[0]; if $n[1] | terminal then $n[1] else (.agents[$n[0]] | uncertain) end) + else . end) | reduce ($m.content | said - | scan("]*>\\s*\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"")) as $n - (. ; if .names | has($n[0]) then .agents[.names[$n[0]]] = "completed" else . end); + | scan("]*>\\s*(\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"[\\s\\S]*?)\\s*")) as $n + (. ; if .names | has($n[0]) then .names[$n[0]] as $t + | ([$n[1] | fromjson? | objects][0]) as $notice + | changed($t; if $notice == null then (.agents[$t] | uncertain) else settled(.agents[$t]; $notice) end) + else . end); reduce (inputs | fromjson | objects + | select(.type == "assistant" or .type == "user" or .type == "queue-operation") | . as $record | if .type == "queue-operation" then select(.operation == "enqueue") | {role: "user", content: (.content | strings)} - else .message? | objects end) as $m ({calls: {}, agents: {}, names: {}, unclassified: false}; + else .message? | objects | . + {result: $record.toolUseResult} end) as $m + ({calls: {}, agents: {}, names: {}, generation: 0, unclassified: false}; if $m.role == "assistant" then reduce ($m.content | if type == "array" then .[] else empty end | select(.type == "tool_use" and (.id | type) == "string")) as $c (. ; if $c.name == "Agent" or $c.name == "Task" then - .calls[$c.id] = {sync: (($c.input.run_in_background | tostring) != "true" + .generation += 1 | .calls[$c.id] = {pending: true, sync: (($c.input.run_in_background | tostring) != "true" and (($c.input.name | type) != "string" or $c.input.name == ""))} elif $c.name == "SendMessage" and ($c.input.to | type) == "string" then - if ($c.input.message | type) == "string" then .calls[$c.id] = {to: $c.input.to, status: "running"} + .generation += 1 + | if ($c.input.message | type) == "string" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "running"} elif ($c.input.message | type) == "object" - and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {to: $c.input.to, status: "completed"} + and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "completed"} else . end elif $c.name == "TaskStop" and ($c.input.task_id | type) == "string" then - .calls[$c.id] = {to: $c.input.task_id, status: "killed"} + .generation += 1 | .calls[$c.id] = {pending: true, to: $c.input.task_id, status: "killed"} + elif $c.name == "ListAgents" then + .calls[$c.id] = {list: .generation, overlap: any(.calls[]; .pending == true)} else . end) elif $m.role == "user" then reduce ($m | results | [.content | text | launched][0] | objects) as $l - (. ; .agents[$l.id] = "running" + (. ; changed($l.id; "running") | if $l.name != null then .names[$l.name] = $l.id else . end) | noted($m) | reduce ($m | results) as $r (. ; .calls[$r.tool_use_id | if type == "string" then . else "" end] as $c + | if $c != null then .calls[$r.tool_use_id].pending = false + | .generation += (if $c.pending then 1 else 0 end) else . end | if [$r.content | text | launched][0] != null or $c == null or $r.is_error == true then . + elif $c | has("list") then + if $c.overlap or $c.list != .generation then . + elif ([$m | results] | length) != 1 or ($m.result.sections | type) != "array" then + reduce (.agents | to_entries[] | select(.value == "running" or (.value | queued))) as $a + (. ; changed($a.key; ($a.value | uncertain))) + else reduce ($m.result.sections[] | select(.kind == "teammates") | .rows[]) as $a + (. ; if .names | has($a.name) then .names[$a.name] as $t + | if .agents[$t] | queued then + if $a.status == "idle" or $a.status == "running" then . + else changed($t; (.agents[$t] | uncertain)) end + elif $a.status == "idle" then changed($t; "completed") + elif $a.status == "running" then changed($t; "running") + else changed($t; (.agents[$t] | uncertain)) end + else . end) + end elif $c | has("to") then (.names[$c.to] // $c.to) as $t - | if .agents | has($t) then .agents[$t] = $c.status else . end + | if (.agents | has($t)) == false then . + elif $c.status == "running" and .agents[$t] == "killed" then + (if ([$m | results] | length) == 1 and ($m.result | type) == "object" then $m.result + else [$r.content | text | fromjson?][0] end + | if type == "object" then . else {} end) as $sent + | if $sent.success == true + and ((($sent.message | strings) // "") | test("^Teammate \"[^\"]+\" was not running; resumed it as an in-process teammate with [0-9]+ prior messages and your message as its next prompt\\.$")) + then changed($t; "queued") + else changed($t; "unconfirmed") end + else changed($t; if $c.status != "running" then $c.status + elif .agents[$t] | terminal then "queued" + elif .agents[$t] == "unknown" or .agents[$t] == "unconfirmed" or .agents[$t] == "unconfirmed-active" then "unconfirmed-active" + else "queued-active" end) end elif $c.sync then . else .unclassified = true end) else . end) - | (.agents | to_entries[] | [.key, .value] | @tsv), + | (.agents | to_entries[] | [.key, + (if .value == "queued" or .value == "queued-active" then "running" + elif .value == "unconfirmed" or .value == "unconfirmed-active" then "unknown" else .value end)] | @tsv), (select(.unclassified) | ["?", "agent-result"] | @tsv) ' <"$TRANSCRIPT" 2>"$WORK_DIR/agents.err") || scan_rc=$? if [ "$scan_rc" -ne 0 ]; then @@ -2075,7 +2144,7 @@ claude_attached_agents() { # [emergency STATE_FILE] while IFS="$TAB" read -r id status; do [ -n "$id" ] || continue if [ "$id" = '?' ]; then unclassified=$status; continue; fi - case "$status" in completed | failed | killed) continue ;; esac + case "$status" in completed | failed | killed) continue ;; unknown) unclassified=agent-status ;; esac AGENT_STATE=out output=unknown # https://code.claude.com/docs/en/hooks#subagentstop @@ -2904,6 +2973,18 @@ pi_account() { # Called plainly, so errexit is live throughout this body: a status left to it # would exit the hook with neither 0 nor 2 and end the turn with no mark # judged. Every status is therefore tested where it is taken. +model_item_state() { # FILE STATE_DIR + # Reuse the state writer's lock when a launch predates its item state. + # Initialize only under that lock so concurrent callbacks cannot erase work. + "$BASH" -euo pipefail -c ' + . "$1/lib/file-lock.sh" + mkdir -p -- "$2" + (orch_take_lock 200 "$3.lock" 10 || exit 1 + [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null + ) 200>"$3.lock" + ' _ "$SCRIPTS" "$2" "$1" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err" +} + model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] local cause="$1" observed="$2" detail="${3:-}" file sd report notice notice="lane-mail-check: model-unavailable=$cause" @@ -2912,15 +2993,7 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice"; return 0; } sd=${file%/*} - # Reuse the state writer's lock when a launch predates its item state. - # Initialize only under that lock so concurrent callbacks cannot erase work. - if ! "$BASH" -euo pipefail -c ' - . "$1/lib/file-lock.sh" - mkdir -p -- "$2" - (orch_take_lock 200 "$3.lock" 10 || exit 1 - [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null - ) 200>"$3.lock" - ' _ "$SCRIPTS" "$sd" "$file" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err"; then + if ! model_item_state "$file" "$sd"; then hand_over "$notice$NL$(cat -- "$WORK_DIR/model.err")" return 0 fi @@ -2936,6 +3009,28 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice" } +# The SSH prefix owns the native PID record. A callback must still descend +# from that live predecessor; a delayed callback must not stop a replacement. +model_native_record() { + local file record launch table descendants hops pid current=false + launch=$("$SCRIPTS/lane-marker" --launch-path "$COMMON" "$ITEM") || return 1 + [ -f "$launch" ] && [ ! -L "$launch" ] || return 1 + record=$(cat -- "$launch") || return 1 + "$BASH" -euo pipefail -c '. "$1/lib/lane-state.sh"' _ "$SCRIPTS" || return 1 + . "$SCRIPTS/lib/lane-state.sh" + file=$(jq -er '.identity_file | strings | select(startswith("/"))' <<<"$record") || return 1 + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + lane_identity_read "$file" && lane_identity_status "$LANE_IDENTITY_PID" "$LANE_IDENTITY_START" || return 1 + table=$(lane_process_table) || return 1 + descendants=$(lane_process_below "$table" "$LANE_IDENTITY_PID" '.*' 1 "" pids) || return 1 + while read -r hops pid; do + [ "$pid" != "$$" ] || current=true + done <<<"$descendants" + [ "$current" = true ] || return 1 + jq -c --arg session "$SESSION" --arg pid "$LANE_IDENTITY_PID" --arg start "$LANE_IDENTITY_START" \ + '. + {session_id:$session,launch:{pid:$pid,start:$start}}' <<<"$record" +} + model_lane_association() { # RECORD SERVER jq -r --arg session "$SESSION" --arg pane "${TMUX_PANE:-}" --arg server "$2" ' if $session == "" then "unbound" @@ -3015,24 +3110,38 @@ handoff_check() { if [ "$ARM" = model-switch ]; then [ "$HARNESS" = claude ] || return 0 - local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery + local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery native=false file observed=$(jq -er '.to_model | strings | select(length > 0)' <<<"$INPUT" 2>/dev/null) || observed="" fleet=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path oversee 2>"$WORK_DIR/model.err") || { model_unavailable_notice state-unread "$observed"; return 0; } sd=${fleet%/*} - record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { - model_unavailable_notice launch-unread "$observed"; return 0; - } - if [ "$ROLE" = overseer ]; then - record_path='["overseer"]' + if [ "$ROLE" != overseer ] && [ ! -e "$fleet" ]; then + native=true + record=$(model_native_record 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + file=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$ITEM" 2>"$WORK_DIR/model.err") || { + model_unavailable_notice state-unread "$observed"; return 0; + } + sd=${file%/*} + model_item_state "$file" "$sd" || { + model_unavailable_notice state-unread "$observed"; return 0; + } else - record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 - fi - record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 - if [ "$record" = null ] || [ -z "$record" ]; then - model_unavailable_notice launch-missing "$observed" - return 0 + record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed"; return 0; + } + if [ "$ROLE" = overseer ]; then + record_path='["overseer"]' + else + record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 + fi + record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 + if [ "$record" = null ] || [ -z "$record" ]; then + model_unavailable_notice launch-missing "$observed" + return 0 + fi fi # A delayed event from a replaced session never acts on the replacement. if [ -n "${TMUX_PANE:-}" ]; then @@ -3084,27 +3193,42 @@ handoff_check() { fi outcome=switched [ -z "$cause" ] || outcome=unavailable - report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' - getpath($path) as $current - | if ($role == "overseer" and ($current | ol_model_launch) != $identity) - or $current.session_id != $record.session_id or $current.model != $record.model - or $current.launch != $record.launch or $current.launched_at != $record.launched_at - then {state: ., report: {result:"stale"}} - elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity - and ($current.model_check.outcome == "switched" - or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) - then {state: ., report: {result:"duplicate"}} - else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at - | {outcome:$outcome, session_id:$session, launch_identity:$identity, - launched:$launched, observed:$observed, at:$at, cause:$cause} as $check - | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) - | setpath($path + ["model_check"]; $check) - | {state: ., report: {result:"acquired", record:getpath($path)}} - end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ - --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + if [ "$native" = true ]; then + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report "$ITEM" ' + if .model_checks[$session].launch_identity == $identity + and (.model_checks[$session].outcome == "switched" + or ($outcome == "unavailable" and .model_checks[$session].outcome == "unavailable")) + then {state:.,report:{result:"duplicate"}} + else .model_checks[$session] = {outcome:$outcome,session_id:$session,launch_identity:$identity, + launched:$launched,observed:$observed,cause:$cause,at:(now|strftime("%Y-%m-%dT%H:%M:%SZ"))} + | {state:.,report:{result:"acquired"}} end' --arg session "$SESSION" --argjson identity "$identity" \ + --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" \ 2>"$WORK_DIR/model.err") || { - model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; - } + model_unavailable_notice record-unwritten "$observed"; return 0; + } + else + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' + getpath($path) as $current + | if ($role == "overseer" and ($current | ol_model_launch) != $identity) + or $current.session_id != $record.session_id or $current.model != $record.model + or $current.launch != $record.launch or $current.launched_at != $record.launched_at + then {state: ., report: {result:"stale"}} + elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity + and ($current.model_check.outcome == "switched" + or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) + then {state: ., report: {result:"duplicate"}} + else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at + | {outcome:$outcome, session_id:$session, launch_identity:$identity, + launched:$launched, observed:$observed, at:$at, cause:$cause} as $check + | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) + | setpath($path + ["model_check"]; $check) + | {state: ., report: {result:"acquired", record:getpath($path)}} + end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ + --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + 2>"$WORK_DIR/model.err") || { + model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + fi result=$(jq -r '.result' <<<"$report") || return 0 [ "$result" = acquired ] || return 0 if [ "$outcome" = unavailable ]; then @@ -3144,17 +3268,41 @@ handoff_check() { [ -z "$HANDOFF_FILE" ] || handoff_sd=${HANDOFF_FILE%/*} claude_attached_agents emergency "$handoff_sd/workflow-state-$ITEM.json" handoff=$(jq -nc --arg branch "$BRANCH" --arg worktree "$ROOT" --arg agents "$AGENT_ROWS" \ - --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --argjson prior "${HANDOFF_RECORD:-null}" \ + --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --arg model "$launched" --argjson prior "${HANDOFF_RECORD:-null}" \ '($prior // {merged:[],remaining:["Resume durable workflow state after model recovery."], branch:$branch,worktree:$worktree,open_pr:null}) | .traps = (((.traps // []) + [$agents]) | unique) - | .session_id=$session | .pane_key=$pane | .cause="model-switched"') || return 0 + | del(.stop_failure) + | .session_id=$session | .pane_key=$pane | .cause="model-switched" | .model=$model') || return 0 # Preserve existing workflow state, including unpushed round work. { [ -f "$handoff_sd/workflow-state-$ITEM.json" ] || "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" init "$ITEM" --worktree "$ROOT" >/dev/null 2>"$WORK_DIR/model.err"; } && "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" set "$ITEM" handoff "$handoff" >/dev/null 2>>"$WORK_DIR/model.err" || - model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + { + if [ "$native" = true ]; then + # The acquired switch check already occupies the notice slot. + hand_over "lane-mail-check: model-unavailable=handoff-unwritten$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + } # Recovery owns the stop and runs outside this hook's process group. + if [ "$native" = true ]; then + # The controller reads this handoff through its existing host state + # transport and relaunches there. This host only stops its predecessor. + if ! lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ + "$BASH" -euo pipefail -c ' + . "$1/lib/lane-state.sh" + . "$1/lib/lane-relaunch.sh" + lane_native_model_stop "$2" "$3" "$1" "$4" "$5" + ' _ "$SCRIPTS" "$record" "$handoff_sd" "$ITEM" "$SESSION" 2>"$WORK_DIR/model.err"; then + # The switched check already occupies the once-only unavailable slot. + hand_over "lane-mail-check: model-unavailable=recovery-unstarted$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + message model-switched "$launched observed=$observed" + return 0 + fi lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ "$BASH" "$SCRIPTS/lib/lane-relaunch.sh" model-recovery "$record" "$sd" "$SESSION" message model-switched "$launched observed=$observed" diff --git a/.kendex-lock.json b/.kendex-lock.json index 8c1457f1..75f613c0 100644 --- a/.kendex-lock.json +++ b/.kendex-lock.json @@ -8,7 +8,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f61907f3d571a02cf2a775ebb0034f787a1d5ffe2d68e52585124f28badd368", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -43,7 +43,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f61907f3d571a02cf2a775ebb0034f787a1d5ffe2d68e52585124f28badd368", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -78,7 +78,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f61907f3d571a02cf2a775ebb0034f787a1d5ffe2d68e52585124f28badd368", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -113,7 +113,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f61907f3d571a02cf2a775ebb0034f787a1d5ffe2d68e52585124f28badd368", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -148,7 +148,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "5d2ba692c5b40abe71c7af38392e2ee2aecc2a3b07d256d9b44a4cb4d6c06090", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -185,7 +185,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "5d2ba692c5b40abe71c7af38392e2ee2aecc2a3b07d256d9b44a4cb4d6c06090", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -222,7 +222,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "5d2ba692c5b40abe71c7af38392e2ee2aecc2a3b07d256d9b44a4cb4d6c06090", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -259,7 +259,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "5d2ba692c5b40abe71c7af38392e2ee2aecc2a3b07d256d9b44a4cb4d6c06090", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -296,7 +296,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4a25e8a4c3e339bdf1890993e437e938feed7baa0a565bbe09e873ec62a75c64", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -327,7 +327,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4a25e8a4c3e339bdf1890993e437e938feed7baa0a565bbe09e873ec62a75c64", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -358,7 +358,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4a25e8a4c3e339bdf1890993e437e938feed7baa0a565bbe09e873ec62a75c64", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -389,7 +389,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4a25e8a4c3e339bdf1890993e437e938feed7baa0a565bbe09e873ec62a75c64", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -420,7 +420,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a1802df17bd986fd550df5df1d70792098e98f5b2e518364a51337835cab6bb7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -453,7 +453,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a1802df17bd986fd550df5df1d70792098e98f5b2e518364a51337835cab6bb7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -486,7 +486,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a1802df17bd986fd550df5df1d70792098e98f5b2e518364a51337835cab6bb7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -519,7 +519,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a1802df17bd986fd550df5df1d70792098e98f5b2e518364a51337835cab6bb7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -552,7 +552,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3002276f894146f3c381e406e2c14d3ef3f75e64e0d7a51c1a8be30486bfa79e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -582,7 +582,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3002276f894146f3c381e406e2c14d3ef3f75e64e0d7a51c1a8be30486bfa79e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -612,7 +612,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3002276f894146f3c381e406e2c14d3ef3f75e64e0d7a51c1a8be30486bfa79e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -642,7 +642,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3002276f894146f3c381e406e2c14d3ef3f75e64e0d7a51c1a8be30486bfa79e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -672,7 +672,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "64f1a2e0e2bb4c4bac776f8c489c4fbaa17f149f955092737e47c72cc31bb7b2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -702,7 +702,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "64f1a2e0e2bb4c4bac776f8c489c4fbaa17f149f955092737e47c72cc31bb7b2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -732,7 +732,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "64f1a2e0e2bb4c4bac776f8c489c4fbaa17f149f955092737e47c72cc31bb7b2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -762,7 +762,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "64f1a2e0e2bb4c4bac776f8c489c4fbaa17f149f955092737e47c72cc31bb7b2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -792,7 +792,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c29a9dd689ce336a07824eb0fb8d518191124b6be0c5ca037a65ca618cd07106", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -822,7 +822,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c29a9dd689ce336a07824eb0fb8d518191124b6be0c5ca037a65ca618cd07106", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -852,7 +852,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c29a9dd689ce336a07824eb0fb8d518191124b6be0c5ca037a65ca618cd07106", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -882,7 +882,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c29a9dd689ce336a07824eb0fb8d518191124b6be0c5ca037a65ca618cd07106", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -912,7 +912,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e0a3a3a908ecfb00b4e9ae083e0ce2a2db55183f0ab878a38d8092eaaee6d8ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -942,7 +942,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e0a3a3a908ecfb00b4e9ae083e0ce2a2db55183f0ab878a38d8092eaaee6d8ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -972,7 +972,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e0a3a3a908ecfb00b4e9ae083e0ce2a2db55183f0ab878a38d8092eaaee6d8ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1002,7 +1002,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e0a3a3a908ecfb00b4e9ae083e0ce2a2db55183f0ab878a38d8092eaaee6d8ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1032,7 +1032,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "954fde6a7332eac489845daa35b7413e7a216823cba8bcf73aaeecf180d73f3d", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1062,7 +1062,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "954fde6a7332eac489845daa35b7413e7a216823cba8bcf73aaeecf180d73f3d", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1092,7 +1092,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "954fde6a7332eac489845daa35b7413e7a216823cba8bcf73aaeecf180d73f3d", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1122,7 +1122,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "954fde6a7332eac489845daa35b7413e7a216823cba8bcf73aaeecf180d73f3d", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1152,7 +1152,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ac74145015e2575770fdaa3f2730ef7672b1cc72c0159a5a4f390e1b1f474f2f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1182,7 +1182,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ac74145015e2575770fdaa3f2730ef7672b1cc72c0159a5a4f390e1b1f474f2f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1212,7 +1212,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ac74145015e2575770fdaa3f2730ef7672b1cc72c0159a5a4f390e1b1f474f2f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1242,7 +1242,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ac74145015e2575770fdaa3f2730ef7672b1cc72c0159a5a4f390e1b1f474f2f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1272,7 +1272,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5bba5231d21a1ecf327af004dbcc34c36fe074fa2ce01fc855848017d7ccd8a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1302,7 +1302,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5bba5231d21a1ecf327af004dbcc34c36fe074fa2ce01fc855848017d7ccd8a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1332,7 +1332,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5bba5231d21a1ecf327af004dbcc34c36fe074fa2ce01fc855848017d7ccd8a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1362,7 +1362,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5bba5231d21a1ecf327af004dbcc34c36fe074fa2ce01fc855848017d7ccd8a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1392,7 +1392,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5c7aad291389053ed6aae2b30ced684b9818e83d3faf332c316bc80d8b126f6", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1422,7 +1422,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5c7aad291389053ed6aae2b30ced684b9818e83d3faf332c316bc80d8b126f6", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1452,7 +1452,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5c7aad291389053ed6aae2b30ced684b9818e83d3faf332c316bc80d8b126f6", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1482,7 +1482,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b5c7aad291389053ed6aae2b30ced684b9818e83d3faf332c316bc80d8b126f6", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1512,7 +1512,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e9b4d7ef81ad5f5439354dee4ce7f41a9a5b02e4e2c196a202b649bfbb489342", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1542,7 +1542,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e9b4d7ef81ad5f5439354dee4ce7f41a9a5b02e4e2c196a202b649bfbb489342", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1572,7 +1572,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e9b4d7ef81ad5f5439354dee4ce7f41a9a5b02e4e2c196a202b649bfbb489342", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1602,7 +1602,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e9b4d7ef81ad5f5439354dee4ce7f41a9a5b02e4e2c196a202b649bfbb489342", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1632,7 +1632,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "23ac64c27be9ea4619067d5facaa78f01edf2d37e986ec0a6f023eb51c61dc01", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1667,7 +1667,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "23ac64c27be9ea4619067d5facaa78f01edf2d37e986ec0a6f023eb51c61dc01", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1702,7 +1702,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "23ac64c27be9ea4619067d5facaa78f01edf2d37e986ec0a6f023eb51c61dc01", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1737,7 +1737,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "23ac64c27be9ea4619067d5facaa78f01edf2d37e986ec0a6f023eb51c61dc01", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1772,7 +1772,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b264e304113e038b3eb1fd220e50a7122ac11b270fbd4e6bc145e95f4bc9bd65", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1807,7 +1807,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b264e304113e038b3eb1fd220e50a7122ac11b270fbd4e6bc145e95f4bc9bd65", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1842,7 +1842,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b264e304113e038b3eb1fd220e50a7122ac11b270fbd4e6bc145e95f4bc9bd65", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1877,7 +1877,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b264e304113e038b3eb1fd220e50a7122ac11b270fbd4e6bc145e95f4bc9bd65", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1912,7 +1912,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1510d851b6a09b92fc0c50f142733a8273e0fdc85e87a0bbde43dd9fbee280f9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1943,7 +1943,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1510d851b6a09b92fc0c50f142733a8273e0fdc85e87a0bbde43dd9fbee280f9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -1974,7 +1974,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1510d851b6a09b92fc0c50f142733a8273e0fdc85e87a0bbde43dd9fbee280f9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2005,7 +2005,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1510d851b6a09b92fc0c50f142733a8273e0fdc85e87a0bbde43dd9fbee280f9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2036,7 +2036,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f04b302951c4fe6adc0e0dc7124417a847b00ea2902666c8f8a869bc79895d88", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2070,7 +2070,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f04b302951c4fe6adc0e0dc7124417a847b00ea2902666c8f8a869bc79895d88", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2104,7 +2104,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f04b302951c4fe6adc0e0dc7124417a847b00ea2902666c8f8a869bc79895d88", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2138,7 +2138,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f04b302951c4fe6adc0e0dc7124417a847b00ea2902666c8f8a869bc79895d88", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2172,7 +2172,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d2dac90f8f53f19ae855924d27b53c54d2b28871a4f84b068d6a2bad0c4f6c36", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2199,7 +2199,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d2dac90f8f53f19ae855924d27b53c54d2b28871a4f84b068d6a2bad0c4f6c36", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2226,7 +2226,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d2dac90f8f53f19ae855924d27b53c54d2b28871a4f84b068d6a2bad0c4f6c36", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2253,7 +2253,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b733e4ad323748222e30134660417d1de58727dbdaee690b9b9f27f5d23f12a7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2285,7 +2285,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b733e4ad323748222e30134660417d1de58727dbdaee690b9b9f27f5d23f12a7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2317,7 +2317,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b733e4ad323748222e30134660417d1de58727dbdaee690b9b9f27f5d23f12a7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2349,7 +2349,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b733e4ad323748222e30134660417d1de58727dbdaee690b9b9f27f5d23f12a7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2381,7 +2381,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "47bf5378331d6d82cf860c3c38150cb95b21f6b7d12841f7ee056c55622799e8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2413,7 +2413,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "47bf5378331d6d82cf860c3c38150cb95b21f6b7d12841f7ee056c55622799e8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2445,7 +2445,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "056014489bdf17897fb9de308ecc1ea22c3ffdf83c3894382b08f0e94b2dbcdb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2477,7 +2477,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "056014489bdf17897fb9de308ecc1ea22c3ffdf83c3894382b08f0e94b2dbcdb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2509,7 +2509,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "056014489bdf17897fb9de308ecc1ea22c3ffdf83c3894382b08f0e94b2dbcdb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2541,7 +2541,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "056014489bdf17897fb9de308ecc1ea22c3ffdf83c3894382b08f0e94b2dbcdb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2573,7 +2573,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "8d06d8ae1faf86625add070a7a6dca48c5be41a600e7cecfbd71e6e151f5e769", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2605,7 +2605,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "8d06d8ae1faf86625add070a7a6dca48c5be41a600e7cecfbd71e6e151f5e769", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2637,7 +2637,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "8d06d8ae1faf86625add070a7a6dca48c5be41a600e7cecfbd71e6e151f5e769", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2669,7 +2669,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "8d06d8ae1faf86625add070a7a6dca48c5be41a600e7cecfbd71e6e151f5e769", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2701,7 +2701,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ec0e477e5664f963929581df503d093d4ae21da6b76d6f3d542f7980c3a9a08a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2733,7 +2733,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ec0e477e5664f963929581df503d093d4ae21da6b76d6f3d542f7980c3a9a08a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2765,7 +2765,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ec0e477e5664f963929581df503d093d4ae21da6b76d6f3d542f7980c3a9a08a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2797,7 +2797,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ec0e477e5664f963929581df503d093d4ae21da6b76d6f3d542f7980c3a9a08a", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2829,7 +2829,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9a2804d7474e0f64052c4c325f39efbdcffb1e1d252bba8f2dff42fd0bf142a4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2870,7 +2870,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9a2804d7474e0f64052c4c325f39efbdcffb1e1d252bba8f2dff42fd0bf142a4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2911,7 +2911,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9a2804d7474e0f64052c4c325f39efbdcffb1e1d252bba8f2dff42fd0bf142a4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2952,7 +2952,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9a2804d7474e0f64052c4c325f39efbdcffb1e1d252bba8f2dff42fd0bf142a4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -2993,7 +2993,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9246462997cb2e9e8166226f651f7598ea6c81bd0e025f9861e7366ffca2d699", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3031,7 +3031,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "158045e0bc3594e6b18d6cb77032b9ef98aad34cee1b35a0f556684c76a09fc9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3063,7 +3063,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "158045e0bc3594e6b18d6cb77032b9ef98aad34cee1b35a0f556684c76a09fc9", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3094,13 +3094,13 @@ "harness": "claude", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "63c86b86ec60f0dbb7c94ef8357bac86afe47d4a7f805635d11bc7d78419898e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "9b7e90528d47ed623bf512915852fa1d4a7d17ba0daf797be12f05e20f0b6e05", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "549ca2d7429db42b1fcd1315821c3e5648023c3c756c7930308690128b30354b", + "renderedHash": "c17fc138cda1e56556b0ca9ec093cdbc4f0f3a19f81b57926f275806f79b1aad", "enabled": true, "emitted": { "kind": "hook", @@ -3189,13 +3189,13 @@ "harness": "codex", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "63c86b86ec60f0dbb7c94ef8357bac86afe47d4a7f805635d11bc7d78419898e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "9b7e90528d47ed623bf512915852fa1d4a7d17ba0daf797be12f05e20f0b6e05", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "549ca2d7429db42b1fcd1315821c3e5648023c3c756c7930308690128b30354b", + "renderedHash": "c17fc138cda1e56556b0ca9ec093cdbc4f0f3a19f81b57926f275806f79b1aad", "enabled": true, "emitted": { "kind": "hook", @@ -3257,13 +3257,13 @@ "harness": "copilot", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "63c86b86ec60f0dbb7c94ef8357bac86afe47d4a7f805635d11bc7d78419898e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "9b7e90528d47ed623bf512915852fa1d4a7d17ba0daf797be12f05e20f0b6e05", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "549ca2d7429db42b1fcd1315821c3e5648023c3c756c7930308690128b30354b", + "renderedHash": "c17fc138cda1e56556b0ca9ec093cdbc4f0f3a19f81b57926f275806f79b1aad", "enabled": true, "emitted": { "kind": "hook", @@ -3352,13 +3352,13 @@ "harness": "pi", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "63c86b86ec60f0dbb7c94ef8357bac86afe47d4a7f805635d11bc7d78419898e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "9b7e90528d47ed623bf512915852fa1d4a7d17ba0daf797be12f05e20f0b6e05", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "549ca2d7429db42b1fcd1315821c3e5648023c3c756c7930308690128b30354b", + "renderedHash": "c17fc138cda1e56556b0ca9ec093cdbc4f0f3a19f81b57926f275806f79b1aad", "enabled": true, "emitted": { "kind": "hook", @@ -3430,7 +3430,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1bfbcb2c43e6fab13888956bdc98691ec72d4eadf293187f4867af817d60bdbe", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3462,7 +3462,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7a99ebbd12769b351b8656699134c0f93d7ab06af43b4cb4ea8ac410fcaf64aa", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3503,7 +3503,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7a99ebbd12769b351b8656699134c0f93d7ab06af43b4cb4ea8ac410fcaf64aa", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3544,7 +3544,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7a99ebbd12769b351b8656699134c0f93d7ab06af43b4cb4ea8ac410fcaf64aa", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3585,7 +3585,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7a99ebbd12769b351b8656699134c0f93d7ab06af43b4cb4ea8ac410fcaf64aa", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3626,7 +3626,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4fa45a9fc7c4f7a5bb5ff5bb66e392460442ac8ed4ef14832d17832a2a4515eb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3667,7 +3667,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4fa45a9fc7c4f7a5bb5ff5bb66e392460442ac8ed4ef14832d17832a2a4515eb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3708,7 +3708,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4fa45a9fc7c4f7a5bb5ff5bb66e392460442ac8ed4ef14832d17832a2a4515eb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3749,7 +3749,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "4fa45a9fc7c4f7a5bb5ff5bb66e392460442ac8ed4ef14832d17832a2a4515eb", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3790,7 +3790,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "eef427b33e0fbbfbb8b807dd0986ffa66ce350033aac9bb23255988b03925024", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3828,7 +3828,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b128f266b3d67697e7ac128f8aabc97b472a03cc7fe6f05e4fc9b145cc315442", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3860,7 +3860,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c7905659a294ec88a2b38b9bfbf071d0160a45689ece954a0f9564e3c5791ba2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3901,7 +3901,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d8a11a3b614b97c3b0c304cae7494a16510c998f1f325f0ad10874957c43b917", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3933,7 +3933,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d8a11a3b614b97c3b0c304cae7494a16510c998f1f325f0ad10874957c43b917", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3965,7 +3965,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d8a11a3b614b97c3b0c304cae7494a16510c998f1f325f0ad10874957c43b917", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -3997,7 +3997,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d8a11a3b614b97c3b0c304cae7494a16510c998f1f325f0ad10874957c43b917", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4029,7 +4029,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "87dd5148fca28408dab96f6a6819bb3d43cf6da54ad02c4cf60fc953e50bbc43", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4061,7 +4061,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "87dd5148fca28408dab96f6a6819bb3d43cf6da54ad02c4cf60fc953e50bbc43", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4093,7 +4093,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "26090ae471accc49d86a2b1c10fd364b72df1ce4f6a767fd10e82318770d3105", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4125,7 +4125,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "26090ae471accc49d86a2b1c10fd364b72df1ce4f6a767fd10e82318770d3105", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4157,7 +4157,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "26090ae471accc49d86a2b1c10fd364b72df1ce4f6a767fd10e82318770d3105", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4189,7 +4189,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d7dad268fbd914b6517b1afe088558166c009cd0ac221ce83fed82de7e8488e3", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4221,7 +4221,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d7dad268fbd914b6517b1afe088558166c009cd0ac221ce83fed82de7e8488e3", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4253,7 +4253,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "d7dad268fbd914b6517b1afe088558166c009cd0ac221ce83fed82de7e8488e3", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4285,7 +4285,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f8fa08d86e3482b5f30034dd7522ce3236f6e6a5000b8f64d6009093dccc5577", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4326,7 +4326,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f8fa08d86e3482b5f30034dd7522ce3236f6e6a5000b8f64d6009093dccc5577", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4367,7 +4367,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f8fa08d86e3482b5f30034dd7522ce3236f6e6a5000b8f64d6009093dccc5577", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4408,7 +4408,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f8fa08d86e3482b5f30034dd7522ce3236f6e6a5000b8f64d6009093dccc5577", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4449,7 +4449,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fb43be6b7dee642b200268fbe71e14cc96282ea06a1554012ab80ce52a92ba79", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4490,7 +4490,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fb43be6b7dee642b200268fbe71e14cc96282ea06a1554012ab80ce52a92ba79", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4531,7 +4531,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fb43be6b7dee642b200268fbe71e14cc96282ea06a1554012ab80ce52a92ba79", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4572,7 +4572,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fb43be6b7dee642b200268fbe71e14cc96282ea06a1554012ab80ce52a92ba79", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4613,7 +4613,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "101caf97829906294354478ca77d288f5311a70a3c6ba90ae2babf9e3bf34624", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4654,7 +4654,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "101caf97829906294354478ca77d288f5311a70a3c6ba90ae2babf9e3bf34624", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4695,7 +4695,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a334bd710c5b926e24b2dd2a066d3b4dcd6747030a111bf7cabcc545ea242ad7", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4727,7 +4727,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "9425ae12b1eb7229702d033513e1758ae9a092db8f561b912e74c6740bbfa2ef", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4768,7 +4768,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3e981ce407351f70b1e2dbc8e46840b64be2ecd8b17fe16ccdff0960edf2f7ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4809,7 +4809,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3e981ce407351f70b1e2dbc8e46840b64be2ecd8b17fe16ccdff0960edf2f7ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4841,7 +4841,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3e981ce407351f70b1e2dbc8e46840b64be2ecd8b17fe16ccdff0960edf2f7ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4873,7 +4873,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3e981ce407351f70b1e2dbc8e46840b64be2ecd8b17fe16ccdff0960edf2f7ad", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4905,7 +4905,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7e694d75dda8c2899d887e21f681d4a521c5e675d5185c06113ffd6fc0203241", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4942,7 +4942,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7e694d75dda8c2899d887e21f681d4a521c5e675d5185c06113ffd6fc0203241", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -4978,7 +4978,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7e694d75dda8c2899d887e21f681d4a521c5e675d5185c06113ffd6fc0203241", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5014,7 +5014,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7e694d75dda8c2899d887e21f681d4a521c5e675d5185c06113ffd6fc0203241", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5050,7 +5050,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "bed7626f1d7e1f047a4f3f49ff2d2b83167d03ea2610d85e0c4cddd892353157", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5105,7 +5105,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "bed7626f1d7e1f047a4f3f49ff2d2b83167d03ea2610d85e0c4cddd892353157", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5159,7 +5159,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "bed7626f1d7e1f047a4f3f49ff2d2b83167d03ea2610d85e0c4cddd892353157", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5213,7 +5213,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "bed7626f1d7e1f047a4f3f49ff2d2b83167d03ea2610d85e0c4cddd892353157", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5267,7 +5267,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "98fc478b875c8c5b7d831bce38f1eb2665925e6c5c76eeba6184e80d1b5554ac", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5322,7 +5322,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "98fc478b875c8c5b7d831bce38f1eb2665925e6c5c76eeba6184e80d1b5554ac", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5376,7 +5376,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "98fc478b875c8c5b7d831bce38f1eb2665925e6c5c76eeba6184e80d1b5554ac", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5430,7 +5430,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "98fc478b875c8c5b7d831bce38f1eb2665925e6c5c76eeba6184e80d1b5554ac", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5484,7 +5484,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "16e1964d26c75a1334fe63b0d15067cae857935a972fc0be17aa02844dbf571f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5518,7 +5518,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "16e1964d26c75a1334fe63b0d15067cae857935a972fc0be17aa02844dbf571f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5551,7 +5551,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "16e1964d26c75a1334fe63b0d15067cae857935a972fc0be17aa02844dbf571f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5584,7 +5584,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "16e1964d26c75a1334fe63b0d15067cae857935a972fc0be17aa02844dbf571f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5617,7 +5617,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b4a75dca09bb20bb042bde9f3998d836b981a5b8030ec9690ca01e6836a2f752", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5672,7 +5672,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b4a75dca09bb20bb042bde9f3998d836b981a5b8030ec9690ca01e6836a2f752", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5726,7 +5726,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b4a75dca09bb20bb042bde9f3998d836b981a5b8030ec9690ca01e6836a2f752", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5780,7 +5780,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "b4a75dca09bb20bb042bde9f3998d836b981a5b8030ec9690ca01e6836a2f752", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5834,7 +5834,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f07de0df87cd6a53de4d68f1098c408db4e8d73fa88822426de3cad6875f04e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5862,7 +5862,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f07de0df87cd6a53de4d68f1098c408db4e8d73fa88822426de3cad6875f04e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5889,7 +5889,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f07de0df87cd6a53de4d68f1098c408db4e8d73fa88822426de3cad6875f04e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5916,7 +5916,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "7f07de0df87cd6a53de4d68f1098c408db4e8d73fa88822426de3cad6875f04e", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5943,7 +5943,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f2bca9bc82c0e18f23e61cc013e521dd5b6867f5038845a1163ee1effa83abed", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5971,7 +5971,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f2bca9bc82c0e18f23e61cc013e521dd5b6867f5038845a1163ee1effa83abed", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -5998,7 +5998,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f2bca9bc82c0e18f23e61cc013e521dd5b6867f5038845a1163ee1effa83abed", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6025,7 +6025,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "f2bca9bc82c0e18f23e61cc013e521dd5b6867f5038845a1163ee1effa83abed", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6052,7 +6052,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "773e83d938187806999cb81786c268b493dc18f13501728535ef22b6afd03878", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6089,7 +6089,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "773e83d938187806999cb81786c268b493dc18f13501728535ef22b6afd03878", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6125,7 +6125,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "773e83d938187806999cb81786c268b493dc18f13501728535ef22b6afd03878", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6161,7 +6161,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "773e83d938187806999cb81786c268b493dc18f13501728535ef22b6afd03878", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6197,7 +6197,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fd88aed4b9211e14a8250139ebedd785f139c9e508101465548c06a6571fec46", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6225,7 +6225,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fd88aed4b9211e14a8250139ebedd785f139c9e508101465548c06a6571fec46", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6252,7 +6252,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fd88aed4b9211e14a8250139ebedd785f139c9e508101465548c06a6571fec46", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6279,7 +6279,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fd88aed4b9211e14a8250139ebedd785f139c9e508101465548c06a6571fec46", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6306,7 +6306,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "0a859081ace51943009f81b7748b1f08a236bb886fe50495e2d26518524ec195", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6352,7 +6352,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "0a859081ace51943009f81b7748b1f08a236bb886fe50495e2d26518524ec195", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6397,7 +6397,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "0a859081ace51943009f81b7748b1f08a236bb886fe50495e2d26518524ec195", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6442,7 +6442,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "0a859081ace51943009f81b7748b1f08a236bb886fe50495e2d26518524ec195", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6487,7 +6487,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "831b57913311d8fd3bcdd376b7da7ef3344ab22702865bcb731393dc7580662b", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6569,7 +6569,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "831b57913311d8fd3bcdd376b7da7ef3344ab22702865bcb731393dc7580662b", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6650,7 +6650,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "831b57913311d8fd3bcdd376b7da7ef3344ab22702865bcb731393dc7580662b", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6731,7 +6731,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "831b57913311d8fd3bcdd376b7da7ef3344ab22702865bcb731393dc7580662b", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -6811,13 +6811,13 @@ "harness": "claude", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "3168f92b017a42bfd272b4be616ab39c22c1b3f5640f8087e1740b3f8be99666", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "21506aa663fbc893bb864a33a1329b82ab3fc875dfdcb9726916d5de085a2e65", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "370679348d7ae3c354f09baf17c6b8419bc90922a8a906100233127b4c36034b", + "renderedHash": "901280f56ba0885d980eedd282a276a60f6cdbbb8111866c1597ea310f81f241", "enabled": true, "emitted": { "kind": "skill", @@ -6857,13 +6857,13 @@ "harness": "codex", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "3168f92b017a42bfd272b4be616ab39c22c1b3f5640f8087e1740b3f8be99666", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "21506aa663fbc893bb864a33a1329b82ab3fc875dfdcb9726916d5de085a2e65", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "370679348d7ae3c354f09baf17c6b8419bc90922a8a906100233127b4c36034b", + "renderedHash": "901280f56ba0885d980eedd282a276a60f6cdbbb8111866c1597ea310f81f241", "enabled": true, "emitted": { "kind": "skill", @@ -6902,13 +6902,13 @@ "harness": "copilot", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "3168f92b017a42bfd272b4be616ab39c22c1b3f5640f8087e1740b3f8be99666", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "21506aa663fbc893bb864a33a1329b82ab3fc875dfdcb9726916d5de085a2e65", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "370679348d7ae3c354f09baf17c6b8419bc90922a8a906100233127b4c36034b", + "renderedHash": "901280f56ba0885d980eedd282a276a60f6cdbbb8111866c1597ea310f81f241", "enabled": true, "emitted": { "kind": "skill", @@ -6947,13 +6947,13 @@ "harness": "pi", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "3168f92b017a42bfd272b4be616ab39c22c1b3f5640f8087e1740b3f8be99666", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "21506aa663fbc893bb864a33a1329b82ab3fc875dfdcb9726916d5de085a2e65", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "370679348d7ae3c354f09baf17c6b8419bc90922a8a906100233127b4c36034b", + "renderedHash": "901280f56ba0885d980eedd282a276a60f6cdbbb8111866c1597ea310f81f241", "enabled": true, "emitted": { "kind": "skill", @@ -6993,7 +6993,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3ba41c08e765a62757dfa60e6c5c182e18491c2f94223824957cc50187ffc823", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7030,7 +7030,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3ba41c08e765a62757dfa60e6c5c182e18491c2f94223824957cc50187ffc823", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7066,7 +7066,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3ba41c08e765a62757dfa60e6c5c182e18491c2f94223824957cc50187ffc823", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7102,7 +7102,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "3ba41c08e765a62757dfa60e6c5c182e18491c2f94223824957cc50187ffc823", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7138,7 +7138,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a535f6d275d711b1676f0e7787841918808f6a12fd1e60b279aebeaa824960bd", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7181,7 +7181,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a535f6d275d711b1676f0e7787841918808f6a12fd1e60b279aebeaa824960bd", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7223,7 +7223,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a535f6d275d711b1676f0e7787841918808f6a12fd1e60b279aebeaa824960bd", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7265,7 +7265,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "a535f6d275d711b1676f0e7787841918808f6a12fd1e60b279aebeaa824960bd", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7306,13 +7306,13 @@ "harness": "claude", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "8252bd2ad8389089052252859b2de875287a424a12553694d918b4bffe46f644", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "62e53143302dcb17a76a02f31cf93385b023e163e220395cc8b712ac5af6157c", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "7ce1bbb1b76fbba4b4f3b8e815ea19eea01ab47e273c7dc082a460fcdc667a8b", + "renderedHash": "8d99f424126e0ec1047464531eabac18c4762d48cb59bd2c30758d487ccf9c50", "enabled": true, "emitted": { "kind": "skill", @@ -7388,13 +7388,13 @@ "harness": "codex", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "8252bd2ad8389089052252859b2de875287a424a12553694d918b4bffe46f644", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "62e53143302dcb17a76a02f31cf93385b023e163e220395cc8b712ac5af6157c", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "7ce1bbb1b76fbba4b4f3b8e815ea19eea01ab47e273c7dc082a460fcdc667a8b", + "renderedHash": "8d99f424126e0ec1047464531eabac18c4762d48cb59bd2c30758d487ccf9c50", "enabled": true, "emitted": { "kind": "skill", @@ -7469,13 +7469,13 @@ "harness": "copilot", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "8252bd2ad8389089052252859b2de875287a424a12553694d918b4bffe46f644", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "62e53143302dcb17a76a02f31cf93385b023e163e220395cc8b712ac5af6157c", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "7ce1bbb1b76fbba4b4f3b8e815ea19eea01ab47e273c7dc082a460fcdc667a8b", + "renderedHash": "8d99f424126e0ec1047464531eabac18c4762d48cb59bd2c30758d487ccf9c50", "enabled": true, "emitted": { "kind": "skill", @@ -7541,13 +7541,13 @@ "harness": "pi", "source": "kendex", "sourceRepo": "vanillagreencom/kendex", - "sourceHash": "8252bd2ad8389089052252859b2de875287a424a12553694d918b4bffe46f644", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceHash": "62e53143302dcb17a76a02f31cf93385b023e163e220395cc8b712ac5af6157c", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null }, - "renderedHash": "7ce1bbb1b76fbba4b4f3b8e815ea19eea01ab47e273c7dc082a460fcdc667a8b", + "renderedHash": "8d99f424126e0ec1047464531eabac18c4762d48cb59bd2c30758d487ccf9c50", "enabled": true, "emitted": { "kind": "skill", @@ -7614,7 +7614,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e967f26a25614f2028d62a3d0a9a4ae1a45d5e15d438f27f32006afa4de3dc7f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7642,7 +7642,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e967f26a25614f2028d62a3d0a9a4ae1a45d5e15d438f27f32006afa4de3dc7f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7669,7 +7669,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e967f26a25614f2028d62a3d0a9a4ae1a45d5e15d438f27f32006afa4de3dc7f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7696,7 +7696,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "e967f26a25614f2028d62a3d0a9a4ae1a45d5e15d438f27f32006afa4de3dc7f", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7723,7 +7723,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "457ba4b0812f431f86d0b5c4ed21e8f9b2f39ecbf49cfcf1f2e0172797e7d1fe", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7769,7 +7769,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "457ba4b0812f431f86d0b5c4ed21e8f9b2f39ecbf49cfcf1f2e0172797e7d1fe", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7814,7 +7814,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "457ba4b0812f431f86d0b5c4ed21e8f9b2f39ecbf49cfcf1f2e0172797e7d1fe", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7859,7 +7859,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "457ba4b0812f431f86d0b5c4ed21e8f9b2f39ecbf49cfcf1f2e0172797e7d1fe", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7904,7 +7904,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1bbf866e0a8475b09892ab440195b61e3c1ea252497ff568de02bfa921c203f8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7950,7 +7950,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1bbf866e0a8475b09892ab440195b61e3c1ea252497ff568de02bfa921c203f8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -7995,7 +7995,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1bbf866e0a8475b09892ab440195b61e3c1ea252497ff568de02bfa921c203f8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8040,7 +8040,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "1bbf866e0a8475b09892ab440195b61e3c1ea252497ff568de02bfa921c203f8", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8085,7 +8085,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ed4d296626792264d3bff659c01b6fdc3200313ce103765d710868d49a0713d2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8149,7 +8149,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ed4d296626792264d3bff659c01b6fdc3200313ce103765d710868d49a0713d2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8212,7 +8212,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ed4d296626792264d3bff659c01b6fdc3200313ce103765d710868d49a0713d2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8275,7 +8275,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "ed4d296626792264d3bff659c01b6fdc3200313ce103765d710868d49a0713d2", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8338,7 +8338,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c4c87f61d4b51c532387d32daa24f0eda5fb1433ca6fe50d3261473410ec8ae4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8366,7 +8366,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c4c87f61d4b51c532387d32daa24f0eda5fb1433ca6fe50d3261473410ec8ae4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8393,7 +8393,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c4c87f61d4b51c532387d32daa24f0eda5fb1433ca6fe50d3261473410ec8ae4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8420,7 +8420,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "c4c87f61d4b51c532387d32daa24f0eda5fb1433ca6fe50d3261473410ec8ae4", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8447,7 +8447,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fc41af4bc8ffe6d6820d37dec05a39a7afeb358bb79d5f9b4493c9dd47f9e75c", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8493,7 +8493,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fc41af4bc8ffe6d6820d37dec05a39a7afeb358bb79d5f9b4493c9dd47f9e75c", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8538,7 +8538,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fc41af4bc8ffe6d6820d37dec05a39a7afeb358bb79d5f9b4493c9dd47f9e75c", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8583,7 +8583,7 @@ "source": "kendex", "sourceRepo": "vanillagreencom/kendex", "sourceHash": "fc41af4bc8ffe6d6820d37dec05a39a7afeb358bb79d5f9b4493c9dd47f9e75c", - "sourceCommit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7", + "sourceCommit": "877d3c84de86756b122c897ef2ed119ff3d69e77", "selector": { "sourceRev": null, "rev": null @@ -8625,7 +8625,7 @@ "sources": { "kendex": { "repo": "vanillagreencom/kendex", - "commit": "614f59bc3bb5933d1069be781b60f500c7c2c8d7" + "commit": "877d3c84de86756b122c897ef2ed119ff3d69e77" } }, "claudeProjectDefault": "owned" diff --git a/.pi/kendex/hooks/lane-mail-check.sh b/.pi/kendex/hooks/lane-mail-check.sh index 710faefb..3c3b96dd 100644 --- a/.pi/kendex/hooks/lane-mail-check.sh +++ b/.pi/kendex/hooks/lane-mail-check.sh @@ -5,7 +5,7 @@ # 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 native session must also match the recorded harness, readable caller home and payload session id; `ol_record_heal` binds the first matching native id from the recorded pane, and `oversee register` clears that binding after a resume. Pi and any harness whose `lane_context_caller_home` reports no home compare the harness and native session id only. 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. A Jarvis session's wake, the process `ORCH_WAKE_PROCESS` matches, is no such watch: it moves no cursor, so that session is handed each note in full here once and acknowledges it here, and its wake carries no note's body, at most the count and read command `lane-mail watch` prints. Where the payload's `kendex_ack` is `caller`, which the pi-hooks lane mail wake sends, the context carries the reader's `--ack` command as `kendex_ack` and nothing is acknowledged here: the wake runs it once its send returns, so a send that throws leaves the lines unread. 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/mailbox/scripts/lane-mail` or the shared `.agents/skills/mailbox/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/mailbox/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 bound native session the fleet record names, or the overseer it lost whose native identity still matches a nonempty recorded binding, 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. A session start acknowledges them only once that context is written. A prompt leaves the lines unread because Copilot can discard its output, so a later inbox read or hook delivery can repeat them. Neither arm refuses anything: every refusal this description names is its keyed line on stderr at exit 0, with nothing on stdout and nothing acknowledged. Before an attached agent launch, the halt arm also judges the lead of a launched lane or the identified fleet overseer at the context mark, refusing missing context with the orch job runner or a separate session as the route. While an attached agent of the session may still be out, the mark is less ORCH_HANDOFF_AGENT_RESERVE_TOKENS, default 150000 tokens, equality refused: Claude Code reads that roster from its task registry where the payload carries one, else from the transcript's Agent-call results, task notifications, teammate idle notifications and sends, and reads an unreadable roster or an entry neither read classifies as unknown; Codex reads ordered collaboration snapshots, launches and followups from its rollout. Idle, completed, errored, shutdown and not-found children release the reserve; initializing, running and interrupted children keep it. A full unfiltered snapshot replaces earlier unknown observations only when its call does not overlap a launch or followup; pending calls and unreadable current observations keep the roster unknown. After waiting, Codex must call list_agents with no path filter to observe returns. Other harnesses read none, so their rosters are unknown. With none out the launch meets the plain mark. A launch the reserve alone refuses with an agent out is keyed `agent-reserve` and names waiting for those agents, the job runner or a separate session, never a handoff, since the turn end and every succession judge the plain mark. With the roster unknown no wait can be observed, so the refusal is keyed `agent-unobserved` and names the job runner or a separate session, and for a lane its handoff record. On Pi the lead launches through subagent; delegate_subagent carries child identity and is outside this lead-only reserve rule. At a context hold, a live session waits for attached agent returns in this turn; Claude Stop background_tasks is preferred, with the transcript's measured Agent-call results, async launch and teammate spawn records, task notifications, teammate idle notifications and sends as the fallback when absent. Other harnesses name known unfinished children and known output paths, or unknown paths, in the emergency handoff. A standing handoff retains its escape, and the successor reads existing output before repeating work. 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` and the event 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, only for the pane's top-level harness and, on Copilot, only for a session the caller rule names the lead, 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. Run with `lane` by the critical-path-deny hook, it prints `lane` where the launch marker for the session's item binds the session's root and its mailbox directory stands, else `none`, records and judges nothing else, and exits 0; a gap past its payload read is reported on stderr and passed with nothing on stdout. 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 handoff refusals, 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, or by updating `tmp/lane-status-.md` under its launch root, 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`, judging a Claude lane on its own model. The picker owns the account handoff decision. For an ordinary account wall, the hook refuses the turn end and names the binding bucket's headroom against `ORCH_HANDOFF_HEADROOM_PCT` (default 3). When that picker reports `seat-reserve` for an overseer seat, the hook refuses the turn end. That refusal names the binding bucket's headroom and the configured `ORCH_OVERSEER_SEAT_RESERVE_PCT` (default 50) as independent facts. 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`. A lane's context and account mark refusals open `lane-mail-check: context=`, `lane-mail-check: headroom= bucket= mark=` or `lane-mail-check: seat-reserve= bucket= reserve=`. Each carries one instruction: reach the next safe point, write the record with `workflow-state set handoff`, send a `handoff` notice, and exit. The record is read where `workflow-state --help` § Handoff record says, a launched lane naming the root its launch marker binds as the worktree, and the instruction names the state directory the read found, so a record written as told ends both refusals. 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 whose native identity matches a nonempty binding in the fleet record but whose pane only 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, its launch home or its native session id is healed from an authorized native session 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 caller remains unbound until a later binding write succeeds. 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. If `lane_context_transcript_owned` refuses an admitted overseer's transcript binding, the hook reports `transcript-unowned=` with the reason the library returns. 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 binding library's reason, `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 the native `session_id` the payload gives. A payload with no native session id is refused and cannot read mail or write overseer context or gaps. `overseer_identify` owns its native refusal diagnostics. 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 sends and its lane status file update state, never on words in its pane, 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, the word `held` where this judge refused that turn end, and an optional encoded status file update state; no record is a count of zero with no status state. A status file update since that record, including a rewrite of the same text, passes without a notice. 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 follows the orch Coordination notice rule: changed work sends a notice, and a continuing wait on a dispatched agent or an armed wake updates the lane status file only. 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, an optional `held` mark and an optional encoded status state 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` identifies an attached launch. The `lane-mail-halt` description owns the Copilot preToolUse decision and deadline contract. 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, and after a tool call `{"additionalContext":}` with exit 0, since Copilot reads a postToolUse answer only at exit 0. 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. A session the Jarvis runtime launched, which carries `JARVIS_RECORD`, has its lead's tool calls, on Copilot any call under the session id the record's start bound, and on Claude Code its turn ends, stamp that runtime record's launch row once as its Start brief taken, the fact the Jarvis launch worker reads beside the pane; another harness's turn end is no stamp, since Pi's carrier also dispatches it on a run that ended on an error and Codex and Copilot have no turn-failure event to tell one apart; a stamp that cannot be written is reported under `jarvis-rows-unwritten=` and passed, and the next such call writes it. Not run on gemini: it has no Stop event. Not run on antigravity: its Stop payload carries no `stop_hook_active`. # summary: Uses orch and mailbox to hand lanes their overseer's mail and hold turn ends for handoff or idle checks, plus a `wake=unarmed` refusal for the registered overseer without its repeat follow or the Jarvis session whose `ORCH_WAKE_PROCESS` does not match, printing `ORCH_WAKE_START`; empty Jarvis session keys with no watch claim disable the check, and a running single pass is its own wake. Pi's wake check is partial; a continued turn or an unavailable check only warns. -# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. +# 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 the identity read, with `workflow-state path oversee` as its fallback 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, launch home and native session id where a record naming this session's pane lacks one, through `workflow-state update oversee`, 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. For a session the Jarvis runtime launched it also reads, at its lead's tool calls and Claude Code, Codex and Copilot CLI turn ends, the runtime record `JARVIS_RECORD` names, in one child shell sourcing the orch session rows library from the same install, and where that launch is starting and unstamped, reads it again under that record's `.lock` and renames a new record over it once: at a tool call or Claude Code turn end with its brief delivered, stamping its launch row's `brief_taken`; at a Codex or Copilot CLI turn end before the brief, stamping its `kickoff_ended`. 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 the identity read's fleet path, 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 resolves that install and reads the caller's pane key, the fleet record and the server start in a child shell loading the orch context and overseer launch libraries and their dependencies. A required heal loads `lib/overseer-launch.sh` and its dependencies in a child shell and rechecks the recorded native identity before the state update; a session whose native identity matches a nonempty recorded binding but whose pane the state does not name adds one child shell loading the orch context library, one `git-context` call and one read of the overseer's `context.json`; where that record names this session's pane, it reports `pane-unrecorded` and rewrites that record. Any other unbound caller stops before those context reads and writes. 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. The context-usage reader first parses the last 1 MiB, and reads the whole file only when that window carries no usage line. When native Claude Stop background_tasks is absent, the transcript roster fallback reads complete ordered history; its work grows with transcript size. It runs only under the remaining hook deadline, with three seconds reserved for refusal emission and a one-second kill grace. An unavailable bound, exhausted budget, timeout or failed parse reports agents=unknown and discards partial rows. `lanes pick --lane` judges a Claude lane on its own model, 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. Before a tool call it reads `tool_name` or Copilot `toolName`; the question-tool rule and attached-launch check consume that normalized name. 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 reads stat metadata for the fixed regular `tmp/lane-status-.md` file under the lane's launch root, with no symbolic link at the status file itself, and writes that update state beside the count in 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. For a launched lane or the bound fleet overseer, PostModelSwitch reads its to_model target and the recorded launch identity. A native remote lane without a local fleet record reads the separate launch record's named fields and the native identity file its prefix wrote. It checks that the callback descends from that live predecessor, preserves a handoff with the recorded model, and dispatches the existing identity stop. A failed native stop records its cause and process identity on that handoff and emits a keyed recovery-log failure. Its controller reads that handoff over the existing host transport and relaunches with that model pinned. It writes model checks and switch history through workflow-state. A lane replacement that reaches its first Stop without a mismatch writes one model-recovery-reset row and a passed model check in the same fleet state. Lane recovery preserves the standing handoff and checks the recorded arguments before stopping the verified predecessor through the existing lane stop owner. Account selection and launch follow that stop. Overseer recovery passes a durable handoff to oversee-succeed. That owner preserves the executable model selector and account, verifies the successor, then stops its predecessor. Unreadable PostModelSwitch dependencies reach the model as once-only unavailable context. A failed first-turn reset reports a keyed Stop warning on stderr and leaves later checks active. # timeout: 30 # harnesses: [claude, codex, pi, copilot, opencode, cursor] # requires: [lane-mail-deliver, lane-mail-halt, session-start-row, session-end-row, stop-failure-row, critical-path-deny, lane-mail-model-switch] @@ -1937,9 +1937,10 @@ handoff_instruction() { # Stop's task registry is the documented input. Older Claude versions, an # unreachable registry and every tool call omit it; only then use the measured # Agent-call results, task-notification status lines, teammate idle -# notifications, SendMessage calls and TaskStop calls. A tool result is read -# for those notification markers alone; no other part of a child result body -# is interpreted. +# notifications, SendMessage calls, TaskStop calls and ListAgents observations. +# The hook cannot call ListAgents itself; its recorded structured teammate +# rows supply the idle state when no notification arrived. SendMessage results +# prove a stopped agent resumed. Child reports supply notification markers. # AGENT_ROWS names each child still out, and AGENT_STATE says whether any may # be: none, out, or unknown where the roster could not be read or holds an # entry neither read classifies, which callers never read as none. @@ -1994,11 +1995,26 @@ claude_attached_agents() { # [emergency STATE_FILE] scan_bound=("${BOUND_BY[0]}" --kill-after=1 "$scan_seconds") # Each measured record moves one child: an async launch or a teammate # spawn puts it out; a task-notification ends an async child, and a - # teammate's idle_notification, which follows each of its reports, ends a - # teammate; a plain-text SendMessage to either puts it out again, and a + # teammate's terminal idle notification ends a teammate; a plain-text + # SendMessage to either puts it out again, and a # sent shutdown_request ends it, since the lead's transcript never # receives the approval and the lead is owed no further result. A # TaskStop call that did not fail ends the child it names by id or name. + # SendMessage can resume that stopped child as an in-process teammate; + # only its measured resume result proves the stopped run started again. + # ListAgents (2.1.302) records teammate states in toolUseResult.sections; + # its idle rows end the named turn even when no idle notification arrived. + # Every roster transition invalidates an earlier pending list. Claude + # Code 2.1.302 captures list state before its awaited peer reads, so even + # a completion appended to that result is newer than the snapshot. + # SendMessage writes the inbox before the teammate consumes it. No list + # snapshot proves consumption. A send from an ended turn has no earlier + # turn whose available notice could arrive afterward. A send to an + # active turn does: even its available notice can follow an empty drain + # that preceded the send. Keep that work unknown until the task ends. + # In 2.1.302, interrupted notices precede the inbox read, missing reasons + # continue the next prompt, and failed reasons can also continue. Only + # completedStatus=failed proves the latter ended the teammate task. # A foreground Agent call (no run_in_background, no name) returns its # child's finished output, so its result puts nothing out. Any other # result of an async or named call is unclassified. Claude Code appends a @@ -2022,43 +2038,96 @@ claude_attached_agents() { # [emergency STATE_FILE] (select(startswith("Spawned successfully")) | capture("\nagent_id: (?[A-Za-z0-9_-]+)\nname: (?[^\n]+)")?); def results: .content | if type == "array" then .[] else empty end | select(.type == "tool_result"); + def changed($id; $status): .generation += 1 | .agents[$id] = $status; + def awaiting: . == "queued-active" or . == "unconfirmed-active"; + def queued: awaiting or . == "queued" or . == "unconfirmed"; + def terminal: . == "completed" or . == "failed" or . == "killed"; + def uncertain: if terminal then . elif awaiting then "unconfirmed-active" elif queued then "unconfirmed" else "unknown" end; + def settled($prior; $notice): + if $prior | terminal then $prior + elif $notice.completedStatus == "failed" then "failed" + elif $notice.idleReason == "available" then + if $prior | awaiting then ($prior | uncertain) else "completed" end + elif $notice.idleReason == "interrupted" and (($prior | queued) == false) then "completed" + else $prior | uncertain end; def noted($m): reduce ($m.content | said | scan("[\\s\\S]*?([A-Za-z0-9_-]+)[\\s\\S]*?([a-z_]+)[\\s\\S]*?")) as $n - (. ; if .agents | has($n[0]) then .agents[$n[0]] = $n[1] else . end) + (. ; if .agents | has($n[0]) then + changed($n[0]; if $n[1] | terminal then $n[1] else (.agents[$n[0]] | uncertain) end) + else . end) | reduce ($m.content | said - | scan("]*>\\s*\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"")) as $n - (. ; if .names | has($n[0]) then .agents[.names[$n[0]]] = "completed" else . end); + | scan("]*>\\s*(\\{\\s*\"type\"\\s*:\\s*\"idle_notification\"[\\s\\S]*?)\\s*")) as $n + (. ; if .names | has($n[0]) then .names[$n[0]] as $t + | ([$n[1] | fromjson? | objects][0]) as $notice + | changed($t; if $notice == null then (.agents[$t] | uncertain) else settled(.agents[$t]; $notice) end) + else . end); reduce (inputs | fromjson | objects + | select(.type == "assistant" or .type == "user" or .type == "queue-operation") | . as $record | if .type == "queue-operation" then select(.operation == "enqueue") | {role: "user", content: (.content | strings)} - else .message? | objects end) as $m ({calls: {}, agents: {}, names: {}, unclassified: false}; + else .message? | objects | . + {result: $record.toolUseResult} end) as $m + ({calls: {}, agents: {}, names: {}, generation: 0, unclassified: false}; if $m.role == "assistant" then reduce ($m.content | if type == "array" then .[] else empty end | select(.type == "tool_use" and (.id | type) == "string")) as $c (. ; if $c.name == "Agent" or $c.name == "Task" then - .calls[$c.id] = {sync: (($c.input.run_in_background | tostring) != "true" + .generation += 1 | .calls[$c.id] = {pending: true, sync: (($c.input.run_in_background | tostring) != "true" and (($c.input.name | type) != "string" or $c.input.name == ""))} elif $c.name == "SendMessage" and ($c.input.to | type) == "string" then - if ($c.input.message | type) == "string" then .calls[$c.id] = {to: $c.input.to, status: "running"} + .generation += 1 + | if ($c.input.message | type) == "string" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "running"} elif ($c.input.message | type) == "object" - and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {to: $c.input.to, status: "completed"} + and $c.input.message.type == "shutdown_request" then .calls[$c.id] = {pending: true, to: $c.input.to, status: "completed"} else . end elif $c.name == "TaskStop" and ($c.input.task_id | type) == "string" then - .calls[$c.id] = {to: $c.input.task_id, status: "killed"} + .generation += 1 | .calls[$c.id] = {pending: true, to: $c.input.task_id, status: "killed"} + elif $c.name == "ListAgents" then + .calls[$c.id] = {list: .generation, overlap: any(.calls[]; .pending == true)} else . end) elif $m.role == "user" then reduce ($m | results | [.content | text | launched][0] | objects) as $l - (. ; .agents[$l.id] = "running" + (. ; changed($l.id; "running") | if $l.name != null then .names[$l.name] = $l.id else . end) | noted($m) | reduce ($m | results) as $r (. ; .calls[$r.tool_use_id | if type == "string" then . else "" end] as $c + | if $c != null then .calls[$r.tool_use_id].pending = false + | .generation += (if $c.pending then 1 else 0 end) else . end | if [$r.content | text | launched][0] != null or $c == null or $r.is_error == true then . + elif $c | has("list") then + if $c.overlap or $c.list != .generation then . + elif ([$m | results] | length) != 1 or ($m.result.sections | type) != "array" then + reduce (.agents | to_entries[] | select(.value == "running" or (.value | queued))) as $a + (. ; changed($a.key; ($a.value | uncertain))) + else reduce ($m.result.sections[] | select(.kind == "teammates") | .rows[]) as $a + (. ; if .names | has($a.name) then .names[$a.name] as $t + | if .agents[$t] | queued then + if $a.status == "idle" or $a.status == "running" then . + else changed($t; (.agents[$t] | uncertain)) end + elif $a.status == "idle" then changed($t; "completed") + elif $a.status == "running" then changed($t; "running") + else changed($t; (.agents[$t] | uncertain)) end + else . end) + end elif $c | has("to") then (.names[$c.to] // $c.to) as $t - | if .agents | has($t) then .agents[$t] = $c.status else . end + | if (.agents | has($t)) == false then . + elif $c.status == "running" and .agents[$t] == "killed" then + (if ([$m | results] | length) == 1 and ($m.result | type) == "object" then $m.result + else [$r.content | text | fromjson?][0] end + | if type == "object" then . else {} end) as $sent + | if $sent.success == true + and ((($sent.message | strings) // "") | test("^Teammate \"[^\"]+\" was not running; resumed it as an in-process teammate with [0-9]+ prior messages and your message as its next prompt\\.$")) + then changed($t; "queued") + else changed($t; "unconfirmed") end + else changed($t; if $c.status != "running" then $c.status + elif .agents[$t] | terminal then "queued" + elif .agents[$t] == "unknown" or .agents[$t] == "unconfirmed" or .agents[$t] == "unconfirmed-active" then "unconfirmed-active" + else "queued-active" end) end elif $c.sync then . else .unclassified = true end) else . end) - | (.agents | to_entries[] | [.key, .value] | @tsv), + | (.agents | to_entries[] | [.key, + (if .value == "queued" or .value == "queued-active" then "running" + elif .value == "unconfirmed" or .value == "unconfirmed-active" then "unknown" else .value end)] | @tsv), (select(.unclassified) | ["?", "agent-result"] | @tsv) ' <"$TRANSCRIPT" 2>"$WORK_DIR/agents.err") || scan_rc=$? if [ "$scan_rc" -ne 0 ]; then @@ -2075,7 +2144,7 @@ claude_attached_agents() { # [emergency STATE_FILE] while IFS="$TAB" read -r id status; do [ -n "$id" ] || continue if [ "$id" = '?' ]; then unclassified=$status; continue; fi - case "$status" in completed | failed | killed) continue ;; esac + case "$status" in completed | failed | killed) continue ;; unknown) unclassified=agent-status ;; esac AGENT_STATE=out output=unknown # https://code.claude.com/docs/en/hooks#subagentstop @@ -2904,6 +2973,18 @@ pi_account() { # Called plainly, so errexit is live throughout this body: a status left to it # would exit the hook with neither 0 nor 2 and end the turn with no mark # judged. Every status is therefore tested where it is taken. +model_item_state() { # FILE STATE_DIR + # Reuse the state writer's lock when a launch predates its item state. + # Initialize only under that lock so concurrent callbacks cannot erase work. + "$BASH" -euo pipefail -c ' + . "$1/lib/file-lock.sh" + mkdir -p -- "$2" + (orch_take_lock 200 "$3.lock" 10 || exit 1 + [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null + ) 200>"$3.lock" + ' _ "$SCRIPTS" "$2" "$1" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err" +} + model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] local cause="$1" observed="$2" detail="${3:-}" file sd report notice notice="lane-mail-check: model-unavailable=$cause" @@ -2912,15 +2993,7 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice"; return 0; } sd=${file%/*} - # Reuse the state writer's lock when a launch predates its item state. - # Initialize only under that lock so concurrent callbacks cannot erase work. - if ! "$BASH" -euo pipefail -c ' - . "$1/lib/file-lock.sh" - mkdir -p -- "$2" - (orch_take_lock 200 "$3.lock" 10 || exit 1 - [[ -f "$3" ]] || "$1/workflow-state" --state-dir "$2" init "$4" --worktree "$5" >/dev/null - ) 200>"$3.lock" - ' _ "$SCRIPTS" "$sd" "$file" "$ITEM" "$ROOT" 2>"$WORK_DIR/model.err"; then + if ! model_item_state "$file" "$sd"; then hand_over "$notice$NL$(cat -- "$WORK_DIR/model.err")" return 0 fi @@ -2936,6 +3009,28 @@ model_unavailable_notice() { # CAUSE OBSERVED [DEPENDENCY_DETAIL] hand_over "$notice" } +# The SSH prefix owns the native PID record. A callback must still descend +# from that live predecessor; a delayed callback must not stop a replacement. +model_native_record() { + local file record launch table descendants hops pid current=false + launch=$("$SCRIPTS/lane-marker" --launch-path "$COMMON" "$ITEM") || return 1 + [ -f "$launch" ] && [ ! -L "$launch" ] || return 1 + record=$(cat -- "$launch") || return 1 + "$BASH" -euo pipefail -c '. "$1/lib/lane-state.sh"' _ "$SCRIPTS" || return 1 + . "$SCRIPTS/lib/lane-state.sh" + file=$(jq -er '.identity_file | strings | select(startswith("/"))' <<<"$record") || return 1 + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + lane_identity_read "$file" && lane_identity_status "$LANE_IDENTITY_PID" "$LANE_IDENTITY_START" || return 1 + table=$(lane_process_table) || return 1 + descendants=$(lane_process_below "$table" "$LANE_IDENTITY_PID" '.*' 1 "" pids) || return 1 + while read -r hops pid; do + [ "$pid" != "$$" ] || current=true + done <<<"$descendants" + [ "$current" = true ] || return 1 + jq -c --arg session "$SESSION" --arg pid "$LANE_IDENTITY_PID" --arg start "$LANE_IDENTITY_START" \ + '. + {session_id:$session,launch:{pid:$pid,start:$start}}' <<<"$record" +} + model_lane_association() { # RECORD SERVER jq -r --arg session "$SESSION" --arg pane "${TMUX_PANE:-}" --arg server "$2" ' if $session == "" then "unbound" @@ -3015,24 +3110,38 @@ handoff_check() { if [ "$ARM" = model-switch ]; then [ "$HARNESS" = claude ] || return 0 - local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery + local fleet record launched observed launch_id observed_id outcome cause="" report result handoff sd handoff_sd server="" identity association record_path body recovery native=false file observed=$(jq -er '.to_model | strings | select(length > 0)' <<<"$INPUT" 2>/dev/null) || observed="" fleet=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path oversee 2>"$WORK_DIR/model.err") || { model_unavailable_notice state-unread "$observed"; return 0; } sd=${fleet%/*} - record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { - model_unavailable_notice launch-unread "$observed"; return 0; - } - if [ "$ROLE" = overseer ]; then - record_path='["overseer"]' + if [ "$ROLE" != overseer ] && [ ! -e "$fleet" ]; then + native=true + record=$(model_native_record 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + file=$(cd -- "$ROOT" && "$SCRIPTS/workflow-state" path "$ITEM" 2>"$WORK_DIR/model.err") || { + model_unavailable_notice state-unread "$observed"; return 0; + } + sd=${file%/*} + model_item_state "$file" "$sd" || { + model_unavailable_notice state-unread "$observed"; return 0; + } else - record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 - fi - record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 - if [ "$record" = null ] || [ -z "$record" ]; then - model_unavailable_notice launch-missing "$observed" - return 0 + record=$("$SCRIPTS/workflow-state" --state-dir "$sd" get oversee 2>"$WORK_DIR/model.err") || { + model_unavailable_notice launch-unread "$observed"; return 0; + } + if [ "$ROLE" = overseer ]; then + record_path='["overseer"]' + else + record_path=$(jq -c --arg item "$ITEM" '["lanes", ((.lanes // []) | map(.item) | index($item))]' <<<"$record") || return 0 + fi + record=$(jq -c --argjson path "$record_path" 'if $path[-1] == null then null else getpath($path) end' <<<"$record") || return 0 + if [ "$record" = null ] || [ -z "$record" ]; then + model_unavailable_notice launch-missing "$observed" + return 0 + fi fi # A delayed event from a replaced session never acts on the replacement. if [ -n "${TMUX_PANE:-}" ]; then @@ -3084,27 +3193,42 @@ handoff_check() { fi outcome=switched [ -z "$cause" ] || outcome=unavailable - report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' - getpath($path) as $current - | if ($role == "overseer" and ($current | ol_model_launch) != $identity) - or $current.session_id != $record.session_id or $current.model != $record.model - or $current.launch != $record.launch or $current.launched_at != $record.launched_at - then {state: ., report: {result:"stale"}} - elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity - and ($current.model_check.outcome == "switched" - or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) - then {state: ., report: {result:"duplicate"}} - else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at - | {outcome:$outcome, session_id:$session, launch_identity:$identity, - launched:$launched, observed:$observed, at:$at, cause:$cause} as $check - | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) - | setpath($path + ["model_check"]; $check) - | {state: ., report: {result:"acquired", record:getpath($path)}} - end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ - --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + if [ "$native" = true ]; then + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report "$ITEM" ' + if .model_checks[$session].launch_identity == $identity + and (.model_checks[$session].outcome == "switched" + or ($outcome == "unavailable" and .model_checks[$session].outcome == "unavailable")) + then {state:.,report:{result:"duplicate"}} + else .model_checks[$session] = {outcome:$outcome,session_id:$session,launch_identity:$identity, + launched:$launched,observed:$observed,cause:$cause,at:(now|strftime("%Y-%m-%dT%H:%M:%SZ"))} + | {state:.,report:{result:"acquired"}} end' --arg session "$SESSION" --argjson identity "$identity" \ + --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" \ 2>"$WORK_DIR/model.err") || { - model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; - } + model_unavailable_notice record-unwritten "$observed"; return 0; + } + else + report=$("$SCRIPTS/workflow-state" --state-dir "$sd" update-report oversee "$OL_JQ_DEFS"' + getpath($path) as $current + | if ($role == "overseer" and ($current | ol_model_launch) != $identity) + or $current.session_id != $record.session_id or $current.model != $record.model + or $current.launch != $record.launch or $current.launched_at != $record.launched_at + then {state: ., report: {result:"stale"}} + elif $current.model_check.session_id == $session and $current.model_check.launch_identity == $identity + and ($current.model_check.outcome == "switched" + or ($outcome == "unavailable" and $current.model_check.outcome == "unavailable")) + then {state: ., report: {result:"duplicate"}} + else (now | strftime("%Y-%m-%dT%H:%M:%SZ")) as $at + | {outcome:$outcome, session_id:$session, launch_identity:$identity, + launched:$launched, observed:$observed, at:$at, cause:$cause} as $check + | .fleet_log = ((.fleet_log // []) + [$check + {kind:("model-" + $outcome), item:$item, text:("launched=" + $launched + " observed=" + $observed)}]) + | setpath($path + ["model_check"]; $check) + | {state: ., report: {result:"acquired", record:getpath($path)}} + end' --arg item "$ITEM" --arg session "$SESSION" --argjson record "$record" \ + --argjson path "$record_path" --arg role "$ROLE" --arg outcome "$outcome" --arg launched "$launched" --arg observed "$observed" --arg cause "$cause" --argjson identity "$identity" \ + 2>"$WORK_DIR/model.err") || { + model_unavailable_notice record-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")"; return 0; + } + fi result=$(jq -r '.result' <<<"$report") || return 0 [ "$result" = acquired ] || return 0 if [ "$outcome" = unavailable ]; then @@ -3144,17 +3268,41 @@ handoff_check() { [ -z "$HANDOFF_FILE" ] || handoff_sd=${HANDOFF_FILE%/*} claude_attached_agents emergency "$handoff_sd/workflow-state-$ITEM.json" handoff=$(jq -nc --arg branch "$BRANCH" --arg worktree "$ROOT" --arg agents "$AGENT_ROWS" \ - --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --argjson prior "${HANDOFF_RECORD:-null}" \ + --arg session "$SESSION" --arg pane "${CALLER_KEY:-}" --arg model "$launched" --argjson prior "${HANDOFF_RECORD:-null}" \ '($prior // {merged:[],remaining:["Resume durable workflow state after model recovery."], branch:$branch,worktree:$worktree,open_pr:null}) | .traps = (((.traps // []) + [$agents]) | unique) - | .session_id=$session | .pane_key=$pane | .cause="model-switched"') || return 0 + | del(.stop_failure) + | .session_id=$session | .pane_key=$pane | .cause="model-switched" | .model=$model') || return 0 # Preserve existing workflow state, including unpushed round work. { [ -f "$handoff_sd/workflow-state-$ITEM.json" ] || "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" init "$ITEM" --worktree "$ROOT" >/dev/null 2>"$WORK_DIR/model.err"; } && "$SCRIPTS/workflow-state" --state-dir "$handoff_sd" set "$ITEM" handoff "$handoff" >/dev/null 2>>"$WORK_DIR/model.err" || - model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + { + if [ "$native" = true ]; then + # The acquired switch check already occupies the notice slot. + hand_over "lane-mail-check: model-unavailable=handoff-unwritten$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + model_unavailable_notice handoff-unwritten "$observed" "$(cat -- "$WORK_DIR/model.err")" + } # Recovery owns the stop and runs outside this hook's process group. + if [ "$native" = true ]; then + # The controller reads this handoff through its existing host state + # transport and relaunches there. This host only stops its predecessor. + if ! lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ + "$BASH" -euo pipefail -c ' + . "$1/lib/lane-state.sh" + . "$1/lib/lane-relaunch.sh" + lane_native_model_stop "$2" "$3" "$1" "$4" "$5" + ' _ "$SCRIPTS" "$record" "$handoff_sd" "$ITEM" "$SESSION" 2>"$WORK_DIR/model.err"; then + # The switched check already occupies the once-only unavailable slot. + hand_over "lane-mail-check: model-unavailable=recovery-unstarted$NL$(cat -- "$WORK_DIR/model.err")" + return 0 + fi + message model-switched "$launched observed=$observed" + return 0 + fi lane_run_detached "$sd/model-recovery-$ITEM.log" "$sd/model-recovery-$ITEM.log" \ "$BASH" "$SCRIPTS/lib/lane-relaunch.sh" model-recovery "$record" "$sd" "$SESSION" message model-switched "$launched observed=$observed"