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
29 changes: 21 additions & 8 deletions .agents/skills/fmx-respond/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ How the reply lands depends on whether the work finishes during this turn:
- **Work that spawns a real, longer-running job** (dispatching a crewmate, a scout investigation, a ship task) cannot report an outcome yet, so it follows **acknowledge first -> act -> follow up on completion**:
1. **Acknowledge first.** Post an immediate, public-safe reply that you have the captain's order and are on it (the normal answer endpoint, via `bin/fm-x-reply.sh`). This is the legitimate, work-backed version of "aye, will do": it is paired with actually starting the work in the same turn, never a promise left empty.
2. **Act.** Dispatch the work through the normal lifecycle right away.
3. **Link it for the follow-up, before clearing the inbox.** Associate the spawned task with this mention so completion follow-ups can be posted later: `bin/fm-x-link.sh <task-id> <request_id>` (records the request id, a timestamp, a follow-up counter, and reply platform/budget context).
Do this right after the task is spawned, and always **before** removing the inbox file (step 2f).
Linking before cleanup lets `bin/fm-x-link.sh` copy the context directly from the inbox, while the durable per-request context recorded by the poll preserves it independently for delayed and concurrent follow-ups.
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.
3. **Bind the follow-up to wherever the work actually lives, before clearing the inbox.**
**The decision rule: work that stays in this home takes the lightweight link; work routed to a second mate takes a promised-final commitment bound to that second mate's home.**
There is no third option and no fallback between them - each mechanism can only reach the home it was built for, so choosing the wrong one orphans the public promise.
- **Local task (this home spawned it):** `bin/fm-x-link.sh <task-id> <request_id>` (records the request id, a timestamp, a follow-up counter, and reply platform/budget context).
Do this right after the task is spawned, and always **before** removing the inbox file (step 2f).
Linking before cleanup lets `bin/fm-x-link.sh` copy the context directly from the inbox, while the durable per-request context recorded by the poll preserves it independently for delayed and concurrent follow-ups.
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.
- **Second-mate-routed work (the request's project or domain belongs to a registered second mate, so the work is or will be routed there):** the link cannot be used at all.
It writes into this home's own `state/<task-id>.meta`, and a routed task's record lives in the second mate's home, so `bin/fm-x-link.sh` refuses and points you back here.
Register a **typed promised-final commitment bound to that home** up front instead - see "Promised final replies" below for the exact commands - and put its `bin/fm-public-followup.sh brief <obligation-id>` output into the routed worker's instructions so the terminal result comes back as typed data.
Do this in the same turn as the acknowledgement, before routing, so the promise is durable state from the moment it is made.
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.
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.
Expand Down Expand Up @@ -142,9 +149,10 @@ Treat `state/x-inbox/` as the source of truth and process **every** file you fin
When in doubt between an instruction and a question, do the smallest safe lifecycle step the request implies; when in doubt between a question and bare politeness, lean toward skipping - a needless reply is noise on a public bot.
c. **Act on an actionable request through the normal lifecycle.** Treat it exactly as a captain prompt typed in session: run ordinary intake (resolve the project), then file the backlog item, dispatch a crewmate, start a scout, or ship through the gate - whatever the request calls for.
**Destructive, irreversible, or security-sensitive work is the exception** (Relay is a public, relayed channel and does not carry full in-session trust): do not execute it from the mention. Flag it to the captain through the normal trusted channel first - the same carve-out as `yolo` (AGENTS.md §1, §7) - act only on the captain's word, and in step 2d say only that it has been flagged for the captain.
**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>`.
**If the request spawned a real, longer-running task in THIS home** (you ran `bin/fm-spawn.sh` here), 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.
**If intake routes the work to a second mate instead**, do not reach for the link: register the typed promised-final commitment bound to `secondmate:<id>` and brief the routed worker with its reporting command (step 3 of "acknowledge first, act, then follow up on completion", with the commands in "Promised final replies").
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.
Expand Down Expand Up @@ -216,13 +224,18 @@ Never carry one in your head: the moment you promise a specific outcome in a pub
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:**
This is also the **only** mechanism that reaches work outside this home.
The lightweight link of step 3 writes into this home's own task record, so it can never bind a second mate's task; `--work-home secondmate:<id>` here can.
So treat second-mate-routed Relay work as a promised final by construction: the acknowledgement you just posted **is** the promise, and there is no other way to keep it.

**When you promise a final (including every Relay request whose work is routed to a second mate):**

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.
When the work is routed to a second mate rather than spawned here, the routed item's own note carries that same output, so it survives the routing and reaches whoever ends up doing the work.
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:**
Expand All @@ -247,7 +260,7 @@ Treat a commitment as kept only after a validated posted receipt or an explicit
## 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 Relay 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 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.
- 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. Bind those follow-ups by where the work lives: a task in this home takes `bin/fm-x-link.sh`, and work routed to a second mate takes a promised-final commitment registered with `--work-home secondmate:<id>`, which is the only mechanism that reaches another home. 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 and optional `in_reply_to_chain` carries the surrounding transcript 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
3 changes: 3 additions & 0 deletions .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ It refuses a selected item with a single-space or tab-indented continuation rath
It accepts in-scope `## Queued` entries only and refuses `## In flight` and historical `## Done` entries.
Done records stay with their home for pruning or archiving.
It is idempotent; an item already in the secondmate backlog is skipped.
After a successful move it warns for any moved key that still owes a public relay reply bound to `main/<key>`, because that binding no longer names the home owning the work; rebind the commitment to `secondmate:<id>` through the `fmx-respond` promised-final procedure, which owns those commands.
That same rule governs routing generally: a Relay-linked request whose work goes to a secondmate cannot use the home-local mention link at all and needs a promised-final commitment bound to that secondmate's home.
It refuses any destination that is not a genuine seeded firstmate home with safe operational directories and a matching `.fm-secondmate-home` marker, so a move can never land in a project.
Do not hand off `local-only` items.

Expand All @@ -213,6 +215,7 @@ Use the recorded `home=` in meta.
If meta is missing but `data/secondmates.md` still registers the secondmate, respawn from the registry entry and its persistent home.
For a remote route, the same command probes and relaunches only on the configured host.
An SSH transport failure or unreadable remote endpoint remains unknown and must be reconciled on that host; never launch a local replacement.
`stuck-crewmate-recovery`'s remote-secondmate note owns why the endpoint-dead and send-failed verdicts that seem to justify this are themselves unreliable.
Respawn re-resolves the secondmate harness from current config, uses the same guarded pre-launch sync, and re-propagates inherited local material, so recovered secondmates converge inherited config items and shared captain preferences whenever their home validates; tracked-file sync remains guarded separately.
If the secondmate is already running and only inherited local material changed, prefer `bin/fm-config-push.sh` over respawning.
To move a live LOCAL secondmate onto a newly pinned harness, model, or effort without a full recovery, set `config/secondmate-harness` and then relaunch it with `bin/fm-control.sh <id> relaunch`, which re-resolves that pin, stops the agent, and launches the replacement in the same home ([`docs/agent-control.md`](../../../docs/agent-control.md)).
Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ The target window's harness is recorded as `harness=` in `state/<id>.meta`.
This procedure covers ordinary `kind=ship` and `kind=scout` direct reports.
Load `secondmate-provisioning` instead for `kind=secondmate` recovery.

For a REMOTE secondmate, `fm-crew-state`'s `unknown`/`worktree gone` and `fm-send`'s `remote send failed`/`delivery unconfirmed` verdicts are unreliable and routinely false-negative; do not conclude the mate is dead or the send failed from those alone, confirm against the actual remote pane first.
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.
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
33 changes: 30 additions & 3 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@ export default function (pi: ExtensionAPI) {

registerFirstmateSyntheticPresentation(pi);

// Every on-screen tool row Calm currently presents, keyed by the row-local state Pi
// hands its render slots, so Calm can repaint exactly those rows without touching
// Pi's transcript. Pi can re-render a row at any time - the built-in edit row
// invalidates itself once its diff is ready - so a row can be redrawn during the
// window where /export forces stock rendering and keep that stock content
// afterwards. Rows Pi's exporter renders are excluded: those use throwaway state
// and never appear on screen. Cleared per session lifetime, which rebuilds the rows.
const calmToolRowRepaints = new Map<object, () => void>();
const rememberCalmToolRow = (state: object, invalidate: unknown): void => {
if (exportRendering || typeof invalidate !== "function") return;
calmToolRowRepaints.set(state, invalidate as () => void);
};
const repaintCalmToolRows = (): void => {
for (const invalidate of calmToolRowRepaints.values()) invalidate();
};

function wrapBuiltIn<TParams extends TSchema, TDetails, TState>(
factory: DefinitionFactory<TParams, TDetails, TState>,
): ToolDefinition<TParams, TDetails, TState> {
Expand Down Expand Up @@ -262,6 +278,7 @@ export default function (pi: ExtensionAPI) {
theme: RenderTheme<TParams, TDetails, TState>,
context: RenderContext<TParams, TDetails, TState>,
) {
rememberCalmToolRow(context.state as object, context.invalidate);
if (exportRendering) return originalRenderCall(args, theme, context);
if (calmPresentationHides("assistant-tool-call")) return new Container();
if (originalSelfShell) return originalRenderCall(args, theme, context);
Expand All @@ -280,6 +297,7 @@ export default function (pi: ExtensionAPI) {
theme: RenderTheme<TParams, TDetails, TState>,
context: RenderContext<TParams, TDetails, TState>,
) {
rememberCalmToolRow(context.state as object, context.invalidate);
if (exportRendering) return originalRenderResult(result, options, theme, context);
if (calmPresentationHides("tool-result")) return new Container();
if (originalSelfShell) return originalRenderResult(result, options, theme, context);
Expand Down Expand Up @@ -392,6 +410,7 @@ export default function (pi: ExtensionAPI) {

pi.on("session_start", (_event, ctx) => {
reportBuiltInLosses();
calmToolRowRepaints.clear();
exportRendering = false;
setCalmPresentation(loadCalmPreference());
setCalmStockExportRendering(false);
Expand Down Expand Up @@ -423,9 +442,17 @@ export default function (pi: ExtensionAPI) {
exportRendering = false;
setCalmStockExportRendering(false);
publishPresentationState();
const expanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(!expanded);
ctx.ui.setToolsExpanded(expanded);
// Repaint the rows Calm presents, never the whole transcript. Pi's export
// prints "Session exported to: <path>" immediately before this runs, and
// since Pi 0.83.0 setToolsExpanded() emits its own status line; consecutive
// status lines coalesce, so a tools-expanded round-trip here silently
// overwrote the confirmation and left the captain no record of where their
// export landed. Invalidating the rows individually repaints the same
// content with no status line of its own, and setStatus adds the redraw the
// rows that consult Calm live in render(), such as operational user rows,
// need without appending anything to the transcript.
repaintCalmToolRows();
ctx.ui.setStatus("firstmate-calm", undefined);
}, 0);
});
});
Expand Down
Loading
Loading