diff --git a/.agents/skills/harness-adapters/SKILL.md b/.agents/skills/harness-adapters/SKILL.md index 38686f10cf7..1b826256042 100644 --- a/.agents/skills/harness-adapters/SKILL.md +++ b/.agents/skills/harness-adapters/SKILL.md @@ -162,6 +162,12 @@ Natural language is acceptable if uncertain. - grok: `/`, 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 `/` 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: `/`, 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]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 `$` 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. diff --git a/bin/fm-pending-reply-lib.sh b/bin/fm-pending-reply-lib.sh index cc74e5a991d..50908b8206e 100755 --- a/bin/fm-pending-reply-lib.sh +++ b/bin/fm-pending-reply-lib.sh @@ -214,6 +214,29 @@ fm_pending_reply_embed_corr() { # 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() { # + 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() { # diff --git a/bin/fm-send.sh b/bin/fm-send.sh index 6e02b32832c..6c16fffcd78 100755 --- a/bin/fm-send.sh +++ b/bin/fm-send.sh @@ -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 @@ -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 '\$' 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). @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md index 97fdb5dc3a8..68c42a3d7fa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -201,6 +201,7 @@ Secondmates are idle by default: after startup recovery reconciles only work alr When called with `FM_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 `$` 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. diff --git a/tests/fm-send-popup-settle.test.sh b/tests/fm-send-popup-settle.test.sh index f9d55528070..0a3b9c974a9 100755 --- a/tests/fm-send-popup-settle.test.sh +++ b/tests/fm-send-popup-settle.test.sh @@ -93,11 +93,19 @@ first_settle() { #