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/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ For a REMOTE secondmate, `fm-crew-state` and `fm-peek` read the actual remote en
Recover a genuinely stuck remote mate only through `bin/fm-spawn.sh <id> --secondmate`, never raw herdr pane close/kill surgery, which strands the endpoint binding.

Treat the digest's endpoint result as a presence signal, not proof that the task's work or validation run is gone.
A digest line of `endpoint: closed` means the finished worker's pane was retired on purpose while the isolated copy is still retained for landing; do not relaunch that worker.
Read the targeted current state with `bin/fm-crew-state.sh <id>` before deciding to relaunch.
A no-mistakes run matched to the crew's branch and current code remains authoritative when the endpoint is dead: handle a terminal or parked run through the normal lifecycle, and keep supervising an active run instead of creating a duplicate worker.

Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ The worker reports the PR when CI first becomes green rather than waiting for me

For PR-based ship tasks, the ready signal depends on mode: `no-mistakes` reports `done: PR <url> checks green` after CI is green, while `direct-PR` reports `done: PR <url>` after opening the PR.
Run `bin/fm-pr-check.sh <id> <PR url>` - it records `pr=` and the forge's `pr_head=` when available in the task's meta and arms the watcher's merge poll.
A finished ship or scout pane is closed once the worker process has exited, when the captain has opted in with `config/close-exited-panes` or `FM_CLOSE_EXITED_PANES=1`.
The isolated copy stays until landed cleanup, and that empty pane is not a stuck worker.
Tell the captain the PR's full URL, always the complete `https://...` link rather than a bare `#number`, a concise outcome summary, and the no-mistakes risk level when applicable.
A captain instruction to merge is explicit authority; `yolo` is the only standing routine merge authority.
For any custom `state/<id>.check.sh` you write yourself, keep it an ordinary single-link mode-`0700` file, print one line only when firstmate should wake, print nothing otherwise, finish before `FM_CHECK_TIMEOUT`, then bind its current bytes with `bin/fm-check-register.sh <id>` before the watcher may execute it.
Expand Down
15 changes: 15 additions & 0 deletions bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ fm_backend_tmux_capture() { # <target> <lines>
tmux capture-pane -p -t "$1" -S -"$2"
}

# fm_backend_tmux_capture_joined: like fm_backend_tmux_capture, but with `-J`
# so a logical line that soft-wrapped across the terminal width comes back as
# ONE row instead of split across several. Plain fm_backend_tmux_capture must
# stay byte-identical to the legacy inline tmux command (callers above rely on
# that), so this is a separate function rather than an added flag there. Used
# by proof checks that need the FULL logical line - e.g.
# fm-teardown.sh's --close-pane trailing-shell-glyph-only check
# (fm_composer_trailing_shell_glyph_only in bin/fm-composer-lib.sh), where
# inspecting only the last wrapped fragment of a long unsubmitted command can
# read a lone trailing prompt glyph as proof of an empty prompt (Greptile P1:
# task fm-close-exited-panes review).
fm_backend_tmux_capture_joined() { # <target> <lines>
tmux capture-pane -p -J -t "$1" -S -"$2"
}

# fm_backend_tmux_send_key: one named key. Mirrors fm-send.sh's --key path:
# `tmux display-message -p -t "$T" '#{pane_id}' >/dev/null`, then
# `tmux send-keys -t "$T" "$2"`.
Expand Down
38 changes: 38 additions & 0 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,44 @@ fm_backend_capture() { # <backend> <target> <lines> [expected-label]
esac
}

# fm_backend_capture_joined: like fm_backend_capture, but soft-wrapped rows
# come back joined into one logical line where the backend supports it (tmux
# `-J`), instead of split across several physical rows. For a caller that
# must prove something about one FULL logical line - e.g. --close-pane's
# trailing-shell-glyph-only proof (fm-teardown.sh) - inspecting only the last
# physical row of a wrapped line can find a lone trailing prompt glyph and
# wrongly treat unsubmitted typed text as an empty prompt (Greptile P1: task
# fm-close-exited-panes review). Backends without a documented join primitive
# fall back to fm_backend_capture unchanged.
fm_backend_capture_joined() { # <backend> <target> <lines> [expected-label]
local backend=$1
shift
fm_backend_source "$backend" || return 1
case "$backend" in
tmux) fm_backend_tmux_capture_joined "$@" ;;
*) fm_backend_capture "$backend" "$@" ;;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
esac
}

# fm_backend_capture_joined_reliable: 0 only for a backend whose
# fm_backend_capture_joined performs genuine logical-line joining of
# soft-wrapped rows (tmux -J). Every other backend's arm above silently falls
# back to the row-oriented fm_backend_capture, so its "joined" output still
# splits a long unsubmitted command across physical rows. A caller proving a
# full logical line is empty before an irreversible action (fm-teardown.sh's
# --close-pane composer-emptiness fallback proof) must not trust that
# fallback capture on a backend where this returns non-zero - inspecting only
# the last physical row of a wrapped line can find a lone trailing prompt
# glyph and wrongly treat unsubmitted typed text as an empty prompt (Greptile
# P1: task fm-close-exited-panes review - Herdr reaches the same close logic
# through an unchanged row-oriented capture).
fm_backend_capture_joined_reliable() { # <backend>
case "$1" in
tmux) return 0 ;;
*) return 1 ;;
esac
}

