Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7a863f7
clarify PR ready review checks
ruby-dlee Jul 10, 2026
7525a37
fix: keep persistent secondmates out of the main backlog (#398)
kunchenguid Jul 10, 2026
88ab966
Harden session lock identity
ruby-dlee Jul 10, 2026
50f0841
no-mistakes(document): document two-line session lock format in AGENT…
ruby-dlee Jul 10, 2026
600075e
fix(backlog-handoff): move full item blocks including indented bodies…
kunchenguid Jul 10, 2026
732d4d4
feat: isolate browser sessions per spawn
ruby-dlee Jul 10, 2026
7d08b4c
fix: harden browser isolation lifecycle
ruby-dlee Jul 10, 2026
4bc0824
feat(herdr): make Herdr lab lifecycle safety deterministic for briefs…
kunchenguid Jul 10, 2026
22b1d71
fix(watcher): classify arm-command seatbelt by execution position (#403)
kunchenguid Jul 10, 2026
96244f4
fix: reconcile existing AGENTS.md safely (#405)
kunchenguid Jul 10, 2026
08453f9
feat: support project-less secondmate homes (#409)
kunchenguid Jul 10, 2026
31afb8c
fix: delegate backlog handoffs to tasks-axi (#411)
kunchenguid Jul 10, 2026
171207c
fix: ignore secondmate home marker during sync (#417)
kunchenguid Jul 10, 2026
a955a05
fix(composer): prevent dead-shell message injection (#416)
kunchenguid Jul 10, 2026
7788fa3
feat(watcher): add paused external-wait supervision (#421)
kunchenguid Jul 10, 2026
5f808cc
fix: preserve X-mode follow-up platform limits (#425)
kunchenguid Jul 10, 2026
0eaf293
fix(composer): handle ANSI ghost text safely (#429)
kunchenguid Jul 10, 2026
6d90240
fix(spawn): make tmux window handling robust under non-default config…
Deeds67 Jul 10, 2026
3e3dff6
test: isolate session-start suite from ambient harness markers (#432)
kunchenguid Jul 10, 2026
38086ea
fix(teardown): retry transient index locks during worktree return (#435)
kunchenguid Jul 10, 2026
492c937
fix: complete brief help and consolidate documentation (#438)
kunchenguid Jul 10, 2026
bc558c6
fix: detect Git and centralize backend configuration (#445)
kunchenguid Jul 10, 2026
52241a5
feat(daemon): add backend-independent wedge alerts (#444)
kunchenguid Jul 10, 2026
8cd90fe
docs: centralize firstmate operating contracts (#447)
kunchenguid Jul 11, 2026
3f549c1
fix(cmux): close last workspace during teardown (#449)
kunchenguid Jul 11, 2026
0daf674
fix: recover orphaned packed-refs locks during fleet sync (#453)
kunchenguid Jul 11, 2026
1fb1e30
feat(herdr): escalate blocked panes immediately (#472)
kunchenguid Jul 11, 2026
ad39e49
docs(readme): reposition firstmate as an agent distro (#473)
kunchenguid Jul 11, 2026
a6508b7
feat: add deterministic bounded bearings snapshots (#475)
kunchenguid Jul 11, 2026
78f9cce
fix: enforce deterministic ShellCheck parity (#481)
kunchenguid Jul 11, 2026
88e38ea
feat: guard primary shells from persistent cd commands (#483)
kunchenguid Jul 12, 2026
cb6e8ed
brief: add no-mistakes shared-daemon rule to ship and scout scaffolds…
mielyemitchell Jul 12, 2026
9ecd7c4
feat: make bearings concise and accurate (#485)
kunchenguid Jul 12, 2026
ff0936c
merge: sync official upstream through cb6e8ed
ruby-dlee Jul 12, 2026
d444db6
test: include composer helper in gotmp teardown fixture
ruby-dlee Jul 12, 2026
8df5ef7
Revert "fix: harden browser isolation lifecycle"
ruby-dlee Jul 12, 2026
11a2cc2
Revert "feat: isolate browser sessions per spawn"
ruby-dlee Jul 12, 2026
6bfac2c
Revert "clarify PR ready review checks"
ruby-dlee Jul 12, 2026
09769ac
no-mistakes(review): fix lock test cleanup and legacy one-line lock l…
ruby-dlee Jul 12, 2026
e1a6efb
no-mistakes(test): skip Pi TypeScript extension tests on older node
ruby-dlee Jul 12, 2026
2d8143f
no-mistakes(document): document node .ts-import test skip gate in CON…
ruby-dlee Jul 12, 2026
19427c4
merge: sync official upstream through 9ecd7c4
ruby-dlee Jul 12, 2026
4490693
no-mistakes(document): document six missing bin scripts in docs/scrip…
ruby-dlee Jul 12, 2026
07044d1
no-mistakes(ci): restore executable bit on tests/fm-lock.test.sh
ruby-dlee Jul 12, 2026
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
80 changes: 33 additions & 47 deletions .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: afk
description: Enter away-mode supervision. Use when the user invokes /afk (e.g. "/afk", "/afk back in an hour", "going afk"). Sets a durable away-mode flag so the sub-supervisor daemon can self-handle routine wakes and escalate only captain-relevant events as one batched digest, cutting supervision token cost during walk-away stretches. Exit is automatic; any real (unmarked) message returns to full per-wake responsiveness.
description: Enter away-mode supervision. Use when the user invokes /afk (e.g. "/afk", "/afk back in an hour", "going afk"). Sets a durable away-mode flag so the sub-supervisor daemon can self-handle routine wakes and escalate captain-relevant events plus bounded declared-external-wait rechecks as batched digests, cutting supervision token cost during walk-away stretches. Exit is automatic; any real (unmarked) message returns to full per-wake responsiveness.
user-invocable: true
metadata:
internal: true
Expand Down Expand Up @@ -39,9 +39,8 @@ batched digest rather than per-wake injections.
3. **Do not separately arm `fm-watch.sh`.** The daemon manages the watcher as
its child; the singleton lock no-ops a stray arm harmlessly.

4. **Acknowledge** to the captain that away-mode is active: the daemon will
self-handle routine wakes, escalate only captain-relevant events, and the
captain can exit by sending any real message.
4. **Acknowledge** to the captain that away-mode is active.
The daemon will self-handle routine wakes, escalate captain-relevant events and bounded declared-external-wait rechecks, and let the captain exit by sending any real message.

## How to exit afk

Expand Down Expand Up @@ -80,30 +79,23 @@ injection, dispatched through `bin/fm-backend.sh` for the supervisor's own
backend (tmux or herdr; see "Auto-discovered supervisor pane" below):

- **`pane_is_busy`** - the harness shows a busy footer (agent mid-turn) on tmux (shared with `fm-send.sh` via `bin/fm-tmux-lib.sh`); on herdr, tries the native `agent.get`-backed busy state first, trusts only `busy` outright, and corroborates every non-`busy` verdict with the same regex-over-capture reader.
- **`pane_input_pending`** - the composer holds real unsubmitted text (a
human's half-typed line, or a previous injection whose Enter was swallowed).
On tmux, the cursor-line detector **strips the harness's composer box
borders first**, so an idle *bordered* composer (claude draws `│ > … │`) is
correctly read as empty, not pending. Without this, every idle claude pane
looked like pending input and the daemon deferred 100% of escalations
(incident afk-invx-i5). `FM_COMPOSER_IDLE_RE` still overrides empty-composer
matching after border stripping. On herdr, the equivalent ANSI-aware
structural classifier (`fm_backend_herdr_composer_state`,
docs/herdr-backend.md) plays the same role.

Either condition defers the injection; the buffered escalation survives in
`state/.subsuper-escalations` and is retried on the next housekeeping tick. In
afk mode the composer guard is belt-and-suspenders (no human is typing), but it
protects against the race window between the captain returning and their
message landing, and against the daemon's own previous injection sitting unsent.
- **Composer-state guard** - `inject_msg` reads the full `empty`/`pending`/`unknown` verdict from `fm_backend_composer_state` and injects only when it is affirmatively `empty`.
`pending` means real unsubmitted text, while `unknown` includes an unreadable pane and a bare shell prompt left after the agent exits, so both defer.
The shared `bin/fm-composer-lib.sh` owns the content decision after each backend captures and structurally identifies its own composer row.
It preserves idle bordered composers such as claude's `│ > … │` and bare agent glyphs as empty, but a bare shell glyph is unknown unless inside a genuine bordered composer box; see `docs/herdr-backend.md` "Composer-emptiness safety" for the complete contract.
`pane_input_pending` remains the tested predicate for callers that only need to know whether real unsubmitted text is present, but it is insufficient for an injection-safety decision because it cannot distinguish `empty` from `unknown`.

Either condition, or any composer verdict other than `empty`, defers the injection; the buffered escalation survives in `state/.subsuper-escalations` and is retried on the next housekeeping tick.
In afk mode the composer guard is belt-and-suspenders (no human is typing), but it protects against the race window between the captain returning and their message landing, a dead shell, and the daemon's own previous injection sitting unsent.

**Max-defer escape (the daemon must never silently wedge).**
If anything stays buffered past `FM_MAX_DEFER_SECS` (default 300), the daemon
attempts one normal flush, which still requires an idle pane and empty composer.
attempts one normal flush, which still requires an idle pane and an affirmatively empty composer.
If that submit cannot be confirmed, it raises a loud, rate-limited wedge alarm:
an ERROR in the daemon log, a durable
`state/.subsuper-inject-wedged` marker (surface it on the "while you were out"
catch-up if present), and a flash on the supervisor client's status line.
catch-up if present), a tmux status-line flash when applicable, and a configurable backend-independent active alert.
`docs/wedge-alarm.md` owns the alert channel setup and verification record.
So a guard false-positive becomes a visible stall, never an unbounded silent no-op.

## Submit model
Expand All @@ -115,7 +107,7 @@ Enter is retried (Enter only, never a retype) until the backend confirms the
submit landed.
For tmux that confirmation is a cleared composer, using the same corrected,
border-aware detector as the composer guard.
For herdr, normal idle-baseline submits are confirmed by native agent-state showing a real turn started; the ANSI-aware composer classifier remains the pre-injection guard and conservative fallback for non-idle or unreadable baselines.
For herdr, normal idle-baseline submits are confirmed by native agent-state showing a real turn started; the ANSI-aware composer classifier remains the affirmative-empty pre-injection guard and conservative fallback for non-idle or unreadable baselines.
A bordered-empty or ghost-only composer is recognized as empty where that backend uses composer confirmation, rather than mistaken for a swallowed Enter.
`fm-send.sh` uses the same primitive and exits non-zero
when a steer's Enter is positively swallowed, so firstmate learns an instruction
Expand All @@ -126,20 +118,17 @@ did not land instead of leaving it unsubmitted.
The daemon wraps `fm-watch.sh`, runs the watcher as a child, classifies each
wake reason in bash, and self-handles the routine majority without consuming a
firstmate turn.
Only captain-relevant events escalate to firstmate's context, and even then as
one pre-read, single-line, batched digest.
The classification predicates (the captain-relevant verb set, the signal/stale
tests, and the fleet-scan) live in the shared `bin/fm-classify-lib.sh`, the same
library the always-on watcher uses for its own triage when afk is off, so the two
modes apply one identical policy. While `state/.afk` exists the daemon owns the
watcher, so the watcher reverts to one-shot and lets the daemon do the triage -
the two never run their triage at the same time.
Captain-relevant events, plus a bounded recheck of a declared external wait that remains idle, escalate to firstmate's context as one pre-read, single-line, batched digest.
The classification predicates (the captain-relevant verb set, declared-pause vocabulary, signal/stale tests, and fleet-scan) live in the shared `bin/fm-classify-lib.sh`, the same library the always-on watcher uses for its own triage when afk is off, so the two modes apply one identical policy.
While `state/.afk` exists the daemon owns the watcher, so the watcher reverts to one-shot and lets the daemon do the triage - the two never run their triage at the same time.

Classify each wake this way:

- `signal` whose status content has no captain-relevant verb
(`done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged`)
-> self-handle. Captain-relevant verb -> escalate.
- `signal` or `stale` for a declared `paused:` external wait -> self-handle and track the pause rather than a wedge.
If it remains declared and idle past `FM_PAUSE_RESURFACE_SECS` (default 3600s), housekeeping sends one awaiting-external recheck and resets the pause window.
- `check` -> always escalate. Check scripts print only when firstmate should wake.
- `stale` with a terminal status -> escalate. Non-terminal stale is transient:
record a marker and self-handle. If the pane is still idle past
Expand All @@ -163,31 +152,27 @@ the marker lets firstmate distinguish it from a real captain message.
- **Single-line digest** - embedded newlines are collapsed to a literal
separator before injection, so submission is unambiguous regardless of
harness.
- **Composer guard on the supervisor pane** - before injecting, the daemon
checks both `pane_is_busy` (harness busy footer means agent mid-turn) and
`pane_input_pending` (real unsubmitted text on the cursor line means human
mid-typing or previous injection with swallowed Enter). Either condition
defers injection and preserves the buffer for retry. The daemon never merges
its digest into the captain's half-typed line.
- The composer detector, shared with `fm-send.sh` in `bin/fm-tmux-lib.sh`, drops
dim/faint ghost text, then strips harness composer box borders, so a ghost-only
or idle bordered composer such as claude's `│ > ... │` reads as empty, not
pending. Without these filters, idle bordered composers and dim ghost
suggestions can look like pending input and stall supervision. `FM_COMPOSER_IDLE_RE`
still overrides empty-composer matching after dim-ghost and border stripping,
and `FM_BUSY_REGEX` overrides busy footers.
- **Composer guard on the supervisor pane** - before injecting, the daemon checks `pane_is_busy` (harness busy footer means agent mid-turn) and reads `fm_backend_composer_state` directly.
Only `empty` permits injection; `pending` protects half-typed or swallowed input, and `unknown` protects unreadable panes and bare dead-shell prompts.
Every other result preserves the buffer for retry, so the daemon never merges its digest into the captain's half-typed line or types it into a shell.
- The shared composer classifier receives a candidate row only after the active backend performs its own capture and structural row recognition.
tmux and herdr route their raw styled candidate rows through the shared `fm_composer_strip_ghost` extractor, which removes dim/faint and dark-TRUECOLOR ghost/placeholder text before classification.
They read the composer shape from a separately ANSI-stripped plain row because a dark TRUECOLOR border can be stripped with ghost content.
A ghost-only or idle bordered composer such as claude's `│ > ... │` therefore reads empty without allowing an unbordered shell prompt to do the same.
`FM_COMPOSER_IDLE_RE` still overrides tmux empty-composer matching after shared ghost and border stripping, and `FM_BUSY_REGEX` overrides busy footers.
- **Max-defer escape** - the daemon must never silently wedge. If anything stays
buffered past `FM_MAX_DEFER_SECS` (default 300s), the daemon attempts one
normal flush, which still requires an idle pane and empty composer. If that
normal flush, which still requires an idle pane and an affirmatively empty composer. If that
cannot confirm a submit, it raises a loud, rate-limited wedge alarm: ERROR log,
durable `state/.subsuper-inject-wedged` marker, and a status-line flash. A
durable `state/.subsuper-inject-wedged` marker, a tmux status-line flash when
applicable, and a backend-independent active alert. A
composer false-positive surfaces as a visible stall, never an unbounded silent
no-op.
- **Verified type-once submit model** - the digest is typed once (`send-keys -l`
on tmux, `pane send-text` on herdr), then submitted with Enter and verified.
Enter is retried, Enter only and never a retype, until the backend submit
primitive reports `empty` as its caller-facing success verdict.
For tmux that verdict means the dim-ghost-aware and border-aware composer
For tmux that verdict means the shared-ghost-aware and border-aware composer
cleared.
For herdr's normal idle-baseline path it means native agent-state observed a real turn start; herdr uses the ANSI-aware structural classifier for the pre-injection composer guard and fallback paths.
This lets ghost-only or bordered-empty composers count as empty where a composer read is the active confirmation signal.
Expand Down Expand Up @@ -220,6 +205,7 @@ These properties must hold:
- Nothing is lost. The durable queue plus `fm-wake-drain.sh` recover any missed
or crashed injection.
- Wedge detection is bounded-latency, not lossy.
- Declared external waits are rechecked on a separate, bounded cadence rather than being mislabeled as wedges.
- The catch-all scan backs up the keyword classifier.
- The daemon preserves a single-instance portable lock, crash-loop backoff,
a pane-gone guard, and a signal-trapped shutdown that flushes buffered
Expand Down
Loading