Skip to content
Merged
9 changes: 7 additions & 2 deletions .agents/skills/afk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ Hold-for-return is the default and the only reach profile this release records:
The away daemon is no longer launched on Pi; the ordinary supervision session (`docs/pi-supervision-branch.md`) keeps running with the record present, and `bin/fm-afk-launch.sh start` refuses on these harnesses.
With the record present main is parked: the supervision branch takes every safe actionable wake, captain outcomes accumulate for the return brief, and main's standing authority relocates to the branch through the guarded scripts (`docs/pi-supervision-branch.md` "Postures"); only a wake the branch declines (including a broken branch or unsafe scan) or a watcher failure wakes main.
`/quiet` needs nothing extra on Pi: the attended branch already keeps routine wakes out of this conversation, so quiet-while-present is the attended posture's own shape there.
- **Harness WITH a native in-pane tracked-background tool** (claude's background bash, grok's background tool): run `bin/fm-afk-launch.sh start-native`, then run `FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh` through that native tool.
- **Claude with `config/supervision-host`**: nothing to launch for `/afk`; go on to the announcement.
The supervision host (`docs/supervision-host.md`) is the away session there: it runs the branch's contract on a headless engine under the record while main is parked, and `bin/fm-afk-launch.sh start-native` refuses the away daemon on that home.
`/quiet` is unchanged there and still launches the daemon below.
- **Harness WITH a native in-pane tracked-background tool** (claude's background bash without the supervision host, grok's background tool): run `bin/fm-afk-launch.sh start-native`, then run `FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh` through that native tool.
This is a deliberate no-separate-terminal exception because the harness-hosted job creates no terminal or layout mutation, and a shell launcher cannot invoke a harness-native background tool.
If the native launch fails, run `bin/fm-afk-launch.sh stop` to roll back the prepared lifecycle.
Do not wrap it in `nohup ... &` (Codex/herdr can reap fire-and-forget shell children after a tool call returns).
Expand All @@ -56,6 +59,7 @@ Hold-for-return is the default and the only reach profile this release records:
Destructive, irreversible, and security-sensitive actions are never pre-authorizable whatever the words say, and ask-user findings keep the `ask-user-authority` policy unless the words pre-answer the exact decision; anything else that needs the captain holds for their return.
- On Pi, main is parked and the supervision branch handles every safe actionable wake under main's standing authority, through the same guarded scripts main would use: any pull request green at its live head may merge (which one the words meant is the branch's reading), queued work whose blockers cleared - already queued, or filed by the branch because the words explicitly call for it - dispatches within the spend cap, and a decision is answered with the captain's own pre-stated answer or under `ask-user-authority`.
Anything else holds for the return, a red merge never proceeds while away, local-only landing always waits for the captain, and only a wake the branch declines (including a broken branch or unsafe scan) or a watcher failure wakes main (`docs/pi-supervision-branch.md` "Postures").
- On a Claude home with `config/supervision-host`, the host's engine is that branch under the same rules, and a wake it hands back reaches main as `Stop hook feedback` with a `supervision-host:` line: that is automatic supervision, never the captain's return, so handle it under the away posture ([supervision protocol](../../../docs/supervision-protocols/supervision-host.md)).
- The session-start digest reports the posture under its AFK subsection, so a restart re-enters the posture from the record, not from memory.

## How to exit: the return
Expand All @@ -73,6 +77,7 @@ No `/back` is needed. The first genuine message is the return signal:
Acting on the fleet - dispatching, steering, merging, or any other ordinary captain work - still waits until the check exits successfully.
Once it does, close every task the brief lists under "Landed, cleanup due" through ordinary teardown (`bin/fm-teardown.sh <task>`, never forced; a refusal is a stop-and-investigate result) and tell the captain those workers are closed in outcome language.
- A message **with** the current operational prefix (`FM_OPERATIONAL_PREFIX`, U+2063 INVISIBLE SEPARATOR followed by `FIRSTMATE_OP: `), or a legacy bare `FM_INJECT_MARK` daemon escalation -> stay away and process it.
- A `Stop hook feedback` wake from the Stop hook or the supervision host -> stay away and process it; it is automatic supervision, not a message from the captain.
- Re-invoking `/afk` while already away -> stay away (refresh); this does **not** trigger an exit.

