Skip to content

feat(bin): beat optional agent presence from crewmate turn boundaries - #34

Merged
SchokoShake merged 9 commits into
mainfrom
fm/presence-firstmate
Sep 23, 2026
Merged

SchokoShake merged 9 commits into
mainfrom
fm/presence-firstmate

Conversation

@SchokoShake

@SchokoShake SchokoShake commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Intent

PR 3 of the agent-presence design (data/agent-presence-bridge-scout/report.md section 8): the firstmate crew beat. Siblings, both now MERGED: bridge-axi PR 9 (contract 2.10 presence, docs/presence.md, bin/agent-presence.mjs) and logbook PR 65 (the board half).

GOAL. In bin/fm-spawn.sh, add ONE presence command to each harness's EXISTING per-worker turn-boundary wiring - the claude hooks, the opencode plugin, the pi extension, the codex notify, and the grok and kimi global hook bodies - so a crewmate or scout announces itself to the captain's board through bridge-axi's hook-callable agent-presence CLI. Add a presence row per adapter in .agents/skills/harness-adapters/SKILL.md including the muse and cursor limits, and coverage in the existing spawn-wiring tests.

HARD CONSTRAINTS, all deliberate:

  • The command calls agent-presence ONLY if it is on PATH and is a silent no-op otherwise. This is an OPTIONAL dependency firstmate is allowed to lose; bridge-axi must go on depending on nothing.
  • Presence must never block or slow an agent's turn. Every failure is silent and exits 0, and nothing leaves this machine. Output is discarded and status swallowed, matching the || true discipline the busy-state commands beside it already use.
  • Crewmates and scouts ONLY - not secondmates and not the firstmate primary.
  • The consumer identity and the subject come from the WORKER's own session/branch/PR state (report section 4). Firstmate passes nothing about the work at runtime, so no presence field is text firstmate typed. In particular firstmate deliberately does NOT set AGENT_PRESENCE_SUBJECT_KIND/_SUBJECT_REF even though the CLI offers that supervisor path, because a firstmate task id is not a board item id.
  • cursor ONLY if the report's verification condition is met (that its stop event fires for a pane crewmate); it is NOT met - cursor-agent is not installed on this machine - so cursor is documented as not covered, with the exact condition that would unblock it. muse not at all: its plugin engine is the only hook surface and it is off in the default build.

CAPTAIN DECISIONS (data/agent-presence-bridge-scout/captain-decisions.md), binding and superseding the report where they differ: no note or activity text in v1; TTL 900s with the age shown on the card; NO user-level hook in ~/.claude/settings.json and no install subcommand, so presence covers only firstmate's own spawned workers; subject kinds pr+item+branch in v1 with path accepted by the contract but fleet-strip only, never on a card; the Underway strip and the presence chip stay separate and clearly labelled.

LATER ACCEPTED SUPERSESSIONS:

  1. bridge-axi PR 9 merged mid-task; I checked my side against the merged CLI and aligned it. The CLI takes 'beat --state working|waiting' and 'end' - NOT a positional state. I had pinned a positional; verified against the real merged binary that 'beat working' exits 2 as outcome usage while the --state forms exit 0, which under the silent wrapper would have been an invisible total failure. Per-adapter states match the merged docs/presence.md matrix exactly.
  2. The captain then asked me to verify against merged logbook PR 65 whether a BRANCH-keyed row (firstmate crews are branch-keyed, never pr-keyed, because nothing on the crew path may run 'agent-presence subject') actually lights the chip on that task's card, and to add cached subject resolution as a fallback only if it does not. I ran the merged board's own presenceIndex/cardBranchKeys/presenceForItems against the live board body with the exact ref the CLI resolves: 12 of 12 cards with a same-id workstream row lit, 0 dark, and the firstmate repo's own blank-origin case also lights via the bare-name key. So NO fallback was added and the wiring is unchanged; the verified finding is recorded in the skill.

DESIGN DECISIONS A REVIEWER WOULD NOT SEE IN THE DIFF:

  • The rendered command deliberately contains no '&' - if/then/fi rather than &&, and 2>/dev/null rather than 2>&1. codex's beat reaches its launch command through a bash ${LAUNCH//PRESENCEWAITING/...} replacement, and bash 5.2's patsub_replacement expands an unescaped & in a replacement to the matched text, which silently rewrote the command into nonsense; the escape that fixes that on 5.2 inserts a literal backslash on 5.1 and older. Emitting no & is correct on every bash. This was found by probing the real generated launch command, not theorised.
  • pi beats only on its two SEMANTIC edges (agent_start, confirmed agent_settled) and never on turn_end, because an inner turn boundary is not a run boundary and beating there would flip a settled worker back to working.
  • opencode and pi never beat 'end' (no shutdown event) so their rows retire on the board TTL; codex, grok and kimi have no turn-start event so they beat 'waiting' only. Only claude reaches all four states.
  • The grok and kimi global hooks are shared by every task, so they beat in a subshell cd'd to the worktree their registry token already authorised, and nothing task-specific is passed to them.
  • presence_cmd takes a closed vocabulary (working|waiting|end) and refuses anything else, so the unquoted interpolation into hook bodies can never see a value needing quoting.

NECESSARY ACCOMPANYING CHANGES:

  • bin/fm-kimi-turnend-hook.sh: the global hook BODY changes, which is its first ever version bump, so install and remove now both key ownership off the Firstmate hook prefix rather than exact bytes. install already tolerated that prefix; remove did not, and would have refused to excise a hook written by an older firstmate until a spawn happened to rewrite it. Its --help line range was extended to cover the new header paragraph.
  • bin/fm-test-run.sh: tests/fm-busy-adapter-wiring.test.sh was unclassified, so a bin/fm-spawn.sh change never selected the suite covering its own generated wiring; it is now pure-contract-unit alongside the sibling grok and kimi harness suites.
  • tests/lib.sh gained fm_fake_agent_presence and fm_assert_presence_beats so the three adapter suites share one recorder instead of triplicating it.

TESTING APPROACH: the tests drive the GENERATED artifacts (the claude hook commands through sh, the opencode plugin and pi extension in a plain Node host, the codex notify program recovered from the launch command fm-spawn actually sent and then executed, the grok and kimi global hooks with real payloads) rather than asserting implementation source. The not-installed and failing-CLI cases are asserted through properties that hold either way - status zero, empty stdout, unchanged busy record - so a machine that happens to have the real CLI installed cannot turn them into false failures. Mutation-checked: removing the feature drops the suite from 13 passing to 9.

This is firstmate's own shared tracked material, so the firstmate-coding-guidelines skill applies: one sentence per line in tracked markdown, plain dash never em dash, no agent co-author trailer, bin/fm-lint.sh clean, and tests colocated by extending the existing suites rather than inventing a runner.

