git:20261013.3aa645d to git:20261016.f9025ff

1 added, 1 removed. Audit A to A.

---
name: codex-app-link
description: Create Codex threads and send messages into Codex desktop-app (GUI) threads, then wait for and read their replies, so Claude can orchestrate Codex agents. Use when the user asks to message, direct, steer, start, check on, or hand work to a Codex thread or agent (often given as a thread ID like 01a10323-9edd-...), or to run Codex as a sub-agent.
---
# Codex App Link
`scripts/codex-thread.mjs` drives Codex threads from the terminal. It sends messages through the Codex desktop app, works whether or not the thread is in the foreground, and reads replies from Codex's own session files. By default, output is compact text, to save tokens in an agent's context:
- one line of `key=value` scalars;
- then the agent's reply (`lastAgentMessage`) as raw, unescaped text.
Add `--json` only when you need every field. Do not pipe JSON through `jq` into your context. Errors are one-line JSON on stderr, with exit code 1.
Set `CT` to the script inside this skill's base directory, **fresh in every session**:
```bash
CT=<skill-base-dir>/scripts/codex-thread.mjs
node $CT version # confirms the version and path you are actually running
```
Never reuse a `.../codex-app-link/<version>/...` path remembered from an earlier session or a summary. Plugin updates install side by side, so an old path keeps running the old version and misses its fixes and scripts.
Requirements: macOS, the Codex desktop app (ChatGPT.app) running, and Node.js 22.5 or later (the script reads Codex's thread index through `node:sqlite`).
Multiple Codex instances: set `CODEX_HOME` to the instance's home (for example `CODEX_HOME=~/.codex-work`) on every command. The script then uses that instance's socket, thread index and session files. `open codex://...` always reaches the default instance (`~/.codex`), so for any other instance the script loads a thread by sending the `codex://threads/<id>` link as a GetURL Apple Event to the process that owns that instance's socket. The first use may trigger a macOS Automation prompt. If no process owns the socket, it fails with `not-loaded-in-instance`.
| Command | Purpose |
|---|---|
| `node $CT list [--limit N] [--cwd DIR]` | Recent threads (id, title, cwd, updated) |
| `node $CT status <id>` | `status` (idle/active), current `turnId`, `lastAgentMessage`, `loadedInApp` |
| `node $CT new --cwd DIR --text "..." [--timeout SEC] [-- <codex exec args>]` | Create a thread with the CLI, run its first turn, then load it in the app. Returns `threadId` and `finalMessage` |
| `node $CT send <id> --text "..." [--wait] [--timeout SEC]` | Start a turn in an idle thread through the app. `--wait` blocks until it finishes and returns `lastAgentMessage` |
| `node $CT send <id> --text "..." --steer` | Add a message to the turn that is running now |
| `node $CT wait <id> [--timeout SEC] [--settle SEC] [--stall SEC] [--until-question] [--expect REGEX]` | Block until the **thread** is done (see "Waiting for results"). Prints `result`: `done`, `stalled`, `timeout`, `question` or `paused` (exit 0, 3, 4, 5 or 6), plus `lastAgentMessage` |
| `node $CT read <id> [--last N]` | Final replies of the last N turns |
| `node $CT result <id> [--marker RESULT] [--turns N]` | The latest `RESULT` block in recent replies (in or out of a code fence), parsed into `key=value` lines. Exit 6 if there is none |
- | `node $CT watch (<id>... \| --tracked) [--expect REGEX] [--cursor FILE]` | One event line per `STARTED`, `QUESTION`, `APPROVAL`, `COMPACTED`, `MAINTENANCE` (if configured), `RESULT`, `PAUSED`, `DONE` or `STALLED`, across several threads (see "Managing several threads") |
+ | `node $CT watch (<id>... \| --tracked) [--expect REGEX] [--cursor FILE]` | One event line per `STARTED`, `QUESTION`, `APPROVAL`, `COMPACTED`, `MAINTENANCE` (if configured), `RESULT`, `PAUSED`, `DONE`, `STALLED`, `MISSING` or `FOUND`, across several threads (see "Managing several threads") |
| `node $CT track add <id> --name N [--role R]` / `track rm N` / `track list` | The project's thread registry at `<git root>/.codex-app-link.json`. Every command then accepts the name in place of the ID |
| `node $CT version` | The plugin version, script path and registry path |
| `node $CT questions <id> [--all] [--since ISO]` | Formal questions the thread asked (`request_user_input*`), unanswered by default, with options and the answer. Exit 5 if any are pending |
| `node $CT open <id>` | Make the app load a thread without sending anything |
Long messages: `--file path` or `--stdin` instead of `--text`.
## Waiting for results: the only supported method
Run exactly this as a background command, and act when it exits:
```bash
node $CT wait <id> --timeout 10800 # run_in_background; one command, no loops
```
- **It waits for the thread, not a turn.** It returns when the thread has been continuously idle for `--settle` seconds (default 45). Codex often chains turns: compaction, auto-continue, or context maintenance by other plugins. A turn can also end without a normal completion event. The settle window absorbs chained turns, and nothing is pinned to a turn ID.
- **It cannot hang silently:**
- `result: "stalled"` (exit 3): the thread claims to be active, but its session file has not been written for `--stall` seconds (default 2700, 45 min). **Verify before acting:** a silence of 15+ minutes is normal while a high-effort model reasons, or while a long command runs, because neither writes to the session file until it finishes. Check the last session-file write and running processes. If it is still writing or a process is running, restart `wait`. Treat the turn as dead only if it stays silent past the threshold with nothing running.
- `result: "timeout"` (exit 4): the overall `--timeout` (default 3h) was reached. The current state is printed.
- **On exit, read `result` and `lastAgentMessage` from its output, then judge the work yourself.** Do not require a particular reply format.
- **Questions don't end a wait unless you ask.** Codex asks formal questions with `request_user_input_async`. It is accepted at once and the turn keeps running, so a plain `wait` never returns for it, and the worker proceeds on its own default. Use `wait --until-question` (`result: "question"`, exit 5), or run `codex-progress.py`, which prints `QUESTION` lines. Then answer with `send <id> --steer`. Run `questions <id>` before closing out any handoff.
Never do any of these (each has caused missed completions):
- wait on a specific turn ID (`wait --turn X`, or `send --wait` followed by its turn);
- loop on a regex for an expected report string (e.g. `RESULT slice=`);
- write hand-rolled shell loops around `wait` or `status`.
`--turn` is still accepted, but it only lets an earlier finish count; it never blocks on that turn.
## Watching progress live (process work)
`wait` reports only that a thread finished. For **process work**, meaning live QA tests, computer-use or UI driving, deploys and ops steps, also stream the thread's progress, so that a blocked approval prompt, a missing plugin runtime or a stalled step surfaces within minutes:
```bash
python3 <skill-base-dir>/scripts/codex-progress.py <id> 240 # run under a background monitor
```
It prints one line per new assistant note (`NOTE`), formal question (`QUESTION`, with its options), approval-request event (`APPROVAL_WAIT`) and turn end (`TURN_END`). After 240 seconds without a session write it prints a `SILENT` line; check it before acting, because a silent model may just be reasoning. Run it **alongside** `wait`, not instead of it. Ask the Codex worker to post a short progress note after every step. For **implementation work** (coding a scoped handoff, running tests, opening a PR), `wait` alone is enough. Stop the monitor when the turn ends.
## Managing several threads: track, watch, result
When you run more than one worker, or anything long, use the manager kit rather than separate waits:
1. **Name the threads once per project:** `node $CT track add <id> --name sol-b --role "worker: render"`. The
registry is a small JSON file at the git root, so every later manager of the project inherits the roster.
Commit it.
2. **Run one watch for all of them,** as a background monitor, and re-arm it when the monitor expires:
```bash
node $CT watch --tracked --expect '^RESULT ' --cursor .codex-app-link.cursor.json
```
`--cursor` saves offsets and turn state after every poll, so a re-armed watch resumes exactly where the last
one stopped and loses nothing in between. Keep the cursor file out of git.
3. **Act on each event:**
| Event | What to do |
|---|---|
| `QUESTION` | Answer now with `send <name> --steer`. The worker keeps going on its default otherwise |
| `PAUSED` | The thread went idle without its result: a compaction pause, a "waiting for controller resume" stop, or a crash. Read its last reply, then resume it with `send` |
| `RESULT` | Run `node $CT result <name>`, then verify the work yourself |
| `STALLED` | Check before acting: long reasoning and long commands write nothing |
| `COMPACTED`, `STARTED`, `MAINTENANCE` | Context. `MAINTENANCE` appears only if you configured a maintenance controller's patterns (below) |
| `APPROVAL` | It may need you in the app |
**Context-maintenance controllers (optional).** Some setups run a controller, such as Jevpact, that asks a
worker to pause before compaction and sends a resume afterwards. Its wording is configurable, so the kit matches
nothing by default. To see those steps as `MAINTENANCE` events, and to stop them counting as answers to
questions, add the patterns your controller actually sends to the registry:
```json
"automation": { "pause": "prepare to compact", "resume": "continue the existing unfinished assignment" }
```
(or pass `--pause-pattern` / `--resume-pattern`). Copy the patterns from a real message in your own rollouts,
not from these examples. Whether or not a controller is present, a worker that goes idle without its RESULT
shows up as `PAUSED`. A controller may skip its resume when it judges the work done, and can be wrong.
Ask every worker to end with a RESULT block (`RESULT key=value …`) and to treat only that block as the end of
its task. `--expect` is what tells a finished worker from one that stopped short.
## Managing a project through Codex threads
When the user onboards you as the manager of work that Codex threads carry out, read these once before acting:
- [references/strategic.md](references/strategic.md): roles, likely thread configurations (including the recommended coordinator-plus-work-threads setup), onboarding questions, and how to read the state of a project from its threads.
- [references/operational.md](references/operational.md): the dispatch, review, wait, verify and merge loop, authority rules, and lessons from real orchestration.
- [references/companions.md](references/companions.md): how to use MSW, MSL and Timebox when the project has them, and how to offer them when it does not.
For a single message to a thread, the commands above are enough.
## Orchestration pattern
1. `new` to create a worker thread, or take an existing thread ID from the user.
2. `send`, then start `node $CT wait <id>` as **one background command** and do other work until it exits (see "Waiting for results"). Use `send --wait` only for short foreground turns; it uses the same thread-level wait.
3. Give Codex file paths to read, not pasted content. Ask it to end its turn with a clear result line you can parse.
4. Report the thread ID to the user so they can open it in the app.
## Rules
- Do not message a thread you did not create unless the user named it and asked you to. The user may be working in it.
- `send` refuses (`thread-busy`) while a turn is running. Use `wait` first. Use `--steer` only when the new instruction must change the running turn.
- Never run a CLI process on a thread (`codex resume`, `codex exec resume`) while the app owns it. Only one Codex process may hold a thread.
- New threads inherit `~/.codex/config.toml` defaults (model, sandbox, approvals). Pass `codex exec` flags after `--` to change them, for example `-- --skip-git-repo-check` outside a git repo, or `-- -s workspace-write`.
- Clean up test threads by archiving them in the app. `codex archive <id>` returned "failed to archive session", with no further detail, for threads the app had loaded (2026-10-06; cause unconfirmed).
## How it works
- **Sending:** the app listens on a private socket, `~/.codex/ipc/ipc.sock`. Each message is JSON with a 4-byte little-endian length in front. The script sends `initialize`, then `thread-owner-discovery` to find the app client that has the thread loaded, then `thread-follower-start-turn` (idle) or `thread-follower-steer-turn` (busy) to that client. This was learned from the Jevpact Codex plugin (`plugins/jevpact/src/codex.js`).
- **Loading:** the app only answers for threads it has loaded. When a thread isn't loaded, `send` and `new` open `codex://threads/<id>` with `open -g`. That loads it in the background, but **can switch the thread shown in the app window**. `--no-open` disables this. This only happens for the default instance; see "Multiple Codex instances" above.
- **State:** the thread index is `~/.codex/state_5.sqlite` (table `threads`, read-only). Turn state and replies come from the thread's rollout JSONL: `task_started` means busy; `task_complete` (which carries `last_agent_message`) or `turn_aborted` means idle.
- **Creating:** `new` runs `codex exec --json` using the CLI bundled with the app (`/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex`, override with `CODEX_BIN`). The older `codex` on PATH (nvm) may reject the configured model.
- **Queued messages:** `codex queue --thread <id> --message ...` stores a message that runs only when a client loads the thread. Opening the thread delivers it at once, so check `status` after `open` before sending.
The socket protocol is private and undocumented. It was verified on 2026-10-06 against app CLI 0.160.0. If a Codex update breaks it, `request-version-mismatch` or `no-handler-for-request` errors appear. Re-check the method versions in the app bundle with `strings app.asar | grep thread-follower-`.
## Troubleshooting
| Error | Meaning / fix |
|---|---|
| `app-not-running` | Start the Codex desktop app (ChatGPT.app) |
| `not-loaded-in-app` | Deep link did not load the thread within 30s. Open the thread in the app manually and retry |
| `not-loaded-in-instance` | No running app owns the `CODEX_HOME` instance's socket. Start that instance (or open the thread in it) and retry |
| `thread-not-indexed` | Wrong ID, or the thread was created on another machine |
| `thread-busy` | A turn is running. `wait`, or `send --steer` |
| `thread-follower-start-turn-timeout`, or `not-loaded-in-app` on a thread that loads late | The turn often **did** start anyway. Run `status <id>` before retrying; resending would deliver the message twice |
| `request-version-mismatch` / `no-handler-for-request` | The app's private protocol changed. See "How it works" |
| `new` returns `exitCode: 1` and no `finalMessage` | The first turn failed. Read the rollout's `task_complete` event; often an unsupported model or a CLI version mismatch |