Skip to content

docs: document Codex App backend contract#222

Open
SSBrouhard wants to merge 7 commits into
kunchenguid:mainfrom
SSBrouhard:fm/codex-app-backend-contract
Open

docs: document Codex App backend contract#222
SSBrouhard wants to merge 7 commits into
kunchenguid:mainfrom
SSBrouhard:fm/codex-app-backend-contract

Conversation

@SSBrouhard

@SSBrouhard SSBrouhard commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Intent

Revalidate PR #222 after force-with-lease updating the GitHub PR branch to the no-mistakes-validated rebased head. The goal is to confirm the existing Codex App backend contract documentation PR is mergeable on current upstream Firstmate main, preserves the no-fake-backend decision, keeps Codex App out of shell backend resolution until Codex Desktop/OpenAI provides a supported shell-callable API or CLI bridge, preserves the verified status-file smoke result, includes the no-mistakes test/doc fixes already produced by the gate, and lets no-mistakes maintain the PR body/marker without manual PR-body sanitization in this session.

What Changed

  • Documents the Codex App backend acceptance contract, verified Desktop host-tool smoke result, and the missing supported shell-callable bridge.
  • Clarifies backend/spawn guidance so codex-app stays out of selectable backend resolution until the bridge exists.
  • Adds regression coverage that codex-app remains refused as an unknown backend and tightens watcher self-eviction test cadence.

Risk Assessment

✅ Low: Captain, the change is tightly scoped to documenting the blocked Codex App backend contract, preserving the backend refusal path, and making one narrow watcher-test timing adjustment.

Testing

Baseline was already reported green; I reran targeted backend/watcher tests, captured contract-level evidence that docs and CLI behavior preserve the no-fake-backend codex-app decision, reran the full configured test loop successfully, and found no transient worktree artifacts to clean up.

Evidence: Targeted backend and watcher test output
== tests/fm-backend.test.sh ==
ok - fm_backend_name: FM_BACKEND env > config/backend > default tmux
ok - fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, $TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first
ok - fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)
ok - fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)
ok - fm_backend_detect: an inherited cmux bundle id never outranks $TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)
ok - fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain
ok - fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid
ok - fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)
ok - fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged
ok - fm_backend_name: auto-detect selects herdr or cmux (loud notice) or tmux (silent, including nested tmux-in-herdr/tmux-in-cmux)
ok - fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker
ok - fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly
ok - zsh: fm_backend_source recognizes known backends and rejects unknown ones
ok - bash: fm_backend_source recognizes known backends and rejects unknown ones
ok - fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read key=value, default backend to tmux
ok - fm_backend_resolve_selector: session:window literal, fm-<id> via meta (always, even when the meta is missing), ad hoc bare name via tmux list-windows
ok - fm_backend_of_selector: fm-<id> and matching explicit targets inherit metadata backend
ok - fm-send.sh: --key, plain text, and /skill tmux command logs are byte-identical old vs new (send-keys -l, Enter submission preserved)
ok - fm-peek.sh: capture-pane invocation and output are byte-identical old vs new
ok - fm-spawn.sh: tmux command log and printed summary line are byte-identical old vs new for a ship-task claude spawn
ok - fm-teardown.sh: treehouse return + tmux kill-window command log is byte-identical old vs new for a scout task
ok - fm-spawn.sh --backend bogus is refused loudly
ok - fm-spawn.sh --backend codex-app is refused until a supported shell-callable bridge exists
ok - fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly
ok - fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)
ok - fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker
ok - fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end
== tests/fm-watcher-lock.test.sh ==
ok - simultaneous watcher starts leave exactly one live process
ok - killed watcher stale lock is reclaimed
ok - live watcher lock with stale heartbeat is actionable
ok - guard banner leads when down with pending wakes (re-arm-after-drain) and stays silent when fresh
ok - concurrent fm_lock_try_acquire yields exactly one winner
ok - dead-pid stale lock is reclaimed by a single acquirer
ok - concurrent stale-lock steal yields exactly one winner
ok - live steal mutex is not reclaimed
ok - live-held lock is not stolen
ok - empty mid-acquire lock keeps a minimum grace
ok - late original claimant cannot claim a recreated lock
ok - paused mid-acquire claimant backs off to active stealer
ok - watch restart refuses to signal a reused pid
ok - watcher self-evicts when the lock pid no longer names it
ok - arm reports a live fresh watcher as healthy and exits zero
ok - arm starts+confirms a fresh watcher on a clean lock and self-heals a dead-pid lock (never healthy off a dead pid)
ok - arm cleans child watcher and temp output on HUP
ok - arm propagates an immediate watcher wake before confirmation
ok - arm waits for a peer watcher beacon after child stands down
watcher: lock held by live pid 97919 but heartbeat is stale for 836579393s (>300s); inspect or stop that watcher before re-arming.
ok - arm reports FAILED and exits non-zero when no fresh watcher can be confirmed
Evidence: Codex App contract docs plus CLI refusal
## Codex App contract documentation excerpts
# Codex App backend contract

