From 98fdaa7e91c08619311092cc2105b128f83347c8 Mon Sep 17 00:00:00 2001 From: e-jung <8334081+e-jung@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:53:25 +0000 Subject: [PATCH 1/5] fix(orca): link delegates to captain session and de-double fm- prefix Orca delegates were created with --no-parent, making them peers of the captain's session instead of children. When firstmate itself runs under Orca (ORCA_WORKTREE_ID set and repo-qualified), pass --parent-worktree so the sidebar nests delegates under the captain's session. Fall back to --no-parent when no trustworthy parent is discoverable or parent linkage fails (best-effort). Task ids already beginning with fm- (e.g. fm-orca-fix-k2) were doubled to fm-fm-orca-fix-k2 in worktree names, terminal titles, and window= metadata. Add fm_alias_for_id() as the single source of truth for the idempotent fm- prefix, and route all label derivation through it: spawn, expected-label resolution, crew-state, and teardown child cleanup. Legacy doubled selectors keep resolving through the existing single-prefix strip fallback. --- bin/backends/orca.sh | 30 +++++++++- bin/fm-backend.sh | 16 +++++- bin/fm-crew-state.sh | 2 +- bin/fm-spawn.sh | 2 +- bin/fm-teardown.sh | 4 +- docs/orca-backend.md | 4 +- tests/fm-backend-orca.test.sh | 105 +++++++++++++++++++++++++++++++++- tests/fm-backend.test.sh | 35 +++++++++++- 8 files changed, 187 insertions(+), 11 deletions(-) diff --git a/bin/backends/orca.sh b/bin/backends/orca.sh index dc9307de4..bb525f510 100644 --- a/bin/backends/orca.sh +++ b/bin/backends/orca.sh @@ -128,10 +128,36 @@ fm_backend_orca_repo_ensure() { # printf '%s' "$repo_id" } +# fm_backend_orca_parent_selector: resolve a trustworthy parent worktree +# selector from the environment when firstmate itself runs under Orca. Orca +# sets ORCA_WORKTREE_ID to the caller's own worktree (the captain's session) +# in the repo-qualified form ::. A firstmate delegate is by +# definition a child of that session, so it is the correct parent lineage +# target. Returns the id:-prefixed selector on stdout; returns 1 (silently) +# when no trustworthy parent can be discovered so the caller falls back to +# --no-parent. Never guesses a parent from unrelated worktrees. +fm_backend_orca_parent_selector() { + [ -n "${ORCA_WORKTREE_ID:-}" ] || return 1 + case "$ORCA_WORKTREE_ID" in + *'::'*) : ;; + *) return 1 ;; + esac + printf 'id:%s' "$ORCA_WORKTREE_ID" +} + fm_backend_orca_worktree_create() { # - local project=$1 name=$2 repo_id out wt_id wt_path terminal + local project=$1 name=$2 repo_id out wt_id wt_path terminal parent repo_id=$(fm_backend_orca_repo_ensure "$project") || return 1 - out=$(orca worktree create --repo "id:$repo_id" --name "$name" --no-parent --setup skip --json) || return 1 + parent=$(fm_backend_orca_parent_selector 2>/dev/null || true) + if [ -n "$parent" ]; then + out=$(orca worktree create --repo "id:$repo_id" --name "$name" --parent-worktree "$parent" --setup skip --json 2>/dev/null) || { + # Parent linkage is best-effort: a stale or deleted parent is not a + # spawn blocker, so retry without a parent rather than failing. + out=$(orca worktree create --repo "id:$repo_id" --name "$name" --no-parent --setup skip --json) || return 1 + } + else + out=$(orca worktree create --repo "id:$repo_id" --name "$name" --no-parent --setup skip --json) || return 1 + fi wt_id=$(printf '%s' "$out" | fm_backend_orca_json_get worktree-id) || { echo "error: orca worktree create did not return a worktree id for $name" >&2 return 1 diff --git a/bin/fm-backend.sh b/bin/fm-backend.sh index d38bd9f06..f6f621aeb 100644 --- a/bin/fm-backend.sh +++ b/bin/fm-backend.sh @@ -410,10 +410,24 @@ fm_backend_of_selector() { # printf 'tmux' } +# fm_alias_for_id: the stable fm- window/tab label for a task. Idempotent: +# an id already beginning with fm- is returned unchanged so fm-on-firstmate +# task ids (e.g. fm-orca-fix-k2) do not become fm-fm-orca-fix-k2 in worktree +# names, terminal titles, or window= metadata. The single source of truth for +# the fm- prefix; every call site that derives a window label from a task id +# must go through here so spawn, expected-label resolution, crew-state, and +# teardown all agree. +fm_alias_for_id() { # + case "$1" in + fm-*) printf '%s' "$1" ;; + *) printf 'fm-%s' "$1" ;; + esac +} + fm_backend_expected_label_of_selector() { # local raw=$1 state=$2 id id=$(fm_backend_task_id_for_selector "$raw" "$state" 2>/dev/null || true) - [ -n "$id" ] && printf 'fm-%s' "$id" + [ -n "$id" ] && fm_alias_for_id "$id" return 0 } diff --git a/bin/fm-crew-state.sh b/bin/fm-crew-state.sh index ecafc5299..a95dfa616 100755 --- a/bin/fm-crew-state.sh +++ b/bin/fm-crew-state.sh @@ -134,7 +134,7 @@ LOG_VERB=$(status_line_verb "$LOG_LINE") # herdr task is read through fm_backend_capture instead of a bare tmux probe. TASK_BACKEND=$(fm_backend_of_meta "$META") BACKEND_TARGET=$(fm_backend_target_of_meta "$META") -EXPECTED_LABEL="fm-$ID" +EXPECTED_LABEL=$(fm_alias_for_id "$ID") pane_readable() { # case "$TASK_BACKEND" in tmux) tmux display-message -p -t "$1" '#{pane_id}' >/dev/null 2>&1 ;; diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index a4ec0890c..9d3537992 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -692,7 +692,7 @@ validate_spawn_worktree() { # fi } -W="fm-$ID" +W=$(fm_alias_for_id "$ID") case "$BACKEND" in tmux) SES=$(fm_backend_tmux_container_ensure) diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index 83b66cc2a..efc81c28d 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -885,9 +885,9 @@ cleanup_firstmate_home_children() { if [ "$child_backend" = zellij ]; then # Zellij titles are scoped by the owning home tag, so forced secondmate # cleanup must verify child tabs as that child home, not the parent. - ( unset FM_ROOT_OVERRIDE; FM_HOME=$home FM_ROOT=$home fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" ) 2>/dev/null || true + ( unset FM_ROOT_OVERRIDE; FM_HOME=$home FM_ROOT=$home fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "$(fm_alias_for_id "$child_id")" ) 2>/dev/null || true else - fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "fm-$child_id" 2>/dev/null || true + fm_backend_kill "$child_backend" "$child_t" "$(meta_value "$child_meta" zellij_tab_id)" "$(fm_alias_for_id "$child_id")" 2>/dev/null || true fi fi if [ "$child_kind" = secondmate ]; then diff --git a/docs/orca-backend.md b/docs/orca-backend.md index f9ff8b323..2db96b90c 100644 --- a/docs/orca-backend.md +++ b/docs/orca-backend.md @@ -60,6 +60,7 @@ worktree= ``` `window=` remains the shared firstmate alias used by selector-driven supervision tools after a task selector has resolved through metadata. +The alias is idempotent: a task id already beginning with `fm-` (e.g. `fm-orca-fix-k2`) keeps that exact name rather than becoming `fm-fm-orca-fix-k2`. `fm-teardown.sh ` uses the same recorded fields after loading `state/.meta`. For Orca, `window=` keeps the stable firstmate alias while `terminal=` carries the stable Orca terminal handle that backend operations use. The recorded `backend=orca` field tells shared call sites to route capture, send, interrupt, and close through `bin/backends/orca.sh` instead of tmux assumptions. @@ -69,7 +70,7 @@ The recorded `backend=orca` field tells shared call sites to route capture, send Spawn: 1. Ensure the project repo is registered in Orca, adding it with `orca repo add --path` when needed. -2. Create an independent Orca worktree with `orca worktree create --repo id: --name fm- --no-parent --setup skip`. +2. Create an Orca worktree with `orca worktree create --repo id: --name fm- --parent-worktree id:$ORCA_WORKTREE_ID --setup skip` when firstmate itself runs under Orca (so the delegate nests beneath the captain's session in the sidebar), or `--no-parent` when no trustworthy parent is discoverable. 3. Reuse the terminal returned by Orca worktree creation only when it appears in the verified `result.terminal.handle` shape, or create a titled terminal in that worktree when Orca returns only the worktree. 4. Install firstmate's per-harness turn-end hooks in the Orca worktree. 5. Write metadata, then send `GOTMPDIR` export and the selected harness launch through the recorded Orca terminal. @@ -108,6 +109,7 @@ Firstmate intentionally ignores speculative terminal-handle shapes such as bare Fake-Orca tests cover: - helper parsing for repo registration, worktree creation, verified implicit-terminal reuse, terminal creation, terminal sends, and worktree removal; +- parent linkage: `--parent-worktree` is used when firstmate runs under Orca (`ORCA_WORKTREE_ID` set and repo-qualified), with best-effort fallback to `--no-parent` when the parent is absent, stale, or not discoverable; - rejection of undocumented terminal-handle result shapes; - runtime readiness gating through `orca status --json`; - `fm-spawn.sh --backend orca` metadata creation and harness launch; diff --git a/tests/fm-backend-orca.test.sh b/tests/fm-backend-orca.test.sh index 38fd4a9c5..231973fc6 100755 --- a/tests/fm-backend-orca.test.sh +++ b/tests/fm-backend-orca.test.sh @@ -396,7 +396,7 @@ test_worktree_and_terminal_helpers_parse_json() { printf '{"ok":true,"result":{"worktree":{"id":"wt-123","path":"/tmp/orca-wt"}}}\n' > "$RESP/3.out" printf '{"ok":true,"result":{"terminal":{"handle":"term-123"}}}\n' > "$RESP/4.out" out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ - bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) + bash -c 'unset ORCA_WORKTREE_ID; . "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) wt_id=${out%%$'\t'*} wt_path=${out#*$'\t'} [ "$wt_id" = wt-123 ] || fail "worktree helper should print worktree id, got '$wt_id'" @@ -415,6 +415,103 @@ test_worktree_and_terminal_helpers_parse_json() { pass "Orca lifecycle helpers: register repo, create worktree, create terminal, parse stable ids" } +test_parent_selector_returns_id_prefixed_worktree() { + local out + orca_case parent-selector + out=$( bash -c '. "$0/bin/backends/orca.sh"; ORCA_WORKTREE_ID="abc-123::/home/captain/firstmate" fm_backend_orca_parent_selector' "$ROOT" ) + [ "$out" = "id:abc-123::/home/captain/firstmate" ] \ + || fail "parent_selector should prepend id: to a repo-qualified ORCA_WORKTREE_ID, got '$out'" + pass "fm_backend_orca_parent_selector: returns id:-prefixed selector for a repo-qualified worktree id" +} + +test_parent_selector_refuses_empty_or_unqualified_id() { + local out status + orca_case parent-selector-reject + out=$( bash -c 'unset ORCA_WORKTREE_ID; . "$0/bin/backends/orca.sh"; fm_backend_orca_parent_selector' "$ROOT" 2>&1 ) + status=$? + [ "$status" -ne 0 ] || fail "parent_selector should fail when ORCA_WORKTREE_ID is unset" + out=$( bash -c 'ORCA_WORKTREE_ID=""; . "$0/bin/backends/orca.sh"; fm_backend_orca_parent_selector' "$ROOT" 2>&1 ) + status=$? + [ "$status" -ne 0 ] || fail "parent_selector should fail when ORCA_WORKTREE_ID is empty" + out=$( bash -c 'ORCA_WORKTREE_ID="bare-id-no-separator"; . "$0/bin/backends/orca.sh"; fm_backend_orca_parent_selector' "$ROOT" 2>&1 ) + status=$? + [ "$status" -ne 0 ] || fail "parent_selector should fail when ORCA_WORKTREE_ID lacks the repo-qualified :: separator" + pass "fm_backend_orca_parent_selector: refuses empty or non-repo-qualified ids, falls back to --no-parent" +} + +test_worktree_create_uses_parent_when_orca_worktree_id_set() { + local out log_text + orca_case parent-present + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-parent-present"}}}\n' > "$RESP/2.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-child","path":"/tmp/orca-child"}}}\n' > "$RESP/3.out" + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + ORCA_WORKTREE_ID="captain-uuid-1::/home/captain/firstmate" \ + bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) + log_text=$(cat "$LOG") + assert_contains "$log_text" $'orca\x1f''worktree'$'\x1f''create'$'\x1f''--repo'$'\x1f''id:repo-parent-present'$'\x1f''--name'$'\x1f''fm-task'$'\x1f''--parent-worktree'$'\x1f''id:captain-uuid-1::/home/captain/firstmate'$'\x1f''--setup'$'\x1f''skip'$'\x1f''--json' \ + "worktree create should pass --parent-worktree with id:-prefixed ORCA_WORKTREE_ID when firstmate runs under Orca" + assert_not_contains "$log_text" '--no-parent' \ + "worktree create should NOT use --no-parent when a trustworthy parent is present" + pass "fm_backend_orca_worktree_create: links delegate to captain's session as parent when ORCA_WORKTREE_ID is set" +} + +test_worktree_create_uses_no_parent_when_orca_worktree_id_absent() { + local out log_text + orca_case parent-absent + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-parent-absent"}}}\n' > "$RESP/2.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-no-parent","path":"/tmp/orca-no-parent"}}}\n' > "$RESP/3.out" + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + bash -c 'unset ORCA_WORKTREE_ID; . "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) + log_text=$(cat "$LOG") + assert_contains "$log_text" $'orca\x1f''worktree'$'\x1f''create'$'\x1f''--repo'$'\x1f''id:repo-parent-absent'$'\x1f''--name'$'\x1f''fm-task'$'\x1f''--no-parent'$'\x1f''--setup'$'\x1f''skip'$'\x1f''--json' \ + "worktree create should use --no-parent when ORCA_WORKTREE_ID is not set" + assert_not_contains "$log_text" '--parent-worktree' \ + "worktree create should NOT pass --parent-worktree when no parent is discoverable" + pass "fm_backend_orca_worktree_create: falls back to --no-parent when firstmate is not under Orca" +} + +test_worktree_create_falls_back_to_no_parent_when_parent_link_fails() { + local out log_text create_count + orca_case parent-fallback + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-fallback"}}}\n' > "$RESP/2.out" + # Response 3: --parent-worktree attempt fails (parent stale/deleted). + printf '1\n' > "$RESP/3.exit" + # Response 4: --no-parent retry succeeds. + printf '{"ok":true,"result":{"worktree":{"id":"wt-fallback","path":"/tmp/orca-fallback"}}}\n' > "$RESP/4.out" + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + ORCA_WORKTREE_ID="stale-uuid::/home/captain/firstmate" \ + bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) + log_text=$(cat "$LOG") + assert_contains "$log_text" '--parent-worktree' \ + "worktree create should first try --parent-worktree when ORCA_WORKTREE_ID is set" + assert_contains "$log_text" '--no-parent' \ + "worktree create should fall back to --no-parent when parent linkage fails" + create_count=$(printf '%s\n' "$log_text" | grep -c $'orca\x1fworktree\x1fcreate') + [ "$create_count" -eq 2 ] || fail "parent fallback should produce exactly 2 worktree create calls, got $create_count" + pass "fm_backend_orca_worktree_create: best-effort parent fallback retries --no-parent when parent linkage fails" +} + +test_worktree_create_shell_safely_passes_repo_qualified_parent_id() { + local out log_text + orca_case parent-shell-safe + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-shell"}}}\n' > "$RESP/2.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-shell","path":"/tmp/orca-shell"}}}\n' > "$RESP/3.out" + # A repo-qualified worktree id with a path containing characters that would + # break unquoted shell expansion. The value must arrive as a single arg to + # --parent-worktree, verified by the unit-separator-delimited fakebin log. + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + ORCA_WORKTREE_ID="a1b2-c3d4::/home/captain/my firstmate home" \ + bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" ) + log_text=$(cat "$LOG") + assert_contains "$log_text" $'\x1f''--parent-worktree'$'\x1f''id:a1b2-c3d4::/home/captain/my firstmate home'$'\x1f' \ + "worktree create should pass the full repo-qualified parent id as a single shell-safe argument to --parent-worktree" + pass "fm_backend_orca_worktree_create: shell-safe handling of repo-qualified worktree IDs with spaces" +} + test_worktree_create_removes_worktree_when_path_missing() { local out status orca_case lifecycle-missing-path @@ -1297,6 +1394,12 @@ test_dispatcher_sources_orca_and_routes_primitives test_json_get_ignores_undocumented_terminal_id_shapes test_worktree_and_terminal_helpers_parse_json test_worktree_create_removes_worktree_when_path_missing +test_parent_selector_returns_id_prefixed_worktree +test_parent_selector_refuses_empty_or_unqualified_id +test_worktree_create_uses_parent_when_orca_worktree_id_set +test_worktree_create_uses_no_parent_when_orca_worktree_id_absent +test_worktree_create_falls_back_to_no_parent_when_parent_link_fails +test_worktree_create_shell_safely_passes_repo_qualified_parent_id test_spawn_preserves_orca_metadata_when_pathless_worktree_cleanup_fails test_spawn_writes_orca_metadata_and_launches_harness test_spawn_refuses_orca_secondmate_before_home_mutation diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index cd815f547..7a3f85108 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -531,8 +531,8 @@ test_resolve_selector_three_forms() { || fail "exact fm-* task id should resolve through its exact metadata" [ "$(fm_backend_of_selector 'fm-turnend-all-harnesses-v9' 'default:wB:p3' "$state")" = herdr ] \ || fail "exact fm-* task id should use exact metadata without stripping fm-" - [ "$(fm_backend_expected_label_of_selector 'fm-turnend-all-harnesses-v9' "$state")" = "fm-fm-turnend-all-harnesses-v9" ] \ - || fail "exact fm-* task id should report the spawned fm- label" + [ "$(fm_backend_expected_label_of_selector 'fm-turnend-all-harnesses-v9' "$state")" = "fm-turnend-all-harnesses-v9" ] \ + || fail "exact fm-* task id should report an idempotent fm- label (no doubling)" [ "$(fm_backend_resolve_selector 'fm-task1' "$state")" = "firstmate:fm-task1" ] \ || fail "legacy fm- label should resolve through .meta's window=" @@ -598,6 +598,35 @@ test_backend_of_selector_matches_explicit_target_meta() { pass "fm_backend_of_selector: exact task ids, legacy fm- labels, and matching explicit targets inherit metadata backend" } +test_alias_for_id_is_idempotent() { + [ "$(fm_alias_for_id 'fix-login-k3')" = "fm-fix-login-k3" ] \ + || fail "fm_alias_for_id should prepend fm- to a bare task id" + [ "$(fm_alias_for_id 'fm-orca-task-h2')" = "fm-orca-task-h2" ] \ + || fail "fm_alias_for_id should NOT double the prefix on an id already starting fm-" + [ "$(fm_alias_for_id 'fm-x')" = "fm-x" ] \ + || fail "fm_alias_for_id should leave a single-char fm- id unchanged" + pass "fm_alias_for_id: idempotent fm- prefix (no fm-fm-* doubling)" +} + +test_legacy_doubled_selector_still_resolves() { + local state=$TMP_ROOT/doubled-selector-state + mkdir -p "$state" + fm_write_meta "$state/fm-orca-fix-k2.meta" "window=fm-orca-fix-k2" "backend=orca" + # The bare task id (also starts with fm-) resolves through exact metadata. + [ "$(fm_backend_task_id_for_selector 'fm-orca-fix-k2' "$state")" = "fm-orca-fix-k2" ] \ + || fail "bare fm-* task id should resolve via exact metadata" + # Legacy doubled selector fm-fm- still resolves via the single fm- strip. + [ "$(fm_backend_task_id_for_selector 'fm-fm-orca-fix-k2' "$state")" = "fm-orca-fix-k2" ] \ + || fail "legacy doubled fm-fm- selector must still resolve via single-prefix strip" + # The expected label for the bare id is now single-prefix (idempotent). + [ "$(fm_backend_expected_label_of_selector 'fm-orca-fix-k2' "$state")" = "fm-orca-fix-k2" ] \ + || fail "expected label for an fm-* task id should be idempotent" + # A legacy doubled selector still resolves its label to the single-prefix form. + [ "$(fm_backend_expected_label_of_selector 'fm-fm-orca-fix-k2' "$state")" = "fm-orca-fix-k2" ] \ + || fail "legacy doubled selector label should resolve to the single-prefix idempotent alias" + pass "legacy doubled fm-fm-* selectors still resolve through single-prefix strip; expected_label is idempotent" +} + # --- old vs new: fm-send.sh -------------------------------------------------- make_send_fakebin() { # -> echoes fakebin dir; logs every tmux call to $FM_TMUX_LOG @@ -1079,6 +1108,8 @@ test_backend_validate_spawn_accepts_orca test_meta_get_and_backend_of_meta test_resolve_selector_three_forms test_backend_of_selector_matches_explicit_target_meta +test_alias_for_id_is_idempotent +test_legacy_doubled_selector_still_resolves test_send_conformance_old_vs_new test_peek_conformance_old_vs_new test_spawn_symlinked_project_prefix_avoids_false_refusal From e7d9ea9e72f33b345781fff0b30a620a7b2daa3c Mon Sep 17 00:00:00 2001 From: e-jung <8334081+e-jung@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:03:42 +0000 Subject: [PATCH 2/5] no-mistakes(review): Route fm-teardown kill labels through fm_alias_for_id to de-double --- bin/fm-teardown.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index efc81c28d..a5e5d6a6d 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -1005,7 +1005,7 @@ if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then fi rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" "$WT/.fm-grok-turnend" fi - [ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true + [ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "$(fm_alias_for_id "$ID")" 2>/dev/null || true fm_backend_remove_worktree "$BACKEND" "$ORCA_WORKTREE_ID" elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD) @@ -1031,7 +1031,7 @@ elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then fi if [ "$BACKEND" != orca ]; then - fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true + fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "$(fm_alias_for_id "$ID")" 2>/dev/null || true fi if [ "$KIND" = secondmate ]; then [ -n "$HOME_PATH" ] || HOME_PATH=$WT From 65d09e44d7f4a5331895b0e0405ec189ad909be8 Mon Sep 17 00:00:00 2001 From: e-jung <8334081+e-jung@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:33:08 +0000 Subject: [PATCH 3/5] no-mistakes(review): fix(orca): thread captain parent and complete alias migration --- bin/backends/orca.sh | 6 +-- bin/fm-ff-lib.sh | 5 ++- bin/fm-fleet-snapshot.sh | 4 +- bin/fm-session-start.sh | 2 +- bin/fm-spawn.sh | 3 +- bin/fm-supervise-daemon.sh | 2 +- tests/fm-backend-orca.test.sh | 79 +++++++++++++++++++++++++++++++++++ tests/fm-backend.test.sh | 25 +++++++++++ 8 files changed, 117 insertions(+), 9 deletions(-) diff --git a/bin/backends/orca.sh b/bin/backends/orca.sh index bb525f510..388e6830f 100644 --- a/bin/backends/orca.sh +++ b/bin/backends/orca.sh @@ -145,10 +145,10 @@ fm_backend_orca_parent_selector() { printf 'id:%s' "$ORCA_WORKTREE_ID" } -fm_backend_orca_worktree_create() { # - local project=$1 name=$2 repo_id out wt_id wt_path terminal parent +fm_backend_orca_worktree_create() { # [parent-selector] + local project=$1 name=$2 parent=${3:-} repo_id out wt_id wt_path terminal repo_id=$(fm_backend_orca_repo_ensure "$project") || return 1 - parent=$(fm_backend_orca_parent_selector 2>/dev/null || true) + [ -n "$parent" ] || parent=$(fm_backend_orca_parent_selector 2>/dev/null || true) if [ -n "$parent" ]; then out=$(orca worktree create --repo "id:$repo_id" --name "$name" --parent-worktree "$parent" --setup skip --json 2>/dev/null) || { # Parent linkage is best-effort: a stale or deleted parent is not a diff --git a/bin/fm-ff-lib.sh b/bin/fm-ff-lib.sh index ae290020c..9d86fc9fd 100644 --- a/bin/fm-ff-lib.sh +++ b/bin/fm-ff-lib.sh @@ -24,6 +24,9 @@ # default branch, so the fast-forward advances HEAD only and never moves the # shared default branch or any other worktree's checkout. +# shellcheck source=bin/fm-backend.sh +. "$(dirname -- "${BASH_SOURCE[0]}")/fm-backend.sh" + SUB_HOME_MARKER="${SUB_HOME_MARKER:-.fm-secondmate-home}" # --- helpers --------------------------------------------------------------- @@ -409,7 +412,7 @@ process_secondmate() { if [ "$nudge_requires_instr" = yes ] && [ -z "$FF_INSTR" ]; then return 0 fi - FF_NUDGE_WINDOWS="$FF_NUDGE_WINDOWS fm-$id" + FF_NUDGE_WINDOWS="$FF_NUDGE_WINDOWS $(fm_alias_for_id "$id")" fi } diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index a8b83c669..996878244 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -421,7 +421,7 @@ task_json_lines() { endpoint_exists=null if [ -n "$target" ]; then - if fm_backend_target_exists "$backend" "$target" "fm-$id" >/dev/null 2>&1; then + if fm_backend_target_exists "$backend" "$target" "$(fm_alias_for_id "$id")" >/dev/null 2>&1; then endpoint_exists=true else endpoint_exists=false @@ -853,7 +853,7 @@ terminal_evidence_json() { # /dev/null || true) fi ORCA_ABORT_CLEANUP=0 ORCA_WORKTREE_ID= @@ -769,7 +770,7 @@ EOF ;; orca) set +e - ORCA_WT_RAW=$(fm_backend_orca_worktree_create "$PROJ_ABS" "$W") + ORCA_WT_RAW=$(fm_backend_orca_worktree_create "$PROJ_ABS" "$W" "$ORCA_PARENT_SELECTOR") ORCA_WT_STATUS=$? set -e if [ "$ORCA_WT_STATUS" -ne 0 ]; then diff --git a/bin/fm-supervise-daemon.sh b/bin/fm-supervise-daemon.sh index f9dc9c192..eeac8ec18 100755 --- a/bin/fm-supervise-daemon.sh +++ b/bin/fm-supervise-daemon.sh @@ -582,7 +582,7 @@ task_window_backend() { # stale_window_is_busy() { # local win=$1 state=$2 backend label tail40 bs backend=$(task_window_backend "$win" "$state") - label="fm-$(window_to_task "$win" "$state")" + label=$(fm_alias_for_id "$(window_to_task "$win" "$state")") tail40=$(fm_backend_capture "$backend" "$win" 40 "$label" 2>/dev/null) || return 2 bs=$(fm_backend_busy_state "$backend" "$win" 2>/dev/null) case "$bs" in diff --git a/tests/fm-backend-orca.test.sh b/tests/fm-backend-orca.test.sh index 231973fc6..472d3425d 100755 --- a/tests/fm-backend-orca.test.sh +++ b/tests/fm-backend-orca.test.sh @@ -605,6 +605,82 @@ test_spawn_writes_orca_metadata_and_launches_harness() { pass "fm-spawn.sh --backend orca: reuses implicit terminal, records metadata, launches harness" } +test_worktree_create_honors_explicit_parent_arg_over_cleared_env() { + local out log_text + orca_case parent-arg-over-cleared-env + printf '{"ok":true,"result":{"repo":{"id":"repo-explicit"}}}\n' > "$RESP/1.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-explicit","path":"/tmp/orca-explicit-wt"}}}\n' > "$RESP/2.out" + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + bash -c 'unset ORCA_WORKTREE_ID; . "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task "id:captain-explicit::/home/captain/firstmate"' "$ROOT" ) + log_text=$(cat "$LOG") + assert_contains "$log_text" $'\x1f''--parent-worktree'$'\x1f''id:captain-explicit::/home/captain/firstmate'$'\x1f' \ + "worktree create should use the explicit parent-selector arg even with ORCA_WORKTREE_ID cleared" + assert_not_contains "$log_text" '--no-parent' \ + "worktree create should not fall back to --no-parent when an explicit parent is threaded in" + pass "fm_backend_orca_worktree_create: honors an explicit parent-selector arg over a cleared env" +} + +test_fm_spawn_threads_inherited_captain_parent_into_orca_worktree_create() { + local proj wt data state config id out log + id="orcaspawnparent1" + proj="$TMP_ROOT/spawn-parent-project" + wt="$TMP_ROOT/spawn-parent-wt" + data="$TMP_ROOT/spawn-parent-data" + state="$TMP_ROOT/spawn-parent-state" + config="$TMP_ROOT/spawn-parent-config" + fm_git_worktree "$proj" "$wt" "fm/$id" + mkdir -p "$data/$id" "$state" "$config" + printf 'brief\n' > "$data/$id/brief.md" + touch "$state/.last-watcher-beat" + orca_case spawn-parent + log="$LOG" + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-spawn-parent"}}}\n' > "$RESP/2.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-spawn-parent","path":"%s"},"terminal":{"handle":"term-spawn-parent"}}}\n' "$wt" > "$RESP/3.out" + out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + ORCA_WORKTREE_ID="captain-uuid-9::/home/captain/firstmate" \ + FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ + FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ + "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1 ) + expect_code 0 $? "fm-spawn.sh --backend orca should succeed under a captain Orca session"$'\n'"$out" + assert_grep $'orca\x1fworktree\x1fcreate' "$log" "spawn never reached the orca worktree create call" + assert_grep $'--parent-worktree\x1fid:captain-uuid-9::/home/captain/firstmate' "$log" \ + "spawn should thread the inherited captain id into --parent-worktree even though fm-spawn clears ORCA_WORKTREE_ID before the create call" + assert_no_grep '--no-parent' "$log" "spawn should not fall back to --no-parent when a captain session is inherited" + rm -rf "/tmp/fm-$id" + pass "fm-spawn.sh: captures the captain ORCA_WORKTREE_ID before clearing it and threads it into orca worktree create" +} + +test_fm_spawn_uses_no_parent_when_not_running_under_orca() { + local proj wt data state config id out log + id="orcaspawnnoparent1" + proj="$TMP_ROOT/spawn-no-parent-project" + wt="$TMP_ROOT/spawn-no-parent-wt" + data="$TMP_ROOT/spawn-no-parent-data" + state="$TMP_ROOT/spawn-no-parent-state" + config="$TMP_ROOT/spawn-no-parent-config" + fm_git_worktree "$proj" "$wt" "fm/$id" + mkdir -p "$data/$id" "$state" "$config" + printf 'brief\n' > "$data/$id/brief.md" + touch "$state/.last-watcher-beat" + orca_case spawn-no-parent + log="$LOG" + printf '1\n' > "$RESP/1.exit" + printf '{"ok":true,"result":{"repo":{"id":"repo-spawn-no-parent"}}}\n' > "$RESP/2.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-spawn-no-parent","path":"%s"},"terminal":{"handle":"term-spawn-no-parent"}}}\n' "$wt" > "$RESP/3.out" + out=$( env -u ORCA_WORKTREE_ID \ + PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ + FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ + FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ + "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1 ) + expect_code 0 $? "fm-spawn.sh --backend orca should succeed with no captain session"$'\n'"$out" + assert_grep $'orca\x1fworktree\x1fcreate' "$log" "spawn never reached the orca worktree create call" + assert_grep '--no-parent' "$log" "spawn should use --no-parent when not running under a captain Orca session" + assert_no_grep '--parent-worktree' "$log" "spawn should not pass --parent-worktree when no captain session is inherited" + rm -rf "/tmp/fm-$id" + pass "fm-spawn.sh: falls back to --no-parent when no trustworthy captain ORCA_WORKTREE_ID is inherited" +} + test_spawn_refuses_orca_secondmate_before_home_mutation() { local home subhome data state config id out status id="orcasmz1" @@ -1400,6 +1476,9 @@ test_worktree_create_uses_parent_when_orca_worktree_id_set test_worktree_create_uses_no_parent_when_orca_worktree_id_absent test_worktree_create_falls_back_to_no_parent_when_parent_link_fails test_worktree_create_shell_safely_passes_repo_qualified_parent_id +test_worktree_create_honors_explicit_parent_arg_over_cleared_env +test_fm_spawn_threads_inherited_captain_parent_into_orca_worktree_create +test_fm_spawn_uses_no_parent_when_not_running_under_orca test_spawn_preserves_orca_metadata_when_pathless_worktree_cleanup_fails test_spawn_writes_orca_metadata_and_launches_harness test_spawn_refuses_orca_secondmate_before_home_mutation diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index 7a3f85108..0a535d574 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -627,6 +627,29 @@ test_legacy_doubled_selector_still_resolves() { pass "legacy doubled fm-fm-* selectors still resolve through single-prefix strip; expected_label is idempotent" } +test_fleet_snapshot_expected_label_jq_matches_alias_helper() { + command -v jq >/dev/null 2>&1 || { echo "skip: jq not found"; return 0; } + local id alias_label jq_label + for id in fix-login-k3 fm-orca-fix-k2 fm-x ""; do + alias_label=$( bash -c '. "$0"; fm_alias_for_id "$1"' "$ROOT/bin/fm-backend.sh" "$id" ) + jq_label=$( printf '{"id":%s}' "$(printf '%s' "$id" | jq -Rs .)" \ + | jq -r '(.id // "") as $i | if ($i | startswith("fm-")) then $i else "fm-" + $i end' ) + [ "$jq_label" = "$alias_label" ] \ + || fail "fleet-snapshot expected-label jq ('$jq_label') must match fm_alias_for_id ('$alias_label') for id='$id'" + done + pass "fm-fleet-snapshot expected-label jq is idempotent (no fm-fm-*) and matches fm_alias_for_id" +} + +test_ff_lib_sources_alias_helper() { + local out home + home=$(fm_test_tmproot ff-lib-alias-home) + mkdir -p "$home" + out=$( FM_HOME="$home" bash -c '. "$0/bin/fm-ff-lib.sh"; printf "%s\n" "$(fm_alias_for_id "fm-orca-fix-k2")"' "$ROOT" 2>&1 ) + [ "$out" = "fm-orca-fix-k2" ] \ + || fail "sourcing fm-ff-lib.sh should make fm_alias_for_id available (so fm-update.sh, which sources ff-lib but not fm-backend.sh, gets it) without doubling an fm-* id; got '$out'" + pass "fm-ff-lib.sh: sources fm-backend.sh so fm_alias_for_id is available to every caller" +} + # --- old vs new: fm-send.sh -------------------------------------------------- make_send_fakebin() { # -> echoes fakebin dir; logs every tmux call to $FM_TMUX_LOG @@ -1110,6 +1133,8 @@ test_resolve_selector_three_forms test_backend_of_selector_matches_explicit_target_meta test_alias_for_id_is_idempotent test_legacy_doubled_selector_still_resolves +test_fleet_snapshot_expected_label_jq_matches_alias_helper +test_ff_lib_sources_alias_helper test_send_conformance_old_vs_new test_peek_conformance_old_vs_new test_spawn_symlinked_project_prefix_avoids_false_refusal From 90ae327c5ae558036a473913b6e33fe26b8a8f61 Mon Sep 17 00:00:00 2001 From: e-jung <8334081+e-jung@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:58:46 +0000 Subject: [PATCH 4/5] fix(lint): quote provenance/freshness to resolve SC2100 ShellCheck SC2100 fires on unquoted hyphenated assignments where the prefix/suffix tokens happen to be existing variable names in the sourced scope (parent-event-fallback). Quote the values so the assignment is unambiguous. --- bin/fm-fleet-snapshot.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fm-fleet-snapshot.sh b/bin/fm-fleet-snapshot.sh index 996878244..d5e2bc956 100755 --- a/bin/fm-fleet-snapshot.sh +++ b/bin/fm-fleet-snapshot.sh @@ -1084,8 +1084,8 @@ secondmate_current_json() { # terminal_evidence:$terminal,contradiction:$contradiction}') else if [ -n "$event_raw" ]; then - provenance=parent-event-fallback - freshness=historical-event + provenance="parent-event-fallback" + freshness="historical-event" else provenance=unknown freshness=unknown From 450cd44594699283c7072a9f0db3843d7541c534 Mon Sep 17 00:00:00 2001 From: e-jung <8334081+e-jung@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:29:38 +0000 Subject: [PATCH 5/5] no-mistakes: apply CI fixes --- tests/fm-backend.test.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index 0a535d574..3530cab5d 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -110,7 +110,7 @@ BASE_REF=$(resolve_base_ref) \ # tmux-only conformance run the tmux adapter's behavior is what is under test, # and that is unchanged by any later (e.g. non-tmux backend) addition to # fm-backend.sh's own dispatch surface. -OLD_BIN_UNCHANGED_SIBLINGS="fm-gate-refuse-lib.sh fm-guard.sh fm-lock-lib.sh fm-tangle-lib.sh fm-tmux-lib.sh fm-composer-lib.sh fm-marker-lib.sh fm-wake-lib.sh fm-classify-lib.sh fm-ff-lib.sh fm-config-inherit-lib.sh fm-tasks-axi-lib.sh fm-project-mode.sh fm-harness.sh fm-crew-state.sh fm-backend.sh" +OLD_BIN_UNCHANGED_SIBLINGS="fm-gate-refuse-lib.sh fm-guard.sh fm-supervision-lib.sh fm-lock-lib.sh fm-tangle-lib.sh fm-tmux-lib.sh fm-composer-lib.sh fm-marker-lib.sh fm-wake-lib.sh fm-classify-lib.sh fm-ff-lib.sh fm-config-inherit-lib.sh fm-tasks-axi-lib.sh fm-project-mode.sh fm-harness.sh fm-crew-state.sh fm-backend.sh fm-decision-hold.sh" OLD_BIN_REFACTORED="fm-send.sh fm-peek.sh fm-watch.sh fm-spawn.sh fm-teardown.sh" build_old_bin() { # -> echoes root dir (root/bin/