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
5 changes: 3 additions & 2 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Inheritance also copies the literal `config/crew-dispatch.json` file, so secondm

Each adapter splits into mechanics and knowledge.
The per-task mechanics, including launch command, autonomy flag, and crewmate turn-end hook, live in `bin/fm-spawn.sh`.
When verifying a new adapter, its launch command carries the brief's path and never the brief's text, because a command line is world-readable in the host process table for as long as the agent runs (`docs/brief-off-argv.md`).
The primary-session "no turn ends blind" guard contract and harness hook installation paths live in `docs/turnend-guard.md`.
The primary-session watcher wake protocols are rendered from `docs/supervision-protocols/` by `bin/fm-supervision-instructions.sh`.
The supervision knowledge lives here: busy signature, exit command, interrupt, dialogs, resume behavior, skill invocation, and quirks.
Expand Down Expand Up @@ -338,7 +339,7 @@ The follow-up was verified in the interactive TUI; `opencode run` can exit befor
| Interrupt | single Escape |

Pi has no permission system, so crewmates are always autonomous.
Keep the brief as one positional argument.
Keep the encoded launch pointer as one positional argument; the worker reads the brief file it names.
Multiple positional args become separate queued messages; `fm-spawn`'s template already does this correctly.

Project trust dialog can appear on the first pi run in any not-yet-trusted directory, observed even on clean worktrees.
Expand All @@ -359,7 +360,7 @@ When a secondmate is launched on Pi, `fm-spawn.sh --secondmate` launches Pi with
## grok (VERIFIED 2026-06-29, grok 0.2.73; slash-submit re-verified 2026-07-03 on 0.2.82; reasoning-effort ceiling re-verified 2026-07-13 on 0.2.99; exit paths re-verified 2026-07-19 on grok 0.2.103)

Grok Build TUI (`grok`), a Claude-Code-compatible CLI from xAI.
Launch with a positional prompt: `grok --always-approve "$(cat <brief>)"`.
Launch with a positional prompt, which `bin/fm-spawn.sh` fills with a pointer to the brief file rather than the brief itself: `grok --always-approve "$(fm-operational-input.sh launch-pointer <brief>)"`.
For Grok's supported reasoning-effort values and omission behavior, see the [launch-profile-axes table](#launch-profile-axes).

| Fact | Value |
Expand Down
18 changes: 7 additions & 11 deletions .pi/extensions/fm-calm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// ExtensionUIContext.setToolsExpanded(), setWorkingVisible(), and
// setHiddenThinkingLabel(). The focused tests pin those assumptions. Pi still
// exposes no global renderer for built-in message rows or arbitrary custom tools.
import { readFileSync } from "node:fs";
import type {
ExtensionAPI,
ToolDefinition,
Expand All @@ -27,9 +26,9 @@ import {
calmPresentationIsActive,
classifyFirstmateLaunchInput,
deliverFirstmateSyntheticInput,
encodeFirstmateOperationalInput,
FIRSTMATE_CALM_PRESENTATION_EVENT,
FIRSTMATE_PI_LAUNCH_BRIEF_ENV,
firstmateLaunchBriefPointer,
registerFirstmateSyntheticPresentation,
setCalmPresentation,
setCalmStockExportRendering,
Expand Down Expand Up @@ -67,17 +66,14 @@ export default function (pi: ExtensionAPI) {
let expectedEncodedLaunchBrief: string | undefined;
let removeTerminalInputHandler: (() => void) | undefined;

// The launch input is a POINTER to the brief, not the brief, so the expected
// value is derived from the brief's PATH through the same shell owner that
// bin/fm-spawn.sh's launch command calls. Reading the file and encoding its
// body - what this did before - would rebuild a value the launch no longer
// sends, and the launch input would stop being recognised.
const launchBriefPath = process.env[FIRSTMATE_PI_LAUNCH_BRIEF_ENV];
if (launchBriefPath) {
try {
const launchBriefBody = readFileSync(launchBriefPath, "utf8").replace(/\n+$/, "");
expectedEncodedLaunchBrief = encodeFirstmateOperationalInput(
"launch-brief",
launchBriefBody,
);
} catch {
expectedEncodedLaunchBrief = undefined;
}
expectedEncodedLaunchBrief = firstmateLaunchBriefPointer(launchBriefPath);
}

const publishPresentationState = (): void => {
Expand Down
7 changes: 4 additions & 3 deletions .pi/extensions/lib/fm-calm-visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
type ExtensionAPI,
UserMessageComponent,
} from "@earendil-works/pi-coding-agent";
import { encodeFirstmateOperationalInput } from "./fm-operational-input.ts";

export { encodeFirstmateOperationalInput } from "./fm-operational-input.ts";
export {
encodeFirstmateOperationalInput,
firstmateLaunchBriefPointer,
} from "./fm-operational-input.ts";

export const CALM_TRANSCRIPT_CLASSES = [
"genuine-user-prompt",
Expand Down
15 changes: 15 additions & 0 deletions .pi/extensions/lib/fm-operational-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ function runOperationalInputCommand(
return command === "classify" ? result.stdout.replace(/\n$/, "") : result.stdout;
}

// The exact launch-brief input bin/fm-spawn.sh starts a crewmate with, derived
// from the brief's PATH. It routes through the same shell owner rather than
// re-implementing the pointer here, so this side cannot drift from what spawn
// actually sends. Reading the brief and encoding its body - what this used to
// do - is precisely what must not happen any more: the launch no longer carries
// the body, so a value built that way would never match.
export function firstmateLaunchBriefPointer(briefPath: string): string | undefined {
const result = spawnSync(operationalInputScript, ["launch-pointer", briefPath], {
encoding: "utf8",
maxBuffer: 1024 * 1024,
});
if (result.status !== 0) return undefined;
return result.stdout;
}

export function encodeFirstmateOperationalInput(
kind: FirstmateCurrentOperationalKind,
content: string,
Expand Down
58 changes: 58 additions & 0 deletions bin/fm-operational-input.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
#
# CLI:
# fm-operational-input.sh encode <kind> # body on stdin, encoded input stdout
# fm-operational-input.sh launch-pointer <brief-path> # encoded launch-brief
# # POINTER stdout; reads the path, never
# # the file, so no launch command ever
# # carries a brief body (see
# # fm_launch_brief_pointer below)
# fm-operational-input.sh kind # current input on stdin, kind stdout
# fm-operational-input.sh classify # current or legacy input on stdin
# fm-operational-input.sh body # current generic input on stdin
Expand Down Expand Up @@ -68,6 +73,40 @@ fm_operational_input_construct() { # <kind> <body> <result-var>
fm_operational_input_encode "$kind" "$body" "$result_var"
}

# The launch-brief POINTER body: fixed prose plus the brief's PATH, and never
# one byte of the brief itself.
#
# A launch command is world-readable in the host process table for as long as
# the agent runs, so a brief composed INTO that command is continuously readable
# by every account on the host, and by every same-account sibling worker. The
# brief's bytes therefore travel filesystem -> agent, and only its address
# travels argv -> agent; the worker reads the file itself.
#
# Composed here rather than at the call site because two producers must emit the
# same bytes: bin/fm-spawn.sh builds the launch command, and
# .pi/extensions/fm-calm.ts reconstructs the value it expects to see arrive.
# Both go through this one function (the TypeScript side shells out to the CLI
# below), so Pi's calm-mode launch classification cannot drift from what spawn
# actually sends.
fm_launch_brief_pointer_body() { # <brief-path> <result-var>
local brief_path=${1-} result_var=${2-}
[ -n "$result_var" ] && [ -n "$brief_path" ] || return 2
printf -v "$result_var" '%s%s%s' \
'You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human. Your launch brief is the file ' \
"$brief_path" \
' - read that entire file now, before anything else, and follow it as your instructions for this session. It is deliberately not on this command line, and nothing further will be sent.'
}

# The full encoded launch-brief input carrying that pointer. Same wire kind as
# before - only the body changed from the brief to its address - so every
# consumer that classifies a launch-brief input keeps working unchanged.
fm_launch_brief_pointer() { # <brief-path> <result-var>
local brief_path=${1-} result_var=${2-} pointer_body
[ -n "$result_var" ] && [ -n "$brief_path" ] || return 2
fm_launch_brief_pointer_body "$brief_path" pointer_body || return 2
fm_operational_input_encode launch-brief "$pointer_body" "$result_var"
}

fm_operational_generic_kind() { # <message> <result-var>
local message=${1-} result_var=${2-} remainder parsed_kind body
[ -n "$result_var" ] || return 2
Expand Down Expand Up @@ -201,13 +240,19 @@ fm_operational_usage() {
cat <<'EOF'
Usage:
bin/fm-operational-input.sh encode <kind> # body on stdin
bin/fm-operational-input.sh launch-pointer <brief-path>
bin/fm-operational-input.sh kind # current input on stdin
bin/fm-operational-input.sh classify # syntactically parse current or legacy input on stdin
bin/fm-operational-input.sh body # current input on stdin

Current construction kinds:
session-start watcher turn-end-guard away-supervisor from-firstmate launch-brief

launch-pointer builds the launch-brief input a crewmate is started with. It takes
the brief's PATH and never reads the brief, so the brief body never reaches a
command line - the worker opens the file itself. It refuses a path that is not a
readable file rather than launching a worker at a brief that is not there.

The from-firstmate kind uses its established live-charter-compatible carrier.
Parsing and classification do not authenticate the message or its sender.
EOF
Expand All @@ -225,6 +270,19 @@ fm_operational_main() {
fm_operational_input_construct "$argument" "$input" output || return 2
printf '%s' "$output"
;;
launch-pointer)
[ "$#" -eq 2 ] || return 2
# Fail closed on a brief that is not there: a worker launched at a dangling
# pointer reads nothing and idles, and the operator gets no reason why.
# This gate fires in the crewmate's own pane, where the message is visible,
# and backs up bin/fm-spawn.sh's earlier check at compose time.
if [ ! -f "$argument" ] || [ ! -r "$argument" ]; then
echo "fm-operational-input.sh: no readable launch brief at $argument" >&2
return 2
fi
fm_launch_brief_pointer "$argument" output || return 2
printf '%s' "$output"
;;
kind)
[ "$#" -eq 1 ] || return 2
fm_operational_read_stdin input || return 2
Expand Down
68 changes: 56 additions & 12 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# secondmate in its isolated firstmate home.
# Usage: fm-spawn.sh <task-id> <project-dir> [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>] [--scout]
# fm-spawn.sh <task-id> [<firstmate-home>] [--harness <name>|harness|launch-command] [--model <name>] [--effort <level>] [--backend <name>] --secondmate
# fm-spawn.sh --supported-harnesses
# --harness <name> is the explicit per-spawn harness/profile adapter. The old
# positional harness arg still works for back-compat.
# --model <name> and --effort <low|medium|high|xhigh|max> are concrete profile
Expand Down Expand Up @@ -101,7 +102,16 @@
# multi-task shell loop (the tool shell is zsh, which does not word-split unquoted
# $vars and silently breaks ad-hoc `for ... in $pairs` loops).
# Launch templates live in launch_template() below; placeholders replaced before launch:
# __BRIEF__ absolute path to data/<task-id>/brief.md
# __BRIEF__ absolute path to data/<task-id>/brief.md. Every template passes
# this PATH to `fm-operational-input.sh launch-pointer`, never the
# brief's contents: a launch command is world-readable in the host
# process table for the agent's whole life, so the brief travels
# filesystem -> worker and only its address travels argv -> worker.
# Never reintroduce a template that composes the brief BODY into
# the launch line (an `encode launch-brief < __BRIEF__` shape, a
# `$(cat __BRIEF__)`, or any equivalent);
# tests/fm-spawn-brief-off-argv.test.sh executes every template
# and fails if a brief body reaches any argv.
# __TURNEND__ absolute path to state/<task-id>.turn-ended (for harnesses whose
# turn-end signal rides the launch command, e.g. codex -c notify=[...])
# __CLAUDESETTINGS__ absolute path to the per-task Claude settings overlay; the
Expand All @@ -113,7 +123,7 @@
# written by this script; outside the worktree to avoid pi's trust gate)
# __PITURNEND__ absolute path to .pi/extensions/fm-primary-turnend-guard.ts in a pi secondmate home
# __OPINPUT__ absolute path to the canonical operational-input encoder
# __PIBRIEFENV__ shell assignment identifying the unchanged Pi positional brief
# __PIBRIEFENV__ shell assignment identifying the exact Pi positional launch pointer
# Per-harness turn-end hooks are installed automatically; some live outside the worktree.
# grok uses a firstmate-owned global hook under ${GROK_HOME:-$HOME/.grok}/hooks
# plus a gitignored .fm-grok-turnend worktree pointer and a state token.
Expand All @@ -123,13 +133,15 @@
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUPPORTED_HARNESSES=(claude codex opencode pi grok)

usage() {
sed -n '2,86p' "$0" | sed 's/^# \{0,1\}//'
}

case "${1:-}" in
-h|--help) usage; exit 0 ;;
--supported-harnesses) printf '%s\n' "${SUPPORTED_HARNESSES[@]}"; exit 0 ;;
esac

FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
Expand Down Expand Up @@ -439,9 +451,29 @@ fi

# The verified launch command per adapter. The knowledge half of each adapter
# (busy signature, exit command, dialogs, quirks) lives in the harness-adapters skill.
#
# BRIEF-OFF-ARGV CONTRACT, binding on every template here and on any adapter added
# later: the launch command carries the brief's PATH and never its BODY. Each
# harness's installed CLI was checked for a native prompt-file flag and none has
# one on its supervised interactive path (docs/brief-off-argv.md records the
# versions and the exact help output), so the pointer that
# `fm-operational-input.sh launch-pointer` builds is the one mechanism all five
# share: the worker opens the file itself. A template that puts the brief body
# back on the line republishes every crewmate's instructions to every account on
# the host, and to every same-account sibling worker, for as long as the agent
# runs. Setting a brief-path variable does NOT satisfy this contract on its own -
# __PIBRIEFENV__ already did that while the body still rode the same line, which
# is exactly the half-converted shape that reads as done and is not.
launch_template() {
local harness=$1 kind=${2:-ship}
# shellcheck disable=SC2016 # single quotes are deliberate: $(cat ...) expands in the crewmate pane, not here
local harness=$1 kind=${2:-ship} supported=0 candidate
for candidate in "${SUPPORTED_HARNESSES[@]}"; do
if [ "$candidate" = "$harness" ]; then
supported=1
break
fi
done
[ "$supported" -eq 1 ] || return 1
# shellcheck disable=SC2016 # single quotes are deliberate: the $(...) expands in the crewmate pane, not here
case "$harness" in
# CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false disables claude's interactive
# predicted-next-prompt ghost text, which renders as dim/faint text inside an
Expand All @@ -454,24 +486,24 @@ launch_template() {
# the defense-in-depth backstop for any pane this flag cannot reach.
claude)
if [ "$kind" = secondmate ]; then
printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ launch-pointer __BRIEF__)"'
else
printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --settings __CLAUDESETTINGS__ __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --settings __CLAUDESETTINGS__ __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ launch-pointer __BRIEF__)"'
fi
;;
codex)
if [ "$kind" = secondmate ]; then
printf '%s' 'codex __MODELFLAG____EFFORTFLAG____CODEXCONFIG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' 'codex __MODELFLAG____EFFORTFLAG____CODEXCONFIG__"$(__OPINPUT__ launch-pointer __BRIEF__)"'
else
printf '%s' 'codex __MODELFLAG____EFFORTFLAG____CODEXCONFIG__-c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' 'codex __MODELFLAG____EFFORTFLAG____CODEXCONFIG__-c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(__OPINPUT__ launch-pointer __BRIEF__)"'
fi
;;
opencode) printf '%s' 'OPENCODE_CONFIG_CONTENT='\''{"permission":{"*":"allow"}}'\'' opencode __MODELFLAG__--prompt "$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
opencode) printf '%s' 'OPENCODE_CONFIG_CONTENT='\''{"permission":{"*":"allow"}}'\'' opencode __MODELFLAG__--prompt "$(__OPINPUT__ launch-pointer __BRIEF__)"' ;;
pi)
if [ "$kind" = secondmate ]; then
printf '%s' '__PIBRIEFENV__ pi __MODELFLAG____EFFORTFLAG__-e __PITURNEND__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' '__PIBRIEFENV__ pi __MODELFLAG____EFFORTFLAG__-e __PITURNEND__ "$(__OPINPUT__ launch-pointer __BRIEF__)"'
else
printf '%s' '__PIBRIEFENV__ pi __MODELFLAG____EFFORTFLAG__-e __PIEXT__ "$(__OPINPUT__ encode launch-brief < __BRIEF__)"'
printf '%s' '__PIBRIEFENV__ pi __MODELFLAG____EFFORTFLAG__-e __PIEXT__ "$(__OPINPUT__ launch-pointer __BRIEF__)"'
fi
;;
# grok (Grok Build TUI): a positional prompt starts the supervised interactive
Expand All @@ -481,7 +513,7 @@ launch_template() {
# --dangerously-skip-permissions. grok's turn-end signal does NOT ride the
# launch command - it is a Stop-event hook installed below (global hook +
# per-task pointer), so the template is identical for ship/scout/secondmate.
grok) printf '%s' 'grok --always-approve __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;
grok) printf '%s' 'grok --always-approve __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ launch-pointer __BRIEF__)"' ;;
*) return 1 ;;
esac
}
Expand Down Expand Up @@ -519,6 +551,18 @@ case "$ARG3" in
*) LAUNCH="$launch_head --settings __CLAUDESETTINGS__$launch_tail" ;;
esac
fi
# A raw command is caller-authored, so the brief-off-argv contract that binds
# launch_template() cannot be enforced here without refusing a launch shape
# that works today. Warn instead: a raw command that reads the brief into
# itself publishes this worker's whole brief to every account on the host, and
# to every same-account sibling worker, for as long as the agent runs. Pass
# __BRIEF__ to `fm-operational-input.sh launch-pointer` the way every adapter
# template does (docs/brief-off-argv.md).
case "$LAUNCH" in
*'cat __BRIEF__'*|*'cat < __BRIEF__'*|*'< __BRIEF__'*)
echo "warn: this raw launch command reads the brief into the command line, where every account on this host can read it; use \"\$(<opinput> launch-pointer __BRIEF__)\" instead (docs/brief-off-argv.md)" >&2
;;
esac
;;
'')
# No explicit harness: resolve from config. A secondmate AGENT launches on the
Expand Down
Loading
Loading