What Changed

  • bin/fm-spawn.sh renders one optional agent-presence beat into each adapter's existing per-worker turn-boundary wiring (claude's four lifecycle hooks, the OpenCode plugin, the pi extension, the codex notify launch program, and the grok global Stop hook) for crewmates and scouts only: it runs only when the CLI is on PATH, discards output, swallows status, and passes nothing about the work, so the subject and consumer identity are resolved by the CLI from the worker's own session and worktree. States differ per adapter, with claude reaching working/waiting/end, opencode and pi reaching working/waiting, and codex, grok and kimi beating waiting only.
  • bin/fm-kimi-turnend-hook.sh adds the same waiting beat to its global hook body in a subshell cd'd to the registry-authorised workspace, and both install and remove now key hook ownership off the Firstmate hook prefix instead of exact bytes, so a body change can be upgraded and excised rather than refused; its --help line range was extended to cover the new header paragraph.
  • .agents/skills/harness-adapters/SKILL.md gains a presence section plus a per-adapter row, recording cursor as not covered (with the live stop-event condition that would unblock it), muse as having no hook surface at all, and the measured finding that a branch-keyed row does light its card. tests/lib.sh adds shared fm_fake_agent_presence, fm_assert_presence_beats and fm_presence_absent_path helpers, the three adapter suites drive the generated artifacts for installed, not-installed and failing-CLI cases, and bin/fm-test-run.sh classifies fm-busy-adapter-wiring.test.sh as pure-contract-unit so a spawn change selects the suite covering its own wiring.

Risk Assessment

✅ Low: The change is a strict, additive turn-boundary command guarded by a PATH probe with discarded output and swallowed status, scoped to non-secondmate spawns and to registry-authorised worktrees for the two global hooks, with behaviour-driven coverage that drives the real generated artifacts; I traced every adapter's state sequence and the codex quoting round-trip and found no path that yields a wrong state, a beat for an unlatched session, or a hook that can fail.

Testing

I stood the product up the way an operator runs it - the real bin/fm-spawn.sh into isolated firstmate homes with a fake pane - and then fired each adapter's own generated turn-boundary artifact with a recording stand-in on PATH in place of bridge-axi's agent-presence CLI, so every result below is what the captain's board would actually have been told. Claude reaches all four states (working, waiting on both turn ends, end at session end), a scout is covered the same way, codex beats waiting from the notify program recovered out of the launch command fm-spawn really sent to the pane, opencode and pi beat only on their latched/semantic edges and never on an inner turn boundary, and the grok and kimi global hooks beat waiting from inside the worktree their registry token authorised. The three boundaries the intent names all held under active attack: on a hermetic PATH with no resolvable agent-presence every hook still exited 0 with empty stdout and an unchanged busy record; an agent-presence exiting 3 and shouting on both streams changed neither hook status nor stdout nor state; a forged grok token and a tokenless kimi session produced no beat at all. Secondmates, cursor and muse install no wiring and carry no beat in any artifact or launch command, no spawn wrote a user-level ~/.claude/settings.json, and no unsubstituted PRESENCEWAITING placeholder reached anything - with patsub_replacement confirmed on for the bash that ran the codex substitution. The three suites pass on the real tree and all three fail once the beat is mutated out, so they observe behavior rather than source text, and a bin/fm-spawn.sh-only edit now selects the spawn-wiring suite where the base runner did not select it. There is no screenshot or rendered-UI artifact because this change has no graphical surface: the end-user surface is generated shell hooks, a JS plugin, a TS extension and a launch command, so the reviewer-visible evidence is the CLI transcripts showing those artifacts and the beats they produced. The one scenario I could not drive is the real bridge-axi binary accepting the rendered argv, since agent-presence is on neither PATH nor any repository-local path here.

  • Live validation: ✅ go - 17 of 18 scenarios driven live against the product
Scenario Result Live Evidence
A claude crewmate's four real hooks announce working at turn start, waiting at both turn ends, and end at session end ✅ pass live presence-crew-transcript.txt (generated .claude/settings.local.json fired through sh, beats recorded in order) and bash tests/fm-busy-adapter-wiring.test.sh
A scout, not just a ship crewmate, is announced to the board ✅ pass live presence-crew-transcript.txt SCOUT section: a real --scout spawn's hooks produced working, waiting and end
A codex crewmate beats waiting from the notify program that actually rode the launch command to the pane ✅ pass live presence-crew-transcript.txt CODEX section (launch command shown verbatim, notify program recovered and executed, marker written, one waiting beat) plus patsub-hazard.txt showing patsub_replacement wa…
An OpenCode crewmate beats only for its own latched session, never for a child or foreign session ✅ pass live presence-crew-transcript.txt OPENCODE section: worker busy/child busy/child idle/worker idle yields exactly working then waiting; a foreign session's idle yields no waiting beat
A pi crewmate beats on its two semantic edges only and never on an inner turn boundary ✅ pass live presence-crew-transcript.txt PI section: agent-start beats working, turn-end and a continuing settle beat nothing, the confirmed settle beats waiting
A grok crewmate beats waiting from the shared global hook, running inside its own authorised worktree ✅ pass live presence-globalhooks-transcript.txt GROK section: hook body shown, exit 0, silent, marker written, one waiting beat, recorded pwd equals the task worktree
A kimi crewmate beats waiting from the shared global hook, running inside its own authorised worktree ✅ pass live presence-globalhooks-transcript.txt KIMI section: real Stop payload, exit 0, silent, marker written, one waiting beat, recorded pwd equals the task worktree
Adversarial: with bridge-axi not installed, every hook stays silent, exits zero and leaves firstmate's own busy record unchanged ✅ pass live presence-crew-transcript.txt ADVERSARIAL 1 on a hermetic PATH with no resolvable agent-presence, plus the codex, grok and kimi not-installed cases in the three suites
Adversarial: an installed agent-presence that fails and writes to both streams cannot fail a hook or reach hook stdout ✅ pass live presence-crew-transcript.txt ADVERSARIAL 2: all four hooks exit 0 with empty stdout, the failing CLI was still invoked 4 times, busy record unchanged
Adversarial: a workspace the registry does not authorise produces no beat at all ✅ pass live presence-globalhooks-transcript.txt: grok fired with a forged token and kimi fired for a tokenless workspace both exit 0 silently with an empty beat log
Adversarial: a secondmate and the firstmate primary never appear on the crew presence board ✅ pass live presence-globalhooks-transcript.txt SECONDMATE section: a real seeded secondmate spawn writes no hook settings, no plugin and no grok hook, and its launch command contains no agent-presence; presence-…
Adversarial: cursor and muse stay uncovered and acquire no beat by accident ✅ pass live presence-uncovered-transcript.txt: both real spawns write only a pull-source sidecar, zero turn-boundary wiring, zero agent-presence in any artifact or launch command
Adversarial: no unsubstituted PRESENCEWAITING placeholder survives into any generated artifact or launch command ✅ pass live presence-crew-transcript.txt final section: zero files carrying a raw __PRESENCE token across every harness spawned
The kimi global hook's version bump: install upgrades a body written by an older firstmate, remove excises it, and a hook firstmate does not own is still refused ✅ pass live presence-globalhooks-transcript.txt VERSION BUMP and foreign-hook sections (3-line older body upgraded to 23 lines with the beat, removal restores config.toml byte-for-byte, foreign hook refused by bo…
fm-kimi-turnend-hook.sh --help renders the new presence paragraph without truncating the usage lines ✅ pass live kimi-hook-help.txt
A developer editing only bin/fm-spawn.sh now gets the suite that covers its own generated wiring ✅ pass live test-selection-changed.txt: the base runner selects 49 suites and omits fm-busy-adapter-wiring.test.sh; the target runner selects 50 and includes it
The presence tests observe the beat rather than the source: removing the feature fails all three suites ✅ pass live mutation-check.txt: with presence_cmd rendering a no-op and the OpenCode/pi/kimi copies deleted, each suite fails at a presence assertion ("presence beats were '', expected ...")
The real bridge-axi agent-presence binary accepts the rendered argv (beat --state working|waiting, end) ⏸️ untested no The agent-presence CLI from bridge-axi is not on PATH on this machine and no repository-local path was supplied, so the downstream binary's own argv parsing could not be exercised here. To cover it, i…
Evidence: Beat matrix and adversarial probes, as observed by driving the product

