Motivation
firstmate today only ever spawns and owns its crew: every task gets a fresh treehouse worktree, an fm/<id> branch, and a session the backend created.
Recovery (AGENTS.md §5) deliberately refuses to claim endpoints it did not create ("another firstmate home's child endpoints ... are not this home's orphans"), and there is no adopt/attach verb anywhere in bin/.
That leaves a real gap for a common starting point: a captain who already runs several sessions by hand, especially cmux users, who often keep one window of N workspaces before they ever try firstmate.
The natural first ask is "captain the sessions I already have running," and right now the only answer is "restart that work as new crew," which nobody wants to do.
Proposal (implemented, PR incoming)
bin/fm-adopt.sh <id> --workspace <ws-uuid> [--surface <surf-uuid>] registers an already-running cmux workspace/surface as a supervised task, without spawning anything.
- It resolves the surface if omitted, verifies the endpoint is live and the socket reachable, then writes
state/<id>.meta with kind=adopted, backend=cmux, window=<ws>:<surface>, cmux_workspace_id=/cmux_surface_id=, and a worktree= pointing at the workspace's own live current_directory (never a firstmate-managed worktree). No branch, no treehouse worktree.
fm-peek / fm-send / fm-crew-state already resolve their target from the meta endpoint, so they work on an adopted task unchanged. That is the whole value: read and steer a session you already have open.
- The watcher exempts
kind=adopted from stale-pane escalation exactly as it already does for kind=secondmate, because a human-driven or long-idle session being quiet is healthy, not stuck.
fm-teardown gets a kind=adopted branch that never closes the workspace and never runs treehouse return, and skips the landed-work check: adoption is released by un-registering only (remove state/, leave the workspace and its shell exactly as they were).
The one interesting design point: keeping the human's title
cmux's fm_backend_cmux_target_ready rejects a workspace whose title is not the firstmate-scoped fm-<home>-<id> one, which a human-titled workspace would fail.
Rather than rename the captain's workspace (intrusive, and it would show an fm-... title in their sidebar), the implementation makes fm_backend_expected_label_of_selector return an empty expected-label for kind=adopted, so peek/send target the surface by pure UUID via surface_exists and the human's own title is preserved untouched.
Compatibility with firstmate's invariants
- "firstmate never writes to a project" (§1) is preserved. An adopted task is observe-and-steer-on-command; firstmate still does not edit, commit, or run state-changing project commands itself. Whoever is in the session does.
- Teardown safety is preserved, not bypassed. Adopted tasks have no worktree and no unlanded-work notion, so teardown's refusal-until-landed logic simply does not apply; the new branch makes that explicit rather than forcing a fake worktree through the existing check.
- It is additive: absent
kind=adopted, every existing path behaves exactly as today.
Known limitations (documented, not hidden)
- cmux-only initially. The socket already exposes
send / send-key / read-screen / list-panes against an arbitrary workspace, so cmux is the cleanest first backend; tmux/herdr/zellij can follow.
- Session-scoped on cmux. Workspace UUIDs do not survive an app relaunch (already documented in
docs/cmux-backend.md), and an adopted workspace keeps the human's own title, so title-based recovery cannot re-find it after a relaunch. Re-run fm-adopt then. Stated loudly, not worked around.
- Supervise-only. Adoption has no delivery mode, no PR, no
no-mistakes semantics; it is not a ship task.
- Experimental, the same status as the cmux backend itself.
Verification
Full fake-CLI test at tests/fm-adopt.test.sh (meta shape, absent-surface refusal, no new-workspace/no treehouse, teardown-never-closes, watcher stale exemption, and peek/send by pure UUID), shellcheck-clean, and a real cmux 0.64.17 round-trip recorded in docs/cmux-backend.md: a throwaway fm-test- workspace adopted by UUID, steered and read, then torn down with the workspace confirmed still present, then closed by the verification pass itself.
Open design questions for the maintainer
- Separate
bin/fm-adopt.sh, or a flag on fm-spawn.sh? A separate script keeps spawn's worktree/branch assertions clean, which is why this leans that way.
kind=adopted vs a mode=adopted orthogonal to kind? I lean kind= since adopted tasks share the idle-is-healthy supervision shape with kind=secondmate.
- Title handling: the empty-expected-label approach preserves the human title but forgoes relaunch recovery. If you would rather keep title-based recovery, an opt-in rename to the scoped title is the alternative. Happy to switch.
- Naming:
fm-adopt vs fm-attach.
Happy to reshape any of this to fit your design intent before you spend review time on the PR.
Motivation
firstmate today only ever spawns and owns its crew: every task gets a fresh treehouse worktree, an
fm/<id>branch, and a session the backend created.Recovery (
AGENTS.md§5) deliberately refuses to claim endpoints it did not create ("another firstmate home's child endpoints ... are not this home's orphans"), and there is noadopt/attachverb anywhere inbin/.That leaves a real gap for a common starting point: a captain who already runs several sessions by hand, especially cmux users, who often keep one window of N workspaces before they ever try firstmate.
The natural first ask is "captain the sessions I already have running," and right now the only answer is "restart that work as new crew," which nobody wants to do.
Proposal (implemented, PR incoming)
bin/fm-adopt.sh <id> --workspace <ws-uuid> [--surface <surf-uuid>]registers an already-running cmux workspace/surface as a supervised task, without spawning anything.state/<id>.metawithkind=adopted,backend=cmux,window=<ws>:<surface>,cmux_workspace_id=/cmux_surface_id=, and aworktree=pointing at the workspace's own livecurrent_directory(never a firstmate-managed worktree). No branch, no treehouse worktree.fm-peek/fm-send/fm-crew-statealready resolve their target from the meta endpoint, so they work on an adopted task unchanged. That is the whole value: read and steer a session you already have open.kind=adoptedfrom stale-pane escalation exactly as it already does forkind=secondmate, because a human-driven or long-idle session being quiet is healthy, not stuck.fm-teardowngets akind=adoptedbranch that never closes the workspace and never runstreehouse return, and skips the landed-work check: adoption is released by un-registering only (removestate/, leave the workspace and its shell exactly as they were).The one interesting design point: keeping the human's title
cmux's
fm_backend_cmux_target_readyrejects a workspace whose title is not the firstmate-scopedfm-<home>-<id>one, which a human-titled workspace would fail.Rather than rename the captain's workspace (intrusive, and it would show an
fm-...title in their sidebar), the implementation makesfm_backend_expected_label_of_selectorreturn an empty expected-label forkind=adopted, so peek/send target the surface by pure UUID viasurface_existsand the human's own title is preserved untouched.Compatibility with firstmate's invariants
kind=adopted, every existing path behaves exactly as today.Known limitations (documented, not hidden)
send/send-key/read-screen/list-panesagainst an arbitrary workspace, so cmux is the cleanest first backend; tmux/herdr/zellij can follow.docs/cmux-backend.md), and an adopted workspace keeps the human's own title, so title-based recovery cannot re-find it after a relaunch. Re-runfm-adoptthen. Stated loudly, not worked around.no-mistakessemantics; it is not a ship task.Verification
Full fake-CLI test at
tests/fm-adopt.test.sh(meta shape, absent-surface refusal, nonew-workspace/notreehouse, teardown-never-closes, watcher stale exemption, and peek/send by pure UUID), shellcheck-clean, and a real cmux 0.64.17 round-trip recorded indocs/cmux-backend.md: a throwawayfm-test-workspace adopted by UUID, steered and read, then torn down with the workspace confirmed still present, then closed by the verification pass itself.Open design questions for the maintainer
bin/fm-adopt.sh, or a flag onfm-spawn.sh? A separate script keeps spawn's worktree/branch assertions clean, which is why this leans that way.kind=adoptedvs amode=adoptedorthogonal to kind? I leankind=since adopted tasks share the idle-is-healthy supervision shape withkind=secondmate.fm-adoptvsfm-attach.Happy to reshape any of this to fit your design intent before you spend review time on the PR.