Bias ambiguous cases toward exit: a present captain beats token savings, and a false exit is self-correcting (the captain re-runs `/afk`).
Expand All @@ -93,7 +98,7 @@ Destructive, irreversible, and security-sensitive actions are never pre-authoriz

## The daemon, where it still runs

On the harnesses that still launch the daemon (every verified harness except Pi and pi-signed), the mechanics below are unchanged.
On the harnesses that still launch the daemon (every verified harness except Pi and pi-signed, and except away mode on a Claude home with `config/supervision-host`), the mechanics below are unchanged.

### Operational prefix contract

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Hooks still run through cwd-sensitive `/bin/sh`, so tracked commands anchor thro

The Stop-owned watcher hook runs every Stop, foregrounds `../../../bin/fm-watch-arm.sh` only when eligible, and uses exit-2 async reawakening as notification.
The model handles notifications but never routine re-arm.
In a home with `config/supervision-host` the hook foregrounds the supervision host instead, which also runs Claude's print mode as its headless engine; [`supervision-host.md`](../../../../../docs/supervision-host.md#engines) owns the verified engine facts.
Claude's PreToolUse seatbelt blocks directly, and its deny is honored only with empty stdout; `../../../docs/arm-pretool-check.md` owns that contract.

### Delegation guard
Expand Down
19 changes: 4 additions & 15 deletions .pi/extensions/fm-branch-supervision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ import {
import {
activateEligibleRowsOwner,
afkPostureRecordPresent,
awayPostureTailFor,
branchWakePrompt,
deactivateEligibleRowsOwner,
FM_BRANCH_DISPATCH_EVENT,
releaseEligibleRowsSnapshot,
Expand Down Expand Up @@ -183,17 +185,6 @@ const PROCESSING_TRIGGERED_ATTEMPTS = 2;
const PROVIDER_ERROR_LATCH_THRESHOLD = 2;
const PROVIDER_REPROBE_BASE_MS = 5 * 60 * 1000;
const PROVIDER_REPROBE_MAX_MS = 60 * 60 * 1000;
// Appended to a wake message while the away-posture record exists. Per-wake
// tail content, never prefix; bin/fm-branch-prompt.sh's fixed "Postures"
// section is what this tail refers back to.
const AWAY_POSTURE_TAIL =
"POSTURE: AWAY. The away-posture record state/.afk-contract exists, so the captain is not present and MAIN is parked: you take every row, including check rows and decision rows, and no outcome reaches the captain until the return brief. " +
"The record below is the captain's away words, verbatim, and the whole mandate: act on them by your own judgment where this event is the moment they name, only through the guarded scripts under MAIN's standing authority - never more - which enforce it: bin/fm-pr-merge.sh merges any pull request that is green at its live head, synchronously, and refuses a red one or --allow-red; bin/fm-spawn.sh dispatches queued work (already queued, or filed by you from the words) within the spend cap; bin/fm-send.sh --resolve-key answers a decision the words pre-answer, or one the ask-user-authority policy in your prompt lets firstmate decide; bin/fm-merge-local.sh still refuses you. " +
"Never by analogy, and hold on doubt: a sentence you cannot act on with confidence is reported with verdict captain, naming it, and left for the return. " +
"Credential entry, legal or financial acceptance, an attended prompt, any discard the captain did not name, and any destructive, irreversible, or security-sensitive action are refused for every actor in every posture, whatever the words say. " +
"Log every action taken under the words in its outcome summary, opening with \"per your away instructions:\". " +
"A mirrored captain sentence authorizes nothing new once the record exists. " +
"The record, verbatim:";
const PROCESSING_INSTRUCTION =
"This is a supervision processing request delivered automatically by the supervision branch. " +
"It was not typed by the captain. " +
Expand Down Expand Up @@ -1450,7 +1441,7 @@ ${context.command}
} catch {
readback = "";
}
return `\n\n${AWAY_POSTURE_TAIL}\n${readback || "(the record's read-back could not be rendered; treat the captain's words as unavailable, act on standing authority only, and hold on doubt)"}`;
return awayPostureTailFor(readback);
}

