Skip to content

Feature: fm-adopt — supervise an already-running session (cmux) without spawning #260

Description

@ss251

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions