Skip to content
Closed
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
74 changes: 71 additions & 3 deletions .agents/skills/harness-adapters/SKILL.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"statusLine": {
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/bin/fm-status-bar.sh --adapter claude",
"padding": 0
},
"hooks": {
"SessionStart": [
{
Expand Down
96 changes: 96 additions & 0 deletions .pi/extensions/fm-primary-status-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Firstmate canonical status bar for Pi's native custom-footer API.
import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { truncateToWidth } from "@earendil-works/pi-tui";

type SessionEntry = {
type?: unknown;
message?: {
role?: unknown;
usage?: {
cost?: {
total?: unknown;
};
};
};
};

const extensionFile = fileURLToPath(import.meta.url);
const extensionDir = dirname(extensionFile);
const root = resolve(extensionDir, "../..");
const renderer = `${root}/bin/fm-status-bar.sh`;
const refreshMilliseconds = 1000;

function sessionCost(ctx: ExtensionContext): number {
let cost = 0;
for (const entry of ctx.sessionManager.getEntries() as SessionEntry[]) {
if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
const entryCost = entry.message.usage?.cost?.total;
if (typeof entryCost === "number" && Number.isFinite(entryCost)) cost += entryCost;
}
return cost;
}

function renderCanonicalStatus(pi: ExtensionAPI, ctx: ExtensionContext): string {
const usage = ctx.getContextUsage();
const remaining =
usage?.percent == null ? "--" : String(Math.max(0, Math.min(100, Math.floor(100 - usage.percent))));
const result = spawnSync(
renderer,
[
"--adapter",
"pi",
"--model",
ctx.model?.id || "--",
"--effort",
String(pi.getThinkingLevel?.() || "--"),
"--context-remaining",
remaining,
"--quota-used",
"--",
"--cost",
String(sessionCost(ctx)),
],
{
encoding: "utf8",
env: process.env,
timeout: 500,
},
);
if (result.status !== 0 || !result.stdout) return "\u001b[91;1m⚓ STATUS UNAVAILABLE\u001b[0m";
return result.stdout.replace(/\r?\n$/, "");
}

export default function (pi: ExtensionAPI) {
if (process.env.FM_PRIMARY_HARNESS !== "pi") return;

pi.on("session_start", (_event, ctx) => {
if (ctx.mode !== "tui") return;
ctx.ui.setFooter((tui) => {
let cached = "";
let cachedAt = 0;
const refresh = setInterval(() => tui.requestRender(), refreshMilliseconds);
refresh.unref();

return {
dispose() {
clearInterval(refresh);
},
invalidate() {
cachedAt = 0;
},
render(width: number): string[] {
if (width <= 0) return [""];
const now = Date.now();
if (!cached || now - cachedAt >= refreshMilliseconds) {
cached = renderCanonicalStatus(pi, ctx);
cachedAt = now;
}
return [truncateToWidth(cached, width, "")];
},
};
});
});
}
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ A silent bootstrap section needs no action; for any printed actionable diagnosti
## 4. Harness and runtime dispatch

Load `harness-adapters` before every spawn or recovery and before trust handling, skill invocation, interrupt, exit, resume, or adapter verification.
The verified harnesses are `claude`, `codex`, `opencode`, `pi`, and `grok`; never dispatch on an unverified adapter.
The verified worker adapters are `claude`, `codex`, `opencode`, `pi`, `grok`, and `cursor`; never dispatch on an unverified adapter.
`cursor` is worker-only, the mirror of the Kimi primary-only boundary; never launch a primary on it.
Kimi Code 0.27.0 is verified only as a primary through `bin/fm-primary.sh kimi-k3`; never pass it to `fm-spawn`.
If configured harness data names an unverified adapter, report it and fall back only to a verified adapter rather than launching it.

`docs/configuration.md` owns dispatch-profile and runtime-backend schemas, `bin/fm-dispatch-select.sh` owns selector mechanics, `bin/fm-harness.sh` owns static resolution, and `bin/fm-spawn.sh` owns launch flags and fail-closed validation.
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Full detail on every feature lives in [docs/architecture.md](docs/architecture.m

### Requirements

- A verified agent harness: Claude Code, Grok, Pi, Codex, or OpenCode.
- A verified agent harness: Claude Code, Grok, Pi, Codex, OpenCode, or Kimi Code as a primary-only runtime.
- Git and the GitHub CLI, authenticated through `gh auth login`.
- tmux, for the reference session backend.

Expand All @@ -72,6 +72,7 @@ All three have verified turn-end guard paths when launched with their documented
Pick whichever one matches your subscription and workflow.

Codex and OpenCode are also verified and supported as primary harnesses; Codex uses bounded foreground checkpoints, and OpenCode uses a TUI plugin, so both carry more harness-specific supervision tradeoffs than the three co-primaries.
Kimi Code 0.27.0 is verified as a primary-only runtime pinned to K3; it is not an `fm-spawn` worker adapter.

### Install and launch

Expand All @@ -81,7 +82,20 @@ git clone https://github.com/kunchenguid/firstmate
cd firstmate
```

Then launch one of the co-primary harnesses; AGENTS.md takes over from there:
The guarded profile launcher owns convenient primary aliases and automatic permission bypass:

```sh
bin/fm-primary.sh pi
bin/fm-primary.sh claude-fable
bin/fm-primary.sh codex
bin/fm-primary.sh kimi-k3
```

`bin/fm-primary.sh --help` is the single owner of every profile's exact flags, including the other verified OpenCode and Grok primaries.
`bin/fm-primary.sh --install-shim` can expose `firstmate <profile>` through `~/.local/bin`; it refuses to replace any different file or symlink.
The launcher resolves this tracked root from its own path, refuses another live Firstmate session, and keeps worker selection independent.

Existing direct launch commands remain supported; AGENTS.md takes over from there:

**Claude Code**

Expand Down Expand Up @@ -191,8 +205,10 @@ Firstmate's skills live in two separate places with different audiences:
- [docs/orca-backend.md](docs/orca-backend.md) - setup guide for the experimental Orca backend, plus its lifecycle notes and known gaps.
- [docs/cmux-backend.md](docs/cmux-backend.md) - setup guide for the experimental cmux backend, plus its verification notes and known gaps.
- [docs/codex-app-backend.md](docs/codex-app-backend.md) - Codex App backend boundary, evidence, and rollout contract.
- [docs/cursor-harness.md](docs/cursor-harness.md) - Cursor CLI worker-adapter verification evidence, limitations, and the worker-only boundary.
- [docs/status-bar.md](docs/status-bar.md) - the canonical primary status-bar fields, thresholds, placeholders, adapter surfaces, and verification evidence.
- [docs/turnend-guard.md](docs/turnend-guard.md) - the primary session's structural "no turn ends blind" backstop: verified per-harness hook mechanisms, scoping, loop safety, and fail-open tradeoffs.
- [docs/supervision-protocols/](docs/supervision-protocols/) - rendered primary-harness watcher protocols for Claude, Codex, OpenCode, Pi, Grok, and unknown harness fallback.
- [docs/supervision-protocols/](docs/supervision-protocols/) - rendered primary-harness watcher protocols for Claude, Codex, OpenCode, Pi, Grok, Kimi, and unknown harness fallback.
- [docs/scripts.md](docs/scripts.md) - the `bin/` toolbelt reference.
- [`AGENTS.md`](AGENTS.md) - the distro's always-loaded operating contract and routing index for conditional procedures.
- [CONTRIBUTING.md](CONTRIBUTING.md) - how to contribute, including the dev/test commands.
Expand Down
9 changes: 6 additions & 3 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,11 @@ fm_backend_cmux_parse_target() { # <target>
# (fm_backend_zellij_pane_exists) rather than the design sketch's original
# read-screen-based suggestion.
fm_backend_cmux_surface_exists() { # <workspace_id> <surface_id>
local wsid=$1 sfid=$2
fm_backend_cmux_cli list-panes --workspace "$wsid" --json --id-format uuids 2>/dev/null \
local wsid=$1 sfid=$2 out
# The CLI failure is checked explicitly: jq 1.6's -e exits 0 on empty input
# (fixed in 1.7), so piping a failed call through jq alone false-positives.
out=$(fm_backend_cmux_cli list-panes --workspace "$wsid" --json --id-format uuids 2>/dev/null) || return 1
printf '%s' "$out" \
| jq -e --arg s "$sfid" '[.panes[]? | select(.surface_ids // [] | index($s))] | length > 0' >/dev/null 2>&1
}

Expand Down Expand Up @@ -541,7 +544,7 @@ fm_backend_cmux_capture() { # <target> <lines> [expected-label]
# and keeping the LAST match so an earlier border-shaped line (scrollback, a
# popup) never outranks the real bottom-anchored composer row.
FM_BACKEND_CMUX_COMPOSER_LINES=${FM_BACKEND_CMUX_COMPOSER_LINES:-20}
FM_BACKEND_CMUX_IDLE_RE=${FM_BACKEND_CMUX_IDLE_RE:-'^Type a message\.\.\.$'}
FM_BACKEND_CMUX_IDLE_RE=${FM_BACKEND_CMUX_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}

fm_backend_cmux_composer_state() { # <target> [expected-label] -> empty|pending|unknown
local target=$1 expected_label=${2:-} cap line trimmed stripped="" found=0
Expand Down
Loading