Skip to content
Closed
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
32 changes: 29 additions & 3 deletions bin/backends/orca.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,36 @@ fm_backend_orca_repo_ensure() { # <project-path>
printf '%s' "$repo_id"
}

fm_backend_orca_worktree_create() { # <project-path> <name>
local project=$1 name=$2 repo_id out wt_id wt_path terminal
# 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 <repo-uuid>::<path>. 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() { # <project-path> <name> [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
out=$(orca worktree create --repo "id:$repo_id" --name "$name" --no-parent --setup skip --json) || return 1
[ -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
# 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
Expand Down
16 changes: 15 additions & 1 deletion bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -410,10 +410,24 @@ fm_backend_of_selector() { # <raw-target> <resolved-target> <state-dir>
printf 'tmux'
}

# fm_alias_for_id: the stable fm-<id> 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() { # <task-id>
case "$1" in
fm-*) printf '%s' "$1" ;;
*) printf 'fm-%s' "$1" ;;
esac
}

fm_backend_expected_label_of_selector() { # <raw-target> <state-dir>
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
}

Expand Down
2 changes: 1 addition & 1 deletion bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() { # <target>
case "$TASK_BACKEND" in
tmux) tmux display-message -p -t "$1" '#{pane_id}' >/dev/null 2>&1 ;;
Expand Down
5 changes: 4 additions & 1 deletion bin/fm-ff-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------------
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 4 additions & 4 deletions bin/fm-fleet-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -853,7 +853,7 @@ terminal_evidence_json() { # <parent-task-json> <event-note> <evidence-contradi
backend=$(printf '%s' "$task" | jq -r '.backend // ""')
target=$(printf '%s' "$task" | jq -r '.endpoint.target // ""')
exists=$(printf '%s' "$task" | jq -r '.endpoint.exists // "unknown"')
expected=$(printf '%s' "$task" | jq -r '"fm-" + (.id // "")')
expected=$(printf '%s' "$task" | jq -r '(.id // "") as $i | if ($i | startswith("fm-")) then $i else "fm-" + $i end')
if [ -z "$target" ] || [ "$exists" = false ]; then
[ "$exists" = false ] && reason="recorded endpoint is absent" || reason="no recorded endpoint"
jq -n --arg observed "$SNAPSHOT_NOW" --arg reason "$reason" \
Expand Down Expand Up @@ -1084,8 +1084,8 @@ secondmate_current_json() { # <parent-tasks-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
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ for meta in "$STATE"/*.meta; do
target=$(fm_backend_target_of_meta "$meta")
if [ -n "$window" ]; then
backend=$(fm_backend_of_meta "$meta")
if fm_backend_target_exists "$backend" "${target:-$window}" "fm-$id"; then
if fm_backend_target_exists "$backend" "${target:-$window}" "$(fm_alias_for_id "$id")"; then
printf 'endpoint: alive (backend=%s window=%s)\n' "$backend" "$window"
else
printf 'endpoint: dead (backend=%s window=%s)\n' "$backend" "$window"
Expand Down
5 changes: 3 additions & 2 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ if [ "$BACKEND" = cmux ] && [ "$KIND" = secondmate ]; then
fi
if [ "$BACKEND" = orca ]; then
fm_backend_orca_runtime_check || exit 1
ORCA_PARENT_SELECTOR=$(fm_backend_orca_parent_selector 2>/dev/null || true)
fi
ORCA_ABORT_CLEANUP=0
ORCA_WORKTREE_ID=
Expand Down Expand Up @@ -692,7 +693,7 @@ validate_spawn_worktree() { # <source> <inspect-target>
fi
}

W="fm-$ID"
W=$(fm_alias_for_id "$ID")
case "$BACKEND" in
tmux)
SES=$(fm_backend_tmux_container_ensure)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-supervise-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ task_window_backend() { # <window> <state>
stale_window_is_busy() { # <window> <state>
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
Expand Down
8 changes: 4 additions & 4 deletions bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/orca-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ worktree=<absolute path to the Orca-created git 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 <id>` uses the same recorded fields after loading `state/<id>.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.
Expand All @@ -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:<repo> --name fm-<id> --no-parent --setup skip`.
2. Create an Orca worktree with `orca worktree create --repo id:<repo> --name fm-<id> --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.
Expand Down Expand Up @@ -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;
Expand Down
Loading