Source: Beat matrix and adversarial probes, as observed by driving the product

# Crew presence beat, as observed by driving the real generated wiring

Every row below was produced by running the real `bin/fm-spawn.sh` into an isolated
firstmate home, then firing that adapter's own turn-boundary artifact with a recording
stand-in on PATH in place of bridge-axi's `agent-presence` CLI. "Board saw" is the
literal argv the artifact invoked.

| Adapter | Artifact driven | Board saw | Matches SKILL.md row |
|---|---|---|---|
| claude crewmate | `.claude/settings.local.json` hooks through `sh` | `beat --state working`, `beat --state waiting` (Stop), `beat --state waiting` (StopFailure), `end` (SessionEnd) | yes, all four states |
| claude scout | same, after a `--scout` spawn | `beat --state working`, `beat --state waiting`, `end` | yes, scouts are covered |
| codex crewmate | notify program recovered from the launch command fm-spawn sent to the pane | `beat --state waiting` only | yes, turn-end only |
| opencode crewmate | generated plugin in a plain Node host | `beat --state working`, `beat --state waiting`, latched session only | yes, never `end` |
| pi crewmate | generated extension in a plain Node host | `beat --state working` (agent_start), `beat --state waiting` (confirmed settle); nothing on turn_end or a continuing settle | yes, semantic edges only |
| grok crewmate | global `~/.grok/hooks/fm-turn-end.sh` with a real workspace root | `beat --state waiting`, run in the task worktree | yes, turn-end only, cd'd to the authorised worktree |
| kimi crewmate | global `~/.kimi-code/fm-turn-end.sh` with a real Stop payload | `beat --state waiting`, run in the task worktree | yes, turn-end only, cd'd to the authorised worktree |
| cursor crewmate | real spawn, every generated artifact inspected | nothing: no hook, no beat, only a pull-source sidecar | yes, NOT COVERED |
| muse crewmate | real spawn, every generated artifact inspected | nothing: no hook, no beat, only a pull-source sidecar | yes, NONE |
| secondmate | real seeded secondmate spawn | nothing: no hook settings, no plugin, nothing in the launch command | yes, crewmates and scouts only |

## Boundaries driven adversarially

| Probe | Result |
|---|---|
| bridge-axi not installed (hermetic PATH with no resolvable `agent-presence`) | every hook exit 0, stdout empty, firstmate's own busy record unchanged |
| `agent-presence` installed but exiting 3 and noisy on stdout and stderr | every hook exit 0, stdout empty, busy record unchanged; the CLI was still invoked 4 times |
| grok hook fired for a workspace with a forged, unregistered token | exit 0, silent, no beat at all |
| kimi hook fired for a workspace with no firstmate token | exit 0, silent, no beat at all |
| a foreign opencode session going idle | no `waiting` beat for this worker |
| pi `turn_end` (an inner turn boundary, not a run boundary) | no beat, so a settled worker is never flipped back to working |
| a raw `__PRESENCEWAITING__` placeholder surviving into any artifact | zero occurrences across every spawn |
| a user-level `~/.claude/settings.json` written by any spawn | zero, as the captain decision requires |
| feature removed (mutation) | all three suites fail; they observe the beat, not the source text |
Evidence: Crewmate transcript: generated claude hooks, codex launch command, opencode/pi drives, scout, adversarial not-installed and failing-CLI cases

Source: Crewmate transcript: generated claude hooks, codex launch command, opencode/pi drives, scout, adversarial not-installed and failing-CLI cases


========== CLAUDE CREWMATE: real spawn, real hooks, four presence states ==========
spawn rc=0
warning: /tmp/fm-presence-drive.YosP8r/claude-crew/home/data/presence-claude-1/brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
spawned presence-claude-1 harness=claude kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-claude-1 worktree=/tmp/fm-presence-drive.YosP8r/claude-crew/wt

-- generated settings.local.json (the artifact Claude Code itself reads)
UserPromptSubmit:
  '~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4/bin/fm-busy-event.sh' apply '/tmp/fm-presence-drive.YosP8r/claude-crew/home/state' 'presence-claude-1' busy --gen 'g1790180744.3487669.28086' --source claude-hook --event user-prompt-submit 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state working >/dev/null 2>/dev/null || true; fi
Stop:
  touch '/tmp/fm-presence-drive.YosP8r/claude-crew/home/state/presence-claude-1.turn-ended'; '~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4/bin/fm-busy-event.sh' apply '/tmp/fm-presence-drive.YosP8r/claude-crew/home/state' 'presence-claude-1' idle --gen 'g1790180744.3487669.28086' --source claude-hook --event stop 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi
StopFailure:
  '~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4/bin/fm-busy-event.sh' apply '/tmp/fm-presence-drive.YosP8r/claude-crew/home/state' 'presence-claude-1' idle --gen 'g1790180744.3487669.28086' --source claude-hook --event stop-failure 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi
SessionEnd:
  '~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4/bin/fm-busy-event.sh' apply '/tmp/fm-presence-drive.YosP8r/claude-crew/home/state' 'presence-claude-1' idle --gen 'g1790180744.3487669.28086' --source claude-hook --event session-end 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence end >/dev/null 2>/dev/null || true; fi

-- firing each hook exactly as Claude Code would
  UserPromptSubmit  exit=0 stdout=''
  Stop              exit=0 stdout=''
  StopFailure       exit=0 stdout=''
  SessionEnd        exit=0 stdout=''
board saw:
    agent-presence beat --state working
    agent-presence beat --state waiting
    agent-presence beat --state waiting
    agent-presence end
firstmate busy record after the beats: idle claude-hook

-- ADVERSARIAL 1: bridge-axi not installed (hermetic PATH, no agent-presence resolvable)
  UserPromptSubmit  exit=0 stdout=''
  Stop              exit=0 stdout=''
  StopFailure       exit=0 stdout=''
  SessionEnd        exit=0 stdout=''
firstmate busy record: idle claude-hook

-- ADVERSARIAL 2: agent-presence installed but failing (exit 3) and noisy on both streams
  UserPromptSubmit  exit=0 stdout=''
  Stop              exit=0 stdout=''
  StopFailure       exit=0 stdout=''
  SessionEnd        exit=0 stdout=''
the failing CLI was still called 4 times; firstmate busy record: idle claude-hook

========== CODEX CREWMATE: the beat rides the launch command through the pane ==========
spawn rc=0
spawned presence-codex-1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-codex-1 worktree=/tmp/fm-presence-drive.YosP8r/codex-crew/wt

-- launch command fm-spawn actually sent to the pane (send-keys), verbatim
codex --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '/tmp/fm-presence-drive.YosP8r/codex-crew/home/state/presence-cod
ex-1.turn-ended'; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi\"]"
 "$('~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-presence-d
rive.YosP8r/codex-crew/home/data/presence-codex-1/brief.md')"

-- notify program recovered from that launch command and executed
  touch '/tmp/fm-presence-drive.YosP8r/codex-crew/home/state/presence-codex-1.turn-ended'; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi
  exit=0 stdout='' turn-end marker written=yes
board saw:
    agent-presence beat --state waiting

========== OPENCODE CREWMATE: generated plugin driven in a plain Node host ==========
spawn rc=0
spawned presence-oc-1 harness=opencode kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-oc-1 worktree=/tmp/fm-presence-drive.YosP8r/oc-crew/wt

