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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ That styled capture is internal to the boolean detector only; `fm-peek` and ever
| Skill invocation | `$<skill>` (e.g. `$no-mistakes`); `/<skill>` is claude-only and codex rejects it as "Unrecognized command" |

Directory trust dialog on first run per repo root ("Do you trust the contents of this directory?") - accept with Enter; the decision persists for the repo, so later worktrees of the same project skip it.
Firstmate-launched codex ship and scout tasks keep the global Codex config intact but add per-launch MCP overrides for non-agent-native project worktrees: repos whose basename is not `agent-native` or `agent-native-*` disable only `agent-native-web-production-e480f` and `agent-native-dispatch`.
Agent-native project worktrees leave those MCP servers enabled.
Secondmate codex launches are treated as firstmate homes, not agent-native project worktrees, so they receive the same per-launch MCP disables and still do not install the parent turn-end notify hook.
Resume after exit: `codex resume <session-id>` (printed on quit).

### opencode (VERIFIED 2026-06-11, v1.15.7-1.17.3)
Expand Down Expand Up @@ -390,7 +393,7 @@ bin/fm-spawn.sh <id1>=projects/<repo1> <id2>=projects/<repo2> [--scout] # batc
Dispatch several tasks in one call by passing `id=repo` pairs instead of a single `<id> <project>`; each pair is spawned through the same single-task path, a shared `--scout` applies to all, and the looping happens inside the script so you never hand-write a multi-task shell loop.
If one pair fails, the rest still run and the batch exits non-zero.