# fm_backend_send_key: one backend-supported named special key.
fm_backend_send_key() { # <backend> <target> <key> [expected-label]
local backend=$1
Expand Down
49 changes: 49 additions & 0 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,55 @@ EOF
return 1
}

# fm_composer_trailing_shell_glyph_only: true when <line>, trimmed, ENDS in
# one of the shell prompt glyphs (FM_COMPOSER_SHELL_PROMPT_GLYPHS) with
# nothing after it. A real interactive shell's PS1 draws arbitrary text
# BEFORE the glyph (user, host, cwd, git branch, ...), unlike an agent CLI's
# minimal glyph-anchored composer, so a live PS1 never matches this library's
# glyph-ANCHORED shapes above and the screen classifier always reads
# `unknown` for it - whether or not anything was typed after the prompt. A
# caller that must positively rule out unsubmitted typed text before trusting
# that `unknown` (e.g. pane-close cleanup, task fm-close-exited-panes) uses
# this instead: unlike fm_composer_leading_shell_glyph_var, the glyph need not
# lead the row, only trail it, so `user@host:~$` reads true and
# `user@host:~$ rm -rf` reads false.
#
# A trailing glyph alone is NOT sufficient proof: a real idle PS1 draws
# exactly one prompt glyph (its own, trailing), but unsubmitted typed text
# ending in a prompt-glyph character - `cat >`, `echo $` - reads as a SECOND
# occurrence once appended after that real prompt (`user@host:~$ cat >`,
# `user@host:~$ echo $`), and a suffix-only check would wrongly call that an
# empty prompt (task fm-close-exited-panes review). So this also requires the
# trailing glyph to be the ONLY prompt-glyph character anywhere in the line;
# two or more occurrences means typed text follows the real prompt and reads
# false.
#
# A bare `>` with NOTHING else on the line is still not proof, even as the
# sole occurrence: bash/zsh/dash all default PS2 (the secondary prompt shown
# mid multiline input - an unclosed quote, heredoc, or paren) to exactly
# `> ` with no host/path/branch prefix, which trims to the same lone `>` a
# real idle PS1 would never draw on its own. A `>` preceded by other prompt
# content (`user@host:~>`) is unambiguous and still accepted (Greptile P1:
# task fm-close-exited-panes review).
fm_composer_trailing_shell_glyph_only() { # <line>
local __fmtg_trimmed=$1 __fmtg_glyph __fmtg_matched='' __fmtg_class __fmtg_count
fm_composer_normalize_trim_var __fmtg_trimmed
[ -n "$__fmtg_trimmed" ] || return 1
while IFS= read -r __fmtg_glyph; do
[ -n "$__fmtg_glyph" ] || continue
case "$__fmtg_trimmed" in
*"$__fmtg_glyph") __fmtg_matched=$__fmtg_glyph; break ;;
esac
Comment thread
greptile-apps[bot] marked this conversation as resolved.
done <<EOF
$FM_COMPOSER_SHELL_PROMPT_GLYPHS
EOF
[ -n "$__fmtg_matched" ] || return 1
[ "$__fmtg_matched" = '>' ] && [ "$__fmtg_trimmed" = '>' ] && return 1
__fmtg_class=$(printf '%s' "$FM_COMPOSER_SHELL_PROMPT_GLYPHS" | tr -d '\n')
__fmtg_count=$(printf '%s' "$__fmtg_trimmed" | tr -dc "$__fmtg_class" | wc -c)
[ "$__fmtg_count" -eq 1 ]
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

fm_composer_idle_matches() {
local content=$1 idle_re=$2 idle_case=$3
[ -n "$idle_re" ] || return 1
Expand Down
5 changes: 4 additions & 1 deletion bin/fm-session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,10 @@ for meta in "$STATE"/*.meta; do

window=$(fm_meta_get "$meta" window)
target=$(fm_backend_target_of_meta "$meta")
if [ -n "$window" ]; then
if [ "$(fm_meta_get "$meta" pane_closed)" = 1 ]; then
backend=$(fm_backend_of_meta "$meta")
printf 'endpoint: closed (backend=%s window=%s)\n' "$backend" "${window:-none}"
elif [ -n "$window" ]; then
backend=$(fm_backend_of_meta "$meta")
if fm_backend_target_exists "$backend" "${target:-$window}" "fm-$id"; then
printf 'endpoint: alive (backend=%s window=%s)\n' "$backend" "$window"
Expand Down
Loading