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
29 changes: 16 additions & 13 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2653,16 +2653,11 @@ fm_backend_herdr_expected_label_matches() { # <target> [expected-label]
# launch-grade fm_backend_herdr_server_ensure used to spawn new crewmates.
#
# Reading a pane or sending keys to it does not care how the server was
# launched - the pane already exists and the server is up. It only needs the
# server to be running and adapter-owned (this HOME's own certificate names the
# live pid), which fm_backend_herdr_server_adapter_owned proves without the
# closed-shell launch certification. This is what keeps peek/steer working when
# FirstMate itself runs INSIDE the herdr session it manages (HERDR_ENV=1): that
# server was not launched through the crewmate adapter's own closed-shell path, so
# closed_shell_environment_ready is false and the full ensure would try to
# restart+recertify - impossible while the session is occupied by live crewmates and
# FirstMate itself. The SPAWN path deliberately keeps the strict ensure so a new
# crewmate still launches only in a certified closed-shell server.
# launched - the pane already exists and the server is up. Native-agent
# production servers therefore need only report running, matching
# fm_backend_herdr_server_ensure after the native cutover. The explicit legacy
# certificate lab retains its adapter-ownership proof so that retired lifecycle
# remains testable without constraining ordinary read/steer.
fm_backend_herdr_server_reachable_for_readsteer() { # <session>
local session=$1 running
if fm_backend_herdr_test_hooks_enabled \
Expand All @@ -2672,6 +2667,9 @@ fm_backend_herdr_server_reachable_for_readsteer() { # <session>
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null \
| fm_backend_herdr_control_jq -r '.server.running // false' 2>/dev/null)
[ "$running" = true ] || return 1
if ! fm_backend_herdr_server_certificate_required; then
return 0
fi
fm_backend_herdr_server_adapter_owned "$session"
}

Expand Down Expand Up @@ -3269,9 +3267,14 @@ fm_backend_herdr_events_capable() { # <session>
case "$protocol" in ''|*[!0-9]*) return 1 ;; esac
[ "$protocol" -ge "$FM_BACKEND_HERDR_MIN_EVENTS_PROTOCOL" ] || return 1
schema=$(fm_backend_herdr_scrubbed_exec "$herdr_bin" api schema --json 2>/dev/null) || return 1
printf '%s' "$schema" | fm_backend_herdr_control_grep -Fq 'events.subscribe' || return 1
printf '%s' "$schema" | fm_backend_herdr_control_grep -Fq 'pane.agent_status_changed' || return 1
return 0
# Parse once and consume the complete schema. Early-exit grep -q pipelines
# close their input as soon as a match is found, which makes the upstream
# printf hit EPIPE and turns a real capability into a false negative whenever
# the caller inherits pipefail.
printf '%s' "$schema" | fm_backend_herdr_control_jq -e '
any(.. | scalars; . == "events.subscribe")
and any(.. | scalars; . == "pane.agent_status_changed")
' >/dev/null 2>&1
}

