Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/firstmate-orca/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Do not manually patch metadata to make an externally-created Orca terminal look
## Supervision

Use `bin/fm-peek.sh`, `bin/fm-send.sh`, `bin/fm-crew-state.sh`, and `bin/fm-teardown.sh` for routine operation.
For steer messages, send short lines through `bin/fm-send.sh fm-<id> '...'`.
For steer messages, send short lines through `bin/fm-send.sh <id> '...'`; the stable `fm-<id>` alias also works.
Put long instructions in the task brief or a temporary file and point the crewmate at that file.

When supervising, treat `state/<id>.meta` as the routing record and Orca's own ids as backend implementation details.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ After those settings are loaded, hook command resolution is still cwd-sensitive
| Skill invocation | `$<skill>` (e.g. `$no-mistakes`); `/<skill>` is claude-only and codex rejects it as "Unrecognized command" |

A `$<skill>` invocation opens a `$`-autocomplete (skill) popup, the same hazard as the `/` slash popup: submitting too fast lets the popup swallow the Enter, so the invocation never lands.
`fm-send` handles it the same way it handles `/` - it gives the popup a longer settle (1.2s) between typing and the first Enter, with the target backend's submit retry as the safety net - but the `$` settle is scoped to `harness=codex`, read from the target's `state/<id>.meta`.
`fm-send` handles it the same way it handles `/` - it gives the popup a longer settle (1.2s) between typing and the first Enter, with the target backend's submit retry as the safety net - but the `$` settle is scoped to `harness=codex`, read from the target metadata for exact task ids or legacy `fm-<id>` labels.
That scope matters because, unlike `/`, a leading `$` commonly starts ordinary text (`$5/month`, `$HOME`), so a universal `$` rule would needlessly slow plain steers to claude/opencode/pi; only a codex target receiving a `$...` message gets the popup-settle.
An explicit `session:window` target has no meta, so its harness is unknown and treated as non-codex (the safe fast-path default).
This is why the validation trigger (`$no-mistakes`) to a codex crew now lands on the first Enter instead of biting the popup.
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,10 @@ Read `data/secondmates.md` before dispatching and compare the work request to ea
Route by the nature of the task, not just the project name.
A project may appear in several `projects:` clone lists, so choose the secondmate whose natural-language scope actually fits the work, such as triage versus feature development.
If the resolved project is `local-only`, keep the work with the main firstmate even when a secondmate scope sounds relevant.
If a secondmate's scope fits, steer that secondmate with one concise instruction via `bin/fm-send.sh fm-<id> '<work request>'` and let it run the normal lifecycle inside its own home.
The bare `fm-<id>` target resolves through this home's `state/<id>.meta`; pass an explicit backend target only when intentionally targeting an endpoint outside this firstmate home.
If a secondmate's scope fits, steer that secondmate with one concise instruction via `bin/fm-send.sh <id> '<work request>'` and let it run the normal lifecycle inside its own home.
The stable `fm-<id>` label printed by lifecycle commands still works, but exact task ids resolve first through this home's `state/<id>.meta`; pass an explicit backend target containing `:` only when intentionally targeting an endpoint outside this firstmate home.
A secondmate is itself a firstmate, so a request reaches it in its own chat, which you never read - the return channel that wakes you is its status file.
So `fm-send` to a bare `fm-<id>` whose meta is `kind=secondmate` automatically prepends a from-firstmate marker (`bin/fm-marker-lib.sh`); the secondmate recognizes it and returns its answer via its status file, or via a doc under its home plus a status pointer for a detailed response, never only in chat.
So `fm-send` to a task selector whose meta is `kind=secondmate` automatically prepends a from-firstmate marker (`bin/fm-marker-lib.sh`); the secondmate recognizes it and returns its answer via its status file, or via a doc under its home plus a status pointer for a detailed response, never only in chat.
Expect and read that response on the status/doc path the same way you read any other status signal; do not peek the secondmate's chat for the answer.
A captain typing directly into the secondmate's window is unmarked and stays a conversational captain intervention, so do not relay captain-destined chat through this path; the marker is applied only by `fm-send` to a `kind=secondmate` target.
Do not spawn a direct crewmate for work that belongs to a secondmate scope unless the secondmate is blocked or the captain explicitly redirects it.
Expand Down
5 changes: 3 additions & 2 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
# remainder is the whole pane id - fm_backend_herdr_parse_target splits on the
# first colon only). This is the value stored in a herdr task's meta window=
# field and is what fm_backend_resolve_selector already returns unchanged for
# both the fm-<id> and explicit backend-target forms (that function has no
# herdr-specific logic; it just returns meta's window= verbatim).
# exact task-id, legacy fm-<id>, and explicit backend-target forms (that
# function has no herdr-specific logic; it just returns meta's window=
# verbatim).
#
# Recovery/orphan discovery (ids may not deterministically match live state
# after a server restart in a differently-configured session; see the
Expand Down
2 changes: 1 addition & 1 deletion bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
. "$FM_BACKEND_LIB_DIR/fm-tmux-lib.sh"

