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
320 changes: 100 additions & 220 deletions .agents/skills/afk/SKILL.md

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ config/secondmate-account-pool optional Agent Fleet pool the PRIMARY uses for S
config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "tasks-axi" = default tasks-axi backend, "manual" = force routine backlog updates to hand-editing; inherited by secondmate homes (section 10)
config/backend runtime session-provider backend override for new tasks; LOCAL, gitignored; absent = falls through to runtime auto-detection (the runtime firstmate itself is executing inside), then tmux; tmux is the verified reference backend, herdr/zellij/cmux are experimental new-task spawn backends, and Orca is legacy-recovery-only (docs/tmux-backend.md, docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, docs/cmux-backend.md) - herdr and cmux can also be selected by runtime auto-detection, zellij and Orca never are (always explicit), and codex-app is not accepted; see docs/codex-app-backend.md; not inherited into secondmate homes
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/wedge-alarm optional active-alert directives for wedged terminal-backed away-mode compatibility delivery; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present
data/ personal fleet records; LOCAL, gitignored as a whole
backlog.md task queue, dependencies, history
Expand All @@ -108,7 +108,7 @@ state/ volatile runtime signals; gitignored
x-outbox/ generated X-mode dry-run reply and dismiss previews; inspect it when FMX_DRY_RUN is set (section 14)
x-poll.error generated X-mode relay diagnostic dedupe marker
.wake-queue durable queued wakes: epoch<TAB>seq<TAB>kind<TAB>key<TAB>payload
.afk durable away-mode flag; present = sub-supervisor may inject escalations (set by /afk, cleared on user return)
.afk durable away-mode flag; present = sub-supervisor may deliver escalations (set by /afk, cleared on user return)
.lock per-home session lock written by fm-lock.sh: harness PID on line 1, holder process start time on line 2; direct readers parse only the first-line PID, while acquire/status treat it as held only when that PID is live with a matching start time (skipped for a legacy one-line lock, which is held while its PID is a live harness) and is not a Codex app-server
.watch.lock .wake-queue.lock watcher singleton and queue serialization locks
.hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch
Expand Down Expand Up @@ -641,15 +641,17 @@ The context-% shown in a peek is not actionable as crew health; ignore it and in

### Away-mode stub

Invoke the `/afk` skill when the captain says `/afk`, says they are going afk, `state/.afk` exists, an incoming message starts with `FM_INJECT_MARK`, or any `state/.subsuper-*` marker is involved.
The skill owns the full daemon procedure: classification policy, batching, injection hardening, max-defer, verified submit, marker stripping, portable lock, dedupe, target discovery, reliability properties, and `FM_INJECT_SKIP`.
Invoke the `/afk` skill when the captain says `/afk`, says they are going afk, `state/.afk` exists, a tracked away task completes with `afk-reap-wake:`, an incoming legacy message starts with `FM_INJECT_MARK`, or any `state/.subsuper-*` marker is involved.
The skill owns the full daemon procedure: classification policy, batching, native reap-wake delivery, terminal-backed compatibility delivery, portable lock, dedupe, reliability properties, and `FM_INJECT_SKIP`.
Inline facts that must survive without a loaded skill:

