From bcfe024c0200e65bbdfc344b3db2747308b6868f Mon Sep 17 00:00:00 2001 From: tommy230 Date: Wed, 24 Jun 2026 09:01:39 -0400 Subject: [PATCH 1/3] fix(spawn): gate agent-native MCP startup --- bin/fm-spawn.sh | 26 +++++++- tests/fm-spawn-codex.test.sh | 117 +++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/fm-spawn-codex.test.sh diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index be4273534dd..b927e9bbc8a 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -18,6 +18,7 @@ # 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//brief.md +# __AGENT_NATIVE_MCP_CONFIG__ context-specific Codex MCP overrides # __TURNEND__ absolute path to state/.turn-ended (for harnesses whose # turn-end signal rides the launch command, e.g. codex -c notify=[...]) # __PIEXT__ absolute path to state/.pi-ext.ts (pi turn-end extension, @@ -121,9 +122,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__)"' ;; @@ -202,6 +203,25 @@ 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 +} + +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 @@ -430,9 +450,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 diff --git a/tests/fm-spawn-codex.test.sh b/tests/fm-spawn-codex.test.sh new file mode 100644 index 00000000000..844741a822c --- /dev/null +++ b/tests/fm-spawn-codex.test.sh @@ -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 From 4eb9e5bd3e69a0791e4c2596e9c6dcee548269f4 Mon Sep 17 00:00:00 2001 From: tommy230 Date: Wed, 24 Jun 2026 09:41:31 -0400 Subject: [PATCH 2/3] no-mistakes: finalize Codex MCP launch gate --- AGENTS.md | 5 ++++- README.md | 2 ++ bin/fm-spawn.sh | 3 +++ tests/fm-spawn-codex.test.sh | 0 4 files changed, 9 insertions(+), 1 deletion(-) mode change 100644 => 100755 tests/fm-spawn-codex.test.sh diff --git a/AGENTS.md b/AGENTS.md index f268e4e6210..4eaebc5b2a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,6 +180,9 @@ That styled capture is internal to the boolean detector only; `fm-peek` and ever | Skill invocation | `$` (e.g. `$no-mistakes`); `/` 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 ` (printed on quit). ### opencode (VERIFIED 2026-06-11, v1.15.7-1.17.3) @@ -390,7 +393,7 @@ bin/fm-spawn.sh =projects/ =projects/ [--scout] # batc Dispatch several tasks in one call by passing `id=repo` pairs instead of a single ` `; 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, 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/.meta`, and launches the agent with the brief. diff --git a/README.md b/README.md index 9ab98ff25fd..3396e3da375 100644 --- a/README.md +++ b/README.md @@ -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 crewmate launches preserve the global Codex config but add per-launch MCP overrides outside repos named `agent-native` or `agent-native-*`, disabling only the two agent-native MCP servers that are noisy in unrelated projects. Runtime tuning via environment variables (defaults shown): @@ -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" ] diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index b927e9bbc8a..0d2b3a6638f 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -214,6 +214,9 @@ is_agent_native_project_path() { 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 diff --git a/tests/fm-spawn-codex.test.sh b/tests/fm-spawn-codex.test.sh old mode 100644 new mode 100755 From f0980e7343a459557de19e28fd4eb41cc35ef59d Mon Sep 17 00:00:00 2001 From: tommy230 Date: Wed, 24 Jun 2026 09:48:45 -0400 Subject: [PATCH 3/3] no-mistakes(document): Sync Codex MCP docs --- AGENTS.md | 2 +- README.md | 2 +- bin/fm-spawn.sh | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4eaebc5b2a6..b4e6ba85e15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -393,7 +393,7 @@ bin/fm-spawn.sh =projects/ =projects/ [--scout] # batc Dispatch several tasks in one call by passing `id=repo` pairs instead of a single ` `; 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, applies contextual Codex MCP launch overrides for non-agent-native projects, 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/.meta`, and launches the agent with the brief. diff --git a/README.md b/README.md index 3396e3da375..5458e9cd7a2 100644 --- a/README.md +++ b/README.md @@ -193,7 +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 crewmate launches preserve the global Codex config but add per-launch MCP overrides outside repos named `agent-native` or `agent-native-*`, disabling only the two agent-native MCP servers that are noisy in unrelated projects. +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): diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 0d2b3a6638f..92890131115 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -18,7 +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//brief.md -# __AGENT_NATIVE_MCP_CONFIG__ context-specific Codex MCP overrides +# __AGENT_NATIVE_MCP_CONFIG__ context-specific Codex MCP overrides; empty +# for agent-native project worktrees # __TURNEND__ absolute path to state/.turn-ended (for harnesses whose # turn-end signal rides the launch command, e.g. codex -c notify=[...]) # __PIEXT__ absolute path to state/.pi-ext.ts (pi turn-end extension,