Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Launching a supported harness inside it for your primary session instantiates yo
## Features

- **One liaison** - you talk only to the first mate; it dispatches, supervises, escalates only real decisions, and reports plain outcomes.
- **A visible crew** - every crewmate works in its own tmux window, Herdr tab, or experimental zellij tab, cmux workspace, or Orca terminal you can watch or type into; the first mate reconciles.
- **A visible crew** - every crewmate works in its own tmux window or Herdr tab, or in an experimental Zellij tab, experimental cmux workspace, or experimental Orca terminal you can watch or type into; the first mate reconciles.
- **Disposable worktrees** - each task runs in a clean [treehouse](https://github.com/kunchenguid/treehouse) git worktree, or an Orca-managed worktree when `backend=orca`, so parallel work on one repo never collides.
- **Two task shapes** - ship tasks deliver authorized changes; scout tasks leave standalone investigation reports when the intake contract warrants separate research.
- **Explicit project modes** - each project ships via `no-mistakes`, `direct-PR`, or `local-only`, with an optional `+yolo` merge-autonomy flag.
Expand Down
2 changes: 1 addition & 1 deletion bin/backends/herdr.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# bin/backends/herdr.sh - the herdr session-provider adapter (EXPERIMENTAL).
# bin/backends/herdr.sh - the verified herdr session-provider adapter.
#
# Design: data/fm-backend-design-d7/herdr-addendum.md ("Interface mapping",
# decisions D1-D6) and the empirical verification recorded in
Expand Down
37 changes: 17 additions & 20 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
# abstraction"). P1 extracted the tmux command sequences that fm-send.sh,
# fm-peek.sh, fm-watch.sh, fm-spawn.sh, and fm-teardown.sh already ran inline
# into bin/backends/tmux.sh, with those SAME command sequences, so the default
# (tmux) path stays byte-identical. P2 adds bin/backends/herdr.sh, an
# EXPERIMENTAL spawn-capable backend behind `--backend herdr`/`FM_BACKEND=herdr`/
# `config/backend`, and behind runtime auto-detection when firstmate itself is
# running inside herdr with no explicit backend setting; see herdr-addendum.md and
# data/fm-backend-design-d7/herdr-verification-p2.md for its empirical basis.
# (tmux) path stays byte-identical. P2 adds bin/backends/herdr.sh, a verified
# spawn-capable backend with its own required CI lane, behind `--backend
# herdr`/`FM_BACKEND=herdr`/`config/backend`, and behind runtime auto-detection
# when firstmate itself is running inside herdr with no explicit backend setting;
# see herdr-addendum.md and data/fm-backend-design-d7/herdr-verification-p2.md for
# its empirical basis.
# P3 adds bin/backends/zellij.sh, also EXPERIMENTAL and spawn-capable, behind
# `--backend zellij`/`FM_BACKEND=zellij`/`config/backend` - NOT behind runtime
# auto-detection (report.md's Open Question #2: start with a dedicated
Expand All @@ -33,8 +34,8 @@
# treats that as `tmux` (fm_backend_of_meta), and fm-spawn.sh does not write
# `backend=tmux` for a default-backend task, so existing and newly spawned
# default-path metas stay byte-identical. Only a task spawned on a non-tmux
# spawn-capable backend, currently experimental herdr, zellij, orca, or cmux,
# carries an explicit `backend=` line.
# spawn-capable backend, currently herdr, zellij, orca, or cmux, carries an
# explicit `backend=` line.
#
# Event-source framing (herdr-addendum "Events as the core abstraction"): a
# backend's supervision surface is conceptually an EVENT SOURCE - it produces
Expand All @@ -56,10 +57,10 @@ FM_BACKEND_CONFIG_DIR="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"

# Verified backend adapters. Extend only after a backend gets its own
# bin/backends/<name>.sh and empirical verification, mirroring AGENTS.md
# section 4's harness-verification discipline. herdr is EXPERIMENTAL (P2;
# data/fm-backend-design-d7/herdr-addendum.md) - verified against the real
# v0.7.1/protocol-14 binary (data/fm-backend-design-d7/herdr-verification-p2.md)
# but newer than tmux's long-proven default path. zellij is EXPERIMENTAL (P3;
# section 4's harness-verification discipline. herdr is verified (P2;
# data/fm-backend-design-d7/herdr-addendum.md) and has its own required CI lane,
# with current coverage in docs/herdr-backend.md and
# docs/verification/runtime-backends.md. zellij is EXPERIMENTAL (P3;
# data/fm-backend-design-d7/report.md "Zellij Backend") - verified against the
# real 0.44.0 binary (docs/zellij-backend.md). orca is EXPERIMENTAL and
# spawn-capable; unlike tmux/herdr/zellij it is also the worktree provider.
Expand Down Expand Up @@ -234,10 +235,9 @@ fm_backend_detect_cmux_app_is_ancestor() {
# per-task `--backend` flag is parsed by the caller (fm-spawn.sh) and takes
# precedence over this resolution entirely; it is not read here. Auto-detect
# fires only when nothing was explicitly configured, so an explicit setting
# always wins. Selecting herdr or cmux via auto-detect prints one loud stderr
# notice (both are experimental); auto-detecting tmux stays silent - it is
# today's default-path behavior and callers must see zero change. The cmux
# notice names the winning signal, so a fallback-detected cmux (bundle id or
# always wins. Auto-detected herdr stays silent like tmux. Selecting cmux via
# auto-detect prints one loud stderr notice because cmux remains experimental;
# the notice names the winning signal, so a fallback-detected cmux (bundle id or
# ancestry, after the claude wrapper stripped CMUX_WORKSPACE_ID) is visibly
# distinct from the primary-marker case.
fm_backend_name() {
Expand All @@ -259,9 +259,6 @@ fm_backend_name() {
# globals survive into the notice below.
if fm_backend_detect >/dev/null; then
detected=$FM_BACKEND_DETECTED
if [ "$detected" = herdr ]; then
echo "NOTICE: auto-detected herdr runtime (HERDR_ENV=1) - spawning into the EXPERIMENTAL herdr backend. Set config/backend or pass --backend tmux to opt out." >&2
fi
if [ "$detected" = cmux ]; then
case "$FM_BACKEND_DETECT_SIGNAL" in
bundle-id) marker="FALLBACK signal __CFBundleIdentifier=$FM_BACKEND_CMUX_BUNDLE_ID; CMUX_WORKSPACE_ID absent, stripped by cmux's bundled claude wrapper" ;;
Expand Down Expand Up @@ -300,8 +297,8 @@ fm_backend_validate_spawn() { # <name>
# single owner of the per-backend dependency delta, so bootstrap follows the
# RESOLVED backend instead of demanding an inactive backend's tools. Each set is:
# - the session-provider CLI itself (tmux/herdr/zellij/orca/cmux);
# - jq, for the JSON-emitting experimental adapters (herdr, zellij, cmux) whose
# spawn/liveness paths parse the backend's JSON output (see each adapter's
# - jq, for the JSON-emitting adapters (herdr, zellij, cmux) whose spawn/liveness
# paths parse the backend's JSON output (see each adapter's
# tool check, e.g. fm_backend_herdr_tool_check);
# - the treehouse worktree provider for every session-provider-only backend
# (tmux, herdr, zellij, cmux); orca owns its own task worktree and terminal,
Expand Down
12 changes: 6 additions & 6 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@
# bin/fm-backend.sh's fm_backend_detect, with cmux fallback details in
# docs/cmux-backend.md),
# then tmux.
# Spawn-capable backends are the reference tmux adapter and experimental
# herdr, zellij, orca, and cmux. Orca owns both the task worktree and
# terminal, so ship/scout Orca spawns do not run treehouse get; cmux is a
# session provider only, exactly like herdr/zellij, so it does. An
# auto-detected herdr or cmux spawn prints a loud stderr notice;
# auto-detected tmux stays silent; zellij and orca are never auto-detected.
# Spawn-capable backends are the reference tmux adapter, verified herdr
# adapter, and experimental zellij, orca, and cmux adapters. Orca owns both
# the task worktree and terminal, so ship/scout Orca spawns do not run
# treehouse get; cmux is a session provider only, exactly like herdr/zellij,
# so it does. Auto-detected herdr stays silent like tmux; auto-detected cmux
# prints a loud stderr notice; 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ The runtime backend is the session-provider layer below firstmate's scripts.
It owns task endpoint creation, bounded capture, text/key sends, current-path reads for spawn-time worktree discovery when the backend does not create the worktree itself, live-window fallback lookup, agent-process liveness probes where verified, and endpoint teardown.
`bin/fm-backend.sh` centralizes backend selection, `state/<id>.meta` helpers, metadata-only cleanup identity validation, selector resolution, and operation dispatch; `bin/backends/tmux.sh` is the verified reference adapter ([`docs/tmux-backend.md`](tmux-backend.md)), `bin/backends/herdr.sh` (P2) has its own required CI lane ([`docs/herdr-backend.md`](herdr-backend.md)), and `bin/backends/zellij.sh` (P3), `bin/backends/orca.sh` (P4), and `bin/backends/cmux.sh` (P5) remain experimental task-spawn adapters with no dedicated real-backend CI lane.
[`configuration.md`](configuration.md#runtime-backend-configbackend--fm_backend) owns new-spawn backend selection precedence and authorization.
Runtime auto-detection is innermost-first: `$TMUX` wins over `HERDR_ENV=1`, which wins over cmux's primary `CMUX_WORKSPACE_ID` marker and documented fallback signals; auto-detected herdr or cmux prints a one-time opt-out notice, auto-detected tmux stays silent, and zellij and orca are never auto-detected (only explicit selection).
Runtime auto-detection is innermost-first: `$TMUX` wins over `HERDR_ENV=1`, which wins over cmux's primary `CMUX_WORKSPACE_ID` marker and documented fallback signals; auto-detected Herdr stays silent like tmux, while auto-detected cmux prints a one-time notice because cmux remains experimental, and zellij and orca are never auto-detected (only explicit selection).
Unknown backend names fail loudly.
For compatibility, default tmux tasks do not write `backend=tmux`; every reader treats a missing `backend=` field as `tmux`.
`fm-watch.sh` decides each window's busy state through the semantic contract above rather than by polling the backend for rendered text.
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Treehouse remains the worktree provider for tmux, herdr, zellij, and cmux, since
New spawns choose the backend in this order: an explicit `--backend` flag that current authority for that exact task alone has authorized (a present captain instruction or the task's own accepted brief; never later-task precedent by analogy), then `FM_BACKEND`, then the first non-empty line of local gitignored `config/backend`, then runtime auto-detection from `$TMUX`, `HERDR_ENV=1`, or cmux runtime signals, then default `tmux`.
If more than one runtime marker is present, detection resolves innermost-first: `$TMUX` is checked before `HERDR_ENV=1`, which is checked before cmux's primary `CMUX_WORKSPACE_ID` marker and its documented fallback signals - tmux or herdr started from inside a cmux terminal is the innermost, currently-executing layer, while cmux itself (a terminal application, not a nestable multiplexer) is always checked last.
See [`docs/cmux-backend.md`](cmux-backend.md#runtime-detection) for why cmux can be selected when `CMUX_WORKSPACE_ID` is absent.
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.
Auto-detected Herdr stays silent like tmux, while auto-detected cmux prints a stderr notice naming `config/backend` and `--backend tmux` because cmux remains experimental.
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.
Expand Down
2 changes: 1 addition & 1 deletion docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Select Herdr with local `config/backend` containing `herdr`, `FM_BACKEND=herdr`
A remote second-mate agent is the one case with no choice: it always runs on Herdr, and [`remote-secondmates.md`](remote-secondmates.md) owns that requirement and the readiness its host must meet.
It is also auto-detected when the primary runs natively under `HERDR_ENV=1` and is not inside tmux.
A tmux pane nested inside Herdr resolves to tmux because the innermost multiplexer wins.
An auto-detected Herdr spawn prints an opt-out notice.
An auto-detected Herdr spawn stays silent, matching the verified tmux default path.

Spawn stops before creating a Herdr container or acquiring a task worktree when `herdr`, `jq`, or the protocol floor is unavailable.
No separate first-run provisioning is required.
Expand Down
2 changes: 1 addition & 1 deletion docs/tmux-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The universal harness and toolchain requirements are in [`configuration.md`](con

tmux is the hard default when no explicit setting or runtime auto-detection selects another backend.
Select it explicitly with local `config/backend` containing `tmux`, with `FM_BACKEND=tmux` for one launch, or by asking Firstmate to use tmux.
An explicit selection is also the opt-out from Herdr or cmux runtime auto-detection.
Explicit tmux selection via `config/backend` or `--backend tmux` overrides runtime auto-detection.

No provisioning is required before the first task.

Expand Down
16 changes: 11 additions & 5 deletions tests/fm-backend-autodetect-smoke.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ assert_contains_local() { # <haystack> <needle> <msg>
*) fail "$3"$'\n'"--- got ---"$'\n'"$1" ;;
esac
}
assert_not_contains_local() { # <haystack> <needle> <msg>
case "$1" in
*"$2"*) fail "$3"$'\n'"--- got ---"$'\n'"$1" ;;
*) : ;;
esac
}

command -v herdr >/dev/null 2>&1 || { echo "skip: herdr not found"; exit 0; }
command -v jq >/dev/null 2>&1 || { echo "skip: jq not found (required by the herdr adapter)"; exit 0; }
Expand Down Expand Up @@ -120,11 +126,11 @@ env -u TMUX -u FM_BACKEND PATH="$PATH" HERDR_ENV=1 \
status=$?
[ "$status" -eq 0 ] || fail "fm-spawn.sh did not succeed auto-detecting herdr"$'\n'"--- stdout ---"$'\n'"$(cat "$OUT_FILE")"$'\n'"--- stderr ---"$'\n'"$(cat "$ERR_FILE")"

assert_contains_local "$(cat "$ERR_FILE")" "NOTICE" \
"fm-spawn.sh did not print the auto-detect notice to stderr when selecting herdr"
assert_contains_local "$(cat "$ERR_FILE")" "EXPERIMENTAL herdr backend" \
"fm-spawn.sh's auto-detect notice did not flag herdr as experimental"
pass "real herdr: fm-spawn.sh auto-detects herdr from HERDR_ENV=1 (no explicit config) and prints the loud notice"
assert_not_contains_local "$(cat "$ERR_FILE")" "EXPERIMENTAL" \
"fm-spawn.sh's Herdr auto-detection retained the obsolete experimental label"
assert_not_contains_local "$(cat "$ERR_FILE")" "--backend tmux to opt out" \
"fm-spawn.sh's Herdr auto-detection retained the obsolete tmux opt-out steer"
pass "real herdr: fm-spawn.sh auto-detects verified herdr from HERDR_ENV=1 (no explicit config) without an opt-out steer"

META="$STATE/$ID.meta"
[ -f "$META" ] || fail "fm-spawn.sh did not write a meta file for $ID"
Expand Down
13 changes: 5 additions & 8 deletions tests/fm-backend.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,9 @@ test_backend_name_cmux_fallback_notice() {

# fm_backend_name's auto-detect step: fires only when FM_BACKEND/config/backend
# are both absent, selects between the three markers exactly as
# fm_backend_detect does, and is loud only when it selects herdr or cmux -
# never when it selects tmux (today's default-path behavior must stay
# byte-for-byte silent).
# fm_backend_detect does, and is loud only when it selects experimental cmux -
# never when it selects verified herdr or tmux (today's default-path behavior
# must stay byte-for-byte silent).
test_backend_name_autodetect_notice() {
local dir cfg out errfile

Expand All @@ -417,10 +417,7 @@ test_backend_name_autodetect_notice() {
: > "$errfile"
out=$(unset TMUX CMUX_WORKSPACE_ID; HERDR_ENV=1 FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
[ "$out" = herdr ] || fail "fm_backend_name should auto-detect herdr from HERDR_ENV=1, got '$out'"
assert_contains "$(cat "$errfile")" "EXPERIMENTAL herdr backend" \
"fm_backend_name did not print a loud notice when auto-detecting herdr"
assert_contains "$(cat "$errfile")" "config/backend" \
"fm_backend_name's auto-detect notice did not name the opt-out"
[ ! -s "$errfile" ] || fail "fm_backend_name must keep verified Herdr auto-detection silent"$'\n'"$(cat "$errfile")"

: > "$errfile"
out=$(unset HERDR_ENV CMUX_WORKSPACE_ID; TMUX='fake,1,0' FM_BACKEND='' FM_BACKEND_CONFIG_DIR="$cfg" fm_backend_name 2>"$errfile")
Expand All @@ -447,7 +444,7 @@ test_backend_name_autodetect_notice() {
[ "$out" = tmux ] || fail "nested tmux-in-cmux should auto-detect tmux (innermost first), got '$out'"
[ -s "$errfile" ] && fail "nested tmux-in-cmux auto-detect (result tmux) must stay silent"$'\n'"$(cat "$errfile")"

pass "fm_backend_name: auto-detect selects herdr or cmux (loud notice) or tmux (silent, including nested tmux-in-herdr/tmux-in-cmux)"
pass "fm_backend_name: verified Herdr and tmux stay silent while experimental cmux remains loud"
}

# Explicit configuration (FM_BACKEND env or config/backend) always wins over
Expand Down
4 changes: 2 additions & 2 deletions tests/fm-session-start.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1074,8 +1074,8 @@ SH
"an explicit Herdr home should not be reported as auto-detected"
else
out=$(TMUX='' HERDR_ENV=1 BASH_ENV="$mask" run_session_start "$home" "$root" "$fakebin:$BASE_PATH")
assert_contains "$out" "NOTICE: auto-detected herdr runtime (HERDR_ENV=1)" \
"session start did not preserve the Herdr runtime auto-detection fallback"
assert_not_contains "$out" "NOTICE: auto-detected herdr runtime" \
"session start should keep verified Herdr runtime auto-detection silent"
fi
assert_contains "$out" "SESSION START - $home" "the real session-start path did not run in the throwaway home"
assert_not_contains "$out" "MISSING: tmux" "Herdr session start falsely required masked tmux"
Expand Down
Loading