Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
df8d8f4
feat: add optional host-root mode
YrFnS Jul 26, 2026
bafae13
no-mistakes(review): Scope host-root lifecycle safeguards correctly
YrFnS Jul 26, 2026
2a41b81
no-mistakes(document): Document host-root overlap and forge scoping
YrFnS Jul 26, 2026
732ebfe
no-mistakes(lint): Annotate dynamic wake library source for lint
YrFnS Jul 26, 2026
7506d74
no-mistakes(review): Captain: harden host-root cleanup and task admis…
YrFnS Jul 26, 2026
36ffb1a
no-mistakes(review): Harden host-root promotion and logical project r…
YrFnS Jul 26, 2026
316bf85
no-mistakes(document): Consolidate host-root documentation ownership
YrFnS Jul 26, 2026
cca78f7
no-mistakes(lint): Suppress intentional literal-dollar ShellCheck war…
YrFnS Jul 26, 2026
a108691
no-mistakes(review): Preserve logical host scout delivery modes
YrFnS Jul 26, 2026
42ace87
no-mistakes(document): Clarify local-only host-root scout documentation
YrFnS Jul 26, 2026
7b03ed8
no-mistakes(review): Captain: Protect retained metadata and host-boun…
YrFnS Jul 26, 2026
826c93a
fix(host-root): preserve recovery and bind tmux aliases
YrFnS Jul 27, 2026
98a51e3
no-mistakes(review): Captain: protect host teardown and preserve Herd…
YrFnS Jul 27, 2026
7860597
no-mistakes(review): Captain: preserve retained host metadata across …
YrFnS Jul 27, 2026
768e4f7
test(host-root): run nudge from synthetic root
YrFnS Jul 27, 2026
d08a6a4
no-mistakes(review): Captain, preserve host authority across aliases …
YrFnS Jul 27, 2026
87673c6
no-mistakes(review): Captain, preserve host children during recursive…
YrFnS Jul 27, 2026
9b1c69a
no-mistakes(review): Captain, preserve indexed tmux and projected Her…
YrFnS Jul 27, 2026
ae2a855
no-mistakes(review): Captain, stabilize tmux authority and teardown i…
YrFnS Jul 27, 2026
9c08e8c
no-mistakes(review): Harden host-root cleanup and endpoint identity
YrFnS Jul 27, 2026
6d7baa4
no-mistakes(review): Bind tmux identity and close cross-window cmux t…
YrFnS Jul 27, 2026
4ac8741
no-mistakes(review): Bind host tmux tasks to their creating sockets
YrFnS Jul 27, 2026
5838064
no-mistakes(review): Scope tmux supervision and recognize vanished so…
YrFnS Jul 27, 2026
6c046b8
test: bound Herdr abort fixture timing
YrFnS Jul 27, 2026
ad2653f
no-mistakes(review): Preserve tmux rollback metadata; Herdr acceptanc…
YrFnS Jul 28, 2026
19d3970
no-mistakes(review): Captain: complete Herdr host-root decision-inven…
YrFnS Jul 28, 2026
4c4f391
no-mistakes(document): Align host-root documentation with runtime saf…
YrFnS Jul 28, 2026
599ea91
no-mistakes(lint): Fix host-root ShellCheck findings
YrFnS Jul 28, 2026
1c3e97f
fix: reconcile host-root mode with endpoint hardening
YrFnS Jul 29, 2026
5d204dc
feat: activate FirstMate from an external Pi host
YrFnS Jul 29, 2026
a997c49
no-mistakes(review): Harden host-root overlap and metadata authority
YrFnS Jul 29, 2026
801bd75
no-mistakes(review): Enforce runtime host-home root isolation
YrFnS Jul 29, 2026
d7fb7cd
no-mistakes(document): Refresh host-root verification evidence
YrFnS Jul 29, 2026
bd0812a
no-mistakes(lint): Captain: fix ShellCheck empty local declaration
YrFnS Jul 29, 2026
606e458
no-mistakes(review): Captain: guard host-root Pi workers from supervi…
YrFnS Jul 30, 2026
39e7244
no-mistakes(document): Document Pi host-root worker activation guard
YrFnS Jul 30, 2026
8caebe3
fix(host-root): launch workers from target worktrees
YrFnS Aug 1, 2026
b174555
no-mistakes(review): Captain: fix host-root review regressions
YrFnS Aug 1, 2026
baa5ef0
no-mistakes(document): Consolidate host-root verification evidence
YrFnS Aug 1, 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
1 change: 1 addition & 0 deletions .agents/skills/firstmate-orca/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Use raw `orca` only when the helper surface cannot answer the inspection questio
## Preflight

