Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,16 @@ If such a dialog is showing, accept it from an active firstmate session using `F

Claude renders a predicted-next-prompt suggestion as dim/faint text inside an otherwise-empty composer after a turn completes.
A plain `tmux capture-pane` cannot tell that ghost text apart from typed text.
Firstmate launches every claude crewmate and secondmate with `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`, scoped to firstmate-launched agents through `bin/fm-spawn.sh`, so it never touches the captain's global config.
Firstmate applies `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false` per kind through `bin/fm-spawn.sh`, scoped to firstmate-launched agents, so it never touches the captain's global config.
A claude crewmate (ship or scout) launches with the suggestion disabled: it is an autonomous worker the captain never drives from its composer, so the ghost text has no reader to help.
A claude secondmate launches with the native suggestion enabled, because the captain reads and drives that window and should see it exactly as in their own session.
The CLI's `--prompt-suggestions` flag is print/SDK-mode only and does not suppress the interactive composer ghost text, verified empirically on v2.1.186.
As defense in depth for any pane that flag cannot reach, including the captain's own firstmate composer that away-mode reads, the shared `fm_composer_strip_ghost` extractor in `bin/fm-composer-lib.sh` removes dim/faint SGR 2 ghost runs before pending-input classification on both ANSI-capable readers (tmux and herdr).
Its broader dark-TRUECOLOR placeholder handling and dark-theme tradeoff are documented in `docs/herdr-backend.md`'s 2026-07-10 incident record.
Every pane the flag does not disable - a secondmate, and the captain's own firstmate composer that away-mode reads - relies on the shared `fm_composer_strip_ghost` extractor in `bin/fm-composer-lib.sh`, which removes dim/faint SGR 2 ghost runs before pending-input classification on both ANSI-capable readers (tmux and herdr).
The plain-screen backends that cannot strip ghost styling - orca, cmux, and zellij - all refuse `--secondmate` spawns in `bin/fm-spawn.sh`, so no secondmate can land on a reader without `fm_composer_strip_ghost`.
That extractor's broader dark-TRUECOLOR placeholder handling and dark-theme tradeoff are documented in `docs/herdr-backend.md`'s 2026-07-10 incident record.
That styled capture is internal to the boolean detector only.
`fm-peek` and every other human or LLM-facing capture path stays plain `tmux capture-pane` with no escape codes.
Note for a future reader: because those paths are plain, a peek of an IDLE secondmate can render its predicted next prompt as apparent typed input, bounded by AGENTS.md forbidding reads of a secondmate's chat and by the documented post-spawn trust-dialog peek above happening while the agent is still processing its charter, before any ghost renders.

**Primary-session guard fact (verified 2026-07-04, Claude Code 2.1.201; preserved 2026-07-08, Claude Code 2.1.204).**
This is separate from the per-task crewmate turn-end hook above (that one just `touch`es a marker file in a task's own `.claude/settings.local.json`).
Expand Down
15 changes: 9 additions & 6 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -705,12 +705,15 @@ fm_backend_target_exists() { # <backend> <target> [expected-label]
# dead - CONFIDENTLY not an agent: a bare shell (tmux) or a
# structurally-gone/no-agent-registered pane (herdr).
# unknown - anything ambiguous, unreadable, or unverified for this backend.
# Scoped to today's --secondmate-spawn-capable backends with an empirically
# verified classifier: tmux (docs/tmux-backend.md "Agent liveness probe") and
# herdr (docs/herdr-backend.md "Agent liveness probe reuses the husk
# classifier"). zellij, orca, and cmux report unknown until independently
# verified - future work, not a functional gap for the two backends
# --secondmate spawns actually support today plus tmux's reference path.
# tmux and herdr are exactly today's --secondmate-spawn-capable backends, and
# both have an empirically verified classifier: docs/tmux-backend.md "Agent
# liveness probe" and docs/herdr-backend.md "Agent liveness probe reuses the
# husk classifier". zellij, orca, and cmux report unknown until independently
# verified; fm-spawn.sh refuses --secondmate on all three (docs/configuration.md
# "Runtime backend" owns that contract). For orca and cmux no secondmate
# endpoint can exist at all, so their unknown is purely an ordinary-crewmate
# gap; for zellij it is that too, and additionally still reached on a LEGACY
# secondmate endpoint, never on a new one.
# Callers must treat unknown exactly like an unreadable target: NEVER license
# an action from it alone - the secondmate-liveness sweep gates a respawn on
# `dead` only, precisely so a momentary read glitch can never duplicate a
Expand Down
53 changes: 36 additions & 17 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
# auto-detected tmux stays silent; zellij and orca are never auto-detected.
# codex-app is not a known backend yet; docs/codex-app-backend.md owns that
# blocked backend contract. Default tmux spawns do not write backend= to meta;
# absent backend= means tmux. cmux does not support --secondmate spawns yet.
# absent backend= means tmux. orca, cmux, and zellij do not support
# --secondmate spawns yet.
# A backend spawn refusal (missing dependency, version gate, unauthenticated
# socket, or unsupported secondmate mode) is terminal for that selected backend;
# callers must surface it instead of silently retrying another backend.
Expand Down Expand Up @@ -178,13 +179,13 @@ else
fi
fm_backend_validate_spawn "$BACKEND" || exit 1
fm_backend_source "$BACKEND" || exit 1
if [ "$BACKEND" = orca ] && [ "$KIND" = secondmate ]; then
echo "error: backend=orca does not support --secondmate spawns yet" >&2
exit 1
fi
if [ "$BACKEND" = cmux ] && [ "$KIND" = secondmate ]; then
echo "error: backend=cmux does not support --secondmate spawns yet" >&2
exit 1
if [ "$KIND" = secondmate ]; then
case "$BACKEND" in
orca | cmux | zellij)
echo "error: backend=$BACKEND does not support --secondmate spawns yet" >&2
exit 1
;;
esac
fi
if [ "$BACKEND" = orca ]; then
fm_backend_orca_runtime_check || exit 1
Expand Down Expand Up @@ -316,15 +317,33 @@ launch_template() {
# shellcheck disable=SC2016 # single quotes are deliberate: $(cat ...) expands in the crewmate pane, not here
case "$harness" in
# CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false disables claude's interactive
# predicted-next-prompt ghost text, which renders as dim/faint text inside an
# otherwise-empty composer and would otherwise read like real typed input when
# firstmate captures the pane (see the harness-adapters skill). It is a per-launch env
# prefix scoped to this firstmate-launched agent; it never touches the captain's
# global config. The CLI's --prompt-suggestions flag is print/SDK-mode only and
# does NOT suppress the interactive ghost text (verified empirically), so the env
# var is the correct control. The dim-aware composer reader in fm-tmux-lib.sh is
# the defense-in-depth backstop for any pane this flag cannot reach.
claude) printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(cat __BRIEF__)"' ;;
# predicted-next-prompt ghost text, which renders as dim/faint (SGR 2) text inside
# an otherwise-empty composer. It is a per-launch env prefix scoped to this
# firstmate-launched agent; it never touches the captain's global config. The CLI's
# --prompt-suggestions flag is print/SDK-mode only and does NOT suppress the
# interactive ghost text (verified empirically), so the env var is the correct control.
#
# The prefix is applied per KIND, because the suggestion's value and its cost differ:
# ship/scout - DISABLED. An autonomous crewmate is a worker the captain never
# drives from its own composer, so the suggestion has no reader to help and is
# pure classifier risk.
# secondmate - ENABLED (no prefix). A secondmate is a captain-facing agent the
# captain does read and drive, so it shows the native suggestion exactly as the
# captain's own session does. Safety rests on the shared fm_composer_strip_ghost
# (bin/fm-composer-lib.sh), the one fleet-wide ANSI-aware extractor of real typed
# content, which every secondmate-reachable composer read routes through - the
# same backstop that already covers the captain's own firstmate pane, which this
# flag never reached either. The plain-screen readers that cannot strip ghost
# styling - orca, cmux, and zellij - all refuse --secondmate spawns above, so no
# secondmate can land on a reader without fm_composer_strip_ghost. See the
# harness-adapters skill for the contract that rule satisfies.
claude)
if [ "$kind" = secondmate ]; then
printf '%s' 'claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(cat __BRIEF__)"'
else
printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(cat __BRIEF__)"'
fi
;;
codex)
if [ "$kind" = secondmate ]; then
printf '%s' 'codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox "$(cat __BRIEF__)"'
Expand Down
1 change: 1 addition & 0 deletions bin/fm-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@ EOF
if [ "$n" -ge 2 ] && ! window_is_busy "$w" "$tail40"; then
# The pane is idle/stale at hash $h. Triage decides whether this wakes
# firstmate. Detection itself is unchanged from above.
# Not dead code: a claude secondmate's native prompt suggestion churns its idle pane, so a paused one usually reaches handle_paused_stale via the hash-changed branch below, but a static paused pane still arrives here.
if [ "$kind" = secondmate ]; then
case "$(pause_state_class "$w" "$task")" in
paused) handle_paused_stale "$w" "$task" "$h" ;;
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ The deeper session-start agent-process liveness probe is separate from that busy
Herdr is experimental and can be selected explicitly or by runtime auto-detection: treehouse remains the worktree provider for it exactly as it is for tmux (herdr is a session provider only), and its full verification - the container shape decision, created-vs-adopted default-tab prune safety, restored-layout husk respawn idempotency, verified CLI facts, ANSI-preserved ghost/placeholder classification through the shared extractor, a verified small-`--lines` capture bug and its workaround, and known gaps - is recorded in `docs/herdr-backend.md`.
Herdr's container shape is workspace-per-home plus tab-per-task: the primary home uses workspace label `firstmate`, secondmate homes use `2ndmate-<secondmate-id>`, and recovery/list-live scopes to the current `FM_HOME`'s workspace.
Zellij is experimental and selected only explicitly: treehouse remains its worktree provider too, and its full verification - the resolved "gaps to verify" list from the original design report, the unconditional-exit-0 CLI quirk and its mitigation, the focus-steal-on-new-tab finding, the home-scoped tab-title collision fix, and known gaps - is recorded in `docs/zellij-backend.md`.
Zellij's container shape is simpler than herdr's: one shared `firstmate` session, one tab per task, with no per-home workspace split; visible tab titles are scoped by the active home label plus a short hash of the resolved `FM_ROOT` path.
Zellij's container shape is simpler than herdr's: one shared `firstmate` session, one tab per task, with no per-home workspace split; visible tab titles are scoped by the active home label plus a short hash of the resolved `FM_ROOT` path, and `--secondmate` spawns are refused, as on Orca and cmux.
Orca is experimental and selected only explicitly: Orca owns both worktree and terminal lifecycle, records `orca_worktree_id=` and `terminal=`, and removes worktrees through `orca worktree rm` only after the usual firstmate teardown checks pass. Its current behavior and limitations are recorded in `docs/orca-backend.md`.
cmux is experimental, GUI-first, macOS-only, and can be selected explicitly or by runtime auto-detection from its primary `CMUX_WORKSPACE_ID` marker plus documented fallback signals: treehouse remains its worktree provider (cmux is a session provider only, like herdr/zellij), and its full verification - the socket access setup requirement with Automation mode recommended, the read-screen-fails-on-a-fresh-surface finding, the close-surface-refuses-on-the-last-surface finding, the source-verified runtime marker and fallback behavior, and known gaps - is recorded in `docs/cmux-backend.md`.
cmux's container shape is one workspace per task with one surface, no per-home container split; workspace titles are scoped by the active home label plus a short hash of the resolved `FM_ROOT` path, and `--secondmate` spawns are refused, mirroring Orca.
cmux's container shape is one workspace per task with one surface, no per-home container split; workspace titles are scoped by the active home label plus a short hash of the resolved `FM_ROOT` path, and `--secondmate` spawns are refused, as on Orca and zellij; `docs/configuration.md` owns that refusal contract, and the secondmate-capable backends are tmux and herdr.
Codex App support is recorded in `docs/codex-app-backend.md`; it is not selectable as a runtime backend.

## Worktrees, not branches in your checkout
Expand Down
6 changes: 5 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ See [`docs/cmux-backend.md`](cmux-backend.md#runtime-auto-detection) for why cmu
Auto-detected herdr or cmux prints a stderr notice naming `config/backend` and `--backend tmux` as opt-outs; auto-detected tmux stays silent to preserve existing default behavior.
Zellij and Orca are never auto-detected; select them by putting the name in a local `config/backend` file, by exporting `FM_BACKEND=<name>`, or by telling the first mate in chat.
Any value other than `tmux`, `herdr`, `zellij`, `orca`, or `cmux` is rejected until another adapter is implemented and verified.
`fm-spawn.sh` accepts `tmux`, `herdr`, `zellij`, `orca`, and `cmux` for ship and scout tasks; `backend=orca` and `backend=cmux` both still refuse `--secondmate` until secondmate launch semantics are designed for each.
`fm-spawn.sh` accepts `tmux`, `herdr`, `zellij`, `orca`, and `cmux` for ship and scout tasks; `backend=orca`, `backend=cmux`, and `backend=zellij` all still refuse `--secondmate`, for two distinct reasons.
`backend=orca` and `backend=cmux` refuse until secondmate launch semantics are designed for each.
`backend=zellij` refuses because it is a plain-screen reader with no ANSI channel, so it cannot strip claude's native prompt-suggestion ghost text through the shared `fm_composer_strip_ghost`.
`codex-app` is not an accepted runtime backend yet; [`docs/codex-app-backend.md`](codex-app-backend.md) owns the Codex App boundary.
The session-start secondmate liveness sweep uses a deeper `fm_backend_agent_alive` probe where verified.
Today that probe can classify tmux and herdr secondmate endpoints as `alive`, `dead`, or `unknown`; zellij, Orca, and cmux report `unknown` until their own agent-process classifiers are verified.
Expand All @@ -74,6 +76,8 @@ For normal herdr operations, `HERDR_SESSION` selects the named session, but dest
Use the explicit guarded cleanup path described in [`docs/herdr-backend.md`](herdr-backend.md) instead of `herdr server stop`.
For normal zellij operations, `FM_ZELLIJ_SESSION` selects the named session and defaults to `firstmate`.
Zellij has no per-home workspace split: primary and secondmate tasks share that one session, and visible tab titles are scoped by the active `FM_HOME` readable label plus a short hash of the resolved `FM_ROOT` path as `fm-<home-label>-<id>`.
That sharing stays live for secondmate homes because `config/backend` is not inherited, so a secondmate home can select zellij itself and spawn its own ship and scout tabs into the same session.
Only a secondmate agent endpoint is legacy here, since `fm-spawn.sh` refuses a `--secondmate` launch on this backend.
Use the guarded cleanup path described in [`docs/zellij-backend.md`](zellij-backend.md) instead of `kill-all-sessions` or `delete-all-sessions`.
cmux has no session layer at all - one workspace per task, in whatever cmux window is open - and its socket password (when configured) is read from local, gitignored `config/cmux-socket-password` under the effective config directory, never committed.
The caller-facing label remains `fm-<id>`, but the actual cmux workspace title is scoped by the active `FM_HOME` readable label plus a short hash of the resolved `FM_ROOT` path as `fm-<home-label>-<id>`.
Expand Down
2 changes: 1 addition & 1 deletion docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ The herdr incident regressions (`tests/fm-backend-herdr.test.sh`'s composer-stat
The captain woke to find away-mode had never injected: 20 escalations buffered, the max-defer wedge marker at 30623s undelivered, the wake queue at 65.
Daemon triage and buffering worked perfectly; the injection leg deferred EVERY attempt with `inject deferred: supervisor pane has pending input (non-empty composer)` - 6524 lifetime occurrences in the daemon log, 2144 of them from the single overnight daemon (`pid 94088`, `backend=herdr`, `target=default:w1:p3`), dominating every other defer reason.

**Root cause.** The primary firstmate runs claude, and claude-code renders a rotating prompt SUGGESTION as ghost text in an otherwise-empty composer (the primary does not set `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`; crews do, via `fm-spawn`, so crew panes never show it).
**Root cause.** The primary firstmate runs claude, and claude-code renders a rotating prompt SUGGESTION as ghost text in an otherwise-empty composer (the primary does not set `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`; ship/scout crewmates do, via `fm-spawn`, so their panes never show it).
Captured read-only from the live primary pane (no Herdr lifecycle touched):

```
Expand Down
2 changes: 1 addition & 1 deletion docs/tmux-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This is the setup guide; for the shared runtime-backend abstraction and selectio

tmux is a terminal multiplexer.
Firstmate gives each crewmate its own tmux window inside a session, so you can attach and watch a task work, or type into its window to intervene directly.
Pick tmux unless you have a specific reason to try an experimental backend (herdr, zellij, Orca, or cmux) - it is the fully verified reference path for secondmate homes, while Orca and cmux are the backends that do not support secondmate spawns.
Pick tmux unless you have a specific reason to try an experimental backend (herdr, zellij, Orca, or cmux) - it is the fully verified reference path for secondmate homes, while Orca, cmux, and zellij are the backends that do not support secondmate spawns.

## Prerequisites

Expand Down
Loading