diff --git a/.agents/skills/harness-adapters/SKILL.md b/.agents/skills/harness-adapters/SKILL.md index 0e441fd6d2b..8a2d0840eca 100644 --- a/.agents/skills/harness-adapters/SKILL.md +++ b/.agents/skills/harness-adapters/SKILL.md @@ -133,6 +133,10 @@ Claude Code's primary watcher protocol is the lowest-friction path: run `bin/fm- ## codex (VERIFIED 2026-06-11, codex-cli 0.139.0) +For ship and scout launches, `fm-spawn.sh` refreshes a firstmate-owned isolated `CODEX_HOME` with no MCP servers or plugins before starting Codex. +It copies only the captain home's current Codex authentication and model catalog, so the captain's `~/.codex` configuration remains untouched. +Secondmate Codex launches intentionally retain their existing home behavior. + | Fact | Value | |---|---| | Busy-pane signature | `esc to interrupt` (shown as `• Working (Xs • esc to interrupt)`) | diff --git a/AGENTS.md b/AGENTS.md index f634af7c0a4..7a855daf62a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,7 +84,9 @@ config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCA config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present data/ personal fleet records; LOCAL, gitignored as a whole backlog.md task queue, dependencies, history - captain.md captain's personal preferences and working style; LOCAL, gitignored, canonical even if harness memory mirrors it, and updated with inspect-then-update + captain.md this home's domain-local captain preferences and working style; LOCAL, gitignored, canonical even if harness memory mirrors it, and updated with inspect-then-update + captain-shared.md main-authoritative shared captain preferences propagated read-only to secondmate homes; LOCAL, gitignored, owned by secondmate-provisioning + codex-crewmate/ task-private Codex homes for ship and scout launches; LOCAL, gitignored, owned by fm-codex-home.py and removed by teardown learnings.md fleet-local operational facts and gotchas; LOCAL, gitignored; dated, evidence-backed, curated, and updated with inspect-then-update - rewrite and prune rather than append forever, the same contract as captain.md; created lazily, absent until this home has a learning to store projects.md thin fleet navigation registry; firstmate-private, parsed by fm-project-mode.sh (section 6) secondmates.md secondmate routing table; firstmate-private, maintained by fm-home-seed.sh (section 6) @@ -95,8 +97,14 @@ state/ volatile runtime signals; gitignored .status appended by crewmates: ": " wake-event lines, not current-state truth .turn-ended touched by turn-end hooks .grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown - .meta written by fm-spawn: window=, 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, appends pr= and GitHub's pr_head= when available; 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) - .check.sh optional slow poll you write per task (e.g. merged-PR check) + .meta written by fm-spawn: window=, worktree=, project=, harness=, model=, effort=, kind=, mode=, yolo=, tasktmp=; Codex ship/scout tasks also record codex_crewmate_home=; 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 GitHub's pr_head= when available; 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) + .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 + .check-trust private content binding created by fm-check-register.sh for an intentional custom check + .pr-poll private validated data sidecar for the byte-static PR merge poll + .pr-poll-registration private transactional provenance record binding the task, canonical metadata identity, sidecar, and static poll publication + .pr-check-quarantine/ private non-runnable storage for checks neutralized by the non-executing migration + .pr-check-migration.log private per-task outcomes distinguishing rebuilt or canonically registered replacement polls, quarantined unarmed polls, and incomplete migrations + .pr-check-migration-scan-v1 private marker proving the non-executing scan disabled every unsafe legacy check; .pr-check-migration-v1 separately records completed private repairs x-watch.check.sh generated X-mode relay poll shim; present only when opted in (section 14) x-inbox/ generated X-mode pending mention payloads; fmx-respond drains it (section 14) x-context/ generated X-mode durable per-request reply context (platform/budget), keyed by request_id; survives inbox cleanup so a delayed follow-up recovers the original platform (section 14; bin/fm-x-lib.sh) diff --git a/bin/backends/cmux.sh b/bin/backends/cmux.sh index 69dc0b53bde..cc90f6dc5d3 100644 --- a/bin/backends/cmux.sh +++ b/bin/backends/cmux.sh @@ -620,8 +620,7 @@ fm_backend_cmux_window_of_workspace() { # -> " /dev/null) } -# fm_backend_cmux_kill: remove the task's whole workspace, best-effort (mirrors -# every other backend's `kill` `|| true` contract). A cmux task owns one +# fm_backend_cmux_kill: remove the task's whole workspace. A cmux task owns one # workspace, so teardown reclaims that workspace and all of its surfaces. # # The selected-workspace teardown bug (docs/cmux-backend.md "Closing the last @@ -639,7 +638,12 @@ fm_backend_cmux_window_of_workspace() { # -> " [unused] [expected-label] local expected_label=${3:-} wsid wininfo win count if [ -n "$expected_label" ]; then - fm_backend_cmux_target_ready "$1" "$expected_label" || return 0 + if ! fm_backend_cmux_target_ready "$1" "$expected_label"; then + [ "${FM_BACKEND_KILL_STRICT:-0}" = 1 ] && return 1 + return 0 + fi + elif [ "${FM_BACKEND_KILL_STRICT:-0}" = 1 ]; then + fm_backend_cmux_target_ready "$1" || return 1 else fm_backend_cmux_parse_target "$1" || return 0 fi @@ -648,9 +652,9 @@ fm_backend_cmux_kill() { # [unused] [expected-label] win=${wininfo%% *} count=${wininfo##* } if [ -n "$win" ] && [ "$count" = 1 ]; then - fm_backend_cmux_cli new-workspace --window "$win" --focus false --id-format uuids >/dev/null 2>&1 || true + fm_backend_cmux_cli new-workspace --window "$win" --focus false --id-format uuids >/dev/null 2>&1 || [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] || return 1 fi - fm_backend_cmux_cli close-workspace --workspace "$wsid" >/dev/null 2>&1 || true + fm_backend_cmux_cli close-workspace --workspace "$wsid" >/dev/null 2>&1 || [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] } # fm_backend_cmux_list_live: recovery/orphan discovery. Lists every workspace diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 7874988b426..31de7c18ca2 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -875,12 +875,14 @@ fm_backend_herdr_send_text_submit() { # done } -# fm_backend_herdr_kill: remove the task's pane, best-effort (mirrors -# tmux-kill-window's `|| true` contract). Verified: closing a tab's only pane +# fm_backend_herdr_kill: remove the task's pane. Verified: closing a tab's only pane # closes the tab too, so a separate tab close is unnecessary. fm_backend_herdr_kill() { # - fm_backend_herdr_target_ready "$1" || return 0 - fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane close "$FM_BACKEND_HERDR_PANE" >/dev/null 2>&1 || true + if ! fm_backend_herdr_target_ready "$1"; then + [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] && return 0 + return 1 + fi + fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane close "$FM_BACKEND_HERDR_PANE" >/dev/null 2>&1 || [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] } # fm_backend_herdr_classify_agent_status: map a raw `agent get` agent_status diff --git a/bin/backends/orca.sh b/bin/backends/orca.sh index dc9307de4f6..91da0309e71 100644 --- a/bin/backends/orca.sh +++ b/bin/backends/orca.sh @@ -331,6 +331,10 @@ fm_backend_orca_send_text_submit() { # - fm_backend_orca_tool_check || return 0 - orca terminal close --terminal "$1" --json >/dev/null 2>&1 || true + fm_backend_orca_tool_check || [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] || return 1 + if [ "${FM_BACKEND_KILL_STRICT:-0}" = 1 ]; then + fm_backend_orca_run_json orca terminal close --terminal "$1" --json + else + orca terminal close --terminal "$1" --json >/dev/null 2>&1 || true + fi } diff --git a/bin/backends/tmux.sh b/bin/backends/tmux.sh index dee5813d64b..9b51f910848 100644 --- a/bin/backends/tmux.sh +++ b/bin/backends/tmux.sh @@ -117,10 +117,9 @@ fm_backend_tmux_send_literal() { # tmux send-keys -t "$1" -l "$2" } -# fm_backend_tmux_kill: remove the task's window, best-effort. Mirrors -# fm-teardown.sh's `tmux kill-window -t "$T" 2>/dev/null || true`. +# fm_backend_tmux_kill: remove the task's window. fm_backend_tmux_kill() { # - tmux kill-window -t "$1" 2>/dev/null || true + tmux kill-window -t "$1" 2>/dev/null || [ "${FM_BACKEND_KILL_STRICT:-0}" != 1 ] } # fm_backend_tmux_current_command: 's live foreground process name - diff --git a/bin/backends/zellij.sh b/bin/backends/zellij.sh index 162ea5477b6..0e052b86f52 100644 --- a/bin/backends/zellij.sh +++ b/bin/backends/zellij.sh @@ -270,8 +270,10 @@ fm_backend_zellij_pane_for_tab() { # # target string; the tab id is looked up fresh rather than trusted stale, # mirroring herdr's label-based, never-trust-a-stored-id recovery posture). fm_backend_zellij_tab_for_pane() { # - local session=$1 pane_id=$2 - fm_backend_zellij_cli "$session" action list-panes --json 2>/dev/null \ + local session=$1 pane_id=$2 panes + panes=$(fm_backend_zellij_cli "$session" action list-panes --json 2>/dev/null) || return 1 + printf '%s' "$panes" | jq -e 'type == "array"' >/dev/null 2>&1 || return 1 + printf '%s' "$panes" \ | jq -r --argjson p "$pane_id" '.[]? | select(.id == $p and .is_plugin == false) | .tab_id' 2>/dev/null | head -1 } @@ -299,7 +301,8 @@ fm_backend_zellij_pane_exists() { # fm_backend_zellij_tab_matches_label() { #