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
6 changes: 6 additions & 0 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ Natural language is acceptable if uncertain.
- grok: `/<skill>`, for example `/no-mistakes` (same form as claude). Verified end to end: grok discovers the user-level `no-mistakes` skill, `/no-mistakes` invokes it, and grok drives a real `no-mistakes axi run`. Like codex's `$`/`/` popups, typing `/<skill>` opens grok's slash-autocomplete, so a too-fast Enter selects the popup entry instead of sending, and for an argument-taking command (like `/no-mistakes`'s optional task-first argument) that first Enter only expands the popup selection into an argument-hint placeholder rather than submitting - a genuine second Enter is required (see the grok section below for the 2026-07-03 incident and fix). `fm_tmux_submit_core`'s retried Enter (used by `fm-send` on the tmux backend) handles this through the structural composer reader; the herdr backend needed a dedicated fix (`fm_backend_herdr_composer_state`, docs/herdr-backend.md) because its prior delta-based verification false-positived on that same popup-close content change.
- kimi: `/<skill>`, for example `/no-mistakes`.

Across every verified harness, a command form is recognized only when its `/` or `$` is the FIRST character of the composer line.
Any prefix demotes the whole line to prose that the agent merely reads, and the agent may then narrate compliance it never performed, so a prefixed command fails silently rather than erroring.
Field evidence: `[fm-from-firstmate]<U+2063>corr=... /exit` reached a secondmate, which answered "Exiting secondmate session as requested" and stayed open (robots-u7gu).
That is why `fm-send` refuses a command to a `kind=secondmate` target: its from-firstmate carrier owns column 0, so no command can be delivered on that path.
The refusal covers both command forms - a leading `/` for any harness, and a leading `$<skill>` when the target's recorded harness is codex (a leading `$` before a non-skill token like `$5` or `$HOME` stays deliverable prose).

## Submission acknowledgement hazards

A send or key action reporting success is not proof that the intended action happened.
Expand Down
23 changes: 23 additions & 0 deletions bin/fm-pending-reply-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,29 @@ fm_pending_reply_embed_corr() { # <message> <corr_id> <result-var>
printf -v "$result_var" '%s' "${FM_FROMFIRST_MARK}${token} ${body}"
}

# Inverse of fm_pending_reply_embed_corr's framing: the request body a marked
# secondmate actually reads, with the from-firstmate carrier and any leading
# correlation token removed. An unmarked or uncorrelated message yields itself.
# Byte-exact and non-normalizing on purpose - callers that reason about what the
# TARGET HARNESS sees (column-0 slash parsing, for example) must not have leading
# blanks or trailing newlines silently rewritten under them. Use
# fm_pending_reply_summarize instead when a short human-facing label is wanted.
fm_pending_reply_carrier_body() { # <message> <result-var>
local message=$1 result_var=$2 body existing
[ -n "$result_var" ] || return 2
body=${message#"$FM_FROMFIRST_MARK"}
# Strip a leading corr=<16hex> plus following blanks (space/tab only).
existing=${body:0:21}
case "$existing" in
corr=[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9])
body=${body:21}
while [ "${body# }" != "$body" ]; do body=${body# }; done
while [ "${body#$'\t'}" != "$body" ]; do body=${body#$'\t'}; done
;;
esac
printf -v "$result_var" '%s' "$body"
}

# Create a durable pending-reply expectation. Prints corr_id on success.
# Does not deliver anything. Fails if parent paths cannot be prepared.
fm_pending_reply_create() { # <parent-home> <state-dir> <task_id> <request-text>
Expand Down
34 changes: 33 additions & 1 deletion bin/fm-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
# an explicit backend-target escape-hatch target, and the --key path are never
# marked - their behavior is unchanged.
#
# Because that carrier sits at column 0, a marked line can never be a slash
# command: the harness only parses one when the slash is the first character, so
# a marked "/..." would arrive as prose and silently not run. fm-send refuses a
# marked slash command rather than reporting a verified submit for it; use
# fm-teardown to close a secondmate, or an explicit (never-marked) backend target
# to drive its harness directly.
#
# Parent-owned pending-reply expectation: every newly marked secondmate request
# also receives a privacy-safe correlation id and a durable parent record under
# state/pending-replies/ before delivery (bin/fm-pending-reply-lib.sh). Delivery
Expand Down Expand Up @@ -349,6 +356,29 @@ else
exit 0
fi
if [ "$MARK_FROM_FIRSTMATE" = 1 ]; then
# The from-firstmate carrier occupies column 0, and a harness only parses a
# slash command when the slash is the FIRST character of the composer line.
# A marked "/..." therefore arrives as ordinary prose: the agent reads it,
# may narrate compliance, and the command never runs, while fm-send reports a
# verified submit and opens a pending-reply expectation for a command that
# did not execute (robots-u7gu). Refuse loudly rather than deliver that.
# Checked on the body the secondmate would actually read, so an already
# marked/correlated recovery resend is judged on its command, not its carrier.
fm_pending_reply_carrier_body "$MESSAGE" CARRIER_BODY
case "$CARRIER_BODY" in
/*)
SLASH_VERB=${CARRIER_BODY%%[[:space:]]*}
echo "error: refusing to send the slash command '$SLASH_VERB' to secondmate ${TARGET_TASK_ID:-$T}: from-firstmate marked text carries '$FM_FROMFIRST_LABEL' at column 0, so the harness reads the whole line as prose and never runs the command. Recover with one of: bin/fm-teardown.sh to close that agent; an explicit backend target (its own endpoint, e.g. session:window), which is never marked, to drive its harness directly; or the same request as prose." >&2
exit 1
;;
\$[a-z]*)
if [ "$TARGET_HARNESS" = codex ]; then
CODEX_VERB=${CARRIER_BODY%%[[:space:]]*}
echo "error: refusing to send the codex skill command '$CODEX_VERB' to secondmate ${TARGET_TASK_ID:-$T}: from-firstmate marked text carries '$FM_FROMFIRST_LABEL' at column 0, so codex reads the whole line as prose and never runs the '\$<skill>' command. Recover with one of: bin/fm-teardown.sh to close that agent; an explicit backend target (its own endpoint, e.g. session:window), which is never marked, to drive its harness directly; or the same request as prose." >&2
exit 1
fi
;;
esac
# Reuse an existing correlation id for recovery resends; otherwise create a
# durable parent expectation before delivery. Transport success never
# resolves that expectation (see fm-pending-reply-lib.sh).
Expand Down Expand Up @@ -381,7 +411,9 @@ else
# starts ordinary text ("$5/month", "$HOME"), so a universal `$` rule would
# needlessly slow plain text to claude/opencode/pi. The target backend's
# verified submit retry still backs the settle up either way.
case "$*" in
# Matched on the FINAL text, not the arguments: a marked secondmate line starts
# with the carrier, so no popup opens and the fast settle is the correct one.
case "$MESSAGE" in
/*) settle=1.2 ;;
\$*)
if [ "$TARGET_HARNESS" = codex ]; then settle=1.2; else settle=0.3; fi
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ Secondmates are idle by default: after startup recovery reconciles only work alr
When called with `FM_HOME=<this-firstmate-home>` or when `FM_HOME` is already set to the active firstmate home, metadata-routed `fm-send.sh` requests to a live `kind=secondmate` use the live-charter-compatible `from-firstmate` carrier owned by `bin/fm-operational-input.sh`, so the secondmate returns terse answers through status lines and detailed answers through docs plus status pointers instead of replying only in its own chat.
The parent guards every marked request against a missing correlated report without reading the secondmate conversation; `bin/fm-pending-reply-lib.sh` owns the correlation, recovery, escalation, and retention contract.
Explicit backend-target sends and direct human typing stay unmarked, so captain intervention in a secondmate pane remains conversational.
Because that carrier occupies column 0 and a harness only parses a slash command whose slash is the line's first character, a marked request can never be one: `fm-send.sh` refuses a marked slash command and, for a `harness=codex` target, a marked `$<skill>` command, naming the carrier and pointing at `bin/fm-teardown.sh` for a close and at the never-marked explicit backend target for driving that harness directly, rather than reporting a verified submit and opening a correlated expectation for a command the secondmate would only read as prose.
After seeding a secondmate with the tasks-axi backend, `fm-backlog-handoff.sh` validates the fleet-specific handoff, then atomically delegates already-judged in-scope queued item moves to `tasks-axi mv` so the domain queue starts in the right place.
Handoff with the beads backend is not yet supported.
Remote routes move that dependency-closed set into a non-dispatchable backlog-format outbox before transfer, then use an idempotent remote receive under the destination backlog's own lock.
Expand Down
19 changes: 18 additions & 1 deletion tests/fm-send-popup-settle.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,19 @@ first_settle() { # <expected> <label> <harness|--explicit> <message> [selector-
target="fm-popupcase"
meta_id=popupcase
;;
secondmate)
target="fm-popupcase"
meta_id=popupcase
;;
*)
fail "$label: unknown selector form '$selector_form'"
;;
esac
fm_write_meta "$home/state/$meta_id.meta" "window=sess:win" "harness=$harness"
if [ "$selector_form" = secondmate ]; then
fm_write_secondmate_meta "$home/state/$meta_id.meta" "$home" "sess:win" alpha "$harness"
else
fm_write_meta "$home/state/$meta_id.meta" "window=sess:win" "harness=$harness"
fi
fi
: > "$log"
env FM_SEND_SETTLE=0 PATH="$fb:$PATH" \
Expand Down Expand Up @@ -136,3 +144,12 @@ first_settle 1.2 'codex /command -> long settle (slash unchanged)' codex '/help'

# Plain text to codex takes the fast path - the codex scope is `$`-prefixed only.
first_settle 0.3 'codex plain text -> fast path' codex 'just a normal steer'

# A kind=secondmate target prepends the from-firstmate carrier, so the composer
# line starts with '[' and no popup can open no matter what the ARGUMENTS were.
# The settle is therefore selected from the FINAL text: a `$`-prefixed price to a
# codex SECONDMATE takes the fast path, even though the same `$5...` argument to a
# codex crewmate above takes the long settle (robots-u7gu - the argument-vs-final
# mismatch that exposed the carrier defect). A real `$<skill>` to a secondmate is
# refused, not delivered, so it is exercised in fm-send-secondmate-marker.test.sh.
first_settle 0.3 'codex secondmate $price -> fast path (carrier at column 0)' codex '$5/month is cheap' secondmate
Loading
Loading