feat(bin): reach claude workers through Claude Remote Control - #2695
Open
joshuabke wants to merge 1 commit into
Open
feat(bin): reach claude workers through Claude Remote Control#2695joshuabke wants to merge 1 commit into
joshuabke wants to merge 1 commit into
Conversation
Every claude-harness crewmate, scout, and secondmate now launches with `--remote-control <task-id>`, so the captain can read and steer a worker from claude.ai/code or the Claude mobile app without changing anything else about the session. The local gitignored `config/claude-remote-control` file opts a home out with "off"; absent, empty, or "on" enables it, and an unrecognized value warns and keeps the default rather than failing a spawn over a reachability setting. Resolution is guarded by the launch placeholder rather than the harness name, so a raw launch command that happens to start with `claude` is left alone. The flag is version-dependent, so fm-spawn probes the same bare `claude` it launches, exactly like the Pi `--tui-mode` probe: an installed CLI that does not advertise the flag launches unchanged and says so once. The per-task supervision wiring was verified live under the flag on Claude Code 2.1.234: the Stop hook still touches the turn-end notification and records the semantic busy state, and fm-peek and fm-send read and steer such a pane unchanged. docs/verification/ runtime-backends.md owns that evidence.
Owner
|
Speaking as Kun's firstmate: Reviewed the full diff. Claude Remote Control is on by default for every claude-harness spawn ( I approved fork CI. Waiting on green checks including no-mistakes. |
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
Enable Claude Remote Control for firstmate-spawned claude workers.
Background the reviewer needs:
claude --remote-control [name]starts an ordinary interactive Claude Code session that is additionally reachable from claude.ai/code and the Claude mobile app. On 2026-08-13 it was already verified (Claude Code 2.1.226) that such a session fires both UserPromptSubmit and Stop project hooks normally - exactly the two hooks firstmate's crewmate supervision depends on - but two caveats were left untested and are part of this task's acceptance criteria. The captain wants claude workers reachable from his phone. This task changes firstmate's shared tracked material (bin/), so the firstmate-coding-guidelines skill governs it.Requirements as accepted:
In bin/fm-spawn.sh, launch claude-harness crewmates, scouts, and secondmates with
--remote-control <session-name>by default, naming the session after the task id so the captain can identify it in the claude.ai/code session list. Add a local opt-out knobconfig/claude-remote-control(absent or "on" = enabled, "off" = disabled), gitignored like the other config knobs, documented where the other knobs are documented (AGENTS.md layout table + docs/configuration.md, following the existing pattern). Scope strictly to harness=claude; other harnesses are untouched. Checkclaude --helpfor the current flag syntax first; if the installed CLI does not support the flag, degrade gracefully (launch without it, print a loud one-line notice).EMPIRICALLY verify the two open caveats from the 2026-08-13 learning, using a throwaway scratch task/window (never a real fleet task): (a) the per-task turn-end wiring (state/.turn-ended touched by the Stop hook) still works when the worker is launched through the modified fm-spawn with the flag; (b) fm-peek and fm-send behave unchanged against such a pane (send a trivial steer, verify submission and pane read). Record the evidence (commands + observed results) in the PR description. Do NOT edit data/learnings.md - it is firstmate-private and outside this worktree's scope; the done line instead tells firstmate the caveats are now verified so firstmate updates the learning itself.
Tests: follow the repo's colocated-test convention (tests/) for the new config-knob resolution and launch-flag assembly, since the existing fm-spawn test surface already covers similar flags; keep it proportional to the existing test style.
Acceptance criteria:
--remote-control <task-id>unless the knob says off; non-claude harnesses unchanged.Decisions and tradeoffs made while implementing, which a reviewer reading only the diff would not know:
--remote-controlgoes AFTER__MODELFLAG____EFFORTFLAG__and immediately before the positional launch brief. That keeps every existingassert_contains "claude --dangerously-skip-permissions --model 'opus'"substring assertion in other suites valid, so only the single exact-equality canonical-launch assertion needed updating.case "$LAUNCH" in *__CLAUDEREMOTE__*), matching the existing muse/kimi resolutions, NOT bycase "$HARNESS" in claude). This is intentional: HARNESS can also be derived from the raw launch-command escape hatch, so a hand-written command that merely starts withclaudewould otherwise be probed and warned about even though there is no placeholder to substitute.claude --helpprobe mirrors the existingpi_supports_tui_modeprobe (same shape, same grep form) rather than inventing a new mechanism. The loud one-line notice fires only when claude IS installed but does not advertise the flag; whenclaudeis not on PATH at all the code is silent, because that spawn's launch would already fail loudly in the pane and a second warning would just be noise in test and CI environments.config/is already ignored as a directory (pinned by tests/fm-gitignore-config.test.sh), which is what "gitignored like the other config knobs" already means here.claudethat answers--helpwas added to that suite's fakebin. That fake is load-bearing:run_spawnputs the fakebin first on PATH but keeps the rest of PATH, so without it the probe would find the developer's real claude and produce a different verdict than CI, where claude is absent.Empirical evidence for requirement 2, recorded live on Claude Code 2.1.234, tmux 3.7b, macOS 26.5.1 arm64, using a throwaway scout spawned through the modified bin/fm-spawn.sh into an isolated tmux server (separate TMUX_TMPDIR socket) and a scratch firstmate home, so no real fleet task or session was involved:
bin/fm-spawn.sh rc-probe-a1 <scratch-project> --scout --harness claudesucceeded, andps -o pid,ppid,command -axshowed the live process asclaude --dangerously-skip-permissions --remote-control rc-probe-a1 -c<encoded launch brief>, proving the flag reaches the real CLI and does not swallow the positional brief. The pane footer rendered/rc activefor the whole session.state/rc-probe-a1.turn-endedwas created on the first turn's Stop and re-touched on the second turn (19:42 then 19:43:25), and the semantic busy record advanced toseq=5 state=idle source=claude-hook event=stop, proving UserPromptSubmit and Stop both fired from the per-task .claude/settings.local.json across two turns rather than only at launch.bin/fm-crew-state.sh rc-probe-a1readstate: done.bin/fm-peek.sh rc-probe-a1 12exited 0 and captured the transcript, composer, and footer;bin/fm-send.sh rc-probe-a1 "<trivial steer>"exited 0 (submit confirmed) and the steer appeared in the transcript with the worker's answer.Local gates already run green before validation: bin/fm-lint.sh clean (pinned ShellCheck 0.11.0 and actionlint 1.7.12, both installed through the repo's own bin/fm-install-*.sh), bin/fm-doc-audience-check.sh ok, and bin/fm-test-run.sh --changed green with 1007 passing assertions and zero failures.
Re-run note: an earlier run of this exact head (a4aa976) completed review, test, document, and lint green - review raised two informational points and nothing blocking, test found nothing, document found one non-blocking follow-up (an adapter-side pointer in an unrelated skill file), and lint passed - then failed at push with a 403, because the gate had been registered without a fork URL and was pushing straight to origin, where the authenticated account is read-only. The captain approved a fork and the gate is now re-registered with https://github.com/joshuabke/firstmate. The head under validation is unchanged from that green run; nothing in the diff was altered in response to any finding.
What Changed
bin/fm-spawn.shnow resolves a__CLAUDEREMOTE__placeholder in the claude launch template, so claude-harness crewmates, scouts, and secondmates launch with--remote-control <task-id>and are reachable from claude.ai/code and the Claude mobile app under the task's name. Resolution is guarded by the placeholder rather than the harness name, matching the existing muse/kimi resolutions, so a raw launch command that merely starts withclaudeis left alone; every other adapter's launch command is untouched.config/claude-remote-controlknob (absent, empty, oron= enabled;off= opt out; an unrecognized value warns and keeps the default), parsed with the same whole-file whitespace-stripped, case-folded convention asconfig/crew-harnessandconfig/backlog-backend. Aclaude --helpprobe modeled on the neighbouringpi_supports_tui_modedegrades gracefully: an installed CLI that does not advertise the flag launches without it and prints one warning line.AGENTS.mdconfig-layout table and a newdocs/configuration.mdsection (both stating it is not inherited by secondmate homes), recorded the dated live verification of the turn-end/busy-state hooks andfm-peek/fm-sendbehavior indocs/verification/runtime-backends.md, and extendedtests/fm-spawn-dispatch-profile.test.shwith knob-resolution, older-CLI, secondmate, and non-claude-harness cases plus a fakeclaude --helpin the suite's fakebin so the probe verdict does not depend on the host having claude installed.Risk Assessment
✅ Low: The change is tightly scoped to the claude launch template behind a placeholder guard, documented in both required doc surfaces, covered by behavioral tests with a deterministic fake CLI, and the flag syntax matches the installed CLI's real help output; the remaining notes are informational degradation-path polish rather than reachable failures.
Testing
I ran the targeted fm-spawn dispatch-profile suite (all green, including the three new Remote Control cases) plus the two other suites that pin the claude launch command as a substring, which confirms the deliberate flag placement after the model/effort flags did not invalidate them. Because passing unit tests do not show the captain's actual experience, I also re-verified the two open caveats live and independently of the author: a throwaway scout spawned through the modified bin/fm-spawn.sh into an isolated tmux server and scratch firstmate home on claude 2.1.238 ran asclaude --dangerously-skip-permissions --model haiku --remote-control rc-probe-2444 -c<encoded brief>, so the flag parses without swallowing the positional brief; the per-task turn-end notification was created on the first turn's Stop and re-touched on the second (busy record advancing seq=3 to seq=5, source=claude-hook event=stop), and fm-peek and fm-send both exited 0 against that pane with the steer visibly landing and answered. The end-user surface here is a terminal pane rather than a GUI, so the visual artifact is a rendered screenshot of the captured tmux pane showing the/rc activefooter next to the live process line. Two initial probe attempts failed for environmental reasons I fixed (firstmate's gate-agent capability guard, which the repo's own test helpers bypass the same way, and fm-spawn's pre-existing refusal to launch from a pooled worktree with an unfetchable origin), not for any defect in the change. The probe cleaned up its treehouse pool, scratch home, and tmux server, and the worktree is unmodified.Evidence: Same evidence as rendered HTML
Source: Same evidence as rendered HTML
Evidence: Live probe transcript (caveats a and b)
Source: Live probe transcript (caveats a and b)
== claude 2.1.238 (Claude Code) / tmux 3.7b / Darwin 25.5.0 arm64 == -- spawn -- spawned rc-probe-2444 harness=claude kind=scout window=firstmate:fm-rc-probe-2444 worktree=/Users/joshua/.treehouse/rcprobe-2444-3c1dbd/1/rcprobe-2444 -- live process (ps -o command= -ax) -- claude --dangerously-skip-permissions --model haiku --remote-control rc-probe-2444 \040-cFIRSTMATE_OP: v1 launch-brief: You are a crewmate: an autonomous worker agent managed by firstmate. Work on you -- caveat (a) per-task turn-end notification -- state/rc-probe-2444.turn-ended after turn 1: 1787260162 Aug 20 23:09:22 2026 state/rc-probe-2444.turn-ended after turn 2: 1787260171 Aug 20 23:09:31 2026 semantic busy record (state/rc-probe-2444.busy-state tail): v1 gen=g1787260143.4734.20135 seq=3 state=idle source=claude-hook event=stop ts=1787260162 v1 gen=g1787260143.4734.20135 seq=5 state=idle source=claude-hook event=stop ts=1787260171 -- worker status log -- working: rc probe ready needs-decision [key=rc-probe]: awaiting codeword done: received ALBATROSS -- fm-crew-state.sh rc-probe-2444 -- state: done · source: status-log · received ALBATROSS -- caveat (b) fm-send -- (exit 0 = submit confirmed)Evidence: Worker pane as captured by fm-peek (caveat b), footer shows /rc active
Source: Worker pane as captured by fm-peek (caveat b), footer shows /rc active
❯ ALBATROSS Ran 1 shell command ⏺ ✅ RC probe sequence complete. Status file updated: - working: rc probe ready - needs-decision [key=rc-probe]: awaiting codeword - done: received ALBATROSS ──────────────────────────────────────── ❯ ──────────────────────────────────────── ⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents /rc activeEvidence: Reproducible live probe script (isolated tmux server + scratch firstmate home)
Source: Reproducible live probe script (isolated tmux server + scratch firstmate home)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-spawn.sh:1280- bin/fm-spawn.sh:1280 stays silent whenclaudeis not resolvable from fm-spawn's own PATH, on the stated rationale that such a spawn would fail loudly in the pane anyway. That holds only when the pane's PATH equals fm-spawn's PATH. A tmux pane runs the launch through the tmux server's inherited environment, which can differ from the environment of whatever invoked fm-spawn (a daemon-driven spawn, anssh <host> bin/fm-spawn.shinvocation, or the documented non-interactive-SSH case where ~/.local/bin and /opt/homebrew/bin are absent). In that case the worker launches fine but silently loses Remote Control, quietly breaking the default-on promise the docs make. Emitting the same one-line notice whenever the flag is dropped while the knob is on would close the gap, at the cost of the test/CI noise the author deliberately avoided - hence flagging rather than fixing.bin/fm-spawn.sh:1134- claude_supports_remote_control() (bin/fm-spawn.sh:1134) treats any non-zeroclaude --helpas "flag unsupported", so a broken install, a bad CLAUDE_CONFIG_DIR, or any other probe failure is reported to the captain as "the installed claude CLI does not support --remote-control" - a misleading diagnosis for a non-version problem. The probe is also unbounded, unlike the cursor probes in bin/fm-cursor-lib.sh which run undertimeout/gtimeoutand are documented as bounded and fail-closed; a hungclaude --helpwould stall every claude spawn. This matches the existing unbounded pi_supports_tui_mode precedent the author cited, andclaude --helpdoes no network I/O in practice, so this is a note rather than a defect.bin/fm-spawn.sh:1279- bin/fm-control.sh relaunch delegates tofm-spawn --relaunch, which re-resolves the flag and reuses the same task id as the Remote Control session name. The recorded verification covers one fresh spawn across two turns; it does not cover relaunching a task whose previous incarnation already registered that name. If Claude Code ever rejected or shadowed a duplicate session name, the wedged-pane recovery path would be affected. No evidence that it does - noting the untested interaction, not claiming a defect.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-spawn-dispatch-profile.test.sh(all 34 cases, including the newtest_claude_remote_control_knob_selects_reachability,test_claude_remote_control_omitted_when_installed_cli_lacks_the_flag, andtest_claude_secondmate_launch_carries_remote_control)bin/fm-test-run.sh tests/fm-secondmate-harness.test.sh(the other suite asserting the claude launch substring with --model/--effort)bin/fm-test-run.sh tests/fm-backend-orca.test.sh(asserts the claude launch prefix on a non-tmux backend)claude --help | grep -- '--remote-control'on the installed CLI (2.1.238) to confirm the probed flag syntax is currentLive probe scriptrc-live-probe.sh(recorded in the evidence dir): spawned a throwaway scout withbin/fm-spawn.sh rc-probe-2444 <scratch repo> --scout --harness claude --model haikuinto an isolated tmux server (private -L socket), a scratch FM_HOME, and a throwaway git repops -o command= -ax | grep -F -- '--remote-control rc-probe-2444'— proved the real CLI receives the flag and still gets the positional encoded launch briefCaveat (a): comparedstatmtime ofstate/rc-probe-2444.turn-endedafter turn 1 (23:09:22) and after turn 2 (23:09:31), plus the semantic busy recordstate/rc-probe-2444.busy-stateadvancing seq=3 -> seq=5 withsource=claude-hook event=stopCaveat (b):bin/fm-peek.sh rc-probe-2444 30(exit 0, full transcript/composer/footer capture) andbin/fm-send.sh rc-probe-2444 "ALBATROSS"(exit 0 = submit confirmed, worker answered on the next turn)bin/fm-crew-state.sh rc-probe-2444— readstate: donetmux capture-paneof the live worker pane, showing the/rc activefooter the captain seesCleanup verification:git status --porcelainempty,~/.treehousefree of the probe pool, probe tmux socket and scratch home removed.agents/skills/harness-adapters/SKILL.md:183- Judgment call, deliberately not applied: the claude adapter section could carry a one-line pointer noting that firstmate-launched claude panes now run with --remote-control (footer renders/rc active, and the captain may steer the same session from claude.ai/code or the phone concurrently with an agent's fm-send). I left it out because nothing in that section became false: bin/fm-spawn.sh:1112 declares launch commands as the script's own, the skill owns only busy-state/exit/dialogs/quirks, and the fact already has two owners (AGENTS.md layout table + docs/configuration.md "Claude Remote Control") plus its evidence in docs/verification/runtime-backends.md. Adding a third copy to always-loaded agent guidance would trade a real placement rule for a perceived gap. Worth a follow-up only if concurrent human-from-phone steering ever turns into an operational hazard an agent must reason about.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.