Repository navigation
feat(bin): beat optional agent presence from crewmate turn boundaries - #34
Merged
Merged
Conversation
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.
…hip, harden absent-CLI PATH
…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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
DESIGN DECISIONS A REVIEWER WOULD NOT SEE IN THE DIFF:
NECESSARY ACCOMPANYING CHANGES:
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.shrenders one optionalagent-presencebeat into each adapter's existing per-worker turn-boundary wiring (claude's four lifecycle hooks, the OpenCode plugin, the pi extension, the codexnotifylaunch 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 reachingworking/waiting/end, opencode and pi reachingworking/waiting, and codex, grok and kimi beatingwaitingonly.bin/fm-kimi-turnend-hook.shadds the samewaitingbeat to its global hook body in a subshellcd'd to the registry-authorised workspace, and bothinstallandremovenow 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--helpline range was extended to cover the new header paragraph..agents/skills/harness-adapters/SKILL.mdgains a presence section plus a per-adapter row, recording cursor as not covered (with the livestop-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.shadds sharedfm_fake_agent_presence,fm_assert_presence_beatsandfm_presence_absent_pathhelpers, the three adapter suites drive the generated artifacts for installed, not-installed and failing-CLI cases, andbin/fm-test-run.shclassifiesfm-busy-adapter-wiring.test.shaspure-contract-unitso 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.
bash tests/fm-busy-adapter-wiring.test.sh--scoutspawn's hooks produced working, waiting and endEvidence: Beat matrix and adversarial probes, as observed by driving the product
Source: Beat matrix and adversarial probes, as observed by driving the product
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
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
Evidence: Uncovered adapters transcript: cursor and muse spawns install nothing that beats
Source: Uncovered adapters transcript: cursor and muse spawns install nothing that beats
Evidence: Mutation check: with the beat removed, all three suites fail
Source: Mutation check: with the beat removed, all three suites fail
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
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
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
Evidence: Suite output: tests/fm-busy-adapter-wiring.test.sh
Source: Suite output: tests/fm-busy-adapter-wiring.test.sh
Evidence: Suite output: tests/fm-grok-harness.test.sh
Source: Suite output: tests/fm-grok-harness.test.sh
Evidence: Suite output: tests/fm-kimi-harness.test.sh
Source: Suite output: tests/fm-kimi-harness.test.sh
Evidence: Driver script: global hooks and secondmate
Source: Driver script: global hooks and secondmate
Evidence: The claude crewmate's four generated hook commands, as Claude Code reads them
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.shbash tests/fm-kimi-harness.test.shManual driverdrive-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 beatsManual driverdrive-presence-globalhooks.sh: real grok spawn plusbin/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 pathsManual driverdrive-presence-uncovered.sh: real cursor and muse spawns, every generated artifact and the launch command inspected for any beatAdversarial claude runs on a hermetic PATH built byfm_presence_absent_path(no resolvable agent-presence) and against an agent-presence that exits 3 and writes to both streamsReal seeded secondmate spawn (bin/fm-home-seed.sh design ... && bin/fm-spawn.sh design <subhome> claude --secondmate) with the generated wiring and launch command inspectedbin/fm-test-run.sh --list --changed --base HEADafter a bin/fm-spawn.sh-only edit, run against both the base and target versions of the runnerMutation 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-runbin/fm-kimi-turnend-hook.sh --helpshopt patsub_replacementplus a live demonstration of the&-in-replacement hazard the rendering avoids.agents/skills/harness-adapters/SKILL.md:190- bridge-axi'sDEFAULT_BEAT_TIMEOUT_MSof 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 ✅
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.