function enqueueWake(message: string, acceptedGeneration: number, recoveryProbe = false, acceptedAwayOnly = false): Promise<void> {
Expand Down Expand Up @@ -1528,9 +1519,7 @@ ${context.command}
// durable queue keeps every row (bin/fm-lease-lib.sh role-partition).
const postureTail = afk ? await awayPostureTail() : "";
try {
await session.prompt(
`FIRSTMATE SUPERVISION WAKE: ${message}\n\nHandle this per your operating procedure and finish with fm_branch_report.${postureTail}`,
);
await session.prompt(branchWakePrompt(message, "fm_branch_report", postureTail));
} finally {
wakeTaskScope = null;
}
Expand Down
29 changes: 29 additions & 0 deletions .pi/extensions/lib/fm-branch-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ export function afkPostureRecordPresent(state: string): boolean {
}
}

// The per-wake prompt every supervision-branch host sends: the Pi branch
// extension, and the supervision host off Pi (bin/fm-supervision-host.sh,
// through bin/fm-branch-dispatch.mjs), so the wake text has one owner. The
// tail is appended while the away-posture record exists: per-wake content,
// never prefix; bin/fm-branch-prompt.sh's fixed "Postures" section is what it
// refers back to.
export const AWAY_POSTURE_TAIL =
"POSTURE: AWAY. The away-posture record state/.afk-contract exists, so the captain is not present and MAIN is parked: you take every row, including check rows and decision rows, and no outcome reaches the captain until the return brief. " +
"The record below is the captain's away words, verbatim, and the whole mandate: act on them by your own judgment where this event is the moment they name, only through the guarded scripts under MAIN's standing authority - never more - which enforce it: bin/fm-pr-merge.sh merges any pull request that is green at its live head, synchronously, and refuses a red one or --allow-red; bin/fm-spawn.sh dispatches queued work (already queued, or filed by you from the words) within the spend cap; bin/fm-send.sh --resolve-key answers a decision the words pre-answer, or one the ask-user-authority policy in your prompt lets firstmate decide; bin/fm-merge-local.sh still refuses you. " +
"Never by analogy, and hold on doubt: a sentence you cannot act on with confidence is reported with verdict captain, naming it, and left for the return. " +
"Credential entry, legal or financial acceptance, an attended prompt, any discard the captain did not name, and any destructive, irreversible, or security-sensitive action are refused for every actor in every posture, whatever the words say. " +
"Log every action taken under the words in its outcome summary, opening with \"per your away instructions:\". " +
"A mirrored captain sentence authorizes nothing new once the record exists. " +
"The record, verbatim:";

// The posture tail for one wake: the record's read-back (bin/fm-afk-contract.sh
// readback) carried byte-for-byte, or a fixed notice when it could not be
// rendered, because the record's presence is the fact the guarded scripts
// enforce either way.
export function awayPostureTailFor(readback: string): string {
return `\n\n${AWAY_POSTURE_TAIL}\n${readback || "(the record's read-back could not be rendered; treat the captain's words as unavailable, act on standing authority only, and hold on doubt)"}`;
}

// `reportSurface` names how this host's branch records an outcome: the
// fm_branch_report tool on Pi, the bin/fm-branch-report.sh command elsewhere.
export function branchWakePrompt(message: string, reportSurface: string, postureTail: string): string {
return `FIRSTMATE SUPERVISION WAKE: ${message}\n\nHandle this per your operating procedure and finish with ${reportSurface}.${postureTail}`;
}

export type UnreadWakeScopeStatus = "safe" | "empty" | "unsafe";