# fm_backend_tmux_resolve_bare_selector: the live-window-listing fallback for a
# selector that is neither "session:window" nor a bare "fm-<id>" routed
# selector that is neither an explicit target nor a task selector routed
# through meta - an ad hoc window name with no recorded task. Mirrors the
# `tmux list-windows -a ... | grep` pipeline that used to live inline in
# fm-send.sh's and fm-peek.sh's own (until now duplicated) resolve().
Expand Down
8 changes: 4 additions & 4 deletions bin/backends/zellij.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@
# target. Mitigated: send/capture/cwd ops verify session liveness first
# (fm_backend_zellij_session_exists, a passive list-sessions query, never
# auto-creating), verify the specific pane still appears in list-panes JSON,
# and, for metadata-routed fm-<id> operations, verify the pane's tab still
# matches the expected caller-facing task label through the home-scoped or
# and, for metadata-routed task selector operations, verify the pane's tab
# still matches the expected caller-facing task label through the home-scoped or
# unambiguous legacy title before use. Kill verifies the session and, when
# teardown supplies an expected tab label, verifies a tab id still matches
# that label before closing it. Output-SHAPE validation (a bare integer tab
Expand Down Expand Up @@ -595,8 +595,8 @@ fm_backend_zellij_list_live() { # <session>
# posture. Rare path in practice (zellij tasks normally carry meta);
# best-effort. Not wired into fm_backend_resolve_selector's dispatcher
# (bin/fm-backend.sh), mirroring herdr: that bare-selector fallback stays
# tmux-only by design, and zellij/herdr tasks are targeted via fm-<id> meta or
# an explicit recorded target.
# tmux-only by design, and zellij/herdr tasks are targeted via task-selector
# meta or an explicit recorded target.
fm_backend_zellij_resolve_bare_selector() { # <name>
local name=$1 scoped sessions session tabs tab_id count=0 pane_id bare_session='' bare_tab_id=''
scoped=$(fm_backend_zellij_scoped_title "$name")
Expand Down
68 changes: 46 additions & 22 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,36 @@ fm_backend_meta_for_window() { # <target> <state-dir>
return 1
}

fm_backend_of_selector() { # <raw-target> <resolved-target> <state-dir>
local raw=$1 resolved=$2 state=$3 meta
fm_backend_task_id_for_selector() { # <raw-target> <state-dir>
local raw=$1 state=$2 id
case "$raw" in
*:*) return 1 ;;
esac
if [ -f "$state/$raw.meta" ]; then
printf '%s' "$raw"
return 0
fi
case "$raw" in
fm-*)
meta="$state/${raw#fm-}.meta"
[ -f "$meta" ] && { fm_backend_of_meta "$meta"; return 0; }
id=${raw#fm-}
[ -f "$state/$id.meta" ] || return 1
printf '%s' "$id"
return 0
;;
esac
return 1
}

fm_backend_meta_for_selector() { # <raw-target> <state-dir>
local raw=$1 state=$2 id
id=$(fm_backend_task_id_for_selector "$raw" "$state") || return 1
printf '%s/%s.meta' "$state" "$id"
}

fm_backend_of_selector() { # <raw-target> <resolved-target> <state-dir>
local raw=$1 resolved=$2 state=$3 meta
meta=$(fm_backend_meta_for_selector "$raw" "$state" 2>/dev/null || true)
[ -n "$meta" ] && { fm_backend_of_meta "$meta"; return 0; }
if [ -n "$resolved" ]; then
meta=$(fm_backend_meta_for_window "$resolved" "$state" 2>/dev/null || true)
[ -n "$meta" ] && { fm_backend_of_meta "$meta"; return 0; }
Expand All @@ -351,13 +373,10 @@ fm_backend_of_selector() { # <raw-target> <resolved-target> <state-dir>
}

fm_backend_expected_label_of_selector() { # <raw-target> <state-dir>
local raw=$1 state=$2 meta
case "$raw" in
fm-*)
meta="$state/${raw#fm-}.meta"
[ -f "$meta" ] && printf '%s' "$raw"
;;
esac
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"
return 0
}

# fm_backend_source: source the named backend's adapter file, once per shell.
Expand Down Expand Up @@ -404,15 +423,18 @@ fm_backend_source() { # <name>
}

# fm_backend_resolve_selector: resolve a raw fm-send.sh/fm-peek.sh style
# selector to a live session-provider target. Three forms, in order:
# selector to a live session-provider target. Four forms, in order:
# target with ":" used as-is (the escape hatch for a window/pane outside
# this firstmate home) - backend-independent, a literal string.
# "fm-<id>" routed through <state-dir>/<id>.meta's backend target
# exact task id routed through <state-dir>/<id>.meta's backend target
# (`window=` normally, `terminal=` for Orca) -
# backend-independent, a stored value, NOT re-verified
# against a live backend inventory (matches today's
# behavior: tmux window names can be trusted from meta
# without a live re-check).
# "fm-<id>" legacy task window label fallback routed through
# <state-dir>/<id>.meta when no exact
# <state-dir>/fm-<id>.meta exists.
# anything else first matched against recorded `window=`/`terminal=`
# metadata, then treated as an ad hoc bare window name and
# resolved by searching the legacy tmux live inventory.
Expand All @@ -423,16 +445,18 @@ fm_backend_resolve_selector() { # <raw-target> <state-dir>
printf '%s' "$raw"
return 0
;;
esac
meta=$(fm_backend_meta_for_selector "$raw" "$state" 2>/dev/null || true)
if [ -n "$meta" ]; then
window=$(fm_backend_target_of_meta "$meta")
[ -n "$window" ] || { echo "error: no backend target recorded in $meta" >&2; return 1; }
printf '%s' "$window"
return 0
fi
case "$raw" in
fm-*)
meta="$state/${raw#fm-}.meta"
if [ ! -f "$meta" ]; then
echo "error: no metadata for $raw in $state; pass session:window to target a window outside this firstmate home" >&2
return 1
fi
window=$(fm_backend_target_of_meta "$meta")
[ -n "$window" ] || { echo "error: no backend target recorded in $meta" >&2; return 1; }
printf '%s' "$window"
return 0
echo "error: no metadata for $raw in $state; pass session:window to target a window outside this firstmate home" >&2
return 1
;;
*)
meta=$(fm_backend_meta_for_window "$raw" "$state" 2>/dev/null || true)
Expand Down
4 changes: 2 additions & 2 deletions bin/fm-peek.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Print the tail of a crewmate endpoint (bounded, for cheap diagnosis).
# Usage: fm-peek.sh <target> [lines=40]
# <target> may be a bare firstmate task name (fm-xyz), resolved through
# this home's state/<id>.meta, or an explicit backend target.
# <target> may be an exact task id, a legacy fm-<id> task label resolved
# through this home's state/<id>.meta, or an explicit backend target.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down
37 changes: 14 additions & 23 deletions bin/fm-send.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Send one line of literal text to a crewmate endpoint, then Enter.
# Usage: fm-send.sh <target> <text...>
# <target> may be a bare firstmate task name (fm-xyz), resolved through
# this home's state/<id>.meta, or an explicit backend target.
# <target> may be an exact task id, a legacy fm-<id> task label resolved
# through this home's state/<id>.meta, or an explicit backend target.
# Special keys instead of text: fm-send.sh <target> --key Enter
# Key support is backend-specific: tmux/herdr support Escape, Enter, and C-c;
# Orca currently supports Enter and C-c only, and rejects Escape.
Expand All @@ -18,7 +18,7 @@
# Slash commands, and codex `$...` skill invocations resolved through harness
# meta, get a longer pre-Enter settle so completion popups do not swallow Enter.
#
# From-firstmate marker: when the resolved target is a bare `fm-<id>` whose meta
# From-firstmate marker: when the resolved target is a task selector whose meta
# records kind=secondmate, the text is prefixed with the from-firstmate marker
# (bin/fm-marker-lib.sh) so the secondmate routes its reply via its status file
# or a status-pointed doc instead of stranding it in chat the main firstmate
Expand Down Expand Up @@ -46,40 +46,31 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"

RAW_TARGET=$1
T=$(fm_backend_resolve_selector "$1" "$STATE")
TARGET_META=$(fm_backend_meta_for_selector "$RAW_TARGET" "$STATE" 2>/dev/null || true)
shift

# Mark a from-firstmate -> secondmate request. Only a bare `fm-<id>` target,
# resolved through this home's meta and recording kind=secondmate, is marked: the
# Mark a from-firstmate -> secondmate request. Only a task selector resolved
# through this home's meta and recording kind=secondmate is marked: the
# secondmate then routes its reply via the status path (see fm-marker-lib.sh).
# An explicit backend target (the escape hatch for endpoints outside this home)
# and any crewmate/scout target are left unmarked, and so is the --key path.
MARK_PREFIX=""
case "$RAW_TARGET" in
fm-*)
meta="$STATE/${RAW_TARGET#fm-}.meta"
if [ -f "$meta" ] && grep -q '^kind=secondmate$' "$meta" 2>/dev/null; then
MARK_PREFIX="$FM_FROMFIRST_MARK"
fi
;;
esac
if [ -n "$TARGET_META" ] && grep -q '^kind=secondmate$' "$TARGET_META" 2>/dev/null; then
MARK_PREFIX="$FM_FROMFIRST_MARK"
fi