-- worker session busy -> child session busy/idle -> worker session idle
board saw:
    agent-presence beat --state working
    agent-presence beat --state waiting

-- ADVERSARIAL: a foreign session's idle must not speak for this worker
board saw:
    agent-presence beat --state working

========== PI CREWMATE: generated extension driven in a plain Node host ==========
spawn rc=0
spawned presence-pi-1 harness=pi kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-pi-1 worktree=/tmp/fm-presence-drive.YosP8r/pi-crew/wt
  after agent-start       beats so far: beat --state working|
  after turn-end          beats so far: beat --state working|
  after settle-continuing beats so far: beat --state working|
  after settle-idle       beats so far: beat --state working|beat --state waiting|
board saw:
    agent-presence beat --state working
    agent-presence beat --state waiting

========== SCOUT: a scout is a covered worker (crewmates AND scouts) ==========
spawn rc=0
spawned presence-scout-1 harness=claude kind=scout window=firstmate:fm-presence-scout-1 worktree=/tmp/fm-presence-drive.YosP8r/claude-scout/wt
board saw:
    agent-presence beat --state working
    agent-presence beat --state waiting
    agent-presence end

========== NO USER-LEVEL HOOK, AND NO UNSUBSTITUTED PLACEHOLDER ANYWHERE ==========

-- every spawn above ran with its own HOME; a user-level ~/.claude/settings.json would be a captain-decision violation
  user-level claude settings written by any spawn: 0

-- search every generated artifact and every launch command for an unsubstituted __PRESENCE placeholder
  files still carrying a raw __PRESENCE token: 0
Evidence: Global hooks transcript: grok and kimi hook bodies, authorised vs unauthorised workspace, kimi version bump, secondmate

Source: Global hooks transcript: grok and kimi hook bodies, authorised vs unauthorised workspace, kimi version bump, secondmate


========== GROK: one global Stop hook, shared by every task ==========
spawned presence-grok-1 harness=grok kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-grok-1 worktree=/tmp/fm-presence-global.wLIRIf/grok/wt

