diff --git a/AGENTS.md b/AGENTS.md index ad461027057..6e575396dac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "de config/crew-dispatch.json optional crewmate dispatch profiles; LOCAL, gitignored; firstmate-maintained but human-editable natural-language rules that choose a per-task harness/model/effort profile (section 4). Inherited by secondmate homes config/secondmate-harness harness the PRIMARY uses to launch SECONDMATE agents, optionally followed by a model and effort token on the same line (" [] []"; section 4); LOCAL, gitignored; absent or "default" harness falls back to config/crew-harness then firstmate's own. The primary's own setting; NOT inherited into secondmate homes (secondmates do not spawn secondmates) config/backlog-backend backlog backend override; LOCAL, gitignored; absent or "tasks-axi" = default tasks-axi backend, "manual" = force hand-editing; inherited by secondmate homes (section 10) -config/backend runtime session-provider backend override for new tasks; LOCAL, gitignored; absent = falls through to runtime auto-detection (the runtime firstmate itself is executing inside), then tmux; tmux is the verified reference backend (docs/tmux-backend.md), while herdr, zellij, orca, and cmux are experimental spawn backends (docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, docs/cmux-backend.md) - herdr and cmux can also be selected by runtime auto-detection, zellij and orca never are (always explicit); not inherited into secondmate homes +config/backend runtime session-provider backend override for new tasks; LOCAL, gitignored; absent = falls through to runtime auto-detection (the runtime firstmate itself is executing inside), then tmux; tmux is the verified reference backend (docs/tmux-backend.md), while herdr, zellij, orca, and cmux are experimental spawn backends (docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, docs/cmux-backend.md) - herdr and cmux can also be selected by runtime auto-detection, zellij and orca never are (always explicit), and codex-app is not accepted; see docs/codex-app-backend.md; not inherited into secondmate homes config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup") 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 @@ -475,6 +475,7 @@ bin/fm-spawn.sh projects/ --backend herdr # experimental herdr backe bin/fm-spawn.sh projects/ --backend zellij # experimental zellij backend (docs/zellij-backend.md); version-gates at spawn bin/fm-spawn.sh projects/ --backend orca # experimental Orca backend (docs/orca-backend.md); Orca owns worktree + terminal; Escape unsupported bin/fm-spawn.sh projects/ --backend cmux # experimental cmux backend (docs/cmux-backend.md); GUI-first macOS-only, treehouse still owns worktree; requires a one-time socket-access setup (docs/cmux-backend.md "Setup") +# backend=codex-app is not accepted yet; see docs/codex-app-backend.md. bin/fm-spawn.sh projects/ --scout # scout task; records kind=scout in meta bin/fm-spawn.sh --secondmate # launch a registered persistent secondmate in its home bin/fm-spawn.sh --secondmate # launch or recover an explicit secondmate home @@ -485,7 +486,7 @@ Dispatch several tasks in one call by passing `id=repo` pairs instead of a singl If one pair fails, the rest still run and the batch exits non-zero. When `config/crew-dispatch.json` exists, include a shared `--harness` for every crewmate or scout batch after consulting the dispatch rules. -The script resolves the harness (`fm-harness.sh crew` for crewmate/scout tasks only when `config/crew-dispatch.json` is absent, `fm-harness.sh secondmate` for `kind=secondmate`; section 4), resolves the runtime backend (`--backend`, then `FM_BACKEND`, then `config/backend`, then runtime auto-detection - the runtime firstmate itself is executing inside, from `$TMUX`/`HERDR_ENV=1`/cmux runtime signals, nesting resolved innermost-first (`$TMUX`, then `HERDR_ENV=1`, then cmux's primary `CMUX_WORKSPACE_ID` marker and documented macOS-only fallbacks last, since cmux is a terminal application rather than a nestable multiplexer; docs/cmux-backend.md "Runtime auto-detection") - then `tmux`; an auto-detected herdr or cmux spawn prints a loud stderr notice, auto-detected tmux stays silent; zellij and orca are never auto-detected, only explicit `--backend `/`FM_BACKEND=`/`config/backend`), validates the requested backend against spawn-capable adapters, owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `model=`, `effort=`, `kind=`, `mode=`, and `yolo=` in the task's meta; only a non-default runtime backend is recorded as `backend=` because absent means tmux. +The script resolves the harness (`fm-harness.sh crew` for crewmate/scout tasks only when `config/crew-dispatch.json` is absent, `fm-harness.sh secondmate` for `kind=secondmate`; section 4), resolves the runtime backend (`--backend`, then `FM_BACKEND`, then `config/backend`, then runtime auto-detection - the runtime firstmate itself is executing inside, from `$TMUX`/`HERDR_ENV=1`/cmux runtime signals, nesting resolved innermost-first (`$TMUX`, then `HERDR_ENV=1`, then cmux's primary `CMUX_WORKSPACE_ID` marker and documented macOS-only fallbacks last, since cmux is a terminal application rather than a nestable multiplexer; docs/cmux-backend.md "Runtime auto-detection") - then `tmux`; an auto-detected herdr or cmux spawn prints a loud stderr notice, auto-detected tmux stays silent; zellij and orca are never auto-detected, only explicit `--backend `/`FM_BACKEND=`/`config/backend`), validates the requested backend against spawn-capable adapters, rejects `codex-app` as unknown, owns the verified launch templates, resolves the project's delivery mode (`fm-project-mode.sh`) for ship/scout tasks, and records `harness=`, `model=`, `effort=`, `kind=`, `mode=`, and `yolo=` in the task's meta; only a non-default runtime backend is recorded as `backend=` because absent means tmux. A backend spawn refusal - a missing dependency, an unauthenticated socket, or a version gate - must be surfaced to the captain as a blocker; never silently retry the spawn on a different backend to work around it. A non-flag third argument containing whitespace is treated as a raw launch command (only for verifying new adapters). When `config/crew-dispatch.json` exists, the script refuses crewmate or scout launches without an explicit harness because firstmate must have already resolved the profile choice at intake. @@ -493,6 +494,7 @@ When `--model` or `--effort` is omitted, the corresponding meta value is `defaul 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, tmux/herdr/zellij/cmux create a runtime endpoint and run `treehouse get`; Orca creates an Orca-owned worktree, validates it, then creates the terminal. In all cases, the script asserts the resolved worktree is a genuine isolated worktree distinct from the primary checkout (aborting the spawn otherwise, to prevent the worktree tangle of section 8), installs the turn-end hook, records `state/.meta`, and launches the agent with the brief. +Selecting `backend=codex-app` fails as an unknown backend; see `docs/codex-app-backend.md`. For grok, the turn-end hook is one firstmate-owned global hook under `$GROK_HOME/hooks/`, or `~/.grok/hooks/` when `GROK_HOME` is unset, activated only when the worktree holds the per-task `.fm-grok-turnend` token pointer that matches `state/.grok-turnend-token`; teardown removes the pointer and token. For `kind=secondmate`, the script creates the same kind of runtime endpoint but starts directly in the persistent home. With herdr, ordinary crewmate and scout spawns use the current `FM_HOME` workspace; a primary `--secondmate` spawn uses the secondmate target home's workspace, so secondmate-owned tabs do not mix into the primary `firstmate` space. @@ -691,7 +693,7 @@ Heartbeats back off exponentially while they are the only wakes firing (600s dou Due per-task checks run before signal scanning so chatty crewmate status updates cannot starve slow polls like merge detection. Never rely on hooks or status files alone; when a heartbeat wake does reach you, the review of every window is mandatory and unconditional. -Each task's backend live-task inventory is the ground truth (tmux when `backend=` is absent; a task's meta may record a different `backend=` - herdr, zellij, orca, and cmux are the other implemented, spawn-capable, experimental backends today, docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, and docs/cmux-backend.md). +Each task's backend live-task inventory is the ground truth (tmux when `backend=` is absent; a task's meta may record a different `backend=` - herdr, zellij, orca, and cmux are the other implemented, spawn-capable, experimental backends today, docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, and docs/cmux-backend.md; codex-app is not selectable, see docs/codex-app-backend.md). For `kind=secondmate`, an idle pane is healthy. A secondmate may be sitting on its own watcher with no visible pane changes, so parent supervision uses status writes plus heartbeat review, not pane-staleness. `fm-watch.sh` therefore skips stale-pane wakes for windows whose meta records `kind=secondmate`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0fb5d64827..326701a918f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,14 +39,14 @@ See the [no-mistakes quick start](https://kunchenguid.github.io/no-mistakes/star Everything personal to one captain's fleet (`.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`) is gitignored; never commit it. The root `.tasks.toml` is tracked `tasks-axi` config for `data/backlog.md`; compatible `tasks-axi` is the default backend for routine backlog mutations. A local `config/backlog-backend=manual` opt-out forces hand-editing and stays gitignored. - A local `config/backend` file explicitly overrides runtime auto-detection for new task endpoints and stays gitignored; spawn-supported values are `tmux` plus experimental `herdr`, `zellij`, `orca`, and `cmux`. + A local `config/backend` file explicitly overrides runtime auto-detection for new task endpoints and stays gitignored; spawn-supported values are `tmux` plus experimental `herdr`, `zellij`, `orca`, and `cmux`, while `codex-app` is documented only in `docs/codex-app-backend.md`. It does not make `data/` tracked. - Helper scripts in `bin/` are plain bash. Each starts with a usage header comment; keep it accurate when you change behavior. Test scripts and helpers in `tests/` are plain bash too. `shellcheck bin/*.sh bin/backends/*.sh tests/*.sh` must pass, and CI enforces it. - Changes to harness adapters (detection in `bin/fm-harness.sh`, launch and hook mechanics in `bin/fm-spawn.sh`, busy signatures in `bin/fm-watch.sh` and `bin/fm-tmux-lib.sh`, cleanup in `bin/fm-teardown.sh`, and facts in `.agents/skills/harness-adapters/SKILL.md`) must be verified empirically against the real harness, never written from documentation alone. -- Changes to runtime session backends (`bin/fm-backend.sh`, `bin/backends/`, and the scripts that dispatch through them) need empirical adapter notes in the relevant backend guide: `docs/tmux-backend.md`, `docs/herdr-backend.md`, `docs/zellij-backend.md`, `docs/orca-backend.md`, or `docs/cmux-backend.md`. +- Changes to runtime session backends (`bin/fm-backend.sh`, `bin/backends/`, and the scripts that dispatch through them) need empirical adapter notes in the relevant backend guide: `docs/tmux-backend.md`, `docs/herdr-backend.md`, `docs/zellij-backend.md`, `docs/orca-backend.md`, `docs/cmux-backend.md`, or `docs/codex-app-backend.md` for blocked Codex App transport work. - In Markdown, put each full sentence on its own line. - `README.md` stays a concise overview plus pointers: it never carries a wall of inline detail. Route detail to the most specific `docs/` file (architecture, configuration, or a backend guide) and link to it instead. @@ -100,7 +100,7 @@ tests/fm-teardown.test.sh # fm-teardown.sh landed-work safety an tests/fm-review-diff.test.sh # fm-review-diff.sh authoritative review diff coverage: recorded pr_head=, fetched refs/pull//head, no-pr local branch behavior, and warning fallback tests/fm-pr-merge.test.sh # fm-pr-merge.sh records pr= and available pr_head= before merging, parses PR URLs into gh-axi number/--repo calls, defaults to squash, preserves explicit merge methods, rejects malformed URLs and repo overrides, and propagates real merge failures tests/fm-crew-state.test.sh # fm-crew-state.sh current-state reconciliation: run-step authority including closed panes and ci log-tail checks-green detection, stale checks-green and needs-decision/blocked superseded by resumed work, genuine-parked, cross-branch runs-list attribution, pane/status-log fallback, scout skip, torn-down/missing-meta graceful -tests/fm-backend.test.sh # runtime-backend abstraction: fm-backend.sh selection/meta/dispatch helpers, shell-portable sourced backend matching, and old-vs-new fake-tool command-log conformance for fm-send/fm-peek/fm-spawn/fm-teardown +tests/fm-backend.test.sh # runtime-backend abstraction: fm-backend.sh selection/meta/dispatch helpers, shell-portable sourced backend matching, blocked codex-app refusal, and old-vs-new fake-tool command-log conformance for fm-send/fm-peek/fm-spawn/fm-teardown tests/fm-backend-tmux-smoke.test.sh # real (private-socket) tmux smoke test for the tmux adapter: create/duplicate-refuse, send text + Enter, send literal + key, bounded capture, live-window resolve, kill tests/fm-backend-herdr.test.sh # fake herdr CLI unit tests for the experimental herdr adapter, including version/tool gates, target parsing, send/capture, structural composer-state verification, slash-submit retry regression coverage, native busy state, per-home workspace-label resolution, default-tab prune safety, restored-layout husk replacement, and verified CLI bug workarounds tests/fm-backend-herdr-smoke.test.sh # real herdr adapter smoke test, skipped when herdr or jq is unavailable, using an isolated throwaway HERDR_SESSION and guarded session cleanup, including live-agent duplicate refusal and no-agent husk replacement diff --git a/README.md b/README.md index d6d31cd004e..23058ee3cf0 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Setup guides for tmux (the default) and every other supported backend (herdr, ze You chat with the first mate. It routes each request to a crewmate in its own session endpoint and git worktree, supervises the fleet with a zero-token event-driven watcher, and brings you finished PRs, approved local merges, or investigation reports. Optional secondmates extend this to persistent domain supervisors, dispatch profiles let you steer which harness handles which task, and an opt-in X mode lets the same fleet answer public mentions. +`codex-app` is not a runtime backend yet; [docs/codex-app-backend.md](docs/codex-app-backend.md) owns the Codex App boundary. Full architecture - the supervision engine, worktree isolation, secondmates, dispatch profiles, project modes, optional X mode, fleet sync, and self-update - is in [docs/architecture.md](docs/architecture.md). @@ -144,6 +145,7 @@ Firstmate's skills live in two separate places with different audiences: - [docs/zellij-backend.md](docs/zellij-backend.md) - setup guide for the experimental zellij backend, plus its verification notes and known gaps. - [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/turnend-guard.md](docs/turnend-guard.md) - the primary session's structural "no turn ends blind" backstop: verified Claude Code Stop-hook mechanism, scoping, and known gaps. - [docs/scripts.md](docs/scripts.md) - the `bin/` toolbelt reference. - [`AGENTS.md`](AGENTS.md) - firstmate's full operating manual for the orchestrator agent. diff --git a/bin/fm-backend.sh b/bin/fm-backend.sh index ff0a934349b..6aa0d439da4 100644 --- a/bin/fm-backend.sh +++ b/bin/fm-backend.sh @@ -26,6 +26,8 @@ # marker) with no explicit backend setting - unlike Orca, which stays # never-auto-detected because it also owns the task worktree; see # docs/cmux-backend.md for its empirical basis. +# Codex App is intentionally not in the known set yet. +# docs/codex-app-backend.md owns that blocked backend contract. # # Compatibility contract: a task's meta may omit `backend=`; every reader here # treats that as `tmux` (fm_backend_of_meta), and fm-spawn.sh does not write @@ -63,6 +65,7 @@ FM_BACKEND_CONFIG_DIR="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}" # spawn-capable; unlike tmux/herdr/zellij it is also the worktree provider. # cmux is EXPERIMENTAL and spawn-capable, session-provider-only like # herdr/zellij - verified against the real 0.64.17 binary (docs/cmux-backend.md). +# codex-app remains deliberately absent; see docs/codex-app-backend.md. FM_BACKEND_KNOWN="tmux herdr zellij orca cmux" FM_BACKEND_SPAWN="tmux herdr zellij orca cmux" diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index ea59454ab5b..0a6c903f065 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -19,9 +19,10 @@ # herdr, zellij, orca, and cmux. Orca owns both the task worktree and # terminal, so ship/scout Orca spawns do not run treehouse get; cmux is a # session provider only, exactly like herdr/zellij, so it does. An -# auto-detected herdr or cmux spawns print a loud stderr notice; -# auto-detected tmux stays silent; zellij and orca are never auto-detected -# (always explicit). Default tmux spawns do not write backend= to meta; +# auto-detected herdr or cmux spawn prints a loud stderr notice; +# auto-detected tmux stays silent; zellij and orca are never auto-detected. +# codex-app is not a known backend yet; docs/codex-app-backend.md owns that +# blocked backend contract. Default tmux spawns do not write backend= to meta; # absent backend= means tmux. cmux does not support --secondmate spawns yet. # A backend spawn refusal (missing dependency, version gate, unauthenticated # socket, or unsupported secondmate mode) is terminal for that selected backend; diff --git a/docs/architecture.md b/docs/architecture.md index be8b6f5274b..b069bb4378b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -62,6 +62,7 @@ Zellij's container shape is simpler than herdr's: one shared `firstmate` session Orca is experimental and selected only explicitly: Orca owns both worktree and terminal lifecycle, records `orca_worktree_id=` and `terminal=`, and removes worktrees through `orca worktree rm` only after the usual firstmate teardown checks pass. Its current behavior and limitations are recorded in `docs/orca-backend.md`. cmux is experimental, GUI-first, macOS-only, and can be selected explicitly or by runtime auto-detection from its primary `CMUX_WORKSPACE_ID` marker plus documented fallback signals: treehouse remains its worktree provider (cmux is a session provider only, like herdr/zellij), and its full verification - the socket access setup requirement with Automation mode recommended, the read-screen-fails-on-a-fresh-surface finding, the close-surface-refuses-on-the-last-surface finding, the source-verified runtime marker and fallback behavior, and known gaps - is recorded in `docs/cmux-backend.md`. cmux's container shape is one workspace per task with one surface, no per-home container split; workspace titles are scoped by the active home label plus a short hash of the resolved `FM_ROOT` path, and `--secondmate` spawns are refused, mirroring Orca. +Codex App support is recorded in `docs/codex-app-backend.md`; it is not selectable as a runtime backend. ## Worktrees, not branches in your checkout diff --git a/docs/codex-app-backend.md b/docs/codex-app-backend.md new file mode 100644 index 00000000000..6a2bbd8eb22 --- /dev/null +++ b/docs/codex-app-backend.md @@ -0,0 +1,211 @@ +# Codex App backend contract + +Status: blocked for Firstmate as a selectable shell backend. +The Codex Desktop host-tool loop works, including status-file writes, but Firstmate does not yet have a supported shell-callable bridge to those host tools. + +This document replaces the earlier passive visible-thread ledger shape. +A manual ledger is not a backend. + +## Backend acceptance contract + +A Codex App backend must satisfy the same lifecycle contract as the terminal-backed adapters: + +1. Firstmate creates the task endpoint and receives a durable thread id. +2. Firstmate sends the initial prompt and later operator messages to that endpoint. +3. Firstmate observes enough live thread state or transcript to supervise the task. +4. Firstmate can archive, kill, or otherwise stop supervising the endpoint. +5. The Codex thread can report back through Firstmate's normal `state/.status` lifecycle. + +The final point is mandatory. +If a Desktop-owned thread cannot write Firstmate status files, the backend cannot be treated as complete. + +## Verified Desktop host-tool smoke + +Latest verified host-tool smoke date: 2026-07-06. +Environment: Codex Desktop host tools, local host, saved project `/projects/sift`, Desktop-owned worktree ``, Firstmate home ``. +Local absolute path prefixes are redacted as `` and ``; file names, host-tool ids, thread ids, status lines, and report values are otherwise exact. + +Codex Desktop/OpenAI local bundle metadata from the smoke machine: + +```text +$ /usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' /Applications/Codex.app/Contents/Info.plist +26.623.101652 + +$ /usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' /Applications/Codex.app/Contents/Info.plist +4674 + +$ /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' /Applications/Codex.app/Contents/Info.plist +com.openai.codex + +$ stat -f '%Sm %N' -t '%Y-%m-%d %H:%M:%S %z' /Applications/Codex.app/Contents/Info.plist +2026-07-02 21:55:53 -0400 /Applications/Codex.app/Contents/Info.plist +``` + +Smoke target files: + +```text +/state/codex-app-host-smoke-20260706-live.status +/data/codex-app-host-smoke-20260706-live/report.md +``` + +Host-tool operation sequence: + +1. `list_projects` confirmed the saved project target. +2. `create_thread` requested a new Codex Desktop project worktree thread. +3. `list_threads` recovered the created thread id after queued worktree setup. +4. `read_thread` observed the active and completed initial turn. +5. Shell reads verified the status/report files under the Firstmate home. +6. `send_message_to_thread` delivered a follow-up to the same thread. +7. `read_thread` observed the completed follow-up turn. +8. `set_thread_archived` archived the thread. +9. A final `read_thread` still returned the transcript and showed `status.type=notLoaded`. + +Exact host-tool requests and relevant output: + +```text +list_projects: + projectId=/projects/sift + projectKind=local + label=sift + path=/projects/sift + +create_thread request: + target.type=project + target.projectId=/projects/sift + target.environment.type=worktree + prompt smoke_id=codex-app-host-smoke-20260706-live + prompt status_file=/state/codex-app-host-smoke-20260706-live.status + prompt report_file=/data/codex-app-host-smoke-20260706-live/report.md + prompt required status line: working: Codex Desktop thread started + prompt required sentinel: FM_CODEX_APP_HOST_TOOL_SMOKE_20260706_LIVE_OK + +create_thread response: + pendingWorktreeId=local:a4a96438-a0ed-4305-b83c-5a47336f5abf + +list_threads query=codex-app-host-smoke-20260706-live: + id=019f39ea-5cca-7031-bfb0-f8054a2b253a + hostId=local + status=active + cwd= + +read_thread initial turn while active: + thread.id=019f39ea-5cca-7031-bfb0-f8054a2b253a + thread.status.type=active + cwd= + agentMessage: Running the smoke exactly as delegated: repo identity first, then the Firstmate status/report writes, then the requested `sed` checks. + +read_thread initial turn after completion: + thread.status.type=idle + turn.status=completed + durationMs=54923 + +$ pwd + + +$ git rev-parse --show-toplevel + + +$ git branch --show-current + +$ sed -n '1,20p' /state/codex-app-host-smoke-20260706-live.status +working: Codex Desktop thread started + +$ sed -n '1,40p' /data/codex-app-host-smoke-20260706-live/report.md +smoke_id=codex-app-host-smoke-20260706-live +cwd= +git_root= +branch= +status_file=/state/codex-app-host-smoke-20260706-live.status +status_file_write=ok +sentinel=FM_CODEX_APP_HOST_TOOL_SMOKE_20260706_LIVE_OK + +send_message_to_thread request: + threadId=019f39ea-5cca-7031-bfb0-f8054a2b253a + prompt required status line: done: follow-up delivered through send_message_to_thread + +send_message_to_thread response: + threadId=019f39ea-5cca-7031-bfb0-f8054a2b253a + +read_thread follow-up turn: + turn.status=completed + durationMs=7118 + +$ sed -n '1,20p' /state/codex-app-host-smoke-20260706-live.status +working: Codex Desktop thread started +done: follow-up delivered through send_message_to_thread + +set_thread_archived request: + threadId=019f39ea-5cca-7031-bfb0-f8054a2b253a + archived=true + +set_thread_archived response: + threadId=019f39ea-5cca-7031-bfb0-f8054a2b253a + archived=true + +read_thread after archive: + thread.id=019f39ea-5cca-7031-bfb0-f8054a2b253a + thread.status.type=notLoaded + thread.cwd= + transcript still included the initial and follow-up completed turns. +``` + +Result: a Desktop-owned Codex thread can write Firstmate status files when the prompt gives it the absolute status path and the Desktop permission context can write that checkout. +The return channel is real at the Codex Desktop host-tool layer. + +## Codex Desktop API blocker + +Firstmate's backend scripts are Bash entry points. +They can call `tmux`, `herdr`, `zellij`, primitive Orca CLI surfaces, and `cmux` directly. +The Codex Desktop host tools verified above are available to the Codex Desktop conversation, not to arbitrary Firstmate subprocesses. +The missing piece is therefore a supported Codex Desktop transport that a Bash backend can call, not another Firstmate-local ledger. + +The available Codex CLI and app-server probes found useful pieces but not a supported visible-thread backend transport: + +- `codex app-server --stdio` exposes JSON-RPC methods such as `thread/start`, `turn/start`, `thread/read`, and `thread/archive`. +- A one-shot stdio probe could create a thread record, and `thread/archive` worked through that same stdio process. +- The managed daemon path was unavailable in this Desktop install. +- A raw proxy attempt against the Desktop control socket did not accept plain JSON-RPC framing. + +That is not enough to add `codex-app` to `FM_BACKEND_KNOWN` or `FM_BACKEND_SPAWN`. +A Firstmate backend must be able to create a thread, start or continue turns, read live state while turns run, and archive/stop the same endpoint through a Codex Desktop-supported shell-callable API. +Shipping a local ledger would only record intentions; it would not supervise the actual Desktop thread. + +## Required Codex Desktop bridge + +Firstmate should implement a Codex App adapter only after Codex Desktop exposes one of these supported interfaces: + +- A supported CLI wrapper around the Desktop host tools: create thread, send message, read transcript/state, archive thread. +- A documented JSON-RPC or MCP transport that Firstmate can call from Bash with stable request/response framing. +- A small maintained helper binary/script that speaks the supported transport and returns plain JSON to `bin/backends/codex-app.sh`. + +Minimum command semantics: + +```text +create: + input: task id, cwd/worktree request, initial prompt + output: thread id, Desktop-owned cwd if different, initial status + +send: + input: thread id, text + output: accepted/rejected delivery result + +capture/read: + input: thread id, bounded transcript or status cursor + output: enough text/state for fm-peek.sh, fm-watch.sh, and fm-crew-state.sh + +archive/kill: + input: thread id + output: archived/stopped result + +status return channel: + the thread must be able to append Firstmate status lines to state/.status +``` + +Once that bridge exists, the implementation should add a real `bin/backends/codex-app.sh`, persist `backend=codex-app` and `codex_app_thread_id=` in `state/.meta`, and wire spawn/send/peek/watch/teardown through the same dispatcher paths used by the existing adapters. + +## Rollout clause + +After a supported shell-callable Codex Desktop/OpenAI bridge exists, Firstmate should implement Codex App for ship and scout tasks first. +Secondmate support remains out of scope until ship/scout supervision, status return, send/read, and archive/teardown are proven through the normal backend dispatcher. + +Until then, Codex App support remains a verified host-tool smoke plus this blocked backend contract, not a selectable backend. diff --git a/docs/configuration.md b/docs/configuration.md index 44552b7bfd7..6485e5a12c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -28,6 +28,7 @@ Auto-detected herdr or cmux prints a stderr notice naming `config/backend` and ` Zellij and Orca are never auto-detected; select them by putting the name in a local `config/backend` file, by exporting `FM_BACKEND=`, or by telling the first mate in chat. Any value other than `tmux`, `herdr`, `zellij`, `orca`, or `cmux` is rejected until another adapter is implemented and verified. `fm-spawn.sh` accepts `tmux`, `herdr`, `zellij`, `orca`, and `cmux` for ship and scout tasks; `backend=orca` and `backend=cmux` both still refuse `--secondmate` until secondmate launch semantics are designed for each. +`codex-app` is not an accepted runtime backend yet; [`docs/codex-app-backend.md`](codex-app-backend.md) owns the Codex App boundary. A herdr spawn additionally version-gates against the installed `herdr` binary's protocol and requires `jq`, refusing loudly on an incompatible or missing installation. A zellij spawn additionally version-gates against the installed `zellij` binary's version and requires `jq`, refusing loudly when either is missing or the version is older than 0.44. A cmux spawn additionally version-gates against the installed `cmux` binary's version, requires `jq`, and requires the control socket to be reachable and accessible (see [`docs/cmux-backend.md`](cmux-backend.md) "Setup" for the one-time socket-access configuration this needs; Automation mode is the recommended socket control mode, with Password mode supported via `config/cmux-socket-password`), refusing loudly and non-retryably on a `cmuxOnly`/unauthenticated socket. @@ -220,7 +221,7 @@ FM_STATE_OVERRIDE= # alternate state dir, mainly for tests FM_DATA_OVERRIDE= # alternate data dir, mainly for tests FM_PROJECTS_OVERRIDE= # alternate projects dir, mainly for tests FM_CONFIG_OVERRIDE= # alternate config dir, mainly for tests -FM_BACKEND= # optional runtime backend override for new spawns; tmux/herdr/zellij/orca/cmux support ship/scout spawns +FM_BACKEND= # optional runtime backend override for new spawns; tmux/herdr/zellij/orca/cmux support ship/scout spawns, codex-app is not accepted HERDR_SESSION=default # herdr-only: named session for normal backend ops; not enough for destructive cleanup (docs/herdr-backend.md) FM_BACKEND_HERDR_COMPOSER_LINES=20 # herdr-only: tail lines scanned to locate the composer row for submit verification FM_BACKEND_HERDR_IDLE_RE='^Type a message\.\.\.$' # herdr-only: empty-composer placeholder regex after border/prompt stripping diff --git a/docs/scripts.md b/docs/scripts.md index 40e9ffbad3e..3528e83d452 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -16,8 +16,8 @@ If you have changed away from the firstmate home in an interactive shell, invoke | `fm-guard.sh` | Warn when the primary checkout is tangled, when queued wakes are pending, or when a stale or missing watcher needs a prominent banner; `FM_GUARD_READ_ONLY=1` keeps the alarms but suppresses drain, arm, and checkout repair commands | | `fm-turnend-guard.sh` | Claude Code Stop hook, primary-scoped only: blocks (exit 2, exact reason) a primary turn end when work is in flight without a live identity-matched watcher lock and fresh beacon, using Claude Code's own `stop_hook_active` field so it never blocks twice in one turn (docs/turnend-guard.md) | | `fm-home-seed.sh` | Lease/provision a secondmate home transactionally, clone projects, initialize gates, and maintain `data/secondmates.md` | -| `fm-spawn.sh` | Spawn one task, several `id=repo` pairs, or a persistent secondmate with `--secondmate`; accepts concrete `--harness`, `--model`, `--effort`, and `--backend` axes; ship/scout spawns require an explicit resolved harness when dispatch profiles are active and an isolated worktree, install per-harness turn-end signaling, and secondmate spawns resolve the secondmate harness plus optional `config/secondmate-harness` model/effort tokens, locally sync the home, propagate declared inheritable config, land herdr tabs in the target home's workspace, land home-scoped zellij tabs in the selected shared zellij session, land cmux workspaces in the shared cmux app, or create Orca worktrees/terminals before launch | -| `fm-backend.sh` | Runtime session-provider backend selector with explicit/env/config/runtime auto-detection precedence, meta helper, selector resolver, spawn-capability validation, operation dispatcher, and shell-portable backend-name membership for bash-sourced scripts or zsh-sourced diagnostics; defaults absent `backend=` meta to `tmux`; `fm_backend_target_exists` is a cheap read-only alive/dead endpoint check that never starts a server or session; `fm_backend_composer_state` exposes backend composer checks for guarded submit paths | +| `fm-spawn.sh` | Spawn one task, several `id=repo` pairs, or a persistent secondmate with `--secondmate`; accepts concrete `--harness`, `--model`, `--effort`, and `--backend` axes; rejects `backend=codex-app`; ship/scout spawns require an explicit resolved harness when dispatch profiles are active and an isolated worktree, install per-harness turn-end signaling, and secondmate spawns resolve the secondmate harness plus optional `config/secondmate-harness` model/effort tokens, locally sync the home, propagate declared inheritable config, land herdr tabs in the target home's workspace, land home-scoped zellij tabs in the selected shared zellij session, land cmux workspaces in the shared cmux app, or create Orca worktrees/terminals before launch | +| `fm-backend.sh` | Runtime session-provider backend selector with explicit/env/config/runtime auto-detection precedence, meta helper, selector resolver, spawn-capability validation, operation dispatcher, and shell-portable backend-name membership for bash-sourced scripts or zsh-sourced diagnostics; deliberately keeps `codex-app` out of known/spawn-capable backends; defaults absent `backend=` meta to `tmux`; `fm_backend_target_exists` is a cheap read-only alive/dead endpoint check that never starts a server or session; `fm_backend_composer_state` exposes backend composer checks for guarded submit paths | | `fm-backend-hometag-lib.sh` | Shared home-tag derivation for zellij tab titles and cmux workspace titles, using the active `FM_HOME` label plus a short hash of the resolved `FM_ROOT` path | | `backends/tmux.sh` | Verified tmux session-provider adapter used by `fm-backend.sh`; owns create, send, capture, current-path, live-window, and kill primitives | | `backends/herdr.sh` | Experimental herdr session-provider adapter used by `fm-backend.sh`; owns version/tool gating, per-home workspace/tab creation, created-vs-adopted default-tab prune safety, restored-layout husk respawn replacement, session-scoped CLI calls, send with structural composer-state verification, capture, native busy-state, current-path, label-based live discovery, and kill primitives | diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index ef1f620034b..9ea34fb55f2 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -13,8 +13,8 @@ # diffs the two command logs byte-for-byte - the report's P1 checklist # item "run current main scripts and refactored scripts against the same # fake tools and compare command logs". -# 3. Asserts the new `--backend`/`FM_BACKEND` selection refuses an unknown -# backend loudly (tmux is the only verified adapter in P1). +# 3. Asserts the `--backend`/`FM_BACKEND` selection refuses unknown backends +# and the blocked `codex-app` backend loudly. # # fm-watch.sh's signal/stale/check/heartbeat wake-string contract is already # exercised end-to-end against this refactor by tests/fm-watch-triage.test.sh @@ -445,13 +445,15 @@ test_backend_validate_refuses_unknown() { fm_backend_validate tmux 2>/dev/null || fail "fm_backend_validate should accept tmux" fm_backend_validate orca 2>/dev/null || fail "fm_backend_validate should accept orca" local out - # bogus names a backend with no adapter at all; tmux, herdr, zellij, and - # orca are all known adapters, and all four are spawn-supported. + # bogus names a backend with no adapter at all; tmux, herdr, zellij, orca, + # and cmux are all known adapters and spawn-supported. out=$(fm_backend_validate bogus 2>&1) && fail "fm_backend_validate should refuse bogus (no such adapter)" assert_contains "$out" "unknown backend 'bogus'" "fm_backend_validate did not name the rejected backend" + out=$(fm_backend_validate codex-app 2>&1) && fail "fm_backend_validate should refuse codex-app" + assert_contains "$out" "unknown backend 'codex-app'" "fm_backend_validate accepted codex-app" out=$(fm_backend_validate "tmux herdr" 2>&1) && fail "fm_backend_validate should refuse a multi-token backend name" assert_contains "$out" "unknown backend 'tmux herdr'" "fm_backend_validate accepted a multi-token backend name" - pass "fm_backend_validate: implemented adapters accepted, an unknown backend refused loudly" + pass "fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly" } test_backend_source_shell_portable() { @@ -485,8 +487,11 @@ test_backend_validate_spawn_accepts_orca() { fm_backend_validate_spawn herdr 2>/dev/null || fail "fm_backend_validate_spawn should accept herdr" fm_backend_validate_spawn zellij 2>/dev/null || fail "fm_backend_validate_spawn should accept zellij" fm_backend_validate_spawn orca 2>/dev/null || fail "fm_backend_validate_spawn should accept orca" + fm_backend_validate_spawn cmux 2>/dev/null || fail "fm_backend_validate_spawn should accept cmux" out=$(fm_backend_validate_spawn bogus 2>&1) && fail "fm_backend_validate_spawn should still refuse unknown backends" assert_contains "$out" "unknown backend 'bogus'" "fm_backend_validate_spawn did not preserve unknown-backend validation" + out=$(fm_backend_validate_spawn codex-app 2>&1) && fail "fm_backend_validate_spawn should refuse codex-app" + assert_contains "$out" "unknown backend 'codex-app'" "fm_backend_validate_spawn accepted codex-app" out=$(fm_backend_validate_spawn "tmux herdr" 2>&1) && fail "fm_backend_validate_spawn should refuse a multi-token backend name" assert_contains "$out" "unknown backend 'tmux herdr'" "fm_backend_validate_spawn accepted a multi-token backend name" pass "fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported" @@ -942,6 +947,17 @@ test_spawn_refuses_unknown_backend_flag() { pass "fm-spawn.sh --backend bogus is refused loudly" } +test_spawn_refuses_codex_app_backend_flag() { + local out status + out=$(FM_ROOT_OVERRIDE='' FM_HOME='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' \ + FM_PROJECTS_OVERRIDE='' FM_CONFIG_OVERRIDE='' FM_SPAWN_NO_GUARD=1 \ + "$ROOT/bin/fm-spawn.sh" nope-codex-app-z1 projects/none claude --backend codex-app 2>&1) + status=$? + [ "$status" -ne 0 ] || fail "fm-spawn --backend codex-app should refuse" + assert_contains "$out" "unknown backend 'codex-app'" "fm-spawn did not preserve the blocked codex-app contract" + pass "fm-spawn.sh --backend codex-app is refused" +} + test_spawn_refuses_unknown_fm_backend_env() { local out status out=$(FM_ROOT_OVERRIDE='' FM_HOME='' FM_STATE_OVERRIDE='' FM_DATA_OVERRIDE='' \ @@ -1053,6 +1069,7 @@ test_spawn_conformance_old_vs_new test_spawn_symlinked_project_prefix_avoids_false_refusal test_teardown_conformance_old_vs_new test_spawn_refuses_unknown_backend_flag +test_spawn_refuses_codex_app_backend_flag test_spawn_refuses_unknown_fm_backend_env test_spawn_default_backend_writes_no_meta_field test_spawn_explicit_backend_flag_beats_autodetect_herdr_env diff --git a/tests/fm-watcher-lock.test.sh b/tests/fm-watcher-lock.test.sh index 1e9c6420a42..ec84c5d7e86 100755 --- a/tests/fm-watcher-lock.test.sh +++ b/tests/fm-watcher-lock.test.sh @@ -415,7 +415,7 @@ test_watcher_self_evicts_on_lock_takeover() { state="$dir/state" fakebin="$dir/fakebin" out="$dir/watch.out" - PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" & + PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_POLL=0.2 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out" & pid=$! i=0 while [ "$i" -lt 50 ]; do