# Resolve the target's harness from its meta (recorded by fm-spawn), used only to
# scope the codex `$<skill>` popup-settle below. A bare fm-<id> target carries
# scope the codex `$<skill>` popup-settle below. A task selector carries
# meta; an explicit backend-target escape hatch has none, so its harness is
# unknown and treated as non-codex (the safe default that keeps the fast path).
# The target's BACKEND comes from fm-<id> meta, or from matching the resolved
# The target's BACKEND comes from selector meta, or from matching the resolved
# explicit target back to recorded meta, then falls back to tmux.
TARGET_HARNESS=""
TARGET_BACKEND=$(fm_backend_of_selector "$RAW_TARGET" "$T" "$STATE")
EXPECTED_LABEL=$(fm_backend_expected_label_of_selector "$RAW_TARGET" "$STATE")
case "$RAW_TARGET" in
fm-*)
meta="$STATE/${RAW_TARGET#fm-}.meta"
if [ -f "$meta" ]; then
TARGET_HARNESS=$(fm_meta_get "$meta" harness)
fi
;;
esac
if [ -n "$TARGET_META" ]; then
TARGET_HARNESS=$(fm_meta_get "$TARGET_META" harness)
fi

if [ "${1:-}" = "--key" ]; then
fm_backend_send_key "$TARGET_BACKEND" "$T" "$2" "$EXPECTED_LABEL"
Expand Down
Loading