# fm_backend_herdr_normalize_event: THE single normalize point (report section 5
Expand Down
11 changes: 9 additions & 2 deletions bin/fm-ff-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ default_branch() {
# section 8) still yields the true default-branch tip instead of propagating a
# stray feature branch to the fleet. Echoes the commit SHA, or returns 1.
primary_head_commit() {
local root=$1 default
local root=$1 default ref
default=$(default_branch "$root") || return 1
git -C "$root" rev-parse --verify --quiet "refs/heads/$default^{commit}" 2>/dev/null || return 1
ref="refs/heads/$default"
if ! git -C "$root" show-ref --verify --quiet "$ref"; then
# CI-style and isolated worktree checkouts may intentionally omit the local
# default ref. Follow the resolved remote default instead of a detached PR
# merge commit or feature HEAD.
ref="refs/remotes/origin/$default"
fi
git -C "$root" rev-parse --verify --quiet "$ref^{commit}" 2>/dev/null || return 1
}

resolve_path() {
Expand Down
24 changes: 22 additions & 2 deletions bin/fm-home-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ CHECKOUT_LOCK_ROOT=$(fm_checkout_lock_root "$CHECKOUT_STATE_BASE")
. "$SCRIPT_DIR/fm-account-routing-lib.sh"
# shellcheck source=bin/fm-gate-refuse-lib.sh
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
# shellcheck source=bin/fm-tangle-lib.sh
. "$SCRIPT_DIR/fm-tangle-lib.sh"
fm_refuse_if_gate_agent

usage() {
Expand Down Expand Up @@ -431,7 +433,7 @@ acquire_treehouse_home() {
}

ensure_home() {
local id=$1 requested=$2 home
local id=$1 requested=$2 home default default_ref default_tip source_origin
if [ "$requested" = "-" ]; then
home=$(acquire_treehouse_home "$id")
verify_firstmate_home "$home"
Expand All @@ -444,7 +446,25 @@ ensure_home() {
[ -d "$home" ] || { echo "error: $home exists and is not a directory" >&2; return 1; }
else
mkdir -p "$(dirname "$home")"
git clone --quiet "$FM_ROOT" "$home"
default=$(fm_default_branch "$FM_ROOT") || {
echo "error: cannot determine the active firstmate home's default branch" >&2
return 1
}
default_ref="refs/remotes/origin/$default"
if ! git -C "$FM_ROOT" show-ref --verify --quiet "$default_ref"; then
default_ref="refs/heads/$default"
fi
default_tip=$(git -C "$FM_ROOT" rev-parse --verify "$default_ref^{commit}") || {
echo "error: cannot resolve the active firstmate home's default branch tip" >&2
return 1
}
git clone --quiet --no-checkout "$FM_ROOT" "$home"
git -C "$home" checkout --quiet -B "$default" "$default_tip"
git -C "$home" remote set-head origin "$default"
source_origin=$(git -C "$FM_ROOT" remote get-url origin 2>/dev/null || true)
if [ -n "$source_origin" ]; then
git -C "$home" remote set-url origin "$source_origin"
fi
fi
verify_firstmate_home "$home"
}
Expand Down
22 changes: 20 additions & 2 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ spawn_managed_endpoint_state() { # <backend> <target> <label> <kind> <secondmat
fi
}

spawn_managed_agent_alive() { # <backend> <target> <label> <kind> <secondmate-home> [recorded-scoped-target]
local backend=$1 target=$2 label=$3 kind=$4 secondmate_home=${5:-} recorded_scoped_target=${6:-} endpoint_home
endpoint_home=$(fm_backend_endpoint_home "$backend" "$kind" "$FM_HOME" "$secondmate_home")
if [ "$endpoint_home" != "$FM_HOME" ]; then
( unset FM_ROOT_OVERRIDE; FM_HOME="$endpoint_home" FM_ROOT="$endpoint_home" fm_backend_agent_alive "$backend" "$target" "$label" "$recorded_scoped_target" )
else
fm_backend_agent_alive "$backend" "$target" "$label" "$recorded_scoped_target"
fi
}

git_repository_probe() (
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_CEILING_DIRECTORIES
unset GIT_DISCOVERY_ACROSS_FILESYSTEM GIT_IMPLICIT_WORK_TREE GIT_PREFIX
Expand Down Expand Up @@ -3494,8 +3504,16 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ] && [ "$RECOVERY_ACCOUNT"
sleep 1
done
if [ -z "${p:-}" ] || [ "$(real_path_or_raw "$p")" != "$WT_REAL" ]; then
echo "error: task endpoint did not start in leased worktree $WT within 60s; inspect window $T" >&2
exit 1
READINESS_AGENT_STATE=$(spawn_managed_agent_alive "$BACKEND" "$T" "fm-$ID" "$KIND" "$PROJ_ABS" 2>/dev/null)
if [ -n "${p:-}" ]; then
echo "error: task endpoint reported unexpected path $p instead of leased worktree $WT within 60s (liveness=$READINESS_AGENT_STATE); inspect window $T" >&2
exit 1
elif [ "$READINESS_AGENT_STATE" = alive ]; then
echo "warning: task endpoint path was not readable in leased worktree $WT within 60s, but its harness agent is confidently alive; accepting window $T" >&2
else
echo "error: task endpoint did not start in leased worktree $WT within 60s and no live harness agent was confirmed (liveness=$READINESS_AGENT_STATE); inspect window $T" >&2
exit 1
fi
fi
fi
if [ -z "$WT" ] && [ "$BACKEND" = orca ]; then
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ Treehouse-acquired secondmate homes receive the same proof before seeding.
They use the same locked, bounded acquisition entrypoint as ordinary task worktrees.
Explicit secondmate homes are refreshed and must independently match the same live upstream or local default tip before seeding and again before launch.
A stale, dirty, or uninspectable acquisition remains durably leased without forced return and is surfaced for manual recovery.
After a new direct ship/scout endpoint starts, spawn polls its reported current directory for the leased worktree for up to 60 seconds.
If that path remains unreadable, a backend liveness probe may independently accept the spawn only when it confidently confirms a real harness-agent process; `dead`, `unknown`, and backends without a verified agent classifier still fail and enter the normal rollback path.
Any concrete path other than the leased worktree always fails even when the agent is live.
A confirmed-live path-probe timeout commits ordinary successful task metadata without `direct_spawn_cleanup=`, `direct_spawn_artifacts=`, or `rollback_pending=` markers.
If an unmanaged spawn fails after publishing metadata or task artifacts, it restores the prior task generation.
Any failed unmanaged spawn returns its acquired worktree only after its endpoint is confirmed absent or known never to have been created, and only when repository identity, cleanliness, expected detached tip, guarded Treehouse return, and completed removal are all proven.
Otherwise it retains the worktree and cleanup metadata for explicit teardown.
Expand Down
87 changes: 85 additions & 2 deletions docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Herdr is [an agent-native terminal multiplexer](https://herdr.dev) with a socket
Originally verified against herdr 0.7.1, protocol 14, on macOS aarch64; the latest dated evidence below uses herdr 0.7.3, protocol 16.
Current real-herdr verification uses isolated `HERDR_SESSION` names plus the guarded teardown helper in `tests/herdr-test-safety.sh`.
A 2026-07-02 cleanup bug proved that `HERDR_SESSION` alone is not a safe way to target destructive session cleanup; see "Session targeting: the `--session` flag, not `HERDR_SESSION` alone" below.
All real-herdr verification in this document uses isolated sessions and guarded cleanup; the captain's default herdr session and live tmux fleet were never intended targets.
Real-herdr lifecycle verification in this document uses isolated sessions and guarded cleanup.
The 2026-07-29 read/steer incident was instead verified without lifecycle mutation against the fixing task's own pane in the already-running default session.

## Setup

Expand Down Expand Up @@ -248,9 +249,91 @@ Under the explicit legacy certified-lifecycle lab path, a full current-release p
For an older adapter-owned certificate, the adapter queries the explicitly named session's workspace, tab, and pane collections.
An exact-empty session is stopped only after two complete empty proofs plus repeated process-ownership proof, then detached-launched and recertified before the lock is released.
An occupied session - one reporting any workspace, tab, or pane - is not restarted; routing continues in that same running session and may create the workspace for the native-agent spawn path.
Unreadable or indeterminate state still refuses before workspace mutation, and manual or uncertified ownership still fails closed.
Unreadable or indeterminate state still refuses before workspace mutation, and the explicit legacy lab still rejects manual or uncertified ownership.
This convergence path never uses ambient `herdr server stop`, never deletes session state (including `default`), and never moves Herdr ownership into a captain launcher.

## Incident (2026-07-29): native cutover left read and steer behind the retired certificate gate

Every `fm-send.sh` steer to a live Herdr crewmate failed even though Herdr reported the exact recorded pane ids as live and direct `herdr agent send`, `herdr pane send-text`, `herdr pane send-keys`, and `herdr pane read` calls succeeded.
The same stale precondition made `fm-peek.sh` return zero pane lines.
The earlier peek report was therefore reproducible, not stale.

The regression was reproduced against Herdr 0.7.3, protocol 16, and the fixing codex task's own live pane `default:w6:pFZ`.
No Herdr session, workspace, tab, pane, or agent lifecycle command was run.
A task-local `FM_HOME` fixture recorded the live pane and label so the public selector paths were exercised without reading or mutating another firstmate home's files.

Pre-fix `fm-send.sh` evidence:

```text
$ FM_HOME=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home \
FM_SEND_SETTLE=0 bin/fm-send.sh fm-herdr-send-fix-h7 \
'[pre-fix live verification probe: do not alter task direction]'
error: text not sent to default:w6:pFZ (herdr send failed; tried meta=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home/state/fm-herdr-send-fix-h7.meta; backend=from-meta)
exit 1

$ herdr agent get w6:pFZ
{"result":{"agent":{"agent":"codex","agent_status":"working","name":"fm-fm-herdr-send-fix-h7","pane_id":"w6:pFZ",...}}}
```

Pre-fix `fm-peek.sh` returned exit 1 and zero stdout lines against the same recorded target.
Tracing stopped in `fm_backend_herdr_server_adapter_owned`: the default server had no closed-shell certificate, so neither path reached its Herdr read or mutation call.

The root cause was an incomplete native-agent cutover.
Commit `cc0c56c` correctly made `fm_backend_herdr_server_ensure` reuse any running production server because `agent start --env` owns new-agent environment isolation, but `fm_backend_herdr_server_reachable_for_readsteer` still required the retired server certificate added before that cutover.
Production read and steer now require the exact requested session to report running, matching spawn, while the explicitly enabled legacy certificate lab retains the old ownership proof.

The separate `printf: write error: Broken pipe` lead was real but not the read/steer root cause.
`fm_backend_herdr_events_capable` piped the approximately 220 KB schema string into two early-exit `grep -q` calls.
With inherited `pipefail`, the first match closed the consumer before `printf` finished and made a capable Herdr build return false.
The probe now parses the complete schema once with `jq`, eliminating the early close and the diagnostic.

The focused regression fails against the pre-fix adapter:

```text
$ FM_TEST_FOCUSED=readsteer-native-cutover tests/fm-backend-herdr.test.sh
not ok - native read/steer should accept a running server without the retired closed-shell certificate: expected exit 0, got 1
```

The same regression passes after the fix:

```text
$ FM_TEST_FOCUSED=readsteer-native-cutover tests/fm-backend-herdr.test.sh
ok - fm_backend_herdr_server_reachable_for_readsteer: native-agent servers need only be running
ok - fm_backend_herdr_server_reachable_for_readsteer: the explicit legacy lab retains certificate enforcement
ok - fm_backend_herdr_events_capable: consumes the full schema without a broken pipe
```

The repaired public paths were then exercised against that same live pane:

```text
$ FM_HOME=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home \
bin/fm-peek.sh fm-herdr-send-fix-h7 12
exit 0
stdout lines: 11

$ FM_HOME=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home \
FM_SEND_SETTLE=0 bin/fm-send.sh fm-herdr-send-fix-h7 \
'LIVE_E2E_H7: continue the current herdr send-fix task without changing scope; preserve this token in the transcript.'
exit 0

$ FM_HOME=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home \
bin/fm-peek.sh fm-herdr-send-fix-h7 24 \
> /Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home/peek-after-send.out
$ grep -F LIVE_E2E_H7 /Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home/peek-after-send.out
↳ LIVE_E2E_H7: continue the current herdr send-fix task without changing scope; preserve this token in the

$ FM_HOME=/Users/dongkeun/.treehouse/firstmate-21f368/6/firstmate/.task-home \
bash -o pipefail -c '. bin/backends/herdr.sh; fm_backend_herdr_events_capable default; printf "events_rc=%s\n" "$?"'
events_rc=0
stderr bytes: 0
```

The existing submission regressions still cover first-Enter autocomplete swallowing, Enter-only retry, native `working` or `blocked` confirmation, and preservation of unsubmitted text on exhausted retries.

The spawn-readiness implementation and automated coverage built on this repair are green, but no live end-to-end Herdr spawn was run because the fixing task was not provisioned as a Herdr lifecycle lab.
Real backend timing, native process registration, and post-land behavior of the next ordinary production spawn therefore remain unproven.
Firstmate will verify the next ordinary spawn after landing, and an isolated Herdr-lab lifecycle exercise remains a separate follow-up.

## Incident (2026-07-13): the ASCII request separator erased the secondmate marker

A routed request reached a Pi/Herdr secondmate without the visible `[fm-from-firstmate]` label, so the secondmate correctly treated it as direct captain conversation and returned nothing to the parent status path.
Expand Down
Loading
Loading