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
f0d7cbe
fix(herdr): place workers in the launching workspace (#1328)
kunchenguid Jul 30, 2026
3a112a1
fix(calm): refine Calm working boat animation (#1339)
kunchenguid Jul 31, 2026
28b02d2
fix(dispatch): preflight candidate auth before quota escalation (#1349)
kunchenguid Jul 31, 2026
5fca47f
feat(x-mode): reconcile promised public replies deterministically (#1…
kunchenguid Jul 31, 2026
96542a4
feat(bin): replace busy heuristics with semantic lifecycle state (#1327)
kunchenguid Jul 31, 2026
621299a
fix: preserve Calm boat continuity across working periods (#1356)
kunchenguid Jul 31, 2026
f7d0d0a
fix: restore evidence-based dispatch eligibility (#1358)
kunchenguid Jul 31, 2026
3772964
docs: define captain instruction precedence (#1362)
kunchenguid Jul 31, 2026
9fdef64
docs: define validation supersession sequence (#1407)
kunchenguid Jul 31, 2026
000c1db
fix: bind backend overrides to exact-task authority (#1413)
kunchenguid Jul 31, 2026
66b0f77
fix(herdr): prevent focus flashes during projected workspace cleanup …
kunchenguid Jul 31, 2026
a805766
fix: prioritize completion runway in quota-aware dispatch (#1431)
kunchenguid Jul 31, 2026
68641a3
fix(bin): preserve full task contract in no-mistakes intent (#1447)
kunchenguid Aug 1, 2026
1e24757
fix(bin): parse punctuated secondmate registry entries safely (#1452)
kunchenguid Aug 1, 2026
8c21b10
feat(bin): add durable process-event supervision (#1483)
kunchenguid Aug 2, 2026
cd73e75
fix(bin): retire terminal process events and surface queued wakes (#1…
kunchenguid Aug 2, 2026
f5ab708
perf: shard portable serial tests across CI runners (#1544)
kunchenguid Aug 2, 2026
88b2a94
fix(bin): correct session lock and attached watcher supervision (#1545)
kunchenguid Aug 2, 2026
33a4287
fix(bin): harden Claude supervision auto-arm recovery (#1495)
kunchenguid Aug 2, 2026
4ee4a0a
feat(bin): require an explicit per-task delivery contract (#1563)
kunchenguid Aug 3, 2026
4eb1376
feat(spawn): propagate a native W3C traceparent to spawned agents
allstargg Jul 24, 2026
68166d4
fix(spawn): propagate the effective trace-context decision to secondm…
allstargg Jul 24, 2026
fecae4b
no-mistakes(review): Clarify Secondmate trace-context launch snapshots
allstargg Jul 24, 2026
f0ce267
no-mistakes(document): Correct trace-context documentation ownership …
allstargg Jul 24, 2026
bf79952
fix(spawn): resolve the trace-context decision once for carrier and s…
allstargg Jul 25, 2026
16fc4aa
no-mistakes(review): Preserve legacy Secondmate trace boundary
allstargg Jul 25, 2026
bf5aaf7
no-mistakes(document): Correct trace-context verification comparison …
allstargg Jul 25, 2026
3427c3a
no-mistakes(review): Captain, prevent failed trace delivery metadata …
allstargg Jul 25, 2026
a45981a
no-mistakes(review): Captain, align trace-context tests and verificat…
allstargg Jul 25, 2026
6435761
no-mistakes(document): Correct trace-context verification evidence
allstargg Jul 25, 2026
0ac5e7a
no-mistakes(lint): Suppress intentional ShellCheck literal-dollar war…
allstargg Jul 25, 2026
215bb45
no-mistakes(review): Captain: freeze trace context at session start
allstargg Jul 26, 2026
446e3c8
no-mistakes(test): Captain: stabilize scheduler test and document Kim…
allstargg Jul 26, 2026
b84c8f2
no-mistakes(document): Document trace-context safety boundaries
allstargg Jul 26, 2026
b094e04
fix(trace): fail off on stale session snapshots
allstargg Jul 26, 2026
afa1d20
no-mistakes(review): Fix trace spawn failure independence and duplica…
allstargg Jul 26, 2026
ac76306
no-mistakes(document): Refresh trace-context documentation and verifi…
allstargg Jul 26, 2026
0f41592
no-mistakes(review): Clear partial backend input after failed trace s…
allstargg Jul 28, 2026
459d507
no-mistakes(review): Stop unsafe trace delivery before launch append
allstargg Jul 28, 2026
e6b2c4c
no-mistakes(document): Document unsafe trace delivery handling
allstargg Jul 28, 2026
27b47e6
fix(trace): bound each trace to one routed task, never the routing agent
allstargg Jul 31, 2026
9e8c25d
docs(trace): define the per-task trace boundary
allstargg Jul 31, 2026
392edea
test(trace): adopt the explicit per-task delivery contract in spawn f…
allstargg Aug 3, 2026
10411fe
Merge validated PR 995 tree into refreshed fork
allstargg Aug 3, 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
9 changes: 5 additions & 4 deletions .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ The daemon never injects into an in-use pane. Two checks run before every
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.
- **Primary-pane busy guard** - `pane_is_busy` trusts Herdr native `busy` when available, otherwise matches rendered output against only the detected primary harness's signature.
This narrow delivery guard never classifies a recorded worker task and never uses a global union of vendor patterns.
- **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 and injection 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.
A busy primary pane, 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).**
Expand Down Expand Up @@ -181,14 +182,14 @@ the operational prefix 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 `pane_is_busy` (harness busy footer means agent mid-turn) and reads `fm_backend_composer_state` directly.
- **Busy and composer guards on the supervisor pane** - before injecting, the daemon runs the detected-primary-harness rendered busy guard 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.
`FM_COMPOSER_IDLE_RE` still overrides tmux empty-composer matching after shared ghost and border stripping, and `FM_BUSY_REGEX` overrides the rendered delivery guards plus Grok's isolated task-state fallback.
- **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 an affirmatively empty composer. If that
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/firstmate-coding-guidelines/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Every contract - a data format, a state machine, a decision procedure - is state
Every other mention of it is a one-line cross-reference, never a restatement.
A single deliberate one-line reinforcement at a genuine risk point is allowed, for example a "don't forget X" placed exactly where forgetting X is costly.
Restating the contract's substance a second time is not allowed: the two copies will drift the moment only one is edited.
When you touch a contract, grep the repo for its other mentions and update the cross-references, not duplicate the change into a second full copy.
When you touch a contract, patch, replace, or prune the owner's existing language rather than appending a new clause or paragraph wherever possible, then grep the repo for its other mentions and update the cross-references, not duplicate the change into a second full copy.

## Inline-stub pattern

Expand All @@ -61,7 +61,7 @@ Apply the decision tree above to every line you are about to add to `AGENTS.md`.
If an addition needs more than a few lines of conditional detail (detail that matters only in a specific situation) or reference detail (a wire format, an exact schema, historical rationale), you are almost certainly adding it to the wrong file.
`AGENTS.md`'s token cost is paid by every session of every fleet member, every time, whether or not that session ever hits the situation the new lines describe.
A skill's cost is paid only by the sessions that actually load it.
When in doubt, write the fact into the skill or doc first, and add only the one-line trigger to `AGENTS.md`.
When in doubt, write the fact into the skill or doc first by patching that owner's existing language, and add only the one-line trigger to `AGENTS.md`.

## Trigger hygiene

Expand Down
47 changes: 42 additions & 5 deletions .agents/skills/fmx-respond/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ description: >-
Agent-only playbook for handling X mode mentions and follow-ups.
Use on an "x-mention <request_id>" check wake to read the stashed mention, classify it, act autonomously on eligible requests, reply or dismiss, and link spawned work.
Also use on an "x-mode-error ..." check wake to report the X-mode configuration blocker instead of answering a mention.
Also use on milestone and terminal wakes for an X-mode-linked task before posting completion follow-ups, ending terminal outcomes with --final.
Also use on milestone and terminal wakes for an X-mode-linked task before posting completion follow-ups, using typed promised-final reconciliation when registered and --final otherwise.
Also use on a "public-followup ..." check wake, and whenever a promised final public reply must be created, reconciled, or delivered.
Loaded only when X mode is enabled.
user-invocable: false
metadata:
Expand Down Expand Up @@ -56,7 +57,7 @@ How the reply lands depends on whether the work finishes during this turn:
The exact resolution and fail-safe posting contract is owned by `docs/configuration.md`.
If a recovery respawns the same relay request onto a successor task, relink with the paired `--carry-count <n> --carry-ts <epoch>` flags plus any prior `x_platform=` and `x_reply_max_chars=` as `--carry-platform <x|discord> --carry-max <n>` so the successor keeps the consumed follow-up count, original 7-day window, and reply split budget.
4. **Follow up on genuine milestones, sparingly.** Firstmate gets up to **three** follow-ups per mention, within a 7-day window, chained in the same thread - spend them only on changes the captain would actually want to hear about (e.g. investigation done and a build started, work shipped or ready, or the task failing), never on routine internal churn.
The task's final outcome - shipped / reported / merged / failed - is always posted with `--final`, which clears the link regardless of how many follow-ups remain.
A task without a promised-final commitment posts its final outcome - shipped / reported / merged / failed - with `--final`, which clears the link regardless of how many follow-ups remain. A typed promised-final commitment uses the deterministic consumer instead.
That posting happens on the task's milestone and completion wakes (see "Completion follow-up" below), not this turn.

So every drained mention sorts into one of three cases (the worthiness judgment, widened):
Expand Down Expand Up @@ -141,7 +142,7 @@ Treat `state/x-inbox/` as the source of truth and process **every** file you fin
**If the request spawned a real, longer-running task** (you ran `bin/fm-spawn.sh`), link that task to this mention so milestone and completion follow-ups can be posted: `bin/fm-x-link.sh <task-id> <request_id>`.
**Link here, in step 2c, before the step 2f inbox cleanup** - `bin/fm-x-link.sh` can copy both the mention's reply platform and explicit budget from the still-present inbox payload without a relay lookup.
If that local context is incomplete it uses the durable resolution contract in `docs/configuration.md` and warns loudly, while the follow-up path refuses to post unless both values can be resolved authoritatively.
Then step 2d's reply is an **acknowledgement** ("on it, captain"), and genuine milestone updates plus the final outcome come later as follow-ups (see "Completion follow-up" below), with the terminal one posted using `--final`.
Then step 2d's reply is an **acknowledgement** ("on it, captain"), and genuine milestone updates plus the final outcome come later as follow-ups (see "Completion follow-up" below), with the terminal one posted using `--final` when no typed promised-final commitment exists.
If the work completed in this turn (a backlog item filed, a question answered), there is no task to link and step 2d reports the outcome directly.
d. **Compose the reply.** For a **question**, answer `.text` from the fleet state gathered in step 1. For an **actionable request that completed now**, report the outcome of step 2c (what was done, or - for escalated work - that it has been flagged for the captain). For an **actionable request that spawned a linked task**, acknowledge that you have the order and are on it - milestone updates and the final outcome follow later as completion follow-ups, so do not promise a result you do not yet have. Either way keep it short, in firstmate's voice, and public-safe.
Conversation continuity: when `in_reply_to` is present this is a conversation reply - read `in_reply_to.text` (what `in_reply_to.author_handle` said just before) as **context** and continue that thread, resolving "it", "that", "and then?" against the parent; for a fresh mention (`in_reply_to` is null) answer on its own.
Expand Down Expand Up @@ -198,14 +199,50 @@ This skill's own responsibility during the mention-handling turn is linking the
- On each such milestone, firstmate checks whether a follow-up is still due with `bin/fm-x-followup.sh --check <task-id>` (prints the `request_id` when the link exists, the count is under the cap, and the window has not lapsed; silent otherwise, pruning an exhausted or expired link).
- If due, it composes a short, public-safe update and posts it with `bin/fm-x-followup.sh <task-id> --text-file <path>` (or stdin), which posts via the relay's follow-up endpoint; a successful non-final post increments the counter and keeps the link so a later milestone can still post against it.
When the update carries one real visual artifact, add `--image <path>`; the helper forwards it to `bin/fm-x-reply.sh --followup` so the same image contract used for ordinary replies applies here too.
- On a terminal wake (PR merged / scout report / local merge / failed), firstmate posts the task's **final** outcome ("done, here's the result"; for a failure, an honest "this one didn't pan out") with `bin/fm-x-followup.sh <task-id> --final --text-file <path>`, which always clears the link after that post regardless of how many follow-ups remain under the cap.
- On a terminal wake (PR merged / scout report / local merge / failed), firstmate posts the task's **final** outcome ("done, here's the result"; for a failure, an honest "this one didn't pan out") with `bin/fm-x-followup.sh <task-id> --final --text-file <path>` only when no promised-final public commitment is registered for that work. When the promised-final procedure above applies, `bin/fm-public-followup.sh consume` and `deliver` own the terminal reply and clear the legacy link at the validated receipt boundary, so do not call `fm-x-followup.sh --final` for the same outcome. If delivery reports that link cleanup needs reconciliation, do not post anything else; `bin/fm-x-followup.sh --clear <task-id>` is the clear-only recovery command in the bound work home.
- Every follow-up is held to the exact same public-safety bar as every reply here: outcomes only, no task ids, internals, captain-private material, or secrets. Past the window, past the cap, or on the relay's own rejection of an exhausted binding, a follow-up attempt is skipped silently and the link is cleared - never treated as a failure worth retrying.
- If either a follow-up's platform or explicit budget cannot be authoritatively resolved from per-request context, inbox payload, or relay answer, `bin/fm-x-followup.sh` does NOT post it: the fail-safe holds it (the link is kept, exit non-zero) rather than use a local default. This is a retryable hold - a later milestone wake retries it once both values are recoverable.

## Promised final replies (the commitment that must survive compaction)

The follow-up budget above is a courtesy.
A **promised final reply** - "I'll report back when this lands" - is a commitment, and forgetting it is publicly visible.
Never carry one in your head: the moment you promise a specific outcome in a public thread, turn it into durable state and let the scripts reconcile it.
This section is the sole owner of that procedure.
`tasks-axi public-followup --help` owns the typed obligation, its states, and its file contracts; `bin/fm-public-followup.sh --help` owns firstmate's flags; do not restate either here.

**When you promise a final:**

1. Create the typed obligation with `tasks-axi public-followup add` and bind the work with `bind-work`, keeping the public-safe summary and the opaque thread binding in the obligation and the full request context where the poll already put it.
2. Register it with `bin/fm-public-followup.sh register <obligation-id> --relation <relation-id> --work-home <main|secondmate:<id>> --work-id <task-id> --generation <n>`.
This is what makes the commitment reconcilable without you.
3. Put `bin/fm-public-followup.sh brief <obligation-id>` output straight into the worker's brief.
It prints the exact reporting command for that binding.
Never ask a worker to find the thread or post the reply: only this home holds the relay consent and the thread binding.

**When work reports back, or on a `public-followup ...` check wake, or when the session-start digest lists a public commitment:**

1. Run `bin/fm-public-followup.sh consume`.
It reconciles every typed terminal result from disk and prints `ready <obligation-id> <request-id> <platform>` for each commitment that became deliverable.
A refusal prints `rejected <event-id>: <reason>` and quarantines that event; read the reason rather than re-emitting blindly.
2. For each ready commitment, run `bin/fm-public-followup.sh deliver <obligation-id>`.
With no `--text-file` it reuses the accepted terminal outcome exactly, which is the preferred path for a landed result.
Only pass `--text-file` when the outcome genuinely needs composing, and hold it to the same public-safety bar as every other reply here.
Delivery clears the bound task's legacy X link at the validated receipt boundary; if it reports a cleanup failure, use its reconciliation message and do not post a legacy final.
3. Read the outcome and stop guessing at anything it refuses:
- "still waiting on its bound work" means the work has not reported a typed terminal result yet - do not post.
- "recorded as retryable" means nothing was posted; retry on a later wake.
- "held" means the thread's platform or budget is unresolvable right now; retry once it is recoverable.
- "mid-delivery" means a previous post started and its outcome was never recorded. Do NOT deliver again. Establish whether that post landed, then either close it with `record-posted <id> --attempt <n> --chunks <exact-count>` or escalate. Posting again would put a second reply in a public thread.
- "the relay no longer accepts a follow-up" is a captain decision, not a retry.

Cleanup refuses while a commitment is still owed for that exact work, so never reach for `--force` to get past it.
Treat a commitment as kept only after a validated posted receipt or an explicit captain waiver.

## Notes

- The direct author is always your own captain (owner-only routing), and in live mode you answer and act on eligible requests **autonomously**: enabling X mode is the captain's standing authorization, so never ask the captain before posting and never hold a worthwhile reply for a chat-side OK. For reply-worthy mentions, dry-run (`FMX_DRY_RUN`) is the only non-posting path; pure acknowledgments use the relay dismiss path instead.
- An actionable mention is **acted on** through the normal lifecycle (intake, backlog, dispatch, investigate, ship), not merely replied to. Work that finishes now gets one outcome reply; work that spawns a real task gets an **acknowledgement now** plus up to three **completion follow-ups** over time, ending with a `--final` one (link the task with `bin/fm-x-link.sh` so those follow-ups can post). A reply alone, with no work behind an actionable ask, is the bug to avoid.
- An actionable mention is **acted on** through the normal lifecycle (intake, backlog, dispatch, investigate, ship), not merely replied to. Work that finishes now gets one outcome reply; work that spawns a real task gets an **acknowledgement now** plus up to three **completion follow-ups** over time, ending with a `--final` one when no typed promised-final commitment exists (link the task with `bin/fm-x-link.sh` so those follow-ups can post). A reply alone, with no work behind an actionable ask, is the bug to avoid.
- Destructive, irreversible, or security-sensitive asks are flagged to the captain through the trusted channel first and never run straight from a mention; the public reply says only that it has been flagged.
- One answered mention = one reply (plus up to three completion follow-ups for a spawned task, spent only on genuine milestones); a skipped mention posts no reply but is **dismissed at the relay** (`bin/fm-x-dismiss.sh`) so the relay drops it rather than re-offering it (which would otherwise churn every poll and end in an "offline" auto-reply). A single wake may cover several pending mentions - drain them all.
- Conversations: `in_reply_to` carries the parent post for continuity; a pure acknowledgment with nothing to answer is dismissed at the relay and skipped, not replied to. The relay already guards against self-replies and caps replies per conversation, so you only judge "is there something to answer here?".
Expand Down
Loading
Loading