Work from the current firstmate home or repo root.
In host-root mode, work instead from the physical `FM_HOST_ROOT` and invoke FirstMate helpers by their absolute `FM_ROOT/bin/` paths under the [four-root contract](../../../docs/configuration.md#host-root-mode-fm_host_root).
If `FM_HOME` is set, remember that operational state lives under `$FM_HOME` while the helper scripts still run from this repo's `bin/`.

Before switching or spawning against Orca:
Expand Down
33 changes: 27 additions & 6 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ OpenCode uses `.opencode/plugins/fm-primary-watch-arm.js`, which coordinates wit
Pi and pi-signed use the tracked `.pi/extensions/fm-primary-turnend-guard.ts` plus the tracked `.pi/extensions/fm-primary-pi-watch.ts`, both project-local extensions the Pi engine auto-discovers once trusted.
When changing any primary watcher adapter, update `docs/supervision-protocols/`, `docs/turnend-guard.md` if a shared idle or turn-end hook changed, and the relevant concise fact below.

## Host-root task integration

When `FM_HOST_ROOT` is set, only the primary supervisor starts from that physical host root.
Ordinary ship and scout harnesses start from their isolated `FM_TARGET_WORKTREE`, so the target repository's instructions and lifecycle adapters load natively while the host context stays out of focused worker sessions.
FirstMate retains one task completion signal per harness and passes the host and target identities for supervision and recovery without changing the worker cwd:

| Harness | Task completion signal in host-root mode |
|---|---|
| claude | A state-owned settings file passed with `--settings`; target project settings still load from the worker cwd. |
| codex | The existing per-launch `notify` command. |
| opencode | A state-owned task plugin named through `OPENCODE_CONFIG_CONTENT`; target project plugins load from the worker cwd. |
| pi | The existing explicit state-owned `-e` task extension; target project extensions remain subject to target trust. |
| grok | The guarded global FirstMate Stop hook reads a per-process `FM_GROK_TURNEND_TOKEN`. |
| kimi | The guarded global FirstMate Stop hook reads a per-process `FM_KIMI_TURNEND_TOKEN`. |

Secondmate launches explicitly clear inherited `FM_HOST_ROOT` and `FM_TARGET_WORKTREE` and retain their isolated-home adapters.

[`docs/verification/supervision.md`](../../../docs/verification/supervision.md#host-root-task-integration) owns the dated lifecycle evidence and current live-verification limits for these task adapters.

## Launch profile axes

`bin/fm-spawn.sh` accepts concrete `--harness`, `--model`, and `--effort` values chosen by firstmate at intake.
Expand Down Expand Up @@ -320,7 +339,7 @@ The tmux backend's structural `fm_tmux_composer_state` read sees placeholder-fil
The Herdr adapter (`fm_backend_herdr_composer_state`, `bin/backends/herdr.sh`) classifies the composer's own row structurally instead of diffing raw content; see `docs/herdr-backend.md` "Composer and injection safety" for the current boundary and `tests/fm-backend-herdr.test.sh` for regression coverage.

Startup dialog: the "Run Grok Build in a project directory?" project picker appears ONLY when grok is launched from a non-project directory (home, Desktop, Downloads, `/tmp`).
`fm-spawn` launches inside the treehouse worktree (a git repo root), so the picker never appears and grok treats the worktree as a trusted project automatically - no post-launch keystroke is needed.
Every ordinary `fm-spawn`, including host-root mode, launches inside the isolated target worktree, which must be a recognized project directory to avoid the picker.
Pin `[hints] project_picker_disabled = true` in `~/.grok/config.toml` if a non-project launch ever needs to skip it.

**TRUECOLOR placeholder styling: covered (task afk-herdr-false-pending, 2026-07-10).**
Expand All @@ -339,11 +358,12 @@ Turn-end hook: grok fires a `Stop` hook at every turn boundary, giving firstmate
grok loads PROJECT hooks (`<worktree>/.grok/hooks/`, `<worktree>/.claude/settings.local.json`) only after the folder is granted hook-trust in `~/.grok/trusted_folders.toml`, which is not automatic and which firstmate will not establish by editing grok's own managed trust store.
GLOBAL hooks in `~/.grok/hooks/` are always trusted and load on first launch.
So `fm-spawn` installs ONE firstmate-owned global hook, `~/.grok/hooks/fm-turn-end.json`, plus the companion `~/.grok/hooks/fm-turn-end.sh`, guarded as a no-op for every non-firstmate grok session.
Its `Stop` command fires only when the current workspace holds a `.fm-grok-turnend` token pointer that matches the firstmate-owned hook registry under `~/.grok/hooks/fm-turn-end.d/`.
`fm-spawn` writes that per-task pointer (`<worktree>/.fm-grok-turnend`, gitignored via git info/exclude like the other harnesses' worktree hook files) and a matching registry entry naming this task's `state/<id>.turn-ended`.
The hook reads `$GROK_WORKSPACE_ROOT`, which is always set for hooks and equals the worktree.
Its `Stop` command fires only when a launch-scoped `FM_GROK_TURNEND_TOKEN` or the current workspace's `.fm-grok-turnend` pointer matches the firstmate-owned hook registry under `~/.grok/hooks/fm-turn-end.d/`.
`fm-spawn` always writes the matching registry entry naming this task's `state/<id>.turn-ended`.
Default launches write the gitignored pointer under the task worktree, while host-root launches pass the token in the worker environment and write no pointer into the host.
The default pointer path reads `$GROK_WORKSPACE_ROOT`, which Grok sets to the launch workspace.
This keeps the hook outside the worktree, needs no trust grant, and writes only firstmate-owned files.
`fm-teardown` removes the worktree pointer before returning a pooled worktree.
`fm-teardown` removes any default-mode worktree pointer before returning a pooled worktree.
Secondmate spawns skip the pointer (idle panes are healthy, no stale-pane detection for them).

**Primary-session guard fact (verified 2026-07-28, Grok 0.2.112 and 0.2.73).**
Expand Down Expand Up @@ -391,6 +411,7 @@ The spinner match covers the full moon-phase glyph set rather than one frame, bu

[`docs/turnend-guard.md`](../../../docs/turnend-guard.md) owns Kimi's verified global hook surface and captain-approved crew wake integration.
`fm-spawn.sh` installs one marker-delimited Firstmate entry in `$HOME/.kimi-code/config.toml`, one silent always-zero hook script, and one private token registry under `$HOME/.kimi-code/fm-turn-end.d/`.
Each Kimi crew worktree receives a gitignored `.fm-kimi-turnend` token pointer, and the global hook touches that task's `state/<id>.turn-ended` only when the Stop payload's `cwd`, pointer, and registry entry all agree.
Default Kimi crew worktrees receive a gitignored `.fm-kimi-turnend` token pointer, while host-root launches pass `FM_KIMI_TURNEND_TOKEN` in the worker environment and write no pointer into the host.
The global hook touches that task's `state/<id>.turn-ended` only when the Stop payload has a cwd and the selected token resolves through the private registry.
A guarded silent hook cannot be verified from absence of effect, so prove invocation with an unguarded probe before concluding that the hook did not fire.
The guarded turn-end signal supplements the pane busy signature, whose locale- and emoji-font-sensitive limits still apply while a turn is running.
12 changes: 5 additions & 7 deletions .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Release happens only on explicit retirement or seed rollback, never on routine r
`bin/fm-home-seed.sh` copies the charter into the secondmate home as `data/charter.md`.
It also writes the required `.fm-secondmate-home` identity marker, which is gitignored and must remain in place for home validation.
`bin/fm-spawn.sh --secondmate` launches it through the secondmate harness path, resolving `config/secondmate-harness` -> `config/crew-harness` -> the primary's own harness unless an explicit per-spawn harness override is passed.
Secondmates remain outside optional host-root mode: their launch prefix clears any inherited `FM_HOST_ROOT` and `FM_TARGET_WORKTREE`, their metadata has no `host_root=`, and their startup cwd remains the isolated FirstMate home.

`config/secondmate-harness` may also pin a concrete model and effort for the secondmate agent, in the SAME file rather than a new one: the format is a single whitespace-separated line `<harness> [<model>] [<effort>]`, with only the first non-empty, non-comment line parsed.
A bare `<harness>` (today's format, e.g. `claude`) behaves exactly as before - harness only, no model/effort flag - so this is fully backward-compatible.
Expand Down Expand Up @@ -149,13 +150,10 @@ Do not hand off `local-only` items.

## Recovery

For `kind=secondmate` meta with no window, treat the secondmate as a dead persistent direct report and respawn it with:

```sh
bin/fm-spawn.sh <id> --secondmate
```

Use the recorded `home=` in meta.
Retained `kind=secondmate` metadata remains recovery authority even when its window or endpoint field is missing.
Do not delete or rewrite that metadata, and do not invoke a direct same-id `fm-spawn`; direct spawn refuses retained metadata so an ambiguous or incomplete cleanup cannot create a duplicate secondmate.
The locked session-start liveness sweep owns relaunch after the recorded endpoint and verified harness produce a recovery-grade `dead` or `missing` result.
An absent endpoint field, ambiguous process, unreadable probe, or unverified harness is not relaunch authority; preserve the recorded `home=`, metadata, and endpoint evidence and report the exact blocker.
If meta is missing but `data/secondmates.md` still registers the secondmate, respawn from the registry entry and its persistent on-disk home.
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.
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Do not sweep another home's endpoints or infer ownership from a matching window

Before relaunch, prove that no live agent still owns the recorded task and that the existing worktree remains available.
Preserve its uncommitted changes and commits, keep the same task identity, and resume or relaunch the recorded harness in that existing worktree with the same brief plus a concise progress note.
For a host-root task, relaunch the harness from the existing recorded worktree, pass it as `FM_TARGET_WORKTREE`, and retain the recorded physical `host_root=` only as the supervisor authority; [`docs/configuration.md`](../../../docs/configuration.md#host-root-mode-fm_host_root) owns that four-root contract.
Do not use a fresh generic spawn while the recorded worktree is unaccounted for, because allocating another worktree can split one task across two copies.
If the worktree or ownership cannot be reconciled safely, leave all state intact and report the task failed or blocked with the conflicting evidence.

Expand Down
150 changes: 150 additions & 0 deletions .pi/extensions/lib/fm-host-activator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export type FirstmateHostConfig = {
fmRoot: string;
fmHome: string;
hostRoot: string;
backend: string;
};

type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;

const policyMarker = "<!-- firstmate-host-supervisor-policy -->";

function physicalPath(path: string): string | undefined {
try {
return realpathSync(path);
} catch {
return undefined;
}
}

function refuse(pi: ExtensionAPI, reason: string): void {
const message = `FirstMate host activation refused: ${reason}`;
console.error(message);
pi.on("session_start", (_event, ctx) => {
ctx.ui.setStatus(
"firstmate-host",
ctx.ui.theme.fg("error", "FirstMate inactive"),
);
ctx.ui.notify(message, "error");
});
}

function validateConfig(config: FirstmateHostConfig): string | undefined {
if (physicalPath(config.fmRoot) !== config.fmRoot)
return `invalid FirstMate root ${config.fmRoot}`;
if (physicalPath(config.fmHome) !== config.fmHome)
return `invalid FirstMate home ${config.fmHome}`;
if (physicalPath(config.hostRoot) !== config.hostRoot)
return `invalid host root ${config.hostRoot}`;
if (config.fmRoot === config.hostRoot)
return "FirstMate root and host root must differ";
if (!existsSync(resolve(config.hostRoot, "AGENTS.md")))
return `host root has no AGENTS.md: ${config.hostRoot}`;
if (!existsSync(resolve(config.fmRoot, "AGENTS.md")))
return `FirstMate root has no AGENTS.md: ${config.fmRoot}`;

const result = spawnSync(
"bash",
[
"-c",
'. "$1/bin/fm-host-root-lib.sh" && fm_host_root_assert_operational_roots "$3" "$1" "$4" && . "$1/bin/fm-backend.sh" && fm_backend_validate_spawn "$2"',
"fm-host-activator",
config.fmRoot,
config.backend,
config.hostRoot,
config.fmHome,
],
{
encoding: "utf8",
env: {
...process.env,
FM_ROOT_OVERRIDE: config.fmRoot,
FM_HOME: config.fmHome,
FM_HOST_ROOT: config.hostRoot,
FM_BACKEND: config.backend,
},
},
);
if (result.status !== 0)
return result.stderr.trim() || `invalid backend ${config.backend}`;
return undefined;
}

async function loadExtension(pi: ExtensionAPI, path: string): Promise<void> {
const module = (await import(pathToFileURL(path).href)) as {
default?: ExtensionFactory;
};
if (typeof module.default !== "function")
throw new Error(`extension has no default factory: ${path}`);
await module.default(pi);
}

export default async function activateFirstmateHost(
pi: ExtensionAPI,
config: FirstmateHostConfig,
): Promise<void> {
if (
process.env.FM_TARGET_WORKTREE ||
physicalPath(process.cwd()) !== config.hostRoot
)
return;

const invalid = validateConfig(config);
if (invalid) {
refuse(pi, invalid);
return;
}

const expectedEnvironment: Record<string, string> = {
FM_ROOT_OVERRIDE: config.fmRoot,
FM_HOME: config.fmHome,
FM_HOST_ROOT: config.hostRoot,
FM_BACKEND: config.backend,
};
for (const [name, expected] of Object.entries(expectedEnvironment)) {
const ambient = process.env[name];
if (ambient !== undefined && ambient !== "" && ambient !== expected) {
refuse(pi, `${name} is already set to a conflicting value`);
return;
}
}
Object.assign(process.env, expectedEnvironment);

const supervisorPolicy = readFileSync(
resolve(config.fmRoot, "AGENTS.md"),
"utf8",
).trimEnd();
pi.on("resources_discover", () => ({
skillPaths: [resolve(config.fmRoot, ".agents/skills")],
}));
pi.on("before_agent_start", (event) => {
if (event.systemPrompt.includes(policyMarker)) return;
return {
systemPrompt: `${event.systemPrompt}\n\n${policyMarker}\n# FirstMate host supervisor policy\n\nThe host context above remains authoritative for host identity, lifecycle, and cwd.\nApply the following FirstMate supervisor policy additively.\n\n${supervisorPolicy}`,
};
});
pi.on("session_start", (_event, ctx) => {
ctx.ui.setStatus(
"firstmate-host",
ctx.ui.theme.fg("accent", "FirstMate active"),
);
});
pi.on("session_shutdown", (_event, ctx) => {
ctx.ui.setStatus("firstmate-host", undefined);
});

await loadExtension(
pi,
resolve(config.fmRoot, ".pi/extensions/fm-primary-turnend-guard.ts"),
);
await loadExtension(
pi,
resolve(config.fmRoot, ".pi/extensions/fm-primary-pi-watch.ts"),
);
}
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ Never add an agent name as a commit co-author.

`docs/configuration.md` is the single owner of the top-level operational-home layout and configuration schemas; each producing script's header and help own exact child fields and mutation mechanics.
`FM_HOME` selects an instance's private `data/`, `state/`, `config/`, and `projects/`, while scripts continue to come from their tracked code root.
Each secondmate has a persistent isolated `FM_HOME`, including its own state, backlog, projects, and session lock.
Optional `FM_HOST_ROOT` keeps another repository authoritative for the primary supervisor's instructions, lifecycle, and cwd while ordinary workers launch from their isolated `FM_TARGET_WORKTREE`; `docs/configuration.md` owns the four-root contract.
Each secondmate has a persistent isolated `FM_HOME`, including its own state, backlog, projects, and session lock, and secondmate launches clear any inherited host-root variables.
`bin/fm-send.sh` fails closed unless `FM_HOME` is explicit, so a steer cannot silently resolve against another home.

Tracked files hold shared instructions and tooling; `data/` holds durable private fleet records; `state/` holds volatile runtime records and append-only status events; `config/` holds local operating choices; and `projects/` contains clones that are read-only to firstmate except under hard rule 1's concrete captain-approved project operation exception.
Expand Down Expand Up @@ -89,7 +90,7 @@ state/ volatile runtime signals; gitignored
<id>.turn-ended touched by turn-end hooks
<id>.grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown
<id>.kimi-turnend-token firstmate-owned Kimi hook registry token for the task; removed by teardown
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<id>.meta written by fm-spawn: window=, endpoint_task_id=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; an ordinary host-root task also records host_root=; kind=secondmate also records home= and projects=; a non-default runtime backend records further backend-specific fields (docs/configuration.md "Runtime backend"; bin/fm-backend.sh, section 8); fm-pr-check, including through fm-pr-merge, records one canonical pr= and the forge's pr_head= when available (GitHub pull requests and GitLab merge requests; docs/gitlab-merge-watch.md); fm-x-link appends x_request=, x_request_ts=, x_followups=, and optional x_platform=/x_reply_max_chars= for an X-mode-originated task (section 14)
<id>.herdr-presentation quarantinable attempt and restart-binding journal for Herdr's optional visual projection; never task or endpoint authority; see docs/herdr-backend.md "Optional presentation spaces"
<id>.check.sh authenticated slow poll; the watcher dispatches validated PR data and the byte-identified X shim through trusted repository scripts, runs registered custom checks from hash-validated private snapshots, and rejects every other state check without execution
<id>.check-trust private content binding created by fm-check-register.sh for an intentional custom check
Expand Down
Loading