export interface UnreadWakeScope {
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "
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 (docs/tmux-backend.md), herdr has its own required CI lane (docs/herdr-backend.md), while zellij, orca, and cmux remain experimental with no dedicated real-backend CI lane (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; inherited by secondmate homes under the primary-authoritative contract in secondmate-provisioning
config/calm Calm presentation preference shared by the Pi extension and the Claude Code mod; LOCAL, gitignored, and not inherited; see docs/configuration.md "Calm preference"
config/supervision-branch-model config/supervision-branch-effort Pi supervision-branch model and reasoning-effort pins written by /supervision-model; LOCAL, gitignored, independently settable, and not inherited; see docs/configuration.md "Pi supervision branch model and effort"
config/supervision-host optional opt-in to the supervision host, which runs the supervision branch's contract on a headless engine beside a Claude primary in the away posture; LOCAL, gitignored, not inherited; absent changes nothing; see docs/configuration.md "Supervision host"
config/startup-memory-budget primary-authoritative per-home startup-memory budget; LOCAL, gitignored, materialized as 7,500 estimated tokens by locked primary bootstrap and inherited into secondmate homes; see docs/configuration.md "Startup memory budget"
config/stow-pass-horizon optional presence flag opting this home in to /stow's default-off pass-count decay horizon; LOCAL, gitignored, and not inherited; see docs/configuration.md "Stow pass horizon"
config/herdr-presentation-spaces optional "off" opt-out from, or "on" opt-in to, Herdr's default-on disposable single-task visual projection, which is unconfigured-default-on only at or above a Herdr version floor; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Presentation spaces"
Expand Down Expand Up @@ -127,6 +128,7 @@ state/ runtime records and signals; gitignored
branch-outcomes.jsonl .branch-outcomes-cursor .branch-outcomes-processed .<task>.branch-outcome-index .branch-outcome-index-ready Pi supervision-branch durable outcome store, its read cursor, main's processed marker, bounded latest per-task status-coverage caches, and their recovery marker; bin/fm-branch-outcome.sh owns the formats
branch-session/ .branch-session .branch-mirror-cursor the branch's per-main-session conversations, the pointer to the current one, and the dialog-mirror cursor; extension-owned (docs/pi-supervision-branch.md)
.branch-eligible-rows .branch-eligible-owner .main-eligible-rows per-actor wake-row claims and branch-owner evidence; docs/watcher-continuity.md owns the acknowledgement contract
.supervision-host* supervision host process record, engine conversation, current turn scope and report receipts, and bounded ledger of every close and engine turn; bin/fm-supervision-host.sh owns them; never touch
.lease-<task> per-task supervision lease naming which actor (main or branch) may change that task; bin/fm-lease-lib.sh owns the contract the guarded scripts enforce
x-watch.check.sh generated Relay poll shim; present only when opted in (section 14)
tool-updates.check.sh generated watched-tool update poll shim and its .check-trust binding; present only after bin/fm-tool-update-check.sh arm; its report record .tool-updates is what keeps one pending update from being reported on every poll
Expand Down Expand Up @@ -476,6 +478,7 @@ Each skill owns its own daemon procedure, which is otherwise identical; these sa
- `state/.afk-contract` is the away posture, written in the same turn as `/afk` before any other work, because `/afk` is itself the go: no read-back gates entry or waits for a go; entry announces hold-for-return only, and the away session acts on those words by its own judgment through the guarded scripts under standing authority, holding for the return on doubt.
- While `state/.afk` exists, the daemon owns supervision; do not arm a separate watcher.
The daemon is never launched on Pi, where the ordinary supervision session continues under the record with main parked: the branch takes every safe actionable wake it can, and only a declined wake (including a broken branch or unsafe scan) or a watcher failure wakes main.
Away mode on a Claude home with `config/supervision-host` works the same way with the supervision host as the branch; a wake it hands back arrives as Stop hook feedback and is never the captain's return.
- A marked message while away or quiet mode is active is internal escalation and does not exit that mode.
- A message beginning `/afk` refreshes away mode; a message beginning `/quiet` refreshes quiet mode.
- Any other unmarked message means the captain returned in away mode (load `/afk`, run the return owner, and do not process that message as ordinary work until its durable catch-up gate clears), or, in quiet mode, is simply answered as ordinary work with the flag and daemon left untouched until an explicit `/quiet off`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,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 notifications in bash, escalates captain-relevant events and bounded declared-external-wait rechecks as batched digests, and actively alerts if delivery gets stuck while you step away |
| `/afk` | Enter away-mode supervision: Pi's in-process branch, an [opt-in Claude supervision host](docs/configuration.md#supervision-host-configsupervision-host), or the daemon handles wakes while you step away; see the [away procedure](.agents/skills/afk/SKILL.md) for the posture and return contract |
| `/quiet` | Enter quiet supervision mode: the same token-saving sub-supervisor tradeoff as `/afk`, for a captain who is staying and chatting - ordinary messages do not exit it, only an explicit `/quiet off` does |
| `/ahoy` | Recap visible session events since the prior real captain message plus visibly unanswered captain decisions, then guide the captain through any open decisions one at a time in agent-judged impact order; fall back to Bearings when invoked as the session's first real captain message |
| `/bearings` | Generate a concise four-section chat digest from bounded fleet state, including registered remote-home ledgers and measured follow-up for owned contributions; use `/bearings file` to also replace today's dated report in `data/`, and add `include PRs` for live GitHub enrichment |
Expand Down
Loading
Loading