-- generated global hook body (/tmp/fm-presence-global.wLIRIf/grok/grok-home/hooks/fm-turn-end.sh)
  #!/usr/bin/env bash
  set -u
  auth_dir='/tmp/fm-presence-global.wLIRIf/grok/grok-home/hooks/fm-turn-end.d'
  workspace=${GROK_WORKSPACE_ROOT:-}
  [ -n "$workspace" ] || exit 0
  p="$workspace/.fm-grok-turnend"
  [ -f "$p" ] || exit 0
  first=
  IFS= read -r -n 256 first < "$p" 2>/dev/null || [ -n "$first" ] || exit 0
  case "$first" in token=*) token=${first#token=} ;; *) exit 0 ;; esac
  case "$token" in fm.????????????) : ;; *) exit 0 ;; esac
  case "$token" in *[!A-Za-z0-9._-]*) exit 0 ;; esac
  t=$(cat "$auth_dir/$token" 2>/dev/null) || exit 0
  case "$t" in /*.turn-ended) : ;; *) exit 0 ;; esac
  touch "$t" 2>/dev/null || true
  ( cd "$workspace" 2>/dev/null || exit 0; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi )
  exit 0

-- authorised workspace (this task's registry token) fires the hook
  exit=0 stdout+stderr='' turn-end marker=written
board saw:
    agent-presence beat --state waiting
  the beat ran in: /tmp/fm-presence-global.wLIRIf/grok/wt
  the task worktree is: /tmp/fm-presence-global.wLIRIf/grok/wt

-- ADVERSARIAL: an UNAUTHORISED workspace (forged pointer, no registry token)
  exit=0 stdout+stderr=''
board saw:
    (nothing)

========== KIMI: the global hook body whose version bump this change required ==========
install: ok

-- the beat line the installer wrote into the global hook
  22:( cd "$workspace" 2>/dev/null || exit 0; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi )

-- a real Kimi Stop payload for the authorised workspace
  exit=0 stdout+stderr='' turn-end marker=written
board saw:
    agent-presence beat --state waiting
  the beat ran in: /tmp/fm-presence-global.wLIRIf/kimi-wt
  the task worktree is: /tmp/fm-presence-global.wLIRIf/kimi-wt

-- ADVERSARIAL: a Kimi session in a workspace with no Firstmate token
  exit=0 stdout+stderr=''
board saw:
    (nothing)

-- VERSION BUMP: a hook body written by an OLDER firstmate (no beat in it)
  hook on disk before upgrade: 3 lines, beat present=0
  install upgraded it: ok
  hook on disk after upgrade:  23 lines, beat present=1
  remove excised an older firstmate hook: ok (hook present=no)
  config.toml restored byte-for-byte: yes

-- ADVERSARIAL: a hook at that path firstmate does NOT own must still be refused
  install rc=1 msg=fm-kimi-turnend-hook: refused: Firstmate hook path has unexpected content at /tmp/fm-presence-global.wLIRIf/kimi-home/.kimi-code/fm-turn-end.sh.
  remove  rc=1 msg=fm-kimi-turnend-hook: refused: Firstmate hook script has unexpected content at /tmp/fm-presence-global.wLIRIf/kimi-home/.kimi-code/fm-turn-end.sh.
  foreign hook left intact: 1

========== SECONDMATE: a direct report that must NEVER appear on the crew presence board ==========
Note: switching to 'c3b3db7ba4987239ae6f4d96af218547e5796197'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

spawned design harness=claude kind=secondmate mode=secondmate yolo=off window=firstmate:fm-design worktree=/tmp/fm-presence-global.wLIRIf/design-home

-- turn-boundary wiring installed in the secondmate's own home
  claude hook settings (.claude/settings.local.json): 0
  opencode plugins / pi extensions / grok hooks: 0

-- every file under the secondmate home that mentions agent-presence at all
  tests/lib.sh
  tests/fm-kimi-harness.test.sh
  tests/fm-grok-harness.test.sh
  tests/fm-busy-adapter-wiring.test.sh
  bin/fm-spawn.sh
  bin/fm-kimi-turnend-hook.sh
  .agents/skills/harness-adapters/SKILL.md
  ... all of which are the firstmate SOURCE tree the seed clones, not generated wiring:
    top-level dir: .agents
    top-level dir: bin
    top-level dir: tests

-- the launch command fm-spawn sent for the secondmate
  send-keys -t firstmate:fm-design export GOTMPDIR=/tmp/fm-design/gotmp Enter
  send-keys -t firstmate:fm-design -l FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_PUBLIC_FOLLOWUP_PRIMARY_HOME='/tmp/fm-presence-global.wLIRIf/mai
  launch command mentions agent-presence: 0
Evidence: Uncovered adapters transcript: cursor and muse spawns install nothing that beats

Source: Uncovered adapters transcript: cursor and muse spawns install nothing that beats


========== CURSOR: presence NOT COVERED until its crewmate stop event is live-verified ==========
spawn rc=0: spawned presence-cursor-1 harness=cursor kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-cursor-1 worktree=/tmp/fm-presence-uncovered.KGR1J6/cursor/wt
-- cursor crewmate
  turn-boundary wiring generated in the worktree: 0
  sidecar the adapter DOES write (a pull-source binding, no writer, no beat):
    presence-cursor-1.cursor-session
  launch command sent to the pane mentions agent-presence: 0
0
  any generated artifact under the worktree or state mentions agent-presence: 0
  launch command:
    send-keys -t firstmate:fm-presence-cursor-1 Enter

========== MUSE: presence NONE, its only hook surface is off in the default build ==========
spawn rc=0: spawned presence-muse-1 harness=muse kind=ship mode=no-mistakes yolo=off window=firstmate:fm-presence-muse-1 worktree=/tmp/fm-presence-uncovered.KGR1J6/muse/wt
-- muse crewmate
  turn-boundary wiring generated in the worktree: 0
  sidecar the adapter DOES write (a pull-source binding, no writer, no beat):
    presence-muse-1.muse-session
  launch command sent to the pane mentions agent-presence: 0
0
  any generated artifact under the worktree or state mentions agent-presence: 0
  launch command:
    send-keys -t firstmate:fm-presence-muse-1 Enter
Evidence: Mutation check: with the beat removed, all three suites fail

Source: Mutation check: with the beat removed, all three suites fail

\### Mutation: the presence beat removed from every adapter (presence_cmd renders a no-op,
\### the OpenCode/pi execFile calls deleted, the kimi hook body's line deleted).
\### Expectation: the suites that claim to cover this feature must now FAIL.

$ bash tests/fm-busy-adapter-wiring.test.sh   (on the MUTATED tree)
ok - pi extension reports agent_start busy, settles idle only via ctx.isIdle(), and keeps turn_end a notification
ok - pi extension awaits agent_settled before the next agent_start without a test delay
ok - pi extension events from a superseded incarnation are rejected as stale
not ok - presence beats were '', expected 'beat --state working'
exit=1

$ bash tests/fm-grok-harness.test.sh   (on the MUTATED tree)
ok - grok global hook requires a firstmate registry token
ok - grok teardown removes pointer and token state
not ok - presence beats were '', expected 'beat --state waiting'
exit=1

$ bash tests/fm-kimi-harness.test.sh   (on the MUTATED tree)
ok - Kimi hook install refuses missing, malformed, and surprising config without writing
ok - Kimi hook ownership keys off the Firstmate prefix for both install and removal
ok - Kimi hook install refuses without jq before any config write
ok - fm-spawn: kimi launches, delivers its brief, and registers a guarded turn-end token
ok - Kimi hook stays silent and inert without a Firstmate registry token
not ok - presence beats were '', expected 'beat --state waiting'
exit=1
Evidence: Test selection before and after: a bin/fm-spawn.sh-only edit now selects the spawn-wiring suite

Source: Test selection before and after: a bin/fm-spawn.sh-only edit now selects the spawn-wiring suite

\### A developer edits ONLY bin/fm-spawn.sh and asks the runner which suites that change needs.
\### fm-busy-adapter-wiring.test.sh is the suite that covers fm-spawn's own generated wiring.

--- with bin/fm-test-run.sh from the base commit ---
exit=0; suites selected: 49
NOT SELECTED   tests/fm-busy-adapter-wiring.test.sh  <-- the spawn-wiring suite would not have run
    tests/fm-grok-harness.test.sh
    tests/fm-kimi-harness.test.sh

--- with bin/fm-test-run.sh from the target commit ---
exit=0; suites selected: 50
SELECTED   tests/fm-busy-adapter-wiring.test.sh
    tests/fm-busy-adapter-wiring.test.sh
    tests/fm-grok-harness.test.sh
    tests/fm-kimi-harness.test.sh
Evidence: patsub_replacement was active, and the rendered beat survives the codex substitution verbatim

Source: patsub_replacement was active, and the rendered beat survives the codex substitution verbatim

$ bash --version | head -1
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)

# The codex beat reaches its launch command through a bash ${LAUNCH//__PRESENCEWAITING__/...}
# replacement. patsub_replacement (bash 5.2+) rewrites an unescaped & in the replacement to
# the matched text, which is why the rendered command must contain no &. Confirm the hazard
# shell option was actually ACTIVE while fm-spawn ran above:
$ shopt patsub_replacement
patsub_replacement  	on

# demonstration of the hazard this rendering avoids, on this same shell:
$ L='x __P__ y'; R='a && b 2>&1'; echo "${L//__P__/$R}"
x a __P____P__ b 2>__P__1 y
  ^ an & in the replacement expands to the matched text (__P__), corrupting the command
$ R='if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi'; echo "${L//__P__/$R}"
x if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi y
  ^ the rendered beat carries no &, so it survives the substitution verbatim
Evidence: fm-kimi-turnend-hook.sh --help renders the new presence paragraph and the usage lines

Source: fm-kimi-turnend-hook.sh --help renders the new presence paragraph and the usage lines

$ bin/fm-kimi-turnend-hook.sh --help
Install or remove Firstmate's guarded Kimi crew turn-end hook.

This command is the sole owner of the text-level edit to
$HOME/.kimi-code/config.toml. It validates the existing TOML but never
serializes it: install adds or replaces one marker-delimited Firstmate region,
and remove excises only that region. Missing, malformed, symlinked, partially
marked, or otherwise surprising config is refused without a config write.

The installed Stop hook always exits 0 and stays silent. It reads cwd from the
hook payload, checks for a .fm-kimi-turnend pointer before registry work, and
touches a task turn-end marker only when the pointer names a Firstmate-created
token in $HOME/.kimi-code/fm-turn-end.d/.

After that marker, and only for an authorised workspace, it also beats the
optional agent presence row for the board (bin/fm-spawn.sh's header owns the
contract). Kimi has no turn-START event, so the hook beats "waiting" only. The
beat runs in a subshell cd'd to the authorised workspace because the CLI
resolves its subject from the worker's own worktree, and it stays inside the
hook's silent, always-zero discipline: an uninstalled `agent-presence` is a
no-op and a failing one cannot change the hook's status.

Usage:
  fm-kimi-turnend-hook.sh install
  fm-kimi-turnend-hook.sh remove
exit=0
Evidence: Suite output: tests/fm-busy-adapter-wiring.test.sh

Source: Suite output: tests/fm-busy-adapter-wiring.test.sh

ok - pi extension reports agent_start busy, settles idle only via ctx.isIdle(), and keeps turn_end a notification
ok - pi extension awaits agent_settled before the next agent_start without a test delay
ok - pi extension events from a superseded incarnation are rejected as stale
ok - pi extension beats working on agent_start and waiting on a confirmed settle, never on turn_end
ok - kimi and grok install no unverified semantic wiring and classify through their own gates
ok - opencode plugin classifies from session.status, scoped to the latched worker session
ok - opencode plugin beats working and waiting only for the latched worker session
ok - claude hooks open on UserPromptSubmit and close on Stop, StopFailure, and SessionEnd
ok - claude hook events from a superseded incarnation are rejected without breaking the hook
ok - claude hooks beat working at turn start, waiting at both turn ends, and end at session end
ok - claude presence beats are optional, silent, and cannot fail or pollute a hook
ok - the Kimi hook's own beat is the same command presence_cmd renders for claude, grok and codex
ok - codex classifies unknown until a semantic source is verified, never idle or footer-matched
ok - codex notify touches the turn-end marker and beats waiting, with or without the CLI
all fm-busy-adapter-wiring tests passed
Evidence: Suite output: tests/fm-grok-harness.test.sh

Source: Suite output: tests/fm-grok-harness.test.sh

ok - grok global hook requires a firstmate registry token
ok - grok teardown removes pointer and token state
ok - grok global hook beats waiting in the authorised worktree only, and stays silent and zero
ok - fm-lock recognizes grok harness processes
Evidence: Suite output: tests/fm-kimi-harness.test.sh

Source: Suite output: tests/fm-kimi-harness.test.sh

ok - Kimi hook install is idempotent and removal restores every foreign config byte
ok - Kimi hook removal preserves owned newline boundaries and pristine bytes
ok - Kimi hook install refuses missing, malformed, and surprising config without writing
ok - Kimi hook ownership keys off the Firstmate prefix for both install and removal
ok - Kimi hook install refuses without jq before any config write
ok - fm-spawn: kimi launches, delivers its brief, and registers a guarded turn-end token
ok - Kimi hook stays silent and inert without a Firstmate registry token
ok - Kimi global hook beats waiting in the authorised worktree only, and stays silent and zero
ok - fm-spawn: unsafe Kimi global config refuses before pane creation
ok - fm-teardown: Kimi task pointer and registry token are removed
ok - fm-spawn: Kimi fallback expands the active HOME
ok - fm-spawn: missing Kimi executable refuses before pane creation
ok - fm-spawn: kimi treats a silent pointer drop as a failed spawn
ok - fm-spawn: kimi never sends the brief pointer before an observable ready signal
ok - fm-harness: markerless kimi is detected by ancestry after env-marker precedence
lock acquired: harness pid 3360029
ok - fm-lock recognizes Kimi ancestry and live lock holders
ok - busy detection: real Kimi moon-plus-middot captures require its harness while idle labels stay idle
ok - fm-watch classifies Kimi as unknown rather than from its spinner, and Grok's fallback stays isolated
ok - composer classifier: kimi's existing bordered > shape is already safe without an override
Evidence: Driver script: global hooks and secondmate

Source: Driver script: global hooks and secondmate

#!/usr/bin/env bash
# Manual product driver, part 2: the two GLOBAL turn-end hooks (grok, kimi),
# which are shared by every task, and the crewmates-and-scouts-only boundary.
set -u
ROOT=~/.no-mistakes/worktrees/5f97ed91bec6/01M37BVYP4KRZEJ5GFH74YYCV4
# shellcheck source=/dev/null
. "$ROOT/tests/secondmate-helpers.sh"
SPAWN="$ROOT/bin/fm-spawn.sh"
TMP_ROOT=$(fm_test_tmproot fm-presence-global)

hr() { printf '\n========== %s ==========\n' "$*"; }
sub() { printf '\n-- %s\n' "$*"; }
show_beats() {
  printf 'board saw:\n'
  if [ -s "$1" ]; then sed 's/^/    agent-presence /' "$1"; else printf '    (nothing)\n'; fi
}

##############################################################################
hr "GROK: one global Stop hook, shared by every task"
CASE="$TMP_ROOT/grok"
HOME_DIR="$CASE/home"; PROJ="$CASE/project"; WT="$CASE/wt"; GHOME="$CASE/grok-home"
ID=presence-grok-1
FAKEBIN=$(fm_fakebin "$CASE/fake")
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
case "${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
exit 0
SH
chmod +x "$FAKEBIN/tmux"
fm_fake_exit0 "$FAKEBIN" gh-axi gh
fm_fake_treehouse "$FAKEBIN"
mkdir -p "$HOME_DIR/data/$ID" "$HOME_DIR/projects" "$HOME_DIR/state" "$HOME_DIR/config" "$GHOME"
printf 'brief\n' > "$HOME_DIR/data/$ID/brief.md"
fm_git_worktree "$PROJ" "$WT" "fm/$ID"
touch "$HOME_DIR/state/.last-watcher-beat"
FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" \
  FM_DATA_OVERRIDE="$HOME_DIR/data" FM_PROJECTS_OVERRIDE="$HOME_DIR/projects" \
  FM_CONFIG_OVERRIDE="$HOME_DIR/config" FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$WT" \
  TMUX="fake,1,0" GROK_HOME="$GHOME" PATH="$FAKEBIN:$PATH" \
  "$SPAWN" "$ID" "$PROJ" grok --mode no-mistakes --yolo off 2>&1 | tail -1

HOOK="$GHOME/hooks/fm-turn-end.sh"
sub "generated global hook body ($HOOK)"
sed 's/^/  /' "$HOOK"
BIN="$CASE/presence-bin"; LOG="$CASE/presence.log"
fm_fake_agent_presence "$BIN" "$LOG"
sub "authorised workspace (this task's registry token) fires the hook"
out=$(PATH="$BIN:$PATH" GROK_WORKSPACE_ROOT="$WT" bash "$HOOK" 2>&1); rc=$?
printf '  exit=%s stdout+stderr=%s turn-end marker=%s\n' "$rc" "$(printf '%q' "$out")" \
  "$([ -f "$HOME_DIR/state/$ID.turn-ended" ] && echo written || echo missing)"
show_beats "$LOG"
printf '  the beat ran in: %s\n  the task worktree is: %s\n' "$(cat "$LOG.pwd")" "$(cd "$WT" && pwd -P)"

sub "ADVERSARIAL: an UNAUTHORISED workspace (forged pointer, no registry token)"
EVIL="$CASE/evil"; mkdir -p "$EVIL"; printf 'token=%s\n' not-a-token > "$EVIL/.fm-grok-turnend"
: > "$LOG"
out=$(PATH="$BIN:$PATH" GROK_WORKSPACE_ROOT="$EVIL" bash "$HOOK" 2>&1); rc=$?
printf '  exit=%s stdout+stderr=%s\n' "$rc" "$(printf '%q' "$out")"
show_beats "$LOG"

##############################################################################
hr "KIMI: the global hook body whose version bump this change required"
KHOME="$TMP_ROOT/kimi-home"
mkdir -p "$KHOME/.kimi-code" "$KHOME/state"
printf 'default_model = "test"\n' > "$KHOME/.kimi-code/config.toml"
cp "$KHOME/.kimi-code/config.toml" "$TMP_ROOT/kimi-config.original"
HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" install && printf 'install: ok\n'
KHOOK="$KHOME/.kimi-code/fm-turn-end.sh"
sub "the beat line the installer wrote into the global hook"
grep -n 'agent-presence' "$KHOOK" | sed 's/^/  /'
KWT="$TMP_ROOT/kimi-wt"; mkdir -p "$KWT"
TARGET="$KHOME/state/kimi-task.turn-ended"
TOKEN=fm.driveabc1234
printf '%s\n' "$TARGET" > "$KHOME/.kimi-code/fm-turn-end.d/$TOKEN"
printf 'token=%s\n' "$TOKEN" > "$KWT/.fm-kimi-turnend"
BIN="$TMP_ROOT/kimi-presence-bin"; LOG="$TMP_ROOT/kimi-presence.log"
fm_fake_agent_presence "$BIN" "$LOG"
sub "a real Kimi Stop payload for the authorised workspace"
out=$(printf '{"hook_event_name":"Stop","session_id":"crew","cwd":"%s","stop_hook_active":false}\n' "$KWT" \
  | HOME="$KHOME" PATH="$BIN:$PATH" bash "$KHOOK" 2>&1); rc=$?
printf '  exit=%s stdout+stderr=%s turn-end marker=%s\n' "$rc" "$(printf '%q' "$out")" \
  "$([ -f "$TARGET" ] && echo written || echo missing)"
show_beats "$LOG"
printf '  the beat ran in: %s\n  the task worktree is: %s\n' "$(cat "$LOG.pwd")" "$(cd "$KWT" && pwd -P)"

sub "ADVERSARIAL: a Kimi session in a workspace with no Firstmate token"
: > "$LOG"
out=$(printf '{"hook_event_name":"Stop","session_id":"x","cwd":"%s","stop_hook_active":false}\n' "$TMP_ROOT" \
  | HOME="$KHOME" PATH="$BIN:$PATH" bash "$KHOOK" 2>&1); rc=$?
printf '  exit=%s stdout+stderr=%s\n' "$rc" "$(printf '%q' "$out")"
show_beats "$LOG"

sub "VERSION BUMP: a hook body written by an OLDER firstmate (no beat in it)"
OLD="$TMP_ROOT/older-firstmate-hook.sh"
{ head -n 2 "$KHOOK"; printf 'exit 0\n'; } > "$OLD"
cp "$OLD" "$KHOOK"; chmod 0700 "$KHOOK"
printf '  hook on disk before upgrade: %s lines, beat present=%s\n' \
  "$(wc -l < "$KHOOK")" "$(grep -c agent-presence "$KHOOK")"
HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" install && printf '  install upgraded it: ok\n'
printf '  hook on disk after upgrade:  %s lines, beat present=%s\n' \
  "$(wc -l < "$KHOOK")" "$(grep -c agent-presence "$KHOOK")"
cp "$OLD" "$KHOOK"; chmod 0700 "$KHOOK"
HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" remove && printf '  remove excised an older firstmate hook: ok (hook present=%s)\n' \
  "$([ -e "$KHOOK" ] && echo yes || echo no)"
cmp -s "$TMP_ROOT/kimi-config.original" "$KHOME/.kimi-code/config.toml" \
  && printf '  config.toml restored byte-for-byte: yes\n' || printf '  config.toml restored byte-for-byte: NO\n'

sub "ADVERSARIAL: a hook at that path firstmate does NOT own must still be refused"
HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" install >/dev/null
printf '#!/usr/bin/env bash\n# Someone else owns this path.\nexit 0\n' > "$KHOOK"; chmod 0700 "$KHOOK"
out=$(HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" install 2>&1); rc=$?
printf '  install rc=%s msg=%s\n' "$rc" "$out"
out=$(HOME="$KHOME" "$ROOT/bin/fm-kimi-turnend-hook.sh" remove 2>&1); rc=$?
printf '  remove  rc=%s msg=%s\n' "$rc" "$out"
printf '  foreign hook left intact: %s\n' "$(grep -c 'Someone else owns this path' "$KHOOK")"

##############################################################################
hr "SECONDMATE: a direct report that must NEVER appear on the crew presence board"
SHOME="$TMP_ROOT/main-home"; SUB="$TMP_ROOT/design-home"
mkdir -p "$SHOME/projects" "$SHOME/data" "$SHOME/state"
fm_git_init_commit "$SHOME/projects/alpha"
fm_git_add_origin "$SHOME/projects/alpha" "$TMP_ROOT/remotes/alpha.git"
printf '%s\n' '- alpha [direct-PR] - alpha project (added 2026-06-22)' > "$SHOME/data/projects.md"
SFAKE=$(make_fake_tmux "$TMP_ROOT/sm-fake")
make_fake_no_mistakes "$TMP_ROOT/sm-fake" >/dev/null
fm_fake_exit0 "$SFAKE" claude
export FM_BACKEND=tmux
FM_SECONDMATE_SCOPE='design scope' \
  scaffold_secondmate_charter "$SHOME" design 'design charter' alpha || printf 'charter scaffold failed\n'
PATH="$SFAKE:$PATH" FM_HOME="$SHOME" "$ROOT/bin/fm-home-seed.sh" design "$SUB" alpha >/dev/null \
  || printf 'seed failed\n'
SMLOG="$TMP_ROOT/sm-tmux.log"; SMPANE="$TMP_ROOT/sm-pane.txt"; printf '❯\n' > "$SMPANE"
: > "$SMLOG"
PATH="$SFAKE:$PATH" FM_HOME="$SHOME" FM_FAKE_TMUX_LOG="$SMLOG" FM_FAKE_TMUX_CAPTURE="$SMPANE" \
  "$ROOT/bin/fm-spawn.sh" design "$SUB" claude --secondmate 2>&1 | tail -1
sub "turn-boundary wiring installed in the secondmate's own home"
printf '  claude hook settings (.claude/settings.local.json): %s\n' \
  "$(find "$SUB" -name 'settings.local.json' 2>/dev/null | wc -l)"
printf '  opencode plugins / pi extensions / grok hooks: %s\n' \
  "$(find "$SUB" \( -name 'fm-busy-state.js' -o -name '*.pi-ext.ts' -o -name 'fm-turn-end.sh' \) 2>/dev/null | wc -l)"
sub "every file under the secondmate home that mentions agent-presence at all"
grep -rl 'agent-presence' "$SUB" 2>/dev/null | sed "s|^$SUB/|  |"
printf '  ... all of which are the firstmate SOURCE tree the seed clones, not generated wiring:\n'
grep -rl 'agent-presence' "$SUB" 2>/dev/null | sed "s|^$SUB/||" | sed 's|/.*||' | sort -u | sed 's/^/    top-level dir: /' 
sub "the launch command fm-spawn sent for the secondmate"
grep -o 'send-keys.*' "$SMLOG" | head -2 | cut -c1-200 | sed 's/^/  /'
printf '  launch command mentions agent-presence: %s\n' \
  "$(grep -c 'agent-presence' "$SMLOG")"
Evidence: The claude crewmate's four generated hook commands, as Claude Code reads them
UserPromptSubmit:
fm-busy-event.sh apply <state> <id> busy --gen ... --event user-prompt-submit 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state working >/dev/null 2>/dev/null || true; fi
Stop:
touch <turnend>; fm-busy-event.sh apply ... idle --event stop 2>/dev/null || true; if command -v agent-presence >/dev/null 2>/dev/null; then agent-presence beat --state waiting >/dev/null 2>/dev/null || true; fi
StopFailure:
... --event stop-failure ...; if command -v agent-presence ...; then agent-presence beat --state waiting ...; fi
SessionEnd:
... --event session-end ...; if command -v agent-presence ...; then agent-presence end >/dev/null 2>/dev/null || true; fi

firing each hook exactly as Claude Code would:
UserPromptSubmit exit=0 stdout=''
Stop exit=0 stdout=''
StopFailure exit=0 stdout=''
SessionEnd exit=0 stdout=''
board saw:
agent-presence beat --state working
agent-presence beat --state waiting
agent-presence beat --state waiting
agent-presence end
firstmate busy record after the beats: idle claude-hook

Pipeline

Updates from git push no-mistakes

... (62 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

✅ **Test** - passed
  • bash tests/fm-busy-adapter-wiring.test.sh (14 checks, the suite that drives the generated claude hooks, codex notify program, OpenCode plugin and pi extension)
  • bash tests/fm-grok-harness.test.sh
  • bash tests/fm-kimi-harness.test.sh
  • Manual driver drive-presence-crew.sh: real fm-spawn for claude crewmate, claude scout, codex, opencode and pi; fired every hook/plugin edge with a recording agent-presence and read back the beats
  • Manual driver drive-presence-globalhooks.sh: real grok spawn plus bin/fm-kimi-turnend-hook.sh install, both global hooks fired with real payloads, authorised and unauthorised workspaces, plus the kimi hook version-bump upgrade/remove/foreign-refusal paths
  • Manual driver drive-presence-uncovered.sh: real cursor and muse spawns, every generated artifact and the launch command inspected for any beat
  • Adversarial claude runs on a hermetic PATH built by fm_presence_absent_path (no resolvable agent-presence) and against an agent-presence that exits 3 and writes to both streams
  • Real seeded secondmate spawn (bin/fm-home-seed.sh design ... &amp;&amp; bin/fm-spawn.sh design &lt;subhome&gt; claude --secondmate) with the generated wiring and launch command inspected
  • bin/fm-test-run.sh --list --changed --base HEAD after a bin/fm-spawn.sh-only edit, run against both the base and target versions of the runner
  • Mutation check: presence_cmd rendered a no-op, the OpenCode/pi execFile calls and the kimi hook's beat line deleted, then the three suites re-run
  • bin/fm-kimi-turnend-hook.sh --help
  • shopt patsub_replacement plus a live demonstration of the &amp;-in-replacement hazard the rendering avoids
⚠️ **Document** - 2 infos
  • ℹ️ .agents/skills/harness-adapters/SKILL.md:190 - bridge-axi's DEFAULT_BEAT_TIMEOUT_MS of 2000 ms is stated in two firstmate surfaces: the presence header at bin/fm-spawn.sh:204-206 (declared owner of the contract) and .agents/skills/harness-adapters/SKILL.md:190, which otherwise defers to that header. The constant belongs to another repo, so nothing in firstmate can check it for drift, and a bridge-axi change silently makes both copies wrong at once. I did not edit it: both copies agree today, the sentence is the negotiated narrowing of the round-4 'presence can never slow a turn' claim, and the concrete number is arguably load-bearing for an agent reading the skill without opening a shell script. If you want the duplicate closed, the smallest form is to leave the header's number and reduce SKILL.md:190 to 'the duration bound is bridge-axi's own, recorded in bin/fm-spawn.sh's header' - a follow-up, not something this change requires.
  • ℹ️ docs/configuration.md:182 - No operator-facing document mentions that a crewmate or scout now announces itself to a logbook board when bridge-axi's agent-presence is on PATH. A captain who has bridge-axi installed will see new rows appear with nothing in README.md or docs/ explaining where they came from. I deliberately added nothing: agent-presence is an optional dependency with no install step, no config knob and no bootstrap check, so it does not belong in docs/configuration.md's 'Toolchain' section (the single owner of the REQUIRED toolchain list), and the 'Board wake-ups' section owns the inbound nudge path rather than an outbound signal - adding presence there would be creating a surface to close a perceived gap rather than repairing a stale fact. The intent scoped this documentation to .agents/skills/harness-adapters/SKILL.md, which now owns it. Raising it only so the choice is yours: if presence should be operator-visible, the right home is a short paragraph in docs/configuration.md under 'Board wake-ups' pointing at the skill, not a new page.
🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Each adapter that already has per-worker turn-boundary wiring now carries one
added command that announces a crewmate or scout to the captain's board through
bridge-axi's hook-callable `agent-presence` CLI: `working` at turn start,
`waiting` at turn end, and `end` at session end.

The beat is a strict addition. It runs only when `agent-presence` is on the
worker's PATH, its output is discarded and its status swallowed, and firstmate
passes nothing about the work - the consumer identity and the subject are
resolved by the CLI from the worker's own session and worktree, so no presence
field is text firstmate typed. Secondmates and the primary never beat.

Per adapter: claude reaches all four states through its four hooks; opencode
and pi beat working/waiting on their semantic edges only, never on pi's inner
`turn_end`; codex, grok, and kimi have no turn-start event and beat waiting
only, the two global hooks doing so in a subshell cd'd to the worktree their
registry token already authorised. cursor stays uncovered until its crewmate
`stop` event is measured for a pane worker, and muse has no hook surface at all.

Two details are load-bearing and easy to lose:

- The rendered command contains no `&` - `if/then/fi` rather than `&&`, and
  `2>/dev/null` rather than `2>&1`. codex's beat reaches its launch command
  through a `${LAUNCH//...}` replacement, and bash 5.2's patsub_replacement
  expands an unescaped `&` there to the matched text, which silently rewrote
  the command into nonsense; the escape that fixes that on 5.2 inserts a
  literal backslash on 5.1 and older.
- The state is `--state`, not a positional. agent-presence parses strictly and
  answers an unexpected positional with outcome `usage`, which exits 2 - and
  under the wrapper that is invisible rather than loud.

Kimi's global hook body changes, so install and remove now both key ownership
off the Firstmate hook prefix rather than exact bytes. Install already did;
remove did not, and would have refused to excise a hook written by an older
Firstmate until a spawn happened to rewrite it.

Also classifies tests/fm-busy-adapter-wiring.test.sh as pure-contract-unit so a
bin/fm-spawn.sh change selects the suite that covers its generated wiring.
…card

Firstmate crews beat branch-keyed rows, never pr-keyed ones, because nothing on
the crew path may run `agent-presence subject` - a task's branch does not exist
at spawn, and a turn boundary must not pay for a `gh` call. The open question
that left was whether such a row reaches the card at all.

It does. A board reaches the card through the workstream row sharing the card's
id and carrying the crew's branch, and the repo half is indexed under both the
full origin and the bare name, so the agent's `<owner>/<repo>` derivation and a
card's clone name meet even when the board holds no origin for that repo.

Measured against the merged board and the captain's live board rather than
assumed; the evidence is in the PR. No code change: the wiring already produces
exactly the ref that join wants.
…t. Cause: `fm_presence_absent_path` in tests/lib.sh probed with `( PATH=$dir; command -v agent-presence )`. Because bin/fm-lint.sh runs ShellCheck with `--external-sources` and every test file sources tests/lib.sh, that subshell PATH modification propagated into all consumers, making each later `PATH=` reference an SC2031. CI's full-set lint emitted 790 SC2031 diagnostics across 66 test files, mostly files this PR never touched (fm-watcher-lock, fm-turnend-guard, fm-tmux-submit-busy, fm-x-mode, ...). Last round's per-line disables only covered the two changed files, so the gate stayed red. Fix, at the one shared boundary (tests/lib.sh:365): the helper returns a PATH that is exactly `$dir`, so an agent-presence resolves on it only if one is executable there — the subshell probe became `[ -x "$dir/agent-presence" ]`, with the doc comment updated to state that equivalence. It is the only PATH modification in the library. Removals the fix made unreachable (verified site by site): all five SC2031 disables in tests/fm-grok-harness.test.sh (a PATH env prefix on an external command raises nothing by itself) and the run_spawn disable in tests/fm-busy-adapter-wiring.test.sh. Kept `with_path`'s disable (its `( PATH=...; "$@" )` isolation is a real in-file SC2030, narrowed from SC2030,SC2031 to SC2030) and the Kimi-hook SC2031 disable that follows it. Verified locally: `CI=true bin/fm-lint.sh` over the full canonical set now exits 0 with zero diagnostics (was 790); tests/fm-grok-harness.test.sh, tests/fm-busy-adapter-wiring.test.sh and tests/fm-kimi-harness.test.sh all pass; and the helper's refusal branch still fires when an executable agent-presence is planted in the directory. No new tests — the lint gate is the guard, and a source-text assertion would be the forbidden anti-pattern. Files changed: tests/lib.sh, tests/fm-grok-harness.test.sh, tests/fm-busy-adapter-wiring.test.sh (5 insertions, 10 deletions)
@SchokoShake
SchokoShake merged commit 187ed1d into main Sep 23, 2026
13 checks passed
@SchokoShake
SchokoShake deleted the fm/presence-firstmate branch September 23, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant