Skip to content
Open
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ If a secondmate's scope fits, steer that secondmate with one concise instruction
The bare `fm-<id>` target resolves through this home's `state/<id>.meta`; pass `session:window` only when intentionally targeting a window 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.
For codex secondmates, that marked ordinary-text path also uses the longer pre-Enter settle so the already-typed request is not left unsubmitted by input timing.
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ tests/fm-watcher-lock.test.sh # watcher singleton, lock-race, watch-
tests/fm-watch-triage.test.sh # always-on watcher triage: benign absorb, actionable surface, stale wedge threshold, heartbeat backstop, and afk one-shot coherence
tests/fm-daemon.test.sh # sub-supervisor classifier, /afk presence-gating, max-defer, composer, and fm-send submit tests
tests/fm-send-settle.test.sh # fm-send post-submit settle pause, tuning, disable, and --key bypass tests
tests/fm-send-popup-settle.test.sh # fm-send pre-Enter popup-settle selection for slash commands and codex $skill invocations
tests/fm-send-popup-settle.test.sh # fm-send pre-Enter popup-settle selection for slash commands, codex $skill invocations, and marked codex secondmate text
tests/fm-send-secondmate-marker.test.sh # fm-send from-firstmate marker for kind=secondmate targets: marked vs crewmate/explicit/--key, and the exact marker byte sequence
tests/fm-wake-daemon-lifecycle-e2e.test.sh # watcher + daemon lifecycle e2e: restart catch-up, batching, dedupe, stale-pane routing, and digest injection
tests/fm-composer-ghost.test.sh # dim-ghost stripping, ghost-only composer detection, and escape-free peek tests
Expand Down
18 changes: 14 additions & 4 deletions bin/fm-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# instead of silently leaving an unsubmitted instruction (incident afk-invx-i5).
# The composer/submit logic is shared with the away-mode daemon via
# bin/fm-tmux-lib.sh. Tune with FM_SEND_RETRIES (default 3) / FM_SEND_SLEEP (0.4).
# Slash commands, and codex `$...` skill invocations resolved through harness
# meta, get a longer pre-Enter settle so completion popups do not swallow Enter.
# Slash commands, codex `$...` skill invocations resolved through harness meta,
# and marked codex secondmate text get a longer pre-Enter settle so completion or
# input timing does not swallow Enter.
#
# From-firstmate marker: when the resolved target is a bare `fm-<id>` whose meta
# records kind=secondmate, the text is prefixed with the from-firstmate marker
Expand Down Expand Up @@ -102,13 +103,22 @@ else
# `$` case is scoped to codex on purpose: unlike `/`, a leading `$` commonly
# starts ordinary text ("$5/month", "$HOME"), so a universal `$` rule would
# needlessly slow plain text to claude/opencode/pi. The retried Enter in
# fm_tmux_submit_core still backs the settle up either way.
# fm_tmux_submit_core still backs the settle up either way. A marked ordinary
# message to a codex secondmate also uses the longer settle: live Codex panes
# have swallowed Enter on that path while leaving the already-typed request in
# the composer, and the marker is present only for bare kind=secondmate targets.
case "$*" in
/*) settle=1.2 ;;
\$*)
if [ "$TARGET_HARNESS" = codex ]; then settle=1.2; else settle=0.3; fi
;;
*) settle=0.3 ;;
*)
if [ -n "$MARK_PREFIX" ] && [ "$TARGET_HARNESS" = codex ]; then
settle=1.2
else
settle=0.3
fi
;;
esac
retries=${FM_SEND_RETRIES:-3}
sleep_s=${FM_SEND_SLEEP:-0.4}
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The watcher and daemon share `bin/fm-classify-lib.sh` for captain-relevant statu
The always-on watcher also uses that library's provably-working predicate on no-verb signal and non-terminal-stale paths, while the daemon keeps its away-mode stale recheck unchanged.
The daemon escalates only captain-relevant events as one batched, single-line digest (prefixed with an in-band sentinel marker so firstmate can tell daemon injections apart from real messages).
Its injection path shares `bin/fm-tmux-lib.sh` with `fm-send.sh`, so dim-ghost-aware and border-aware composer detection plus verified submit retry stay consistent; stalled escalation delivery raises `state/.subsuper-inject-wedged` after `FM_MAX_DEFER_SECS` instead of silently deferring forever.
`fm-send.sh` selects a pre-Enter popup-settle for slash commands and for codex `$...` skill invocations using the target's recorded `harness=` meta, then adds its own `FM_SEND_SETTLE` pause after successful text sends so immediate peeks catch the receiving turn starting; the sub-supervisor uses only the shared submit core and does not pay that post-submit pause.
`fm-send.sh` selects a pre-Enter popup-settle for slash commands, codex `$...` skill invocations, and marked ordinary text sent to codex secondmates using the target's recorded `harness=` and `kind=` meta, then adds its own `FM_SEND_SETTLE` pause after successful text sends so immediate peeks catch the receiving turn starting; the sub-supervisor uses only the shared submit core and does not pay that post-submit pause.

## Worktrees, not branches in your checkout

Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Each file also starts with a short header comment.
| `fm-wake-drain.sh` | Atomically drain queued watcher wakes before handling supervision work, then run the watcher-liveness guard |
| `fm-wake-lib.sh` | Shared durable wake queue and portable lock helpers sourced by the watcher, drain, arm, guard, and daemon |
| `fm-classify-lib.sh` | Shared captain-relevant wake classifier sourced by the watcher and daemon, plus the watcher's provably-working predicate |
| `fm-send.sh` | Send one verified literal line (or `--key Escape`) to a direct-report window; exits non-zero on confirmed swallowed Enter; bare `kind=secondmate` targets are marked as from-firstmate; slash commands and codex `$...` skill invocations get popup-settle before Enter; text sends pause `FM_SEND_SETTLE` seconds after success |
| `fm-send.sh` | Send one verified literal line (or `--key Escape`) to a direct-report window; exits non-zero on confirmed swallowed Enter; bare `kind=secondmate` targets are marked as from-firstmate; slash commands, codex `$...` skill invocations, and marked codex secondmate text get popup-settle before Enter; text sends pause `FM_SEND_SETTLE` seconds after success |
| `fm-tmux-lib.sh` | Shared tmux pane primitives for busy detection, dim-ghost-aware and border-aware composer detection, and verified submit retry |
| `fm-peek.sh` | Print a bounded tail of a crewmate pane |
| `fm-pr-check.sh` | Record `pr=` and GitHub's `pr_head=` when available for a PR-ready task, then arm the watcher's merge poll |
Expand Down
49 changes: 36 additions & 13 deletions tests/fm-send-popup-settle.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# $... explicit -> 0.3 (session:window target has no meta -> harness unknown
# -> non-codex safe default)
# plain text -> 0.3 (fast path)
# marked codex secondmate plain text -> 1.2 (long settle before submit)
#
# The popup-settle is the FIRST sleep recorded: fm_tmux_submit_core types the text,
# then `sleep "$settle"`, then the Enter-retry loop (sleep 0.4 each) and finally
Expand Down Expand Up @@ -67,24 +68,24 @@ SH
printf '%s\n' "$fb"
}

# first_settle <expected> <label> <harness|--explicit> <message>: build a fresh
# home, send <message> to a target whose meta records <harness> (or to a bare
# session:window with NO meta when --explicit), and assert the FIRST recorded sleep
# (the popup-settle) equals <expected>. FM_SEND_SETTLE=0 strips the trailing
# post-submit pause so the log holds only the popup-settle plus the 0.4 Enter wait,
# keeping the head assertion crisp. FM_ROOT_OVERRIDE points at a non-repo dir so
# fm-guard's tangle check stays silent; its watcher-liveness note goes to stderr
# (discarded).
first_settle() { # <expected> <label> <harness|--explicit> <message>
local expected=$1 label=$2 harness=$3 msg=$4
# first_settle_for_kind <expected> <label> <harness|--explicit> <kind> <message>:
# build a fresh home, send <message> to a target whose meta records <harness> and
# <kind> (or to a bare session:window with NO meta when --explicit), and assert
# the FIRST recorded sleep (the popup-settle) equals <expected>. FM_SEND_SETTLE=0
# strips the trailing post-submit pause so the log holds only the popup-settle
# plus the 0.4 Enter wait, keeping the head assertion crisp. FM_ROOT_OVERRIDE
# points at a non-repo dir so fm-guard's tangle check stays silent; its
# watcher-liveness note goes to stderr (discarded).
first_settle_for_kind() { # <expected> <label> <harness|--explicit> <kind> <message>
local expected=$1 label=$2 harness=$3 kind=$4 msg=$5
local dir fb log home target rc first
dir="$TMP_ROOT/case-$RANDOM"; mkdir -p "$dir/state"
fb=$(make_stubs "$dir"); log="$dir/sleep.log"; home="$dir"
if [ "$harness" = --explicit ]; then
target="sess:win"
else
target="fm-popupcase"
fm_write_meta "$home/state/popupcase.meta" "window=sess:win" "harness=$harness"
fm_write_meta "$home/state/popupcase.meta" "window=sess:win" "harness=$harness" "kind=$kind"
fi
: > "$log"
env FM_SEND_SETTLE=0 PATH="$fb:$PATH" \
Expand All @@ -96,6 +97,18 @@ first_settle() { # <expected> <label> <harness|--explicit> <message>
pass "fm-send popup-settle: $label -> ${expected}s"
}

# first_settle <expected> <label> <harness|--explicit> <message>: build a fresh
# home, send <message> to a target whose meta records <harness> (or to a bare
# session:window with NO meta when --explicit), and assert the FIRST recorded sleep
# (the popup-settle) equals <expected>. FM_SEND_SETTLE=0 strips the trailing
# post-submit pause so the log holds only the popup-settle plus the 0.4 Enter wait,
# keeping the head assertion crisp. FM_ROOT_OVERRIDE points at a non-repo dir so
# fm-guard's tangle check stays silent; its watcher-liveness note goes to stderr
# (discarded).
first_settle() { # <expected> <label> <harness|--explicit> <message>
first_settle_for_kind "$1" "$2" "$3" ship "$4"
}

# Codex `$<skill>` gets the long settle so its `$` popup clears (the fix).
first_settle 1.2 'codex $skill -> long settle' codex '$no-mistakes'

Expand All @@ -117,5 +130,15 @@ first_settle 1.2 'claude /command -> long settle (slash unchanged)' claude '/no-
# A `/` to codex is likewise still the long settle (slash path untouched).
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'
# Plain text to a codex crewmate takes the fast path - the long ordinary-text
# settle is only for marked secondmate requests.
first_settle 0.3 'codex crewmate plain text -> fast path' codex 'just a normal steer'

# A marked ordinary request to a codex secondmate gets the longer settle before
# Enter. This protects the from-firstmate message path where live Codex panes have
# swallowed Enter while the already-typed text remained in the composer.
first_settle_for_kind 1.2 'codex secondmate marked plain text -> long settle' codex secondmate 'route this work'

# A non-codex secondmate still keeps the ordinary text fast path; the timing
# workaround is intentionally scoped to Codex.
first_settle_for_kind 0.3 'claude secondmate marked plain text -> fast path' claude secondmate 'route this work'