- Every daemon injection is prefixed with `FM_INJECT_MARK`, ASCII unit separator `0x1f`, so internal escalations are distinguishable from a captain message.
- On a native background-notify harness such as Claude, run the away daemon as its own tracked background task so completing that task is the captain-relevant wake primitive.
- While `state/.afk` exists, the daemon owns the watcher; do not separately arm `fm-watch-arm.sh` or `fm-watch.sh`.
- If firstmate receives a marked message while afk is active, it is an internal escalation: stay afk and process it.
- If the tracked away task completes with `afk-reap-wake:`, stay afk, drain the durable wake queue, process the batch, and restart the away daemon as a fresh native tracked task if the flag still exists.
- `FM_INJECT_MARK`, ASCII unit separator `0x1f`, identifies only legacy terminal-backed injections and is never used by native reap-wake delivery.
- If firstmate receives a legacy marked message while afk is active, it is an internal escalation: stay afk and process it.
- If the message starts with `/afk`, stay afk and refresh the flag.
- Any other unmarked message means the captain is back: stop the daemon so its shutdown flush runs while `state/.afk` is still set and clear `state/.afk` last (the `/afk` skill owns this ordering, via `bin/fm-afk-launch.sh stop`; clearing the flag first would make the flush a no-op), flush catch-up from `state/.wake-queue`, `state/.subsuper-escalations`, and `state/.subsuper-inject-wedged`, then resume the emitted primary-harness supervision protocol.
- Any other real user message means the captain is back: stop the daemon through `bin/fm-afk-launch.sh stop`, which clears `state/.afk` last, flush catch-up from `state/.wake-queue` and `state/.subsuper-escalations` plus any legacy `state/.subsuper-inject-wedged`, then resume the emitted primary-harness supervision protocol.
- Afk never changes approval authority; PR merges, ask-user findings, destructive actions, irreversible actions, and security-sensitive choices still require the same approval they required before.
- Bias ambiguous cases toward exit because a present captain beats token savings and a false exit is self-correcting.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Claude and grok use the slash form shown here; codex uses the same names with `$

| Skill | What it does |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `/afk` | Enter away-mode supervision: the sub-supervisor self-handles routine wakes in bash, escalates captain-relevant events and bounded declared-external-wait rechecks as batched digests, and actively alerts if delivery wedges while you step away |
| `/afk` | Enter away-mode supervision: the sub-supervisor self-handles routine wakes in bash and wakes the parked first mate with a batched digest only for captain-relevant events and bounded declared-external-wait rechecks |
| `/bearings` | Generate a "pick up where I left off" status report from the read-only fleet snapshot - backlog, per-task crew state, open PRs, scout reports, pending decisions, and date-gated queued work - written to a dated file in `data/` and surfaced concisely in chat; read-mostly, mutates no task state |
| `/reports` | Browse, search, open, or summarize the machine-global completion report stack, including visual evidence and follow-ups |
| `/updatefirstmate` | Self-update the running firstmate and its secondmates to the latest from origin with fast-forward-only pulls, then re-read instructions and nudge secondmates |
Expand All @@ -191,7 +191,7 @@ Firstmate's skills live in two separate places with different audiences:

- [docs/architecture.md](docs/architecture.md) - how the crew, supervision, worktrees, secondmates, account routing, completion reports, and project modes work.
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend and Agent Fleet account routing, optional X mode, the files you set, and harness support.
- [docs/wedge-alarm.md](docs/wedge-alarm.md) - configure the active alert for a wedged away-mode escalation delivery.
- [docs/wedge-alarm.md](docs/wedge-alarm.md) - configure the active alert for wedged terminal-backed away-mode compatibility delivery.
- [docs/tmux-backend.md](docs/tmux-backend.md) - setup guide for the tmux reference backend: prerequisites, attaching, and watching crew windows.
- [docs/herdr-backend.md](docs/herdr-backend.md) - setup guide for the experimental herdr backend, plus its verification notes and known gaps.
- [docs/zellij-backend.md](docs/zellij-backend.md) - setup guide for the experimental zellij backend, plus its verification notes and known gaps.
Expand Down
10 changes: 5 additions & 5 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2809,11 +2809,11 @@ fm_backend_herdr_composer_state() { # <target> -> empty|pending|unknown
# away-mode daemon. Root cause: composer-content submit confirmation was too
# sensitive to harness rendering details. Real claude/codex use bare prompt
# rows, and real codex adds dynamic idle suggestions after `›`; the later
# ANSI-aware composer classifier now handles the pre-injection guard for that
# Codex shape, but idle-baseline submit confirmation deliberately stays on
# native agent-state so delivery does not depend on composer text. Composer
# content is retained for other callers (the away-mode daemon's PRE-injection
# empty-box guard, still dispatched via fm_backend_composer_state /
# ANSI-aware composer classifier now handles the compatibility pre-injection
# guard for that Codex shape, but idle-baseline submit confirmation deliberately
# stays on native agent-state so delivery does not depend on composer text.
# Composer content is retained for other callers (the terminal-backed away-mode
# compatibility path's empty-box guard, still dispatched via fm_backend_composer_state /
# fm_backend_herdr_composer_state) and for submit attempts whose pre-Enter
# agent-state baseline is not legibly idle.
#
Expand Down
4 changes: 2 additions & 2 deletions bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# inline with these same send/current-path primitives.
#
# The verified composer/busy-detection and verify-and-retry-submit primitives
# already live in bin/fm-tmux-lib.sh, shared with the away-mode daemon
# (bin/fm-supervise-daemon.sh); this adapter sources that file and re-exports
# already live in bin/fm-tmux-lib.sh, shared with the terminal-backed away-mode
# compatibility path; this adapter sources that file and re-exports
# its submit core under the backend's naming convention rather than
# duplicating it, so the two consumers cannot drift apart.
# shellcheck source=bin/fm-tmux-lib.sh
Expand Down
32 changes: 17 additions & 15 deletions bin/fm-afk-launch.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
# fm-afk-launch.sh - the single owner of the away-mode daemon TERMINAL lifecycle:
# launch it in a NON-VISIBLE tracked terminal per backend, record its exact id,
# tear it down by that exact id, and reconcile a leaked one after a crash.
# fm-afk-launch.sh - the single owner of the away-mode daemon lifecycle and any
# compatibility terminal it needs.
# It records exact ownership, tears a terminal down by exact id, and reconciles
# a leaked one after a crash.
#
# Why this exists (docs/herdr-backend.md "Away-mode daemon terminal launch"):
# bin/fm-afk-start.sh execs the supervise daemon in the FOREGROUND of whatever
Expand All @@ -13,11 +14,10 @@
# workspace with --no-focus, or a detached tmux session) that never touches the
# captain's active tab, and NEVER uses shell `&` (which herdr/codex can reap).
#
# Correct supervisor targeting: the daemon finds the captain pane to inject into
# from its OWN inherited env (discover_supervisor_target). Running it in a
# separate terminal would make it discover its OWN pane, so this captures the
# captain pane FIRST (from the pane this script runs in) and passes it in as
# FM_SUPERVISOR_TARGET/FM_SUPERVISOR_BACKEND explicitly.
# Correct compatibility-targeting: a terminal-backed daemon would discover its
# own pane, so this captures the captain pane first and passes it in as
# FM_SUPERVISOR_TARGET/FM_SUPERVISOR_BACKEND for legacy injection delivery.
# Native tracked launches use task completion and never inspect a pane.
#
# Usage:
# fm-afk-launch.sh start Capture the captain pane, then (unless the daemon
Expand All @@ -29,10 +29,12 @@
# fm-afk-launch.sh start-native
# Prepare lifecycle state for a harness-native
# background job and record that no terminal exists.
# fm-afk-launch.sh stop Correct-ordered exit: SIGTERM the daemon so its
# cleanup flushes WHILE state/.afk is still present,
# wait for it, close the recorded terminal by exact
# id, then clear state/.afk last.
# fm-afk-launch.sh stop Correct-ordered exit: SIGTERM the daemon while
# state/.afk is still present, wait for it, close the
# recorded terminal by exact id, then clear
# state/.afk last. Native delivery preserves its
# buffer for catch-up; compatibility injection may
# perform its final guarded flush during cleanup.
# fm-afk-launch.sh reconcile Close a recorded-but-dead daemon terminal by exact
# id and drop the record (recovery after a crash).
#
Expand Down Expand Up @@ -1225,9 +1227,9 @@ fm_afk_launch_stop_locked() {
fm_afk_launch_log "malformed daemon terminal record; refusing to stop away mode"
return 1
fi
# (1) SIGTERM the daemon so its cleanup trap flushes buffered escalations
# WHILE state/.afk is still present (the exit-ordering fix: clearing .afk
# first would make that flush a no-op via inject_msg's presence gate).
# (1) SIGTERM the daemon while state/.afk is still present.
# Terminal-backed compatibility delivery may make its final guarded flush in
# cleanup, while native reap-wake preserves the buffer for firstmate's catch-up.
pid=""
pid_identity=""
if daemon_lock_held_by_live_daemon; then
Expand Down
36 changes: 31 additions & 5 deletions bin/fm-afk-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
# - prints "afk: daemon already running pid=<pid>" then exits 0 when that
# lock is held by a live daemon (a REFRESH: no stale-artifact clear);
# - otherwise clears any prior away session's stale escalation artifacts
# (fm_afk_clear_stale_artifacts) for a direct, non-prepared start, then
# execs bin/fm-supervise-daemon.sh in the foreground. A prepared start was
# already cleared transactionally by bin/fm-afk-launch.sh.
# (fm_afk_clear_stale_artifacts) for a direct, non-prepared start, selects
# reap-wake delivery when the launch record identifies a native tracked
# background task, then execs bin/fm-supervise-daemon.sh in the foreground.
# A prepared start was already cleared transactionally by
# bin/fm-afk-launch.sh.
#
# This file is sourceable: its BASH_SOURCE guard keeps main from running, while
# exposing the daemon-lock helpers and fm_afk_clear_stale_artifacts. Sourcing it
Expand All @@ -20,8 +22,10 @@
# This is the COMMON daemon entry for every backend. HOW it becomes a tracked
# background process differs by harness/backend and is owned elsewhere:
# - Harnesses with a native in-pane tracked-background tool (e.g. claude, grok)
# run this directly via that tool, so the daemon inherits the captain pane's
# env and auto-discovers it.
# run this directly via that tool.
# The native launch record selects reap-wake delivery, so a captain-relevant
# escalation completes this process and lets the harness's own background-task
# notification wake the parked LLM without reading or typing into its pane.
# - Harnesses with NO native background mechanism (e.g. pi) run this THROUGH
# bin/fm-afk-launch.sh, which creates a non-visible tracked terminal per
# backend (herdr tab/workspace, tmux detached session) and passes the
Expand All @@ -40,6 +44,7 @@ FM_AFK_LOCK="$FM_AFK_STATE/.supervise-daemon.lock"
FM_AFK_DAEMON="$FM_AFK_START_DIR/fm-supervise-daemon.sh"
FM_AFK_NATIVE_PROCESS="$FM_AFK_STATE/.afk-native-process"
FM_AFK_NATIVE_HANDOFF_LOCK="$FM_AFK_STATE/.afk-native-handoff.lock"
FM_AFK_LAUNCH_RECORD="$FM_AFK_STATE/.afk-daemon-terminal"
FM_AFK_NATIVE_PROCESS_UNSAFE=0
FM_AFK_NATIVE_PROCESS_MAX_BYTES=4096

Expand Down Expand Up @@ -275,6 +280,21 @@ fm_afk_start_flag_write() {
mv "$pending" "$destination" || { rm -f "$pending"; return 1; }
}

fm_afk_start_select_delivery() {
local record
if [ "${FM_AFK_STATE_PREPARED:-0}" = 1 ]; then
record=$(fm_afk_safe_control_read "$FM_AFK_LAUNCH_RECORD" "$FM_AFK_NATIVE_PROCESS_MAX_BYTES" 2>/dev/null) || return 1
case "$record" in
$'none\t-\tnative') FM_AFK_DELIVERY=reap-wake ;;
$'tmux\t'*|$'herdr\t'*) FM_AFK_DELIVERY=inject ;;
*) return 1 ;;
esac
else
: "${FM_AFK_DELIVERY:=inject}"
fi
export FM_AFK_DELIVERY
}

fm_afk_native_process_identity() {
local pid=$1 out
case "$pid" in
Expand Down Expand Up @@ -390,6 +410,12 @@ fm_afk_start_main() {
return 1
fi

if ! fm_afk_start_select_delivery; then
fm_lock_release "$FM_AFK_NATIVE_HANDOFF_LOCK"
echo "afk: prepared launch record is missing, unsafe, or malformed" >&2
return 1
fi

if ! fm_afk_native_process_write; then
fm_lock_release "$FM_AFK_NATIVE_HANDOFF_LOCK"
echo "afk: could not register native process" >&2
Expand Down
16 changes: 8 additions & 8 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
# agent composer" decision, and the copies drifted. The dangerous drift: a BARE
# shell prompt glyph (`>`, `$`, `%`, `#`) - what a pane shows once its agent has
# exited to a plain login shell - was treated as an empty, ready-to-inject
# AGENT composer. The away-mode escalation injector (bin/fm-supervise-daemon.sh)
# reads composer-emptiness to decide whether a pane is a safe injection target,
# so a dead-shell pane misread as "empty" meant an escalation could be typed
# into (and, worst case, executed by) that shell. Consolidating the one decision
# here means the safety rule cannot silently drift across adapters again.
# AGENT composer. The terminal-backed away-mode compatibility injector reads
# composer-emptiness to decide whether a pane is a safe injection target, so a
# dead-shell pane misread as "empty" meant an escalation could be typed into (and,
# worst case, executed by) that shell. Consolidating the one decision here means
# the safety rule cannot silently drift across adapters again.
#
# THE SAFETY RULE this owner enforces: a bare shell prompt glyph is a genuine
# empty agent composer ONLY when it appears INSIDE a real agent-composer
Expand All @@ -26,9 +26,9 @@
# afk-herdr-false-pending): a harness fills an otherwise-empty composer with
# de-emphasized ghost text - claude's rotating prompt suggestion, codex's idle
# suggestion, grok's placeholder - which a plain capture cannot tell apart from
# text a human typed, so the away-mode injector reads the idle pane as "pending
# input" and defers every escalation (the overnight wedge that motivated this
# consolidation). fm_composer_strip_ghost is the ONE ANSI-aware extractor of
# text a human typed, so the compatibility injector reads the idle pane as
# "pending input" and defers every escalation (the overnight wedge that motivated
# this consolidation). fm_composer_strip_ghost is the ONE ANSI-aware extractor of
# "real typed content": it drops every de-emphasized run - dim/faint (SGR 2, how
# claude and codex render ghost text) AND a dark/muted TRUECOLOR foreground (how
# grok renders placeholder/hint text) - and keeps only normal-intensity,
Expand Down
Loading