The script resolves the harness (`fm-harness.sh crew`), owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, and `yolo=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters).
The script resolves the harness (`fm-harness.sh crew`), owns the verified launch templates, applies contextual Codex MCP launch overrides for non-agent-native projects and secondmate homes, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `kind=`, `mode=`, and `yolo=` in the task's meta; a non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters).
For `kind=secondmate`, the same script launches in the registered or explicit firstmate home instead of running `treehouse get` for a project, records `home=` and `projects=`, and uses the charter brief as the launch prompt.

For ship and scout tasks, the script creates the window (in your current tmux session, or a dedicated `firstmate` session when you are outside tmux), runs `treehouse get`, waits for the worktree subshell, installs the turn-end hook, records `state/<id>.meta`, and launches the agent with the brief.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Set `FM_SECONDMATE_CHARTER` to seed from inline charter text when no filled char
`FM_HOME` selects the operational home for one firstmate instance.
When it is unset, the repo root is the home; when it is set, scripts still run from this repo's `bin/`, but `state/`, `data/`, `config/`, and `projects/` come from `$FM_HOME`.
Harness support is a table in section 4: claude, codex, opencode, and pi are all empirically verified; new harnesses get verified through a supervised trial task before joining the table.
Codex launches preserve the global Codex config but add per-launch MCP overrides outside repos named `agent-native` or `agent-native-*`, and for secondmate homes, disabling only the two agent-native MCP servers that are noisy outside that repo family.

Runtime tuning via environment variables (defaults shown):

Expand Down Expand Up @@ -242,6 +243,7 @@ tests/fm-afk-inject-e2e.test.sh # private-socket end-to-end test of th
tests/fm-bootstrap.test.sh # bootstrap dependency and feature-probe tests
tests/fm-update.test.sh # fast-forward-only self-update, reread, nudge, dedup, and skip-safety tests
tests/fm-secondmate.test.sh # persistent secondmate routing, seeding, idle charter, backlog handoff, spawn, recovery, teardown, and FM_HOME tests
tests/fm-spawn-codex.test.sh # Codex launch template notify and contextual agent-native MCP override tests
tests/fm-teardown.test.sh # fm-teardown.sh safety and reminder checks: local-only fork-remote allow, truly-unpushed refuse, merged-to-main allow, no-mistakes regression, tasks-axi reminder, --force override
[ "$(readlink CLAUDE.md)" = "AGENTS.md" ]
[ "$(readlink .claude/skills)" = "../.agents/skills" ]
Expand Down
30 changes: 28 additions & 2 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
# 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
# __AGENT_NATIVE_MCP_CONFIG__ context-specific Codex MCP overrides; empty
# for agent-native project worktrees
# __TURNEND__ absolute path to state/<task-id>.turn-ended (for harnesses whose
# turn-end signal rides the launch command, e.g. codex -c notify=[...])
# __PIEXT__ absolute path to state/<task-id>.pi-ext.ts (pi turn-end extension,
Expand Down Expand Up @@ -121,9 +123,9 @@ launch_template() {
claude) printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions "$(cat __BRIEF__)"' ;;
codex)
if [ "$kind" = secondmate ]; then
printf '%s' 'codex --dangerously-bypass-approvals-and-sandbox "$(cat __BRIEF__)"'
printf '%s' 'codex --dangerously-bypass-approvals-and-sandbox __AGENT_NATIVE_MCP_CONFIG__"$(cat __BRIEF__)"'
else
printf '%s' 'codex --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(cat __BRIEF__)"'
printf '%s' 'codex --dangerously-bypass-approvals-and-sandbox __AGENT_NATIVE_MCP_CONFIG__-c "notify=[\"bash\",\"-c\",\"touch __TURNEND__\"]" "$(cat __BRIEF__)"'
fi
;;
opencode) printf '%s' 'OPENCODE_CONFIG_CONTENT='\''{"permission":{"*":"allow"}}'\'' opencode --prompt "$(cat __BRIEF__)"' ;;
Expand Down Expand Up @@ -202,6 +204,28 @@ path_is_ancestor_of() {
return 1
}

is_agent_native_project_path() {
local path=$1 name
name=$(basename "$path")
case "$name" in
agent-native|agent-native-*)
return 0
;;
esac
return 1
}

# Codex reads the captain's global MCP config by default. For firstmate-launched
# non-agent-native project worktrees, override only the two agent-native MCP
# servers that are noisy outside that repo family; do not rewrite global config.
agent_native_mcp_config() {
local project_path=$1
if [ "$KIND" != secondmate ] && is_agent_native_project_path "$project_path"; then
return 0
fi
printf '%s' '-c mcp_servers.agent-native-web-production-e480f.enabled=false -c mcp_servers.agent-native-dispatch.enabled=false '
}

validate_firstmate_home_for_spawn() {
local id=$1 home=$2 abs_home abs_active_home abs_root marker_id
abs_home=$(resolved_existing_dir "$home") || return 1
Expand Down Expand Up @@ -430,9 +454,11 @@ mkdir -p "$STATE"
} > "$STATE/$ID.meta"

sq_brief=$(shell_quote "$BRIEF")
agent_native_mcp=$(agent_native_mcp_config "$PROJ_ABS")
sq_turnend=$(shell_quote "$TURNEND")
sq_piext=$(shell_quote "$STATE/$ID.pi-ext.ts")
LAUNCH=${LAUNCH//__BRIEF__/$sq_brief}
LAUNCH=${LAUNCH//__AGENT_NATIVE_MCP_CONFIG__/$agent_native_mcp}
LAUNCH=${LAUNCH//__TURNEND__/$sq_turnend}
LAUNCH=${LAUNCH//__PIEXT__/$sq_piext}
if [ "$KIND" = secondmate ]; then
Expand Down
117 changes: 117 additions & 0 deletions tests/fm-spawn-codex.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Behavior tests for the Codex fm-spawn launch template.
set -u

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SPAWN="$ROOT/bin/fm-spawn.sh"
TMP_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/fm-spawn-codex.XXXXXX")
trap 'rm -rf "$TMP_ROOT"' EXIT

fail() {
printf 'not ok - %s\n' "$1" >&2
exit 1
}

pass() {
printf 'ok - %s\n' "$1"
}

make_fake_tmux() {
local dir=$1 fakebin
fakebin="$dir/fakebin"
mkdir -p "$fakebin"
cat > "$fakebin/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
has-session|new-session|new-window|send-keys)
printf '%s\n' "$*" >> "$FM_FAKE_TMUX_LOG"
exit 0
;;
list-windows)
exit 0
;;
display-message)
printf '%s\n' "$FM_FAKE_WORKTREE"
exit 0
;;
esac
exit 1
SH
chmod +x "$fakebin/tmux"
printf '%s\n' "$fakebin"
}

make_spawn_home() {
local home=$1 worktree=$2 project=$3
mkdir -p "$home/data/codex-launch-z1" "$home/projects/$project" "$home/state" "$worktree"
printf 'ship brief\n' > "$home/data/codex-launch-z1/brief.md"
printf -- '- %s [direct-PR] - test project (added 2026-06-24)\n' "$project" > "$home/data/projects.md"
}

run_codex_spawn() {
local project=$1 home worktree fakebin log
home="$TMP_ROOT/$project-home"
worktree="$TMP_ROOT/$project-worktree"
make_spawn_home "$home" "$worktree" "$project"
fakebin=$(make_fake_tmux "$TMP_ROOT/$project-fake")
log="$TMP_ROOT/$project-fake/tmux.log"
: > "$log"

PATH="$fakebin:$PATH" FM_HOME="$home" FM_FAKE_WORKTREE="$worktree" FM_FAKE_TMUX_LOG="$log" \
"$SPAWN" codex-launch-z1 "projects/$project" codex >/dev/null \
|| fail "codex spawn failed for $project"

printf '%s\n' "$log"
}

assert_codex_launch_has_notify() {
local log=$1 turnend=$2
grep -F 'codex --dangerously-bypass-approvals-and-sandbox' "$log" >/dev/null \
|| fail "spawn did not launch codex"
grep -F -- "-c \"notify=[\\\"bash\\\",\\\"-c\\\",\\\"touch '$turnend'\\\"]\"" "$log" >/dev/null \
|| fail "spawn did not preserve codex notify turn-end config"
}

assert_codex_launch_has_agent_native_mcp_disables() {
local log=$1
grep -F -- '-c mcp_servers.agent-native-web-production-e480f.enabled=false' "$log" >/dev/null \
|| fail "spawn did not disable agent-native-web-production MCP"
grep -F -- '-c mcp_servers.agent-native-dispatch.enabled=false' "$log" >/dev/null \
|| fail "spawn did not disable agent-native-dispatch MCP"
}

assert_codex_launch_lacks_agent_native_mcp_disables() {
local log=$1
if grep -F -- '-c mcp_servers.agent-native-web-production-e480f.enabled=false' "$log" >/dev/null; then
fail "agent-native spawn disabled agent-native-web-production MCP"
fi
if grep -F -- '-c mcp_servers.agent-native-dispatch.enabled=false' "$log" >/dev/null; then
fail "agent-native spawn disabled agent-native-dispatch MCP"
fi
}

test_codex_non_agent_native_launch_disables_agent_native_mcp_servers() {
local project home log
project=alpha
home="$TMP_ROOT/$project-home"
log=$(run_codex_spawn "$project")

assert_codex_launch_has_notify "$log" "$home/state/codex-launch-z1.turn-ended"
assert_codex_launch_has_agent_native_mcp_disables "$log"
pass "codex launch disables agent-native MCP servers outside agent-native repos"
}

test_codex_agent_native_launch_keeps_agent_native_mcp_servers_enabled() {
local project home log
project=agent-native-main
home="$TMP_ROOT/$project-home"
log=$(run_codex_spawn "$project")

assert_codex_launch_has_notify "$log" "$home/state/codex-launch-z1.turn-ended"
assert_codex_launch_lacks_agent_native_mcp_disables "$log"
pass "codex launch keeps agent-native MCP servers available in agent-native repos"
}

test_codex_non_agent_native_launch_disables_agent_native_mcp_servers
test_codex_agent_native_launch_keeps_agent_native_mcp_servers_enabled