Status: blocked for Firstmate as a selectable shell backend because Codex Desktop does not currently expose a supported shell-callable API or CLI bridge to the verified host tools.
The Codex Desktop host-tool loop works, including status-file writes.

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/<id>.status` lifecycle.

The final point is mandatory. If a Desktop-owned thread cannot write Firstmate status files, the backend cannot be treated as complete.


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:

`` `

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/<id>.meta`, and wire spawn/send/peek/watch/teardown through the same dispatcher paths used by the existing adapters.

Until then, Codex App support remains a verified host-tool smoke plus this blocked backend contract, not a selectable backend.

## Backend resolver CLI behavior
$ bash -c '. bin/fm-backend.sh; fm_backend_validate codex-app'
error: unknown backend 'codex-app' (known: tmux herdr zellij orca cmux)

$ FM_SPAWN_NO_GUARD=1 bin/fm-spawn.sh nope-codex-app-z1 projects/none claude --backend codex-app
error: unknown backend 'codex-app' (known: tmux herdr zellij orca cmux)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • bash tests/fm-backend.test.sh
  • bash tests/fm-watcher-lock.test.sh
  • Captured docs/codex-app-backend.md excerpts plus fm_backend_validate codex-app and fm-spawn.sh --backend codex-app refusal output into /var/folders/km/p2bzh7ss6lz_376w0_8b89z00000gn/T/no-mistakes-evidence/01KWSXWYE4M4396T8JV0J9S8X9/codex-app-contract-transcript.txt.
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • git status --short
  • find . -maxdepth 3 \( -name '.pytest_cache' -o -name 'node_modules' -o -name 'dist' -o -name 'build' -o -name '*.tmp' \) -print
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@SSBrouhard SSBrouhard force-pushed the fm/codex-app-backend-contract branch from b9c6c30 to c8d8ecd Compare July 3, 2026 20:32
@SSBrouhard SSBrouhard changed the title docs: document codex app backend contract docs: clarify Codex App backend contract Jul 3, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Thanks for the PR! It looks like this branch has a merge conflict with the base branch right now. When you get a chance, could you rebase onto (or merge in) the latest base branch, resolve the conflict, and push? Once GitHub shows the PR as mergeable again, it'll be picked back up for review.

Noted for firstmate#222 at 6af8f8f7.

@SSBrouhard SSBrouhard force-pushed the fm/codex-app-backend-contract branch from 6af8f8f to 963da95 Compare July 5, 2026 20:00
@SSBrouhard SSBrouhard changed the title docs: clarify Codex App backend contract docs: document Codex App backend contract Jul 5, 2026
@SSBrouhard

Copy link
Copy Markdown
Contributor Author

Rebased onto current main; happy to refresh again when this is ready for review, but I’ll avoid churn rebasing on every upstream commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants