Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
2d833ff
fix: treat quiet records as attended across supervision (#6064)
kunchenguid Sep 29, 2026
00679ae
fix: report supervision host latches accurately in away return briefs…
kunchenguid Sep 29, 2026
40e981d
fix: shorten Claude Code Calm supervision note label (#6086)
kunchenguid Sep 29, 2026
b5fdf74
feat(bin): add a disposable live supervision lab builder (#6037)
kunchenguid Sep 29, 2026
46d58d6
fix: keep watcher arms and reply listeners alive through slow cycles …
kunchenguid Sep 29, 2026
c5f48e4
fix(bin): retry fm-pr-merge a bounded number of times when GitHub mer…
karotkriss Sep 29, 2026
260c4f0
fix(bin): converge every open owner onto a known terminal contributio…
karotkriss Sep 29, 2026
0a2cdf9
feat: enable supervision host by default for Claude primaries (#6124)
kunchenguid Sep 29, 2026
5bddfc4
fix(bin): create the state dir on a fresh primary before the session-…
karotkriss Sep 29, 2026
1f2c954
fix(bin): measure pending-reply grace from turn completion, not deliv…
karotkriss Sep 29, 2026
a774c44
fix(bin): stop provider-table lookup from writing broken-pipe errors …
tiago-peixoto Sep 29, 2026
e2668de
fix: restore portable CI behavior across Pi rendering and remote prov…
kunchenguid Sep 30, 2026
b3d4133
fix: confirm Lavish board replies before worker handoff (#6169)
kunchenguid Sep 30, 2026
12e90e1
fix: inherit supervision host opt-out across secondmates (#6154)
kunchenguid Sep 30, 2026
c35b9a6
fix: reduce supervision exit latency and stabilize host tests (#6179)
kunchenguid Sep 30, 2026
eb77f02
ci: rebalance portable test groups and enforce a packing budget (#6192)
kunchenguid Sep 30, 2026
bd74468
fix(bin): run no repository hook when core.hooksPath is empty (#6216)
karotkriss Sep 30, 2026
65c75b0
fix(bin): let a stale record on a reassigned slot retire records-only…
karotkriss Sep 30, 2026
23e5584
fix(bin): keep the steering doorbell short under deep homes (#6240)
karotkriss Sep 30, 2026
fd325b1
feat(bin): add opt-in config/wait-no-turns so a waiting worker spends…
tiago-peixoto Sep 30, 2026
aedb7bb
fix(bin): close Gerrit-landed backlog items with the change URL as a …
slnkjthien Oct 1, 2026
549e07f
fix: reduce remote-job and supervision polling churn (#6255)
kunchenguid Oct 1, 2026
589ccec
fix(bin): load backend sibling libraries when sourced under zsh (#6221)
guanchengh-lgtm Oct 1, 2026
f593060
fix(bin): exclude a remote mate's own parent channel from self-home s…
kesslerio Oct 1, 2026
b5d9061
fix(bin): document accepted contribution verdict actors (#6307)
mremond Oct 1, 2026
8f756bb
fix(bin): recognize clone roots across path spelling differences (#6306)
mremond Oct 1, 2026
349e189
test: preserve Pi calm transcript captures with Pi 1.0 (#6338)
kunchenguid Oct 1, 2026
6af8331
fix(bin): preserve hold reasons and reject invalid completion invento…
mremond Oct 1, 2026
8690c41
fix: reclaim orphaned watcher arms on the next park (#6335)
kunchenguid Oct 2, 2026
241d461
fix(bin): restore downtime on supervision-host hand-back when the suc…
tiago-peixoto Oct 2, 2026
65e2aa4
fix: reduce remote-job polling process churn (#6363)
kunchenguid Oct 2, 2026
f50e9cd
fix(bin): recognize titled Claude top rules and preserve grey slash c…
RibatTRW Oct 2, 2026
87fa81b
fix: prevent Pi trust prompts in seeded secondmate homes (#6387)
kunchenguid Oct 2, 2026
d719ef3
fix(bin): retry ShellCheck roots that hit the memory ceiling without …
tiago-peixoto Oct 2, 2026
e31bc6e
fix(pi): restore watcher continuity across successor gaps and make ex…
RibatTRW Oct 3, 2026
1f3e769
fix(pi): hide duplicate assistant finals from hidden processing retri…
zestysoft Oct 3, 2026
fede619
test(pi): accept Pi 1.0.1's renamed HTML export renderer lookup (#6530)
kunchenguid Oct 4, 2026
2e659ff
fix(bin): tolerate transient quota read failures (#6490)
jokim1 Oct 4, 2026
918a5bf
fix(bin): clarify scratch guidance and dirty teardown refusals (#6505)
mremond Oct 4, 2026
42dd906
fix(bin): escalate inbox instructions blocked by busy workers (#6518)
mremond Oct 4, 2026
9ea0c41
fix: prevent unnecessary remote worker turnover (#6431)
kunchenguid Oct 4, 2026
f470a01
fix: speed up remote-job sequence claim cleanup (#6575)
kunchenguid Oct 4, 2026
98d80a1
feat(bin): record no-mistakes pipeline spend per task at teardown, op…
tiago-peixoto Oct 5, 2026
6a7b391
feat(bin): start tasks from a named base branch (#6442)
lakshyads05 Oct 5, 2026
e5b9ddd
fix(bin): name the Firstmate skill file as a fallback in the worker r…
tiago-peixoto Oct 5, 2026
4c0331a
feat(bin): retire a contribution whose forge object is permanently go…
shaptonstahl Oct 5, 2026
332de27
test: extend remote-reply whole-log recapture waits (#6639)
cloud-practitioner Oct 5, 2026
70f2ed3
fix(bin): reopen a pending-reply escalation after its resolve (#6654)
tiago-peixoto Oct 5, 2026
2d7daa9
fix: refuse a confirming Enter on the Claude background-task exit pic…
tiago-peixoto Oct 6, 2026
5838f10
fix: restore timeout watchdog compatibility with macOS Bash 3.2 (#6028)
M00NLIG7 Oct 6, 2026
e06a46f
fix(project-management): use subshell form for Initialize command (#6…
AokDesu Oct 6, 2026
99da16d
feat(bin): add armable daily startup growth check (#6725)
Freudator86 Oct 6, 2026
2e8cd9e
fix(bin): reopen the remote-reply continuity decision on a later brea…
tiago-peixoto Oct 7, 2026
e7c3bdc
fix(control): drop busy_gen from the task record when an incarnation …
tiago-peixoto Oct 7, 2026
237e1cf
test: cover PID collisions in harness ancestry detection (#6484)
ironerumi Oct 7, 2026
53b5bc1
fix: share Pi Calm's working-ship widget slot (#1854)
ironerumi Oct 7, 2026
23e71b3
feat(bin): add opt-in --herdr-resume-lock-wait to fm-spawn (#6649)
Thoughts-One Oct 7, 2026
0f34fab
fix(bin): let TERM stop a watcher blocked in a pane capture on bash 3…
svycka Oct 7, 2026
47aff86
feat: add per-home worker tool exclusions (#6750)
cloud-practitioner Oct 7, 2026
ac0811c
feat(bin): defer spawns beyond a declared per-project capacity, opt-i…
tiago-peixoto Oct 7, 2026
228b27d
fix(bin): name the Lavish read message count by the same label as its…
falkoro Oct 7, 2026
8fa2538
fix(herdr): make the presentation lock namespace per OS account (#6780)
asser-akh Oct 7, 2026
0c83b3f
Fix OpenCode arm plugin to decide with the shared supervision predica…
dubiousenvelope Oct 7, 2026
9b85a95
fix(bin): resolve a pending reply only from its own task's status lin…
falkoro Oct 7, 2026
2ce57d0
docs(skills): index the six missing agent-only skill triggers (#6784)
falkoro Oct 7, 2026
329ad4e
test: make agent process fixtures compatible with multicall sleep (#6…
0x7067 Oct 8, 2026
3c58ec8
fix(bin): keep the supervision host's successor watcher alive after t…
menidi Oct 8, 2026
0ec1c5a
test: make tmux Claude readiness checks independent of permission foo…
0x7067 Oct 8, 2026
b062eb9
test(herdr): accept safe exit refusals and clean lab once (#6818)
0x7067 Oct 8, 2026
19fcbbd
test: stabilize watcher lock race fixtures (#6887)
ironerumi Oct 8, 2026
b75658b
fix(bin): keep herdr composer probes observable-only without autostar…
viky-01 Oct 9, 2026
fb75c1f
fix(tests): clear inherited Git repository location in fixture helper…
falkoro Oct 9, 2026
4b9be5e
merge: record upstream through 2ce57d00 as merged (content landed squ…
MrGTV-love Oct 10, 2026
41fbccd
merge: bring in upstream main through fb75c1f9 (upstream #6814, #6787…
MrGTV-love Oct 10, 2026
26fe5ff
fix: restore two upstream hunks the squashed sync (#43) dropped
MrGTV-love Oct 10, 2026
20eaa66
test: retry the interrupted winner read in the stale-lock steal race
MrGTV-love Oct 11, 2026
d6613ba
merge: bring origin/main forward into the upstream sync branch
MrGTV-love Oct 11, 2026
a095da7
merge: bring origin/main forward into the upstream sync branch
MrGTV-love Oct 11, 2026
c3df226
no-mistakes(test): test: align attended live e2e with lab setup and t…
MrGTV-love Oct 11, 2026
ddb416c
no-mistakes(document): Refresh attended live e2e description in super…
MrGTV-love Oct 11, 2026
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 .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Hold-for-return is the default and the only reach profile this release records:
Destructive, irreversible, and security-sensitive actions are never pre-authorizable whatever the words say, and ask-user findings keep the `ask-user-authority` policy unless the words pre-answer the exact decision; anything else that needs the captain holds for their return.
- On Pi, main is parked and the supervision branch handles every safe actionable wake under main's standing authority, through the same guarded scripts main would use: any pull request green at its live head may merge (which one the words meant is the branch's reading), queued work whose blockers cleared - already queued, or filed by the branch because the words explicitly call for it - dispatches within the spend cap, and a decision is answered with the captain's own pre-stated answer or under `ask-user-authority`.
Anything else holds for the return, a red merge never proceeds while away, local-only landing always waits for the captain, and only a wake the branch declines (including a broken branch or unsafe scan) or a watcher failure wakes main (`docs/pi-supervision-branch.md` "Postures").
- On a non-Pi home with `config/supervision-host`, the host's engine is that branch under the same rules; the [supervision protocol](../../../docs/supervision-protocols/supervision-host.md) owns how a handed-back wake reaches main.
- On a non-Pi home that runs the supervision host, the host's engine is that branch under the same rules; the [supervision protocol](../../../docs/supervision-protocols/supervision-host.md) owns how a handed-back wake reaches main.
A wake with a `supervision-host:` line is automatic supervision, never the captain's return, so handle it under the away posture.
- The session-start digest reports the posture under its AFK subsection, so a restart re-enters the posture from the record, not from memory.

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Those sleeps look like recoverable overhead - `fm-watch-triage.test.sh` alone is
Sampling less often does not remove that wait, it only delays detection: raising the interval to 0.5s and charging each sample proportionally measured `fm-watch-triage.test.sh` at 435s and 440s against 390s and 393s for the unchanged script, back to back on 2026-09-03, because each of its ~40 poll-cycle waits and ~73 process-exit waits paid up to half a second more.
Some of those loops are also catching a transient rather than waiting for a settled condition, so a coarser sample can step over the state they assert on.
Discover tests by listing `tests/*.test.sh`: each is a self-contained bash script named `<subject>.test.sh`, and its header comment describes what it covers, so pass one to `bin/fm-test-run.sh` to focus on a subject with canonical timing output.
Shared test helpers live in `tests/lib.sh` (reporters, temp roots, git fixtures, captured-launch execution, and Claude environment/NUL-delimited argv recording), `tests/fixtures.sh` (fake toolchain and spawn-world builders), `tests/wake-helpers.sh`, `tests/secondmate-helpers.sh`, and `tests/git-config-helpers.sh` (fixture Git isolation from the host's global and system configuration, already sourced by `tests/lib.sh` and `tests/herdr-test-safety.sh`; a suite that sources neither must source it itself before its first Git operation so a direct invocation stays isolated).
Shared test helpers live in `tests/lib.sh` (reporters, temp roots, git fixtures, captured-launch execution, and Claude environment/NUL-delimited argv recording), `tests/fixtures.sh` (fake toolchain and spawn-world builders), `tests/wake-helpers.sh`, `tests/secondmate-helpers.sh`, and `tests/git-config-helpers.sh` (fixture Git isolation from the host's global and system configuration and from an inherited repository location such as `GIT_DIR`, already sourced by `tests/lib.sh` and `tests/herdr-test-safety.sh`; a suite that sources neither must source it itself before its first Git operation so a direct invocation stays isolated).
Source those instead of copying a fake toolchain into a new suite.
For focused inheritance fixtures, use the test-only item selection owned by `bin/fm-config-inherit-lib.sh` to avoid repeatedly transferring unrelated absent configuration while retaining every item needed by the behavioral assertions.
Keep fixture edits portable across GNU and BSD tools: write transformed metadata to a temporary file, replace it only on success, and fail the case if preparation fails rather than testing the wrong recorded harness.
Expand Down
15 changes: 12 additions & 3 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1688,7 +1688,7 @@ fm_backend_herdr_server_ensure() { # <session>
unset FM_HOME FM_ROOT_OVERRIDE FM_STATE_OVERRIDE FM_DATA_OVERRIDE FM_PROJECTS_OVERRIDE FM_CONFIG_OVERRIDE \
CURSOR_AGENT CURSOR_INVOKED_AS CLAUDECODE PI_CODING_AGENT FM_PI_HARNESS GROK_AGENT FM_SUPERVISION_MODEL \
COMPACT_ADVISER_DISABLE FM_COMPACT_ADVISER_DISABLE FM_COMPACT_ADVISER_HOOKS
fm_backend_herdr_cli "$session" server >/dev/null 2>&1 &
fm_backend_herdr_cli "$session" server </dev/null >/dev/null 2>&1 &
) || return 1
for i in $(seq 1 20); do
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null)
Expand Down Expand Up @@ -3061,6 +3061,15 @@ fm_backend_herdr_parse_target() { # <target>
[ -n "$FM_BACKEND_HERDR_SESSION" ] && [ -n "$FM_BACKEND_HERDR_PANE" ] && [ "$FM_BACKEND_HERDR_PANE" != "$target" ]
}

# fm_backend_herdr_target_observable: verify <target> is well-formed and its
# recorded session server is currently running, WITHOUT autostarting the server.
# Passive read-only probes (e.g. composer inspection) must stay observable-only
# and fail fast on inactive or bad targets without starting a new server.
fm_backend_herdr_target_observable() { # <target>
fm_backend_herdr_parse_target "$1" || return 1
[ "$(fm_backend_herdr_server_running_state "$FM_BACKEND_HERDR_SESSION")" = running ]
}

fm_backend_herdr_target_ready() { # <target>
fm_backend_herdr_parse_target "$1" || return 1
fm_backend_herdr_server_ensure "$FM_BACKEND_HERDR_SESSION" || return 1
Expand Down Expand Up @@ -3164,12 +3173,12 @@ fm_backend_herdr_capture() { # <target> <lines>
# workaround above - the bound is the pane itself, and asking for a line count
# is what triggers the empty-read bug.
fm_backend_herdr_visible_capture() { # <target>
fm_backend_herdr_target_ready "$1" || return 1
fm_backend_herdr_target_observable "$1" || return 1
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source visible 2>/dev/null
}

fm_backend_herdr_visible_capture_ansi() { # <target>
fm_backend_herdr_target_ready "$1" || return 1
fm_backend_herdr_target_observable "$1" || return 1
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source visible --format ansi 2>/dev/null
}

Expand Down
1 change: 1 addition & 0 deletions bin/fm-control.sh
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ do_exit() {
[ "$VERB" != exit ] || rm -f -- "$STATE/$ID.control-exit"
die "the exit command could not be sent to task $ID on $BACKEND"
fi
# The submitting Enter can open the picker. The agent is still alive, and
# another Enter would confirm the selected row. A dead agent may leave the
# same text behind; that is not a prompt still waiting.
if [ -s "${FM_COMPOSER_DIALOG_SINK:-}" ]; then
Expand Down
13 changes: 9 additions & 4 deletions bin/fm-supervision-host.sh
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,17 @@ start_arm() { # <predecessor-arm-pid or empty> [--restart]; sets the started pi
local predecessor=$1 out pid
shift
out=$(mktemp "$STATE/.supervision-host-arm.XXXXXX") || return 1
# An arm left for main outlives this host, and Claude tears the hook's
# process group down after the exit-2 rewake, so it gets a group of its own
# (the shape start_handling_successor in bin/fm-claude-stop-autoarm.sh uses).
[ "${ARM_OWN_GROUP:-0}" -ne 1 ] || set -m 2>/dev/null || true
if [ -n "$predecessor" ]; then
FM_WATCH_PREDECESSOR_ARM_PID=$predecessor FM_GUARD_GRACE="$GRACE" "$SCRIPT_DIR/fm-watch-arm.sh" "$@" >"$out" 2>&1 &
FM_WATCH_PREDECESSOR_ARM_PID=$predecessor FM_GUARD_GRACE="$GRACE" "$SCRIPT_DIR/fm-watch-arm.sh" "$@" >"$out" 2>&1 </dev/null &
else
FM_GUARD_GRACE="$GRACE" "$SCRIPT_DIR/fm-watch-arm.sh" "$@" >"$out" 2>&1 &
FM_GUARD_GRACE="$GRACE" "$SCRIPT_DIR/fm-watch-arm.sh" "$@" >"$out" 2>&1 </dev/null &
fi
pid=$!
[ "${ARM_OWN_GROUP:-0}" -ne 1 ] || set +m 2>/dev/null || true
record_process arm "$pid"
STARTED_ARM_PID=$pid
STARTED_ARM_OUT=$out
Expand Down Expand Up @@ -703,7 +708,7 @@ detach_successor() {
# downtime (autoarm_commit in bin/fm-claude-stop-autoarm.sh). A failed start
# returns 1; the caller still prints the close unchanged.
leave_successor_for_main() {
if ! start_successor "$CLOSED_ARM_PID"; then
if ! ARM_OWN_GROUP=1 start_successor "$CLOSED_ARM_PID"; then
log_line "pass-through successor-unverified $(printf '%s\n' "$REASON" | head -n 1)"
return 1
fi
Expand Down Expand Up @@ -1116,7 +1121,7 @@ while :; do

# A turn that could outlive the boundary would outlive the hook registration.
turn_crosses_boundary && boundary_exit
if ! start_successor "$CLOSED_ARM_PID"; then
if ! ARM_OWN_GROUP=1 start_successor "$CLOSED_ARM_PID"; then
exit_to_main "the successor watcher cycle could not be verified before handling; this wake is yours"
fi
if [ -n "$SUCCESSOR_GENERATION" ]; then
Expand Down
5 changes: 3 additions & 2 deletions docs/supervision-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ Only an away record is away: no record, or the record daemon-backed quiet mode w
The host asks the Pi branch's offer rule (`branchOfferForWake`, through `bin/fm-branch-dispatch.mjs offer`) whether the branch may take the close.
So a close reaches main off Pi exactly when it would on Pi: a check trigger, a decision-owned signal or stale trigger, and a scan that is unsafe or holds nothing for the branch stay main's.
On that main-only pass-through the host starts the successor watcher cycle and leaves it running, then prints the close unchanged.
It leaves the watcher's recovery marker reading downtime, confirming no handling handoff, because the re-arm owner delivers a close to main only while that marker reads downtime.
That successor, like one a turn hands back at its start (see [Away](#away)), runs in a process group of its own, so the harness tearing down the hook's group after the rewake does not stop it.
The host leaves the watcher's recovery marker reading downtime, confirming no handling handoff, because the re-arm owner delivers a close to main only while that marker reads downtime.
The session's next park without `--restart` requests a take-over to restore a single host-owned arm; the [host header](../bin/fm-supervision-host.sh) owns successor persistence and cleanup, and the [arm header](../bin/fm-watch-arm.sh) owns take-over eligibility and fallback.
OpenCode and omp still launch the host with `--restart`, which takes precedence over recorded take-over and lacks its acknowledgement-preserving handover; changing that first-cycle path remains a follow-up.
The host-off Claude Stop hook's detached handling successor is also unchanged; see [Claude handling successor](watcher-continuity.md#claude-handling-successor).
Expand Down Expand Up @@ -422,7 +423,7 @@ Each arm owner's own suite covers its host mode against a stub host.
| `tests/fm-afk-launch.test.sh` | The home gate on each primary, the `/afk` daemon refusal, and `/quiet` on a home that runs the host: the statement, the paused statement, each named missing part, the quiet daemon fallback that carries its recorded mode, a failed quiet start that archives its quiet record, and the refusal under a live away record until the return. |
| `tests/fm-afk-return.test.sh` | The return's drain-owned read-cursor advance through the away window on a host home, and none on Pi. |
| `tests/fm-supervision-host-live-e2e.test.sh` | Runs a real engine turn; opt-in because it spends tokens. |
| `tests/fm-supervision-host-attended-live-e2e.test.sh` | Opt-in credentialed guard for repeated attended main-only hand-backs to an idle Claude primary, the successor's own close, a close that turns main-only at its turn, and a stand-in remote listener; accepts a pre-fix ref for a negative control. |
| `tests/fm-supervision-host-attended-live-e2e.test.sh` | Opt-in credentialed guard for repeated attended main-only hand-backs to an idle Claude primary, a take-over of the successor's cycle, a close that turns main-only at its turn, and a stand-in remote listener; accepts a pre-fix ref for a negative control. |
| `tests/fm-host-mirror-live-e2e.test.sh` | Proves the Claude and Cursor mirror writers against the real harnesses; opt-in because it spends tokens. |

[verification/supervision.md](verification/supervision.md#supervision-host) records the dated live results.
12 changes: 11 additions & 1 deletion docs/verification/supervision.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,17 @@ ok - cursor 2026.09.23-86fc751: the tracked registrations mirrored the captain p
ok - host mirror live: 2 harness(es) proved their writers
```

The run above exercised these payload fields:
On 2026-10-07, the Claude path passed again on Linux 7.0.0-34-generic x86_64 with Claude Code 2.1.293 (`haiku`) and a managed policy that displayed `auto mode on` at its idle composer.
The guard ran in its private tmux socket and required both an idle verdict from the Claude-scoped rendered busy-state check and an empty composer before submitting its prompt.

```text
$ FM_HOST_MIRROR_LIVE_E2E=1 FM_HOST_MIRROR_LIVE_HARNESSES=claude bash tests/fm-host-mirror-live-e2e.test.sh
ok - claude 2.1.293 (Claude Code): a turn the harness started itself was not mirrored as the captain's words
ok - claude 2.1.293 (Claude Code): the tracked registrations mirrored the captain prompt and main reply
ok - host mirror live: 1 harness(es) proved their writers
```

The runs above exercised these payload fields:

| Primary | Captain text | Main text |
| --- | --- | --- |
Expand Down
47 changes: 40 additions & 7 deletions tests/fm-backend-herdr.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -773,12 +773,16 @@ test_exhausted_settle_window_keeps_a_non_shell_foreground_live() {
}

test_registered_agent_with_an_agent_descendant_outside_the_foreground_stays_alive() {
local lab sleep_bin shell_pid out shell_verdict
local lab standin sleep_bin shell_pid out shell_verdict
sleep_bin=$(command -v sleep) || fail "sleep not found"
lab="$TMP_ROOT/stale-reg-descendant-bin"; mkdir -p "$lab"
# A symlink to a real long-running binary so the kernel records `pi` as the
# executable identity (a copied platform binary fails code signing on macOS).
ln -sf "$sleep_bin" "$lab/pi"
# A symlink to a real long-running stand-in so the kernel records `pi` as the
# executable identity (tests/lib.sh fm_agent_standin owns why not host sleep).
standin=$(fm_agent_standin "$TMP_ROOT/standin") || {
echo "skip: no long-running stand-in survives a rename, so the agent-named descendant case cannot run"
return 0
}
ln -sf "$standin" "$lab/pi"
# A real shell whose child is that agent-named process, while the canned
# foreground view shows only the shell (a suspended or backgrounded agent).
sh -c "'$lab/pi' 300; :" &
Expand All @@ -802,13 +806,16 @@ test_registered_agent_with_an_agent_descendant_outside_the_foreground_stays_aliv
}

test_agent_descendant_under_a_spaced_install_path_stays_alive() {
local lab sleep_bin shell_pid out
sleep_bin=$(command -v sleep) || fail "sleep not found"
local lab standin shell_pid out
standin=$(fm_agent_standin "$TMP_ROOT/standin") || {
echo "skip: no long-running stand-in survives a rename, so the spaced-path descendant case cannot run"
return 0
}
# The executable path the process table reports contains a space (the macOS
# `/Library/Application Support/...` shape), so a field-split read of the
# process table sees only a fragment of the name.
lab="$TMP_ROOT/stale-reg-spaced-bin/Application Support/Some Dir"; mkdir -p "$lab"
ln -sf "$sleep_bin" "$lab/pi"
ln -sf "$standin" "$lab/pi"
sh -c "'$lab/pi' 300; :" &
shell_pid=$!
sleep 0.3
Expand Down Expand Up @@ -4064,6 +4071,30 @@ test_composer_state_unknown_on_capture_failure() {
pass "fm_backend_herdr_composer_state: reports unknown when the pane cannot be captured"
}

test_composer_state_inactive_session_fails_fast_without_server_autostart() {
local dir log resp fb out status
dir="$TMP_ROOT/composer-inactive-session"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log"
# status --json reports stopped server
printf '{"client":{"version":"0.7.1","protocol":14},"server":{"running":false}}\n' > "$resp/1.out"
fb=$(make_herdr_fakebin "$dir")
out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 \
bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state nonexistent:w1:p2' "$ROOT" )
status=$?
[ "$status" -eq 0 ] || fail "composer_state should exit 0 on inactive session"
[ "$out" = unknown ] || fail "composer_state on inactive session should read unknown, got '$out'"
assert_not_contains "$(cat "$log")" $'\x1f''server' "composer probe on inactive session must never autostart a herdr server"
pass "fm_backend_herdr_composer_state: fails fast to unknown for inactive session without starting server"
}

# shellcheck disable=SC2016
test_composer_state_piped_reader_does_not_hang() {
local out rc=0
out=$( timeout 3s bash -c '. "$0/bin/fm-backend.sh"; ( fm_backend_composer_state herdr nonexistent-session:p2 ) 2>&1 | head -20' "$ROOT" ) || rc=$?
[ "$rc" -eq 0 ] || fail "piped composer probe on bad target hung or failed with rc=$rc"
[ "$out" = unknown ] || fail "piped composer probe should print unknown, got '$out'"
pass "fm_backend_composer_state (herdr): piped probe on bad target does not hang EOF-sensitive readers"
}

test_composer_state_unknown_when_no_composer_row_found() {
local dir log resp fb out glyph idx=1
dir="$TMP_ROOT/composer-no-row"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log"
Expand Down Expand Up @@ -6426,6 +6457,8 @@ test_composer_state_real_text_is_pending
test_composer_state_grok_oversized_title_preserves_safe_verdicts
test_composer_state_popup_placeholder_fill_is_pending
test_composer_state_unknown_on_capture_failure
test_composer_state_inactive_session_fails_fast_without_server_autostart
test_composer_state_piped_reader_does_not_hang
test_composer_state_unknown_when_no_composer_row_found
test_composer_state_pi_parked_prompt_is_not_empty
test_composer_state_pi_separator_idle_is_empty
Expand Down
Loading
Loading