diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 870246242..d5b7ac62e 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -5,13 +5,13 @@
},
"metadata": {
"description": "Codex plugins to use in Claude Code for delegation and code review.",
- "version": "1.0.6"
+ "version": "1.0.7-eureka.2"
},
"plugins": [
{
"name": "codex",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
- "version": "1.0.6",
+ "version": "1.0.7-eureka.2",
"author": {
"name": "OpenAI"
},
diff --git a/.github/workflows/export-phase2-source.yml b/.github/workflows/export-phase2-source.yml
new file mode 100644
index 000000000..867d2e187
--- /dev/null
+++ b/.github/workflows/export-phase2-source.yml
@@ -0,0 +1,43 @@
+name: Export Phase 2 Source
+
+on:
+ push:
+ branches:
+ - feat/claude-native-multi-codex-phase-2
+
+permissions:
+ contents: read
+
+jobs:
+ export:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Upload source tree
+ uses: actions/upload-artifact@v4
+ with:
+ name: phase2-source
+ path: |
+ .
+ !.git
+ !node_modules
+ include-hidden-files: true
+ retention-days: 1
+
+ - name: Install current Codex CLI
+ run: npm install -g @openai/codex
+
+ - name: Generate App Server types
+ run: npm run prebuild
+
+ - name: Upload generated App Server types
+ uses: actions/upload-artifact@v4
+ with:
+ name: phase2-app-server-types
+ path: plugins/codex/.generated/app-server-types
+ include-hidden-files: true
+ retention-days: 1
diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml
index ebcff0b65..2d842e705 100644
--- a/.github/workflows/pull-request-ci.yml
+++ b/.github/workflows/pull-request-ci.yml
@@ -8,9 +8,13 @@ permissions:
jobs:
ci:
- name: CI
- runs-on: ubuntu-latest
- timeout-minutes: 10
+ name: CI (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
steps:
- name: Check out repository
@@ -28,6 +32,9 @@ jobs:
- name: Install Codex CLI
run: npm install -g @openai/codex
+ - name: Verify version metadata
+ run: npm run check-version
+
- name: Run test suite
run: npm test
diff --git a/README.md b/README.md
index 937a3037b..68c1bbfa1 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,7 @@
Use Codex from inside Claude Code for code reviews or to delegate tasks to Codex.
-This plugin is for Claude Code users who want an easy way to start using Codex from the workflow
-they already have.
+This fork tracks the OpenAI plugin while adding the GPT-5.6 runtime and prompting updates needed to use Sol, Terra, and Luna reliably from Claude Code.
@@ -12,19 +11,22 @@ they already have.
- `/codex:review` for a normal read-only Codex review
- `/codex:adversarial-review` for a steerable challenge review
- `/codex:rescue`, `/codex:transfer`, `/codex:status`, `/codex:result`, and `/codex:cancel` to delegate work, hand off sessions, and manage background jobs
+- GPT-5.6 model/effort validation through the current Codex model catalog
+- version-neutral `codex-prompting` guidance instead of a generation-pinned rescue skill
## Requirements
- **ChatGPT subscription (incl. Free) or OpenAI API key.**
- Usage will contribute to your Codex usage limits. [Learn more](https://developers.openai.com/codex/pricing).
- **Node.js 18.18 or later**
+- **Codex CLI 0.144.0 or later for GPT-5.6**
## Install
-Add the marketplace in Claude Code:
+Add this fork as a marketplace in Claude Code:
```bash
-/plugin marketplace add openai/codex-plugin-cc
+/plugin marketplace add eureka-pd/codex-plugin-cc
```
Install the plugin:
@@ -72,6 +74,18 @@ One simple first run is:
/codex:result
```
+## GPT-5.6 model policy
+
+GPT-5.6 uses three durable capability tiers. Treat `Sol > Terra > Luna` as the base capability ordering:
+
+- `gpt-5.6-sol`: frontier capability for the hardest coding, diagnosis, and adversarial review work
+- `gpt-5.6-terra`: balanced intelligence and cost for everyday implementation and review
+- `gpt-5.6-luna`: fastest, lowest-cost tier for bounded or high-volume work
+
+Reasoning effort is a separate inference-budget dimension. A higher effort on a lower tier can improve realized performance for a task, but it does not reverse the underlying tier ordering. The plugin leaves model and effort unset unless you select them, so your Codex configuration remains authoritative.
+
+The companion accepts `none`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra` as transport values and asks the current Codex model catalog to validate the selected combination. Availability can vary by model, Codex version, account, and plan.
+
## Usage
### `/codex:review`
@@ -86,7 +100,7 @@ Use it when you want:
- a review of your current uncommitted changes
- a review of your branch compared to a base branch like `main`
-Use `--base [` for branch review. It also supports `--wait` and `--background`. It is not steerable and does not take custom focus text. Use [`/codex:adversarial-review`](#codexadversarial-review) when you want to challenge a specific decision or risk area.
+Use `--base ][` for branch review. It also supports `--wait`, `--background`, `--model`, and `--effort`. It is not steerable and does not take custom focus text. Use [`/codex:adversarial-review`](#codexadversarial-review) when you want to challenge a specific decision or risk area.
Examples:
@@ -94,6 +108,7 @@ Examples:
/codex:review
/codex:review --base main
/codex:review --background
+/codex:review --model gpt-5.6-sol --effort max
```
This command is read-only and will not perform any changes. When run in the background you can use [`/codex:status`](#codexstatus) to check on the progress and [`/codex:cancel`](#codexcancel) to cancel the ongoing task.
@@ -105,7 +120,7 @@ Runs a **steerable** review that questions the chosen implementation and design.
It can be used to pressure-test assumptions, tradeoffs, failure modes, and whether a different approach would have been safer or simpler.
It uses the same review target selection as `/codex:review`, including `--base ][` for branch review.
-It also supports `--wait` and `--background`. Unlike `/codex:review`, it can take extra focus text after the flags.
+It also supports `--wait`, `--background`, `--model`, and `--effort`. Unlike `/codex:review`, it can take extra focus text after the flags.
Use it when you want:
@@ -119,6 +134,7 @@ Examples:
/codex:adversarial-review
/codex:adversarial-review --base main challenge whether this was the right caching and retry design
/codex:adversarial-review --background look for race conditions and question the chosen approach
+/codex:adversarial-review --model gpt-5.6-sol --effort max challenge the retry design
```
This command is read-only. It does not fix code.
@@ -145,11 +161,13 @@ Examples:
/codex:rescue investigate why the tests started failing
/codex:rescue fix the failing test with the smallest safe patch
/codex:rescue --resume apply the top fix from the last run
-/codex:rescue --model gpt-5.4-mini --effort medium investigate the flaky integration test
+/codex:rescue --model gpt-5.6-terra --effort medium investigate the flaky integration test
+/codex:rescue --model gpt-5.6-luna --effort low handle a bounded high-volume task
/codex:rescue --model spark fix the issue quickly
/codex:rescue --background investigate the regression
```
+
You can also just ask for a task to be delegated to Codex:
```text
@@ -159,7 +177,9 @@ Ask Codex to redesign the database connection to be more resilient.
**Notes:**
- if you do not pass `--model` or `--effort`, Codex chooses its own defaults.
-- if you say `spark`, the plugin maps that to `gpt-5.3-codex-spark`
+- if you say `spark`, the plugin maps that to `gpt-5.6-luna`
+- reasoning efforts are `none`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`; the current Codex model catalog validates explicit combinations
+- model names are otherwise passed through, so custom providers and newly released models are not blocked by a plugin allowlist
- follow-up rescue requests can continue the latest Codex task in the repo
### `/codex:transfer`
@@ -270,13 +290,14 @@ The Codex plugin wraps the [Codex app server](https://developers.openai.com/code
### Common Configurations
-If you want to change the default reasoning effort or the default model that gets used by the plugin, you can define that inside your user-level or project-level `config.toml`. For example to always use `gpt-5.4-mini` on `high` for a specific project you can add the following to a `.codex/config.toml` file at the root of the directory you started Claude in:
+If you want to change the default reasoning effort or model used by the plugin, define it in your user-level or project-level `config.toml`. For example, to use Terra at high effort for a specific project, add this to `.codex/config.toml` at the root of the directory where Claude Code starts:
```toml
-model = "gpt-5.4-mini"
+model = "gpt-5.6-terra"
model_reasoning_effort = "high"
```
+
Your configuration will be picked up based on:
- user-level config in `~/.codex/config.toml`
@@ -318,3 +339,25 @@ Yes. If you already use Codex, the plugin picks up the same [configuration](#com
Yes. Because the plugin uses your local Codex CLI, your existing sign-in method and config still apply.
If you need to point the built-in OpenAI provider at a different endpoint, set `openai_base_url` in your [Codex config](https://developers.openai.com/codex/config-advanced/#config-and-state-locations).
+
+## Claude-Native Multi-Codex Orchestration — Read-only Phase 1
+
+`/codex:orchestrate ` lets Claude Root decompose a repository investigation into independent read-only Codex Root packages, schedule them through a bounded worker pool, and persist package evidence and results.
+
+```bash
+/codex:orchestrate investigate the cache regression and independently challenge the concurrency assumptions
+/codex:status orch-...
+/codex:result orch-...
+/codex:cancel orch-...
+```
+
+Automatic entry is disabled by default:
+
+```bash
+/codex:setup --enable-orchestration
+/codex:setup --disable-orchestration
+```
+
+The default workspace pool size is 3 and may be configured from 1–8. The default plugin-wide top-level Root limit is 8 and the active Codex limit, including observed native children, is 12. Model routing follows `Sol > Terra > Luna`, with reasoning effort as a separate dimension.
+
+Phase 1 is read-only: package results must report no changed files. Writer worktrees, integration branches, and automatic Git integration are Phase 2.
diff --git a/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1-self-review.md b/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1-self-review.md
new file mode 100644
index 000000000..db3f3b389
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1-self-review.md
@@ -0,0 +1,457 @@
+# Phase 1 Implementation Plan — Self-Review Amendments
+
+- **Status:** Normative amendment
+- **Applies to:** `2026-08-17-claude-native-multi-codex-phase-1.md`
+- **Date:** 2026-08-17
+
+This document records the implementation-plan self-review required before execution. Implementers must read the base plan first and then apply this amendment. Where the two documents differ, **this amendment takes precedence**.
+
+The review found no unresolved product requirement, but it found several implementation-order and interface inconsistencies that would otherwise create avoidable rework or incomplete enforcement. The corrections below are part of the approved Phase 1 plan.
+
+## 1. Corrected execution order for budgets and plan validation
+
+The base plan temporarily duplicates score-derived budget logic inside `plan-contract.mjs` and later replaces it. Do not implement that temporary duplicate.
+
+Execute the relevant work in this order:
+
+1. Task 1 — workspace keys and generic locks.
+2. Task 2 — configuration and setup flags.
+3. **Task 3A — create `budget-policy.mjs` and its focused tests.**
+4. Task 3B — create plan/result schemas and contracts, importing the real budget policy from the start.
+5. Task 4 — state store.
+6. Task 5 — scheduler only, plus any necessary additions to existing budget tests.
+7. Tasks 6–16 in their existing order.
+
+### Task 3A files
+
+- Create: `plugins/codex/scripts/orchestration/budget-policy.mjs`
+- Create: `tests/orchestration-budget.test.mjs`
+
+### Task 3A required interface
+
+```js
+export function deriveBudgetEnvelope(complexityScore, config) {}
+export function validatePlanAgainstBudget(plan, envelope) {}
+```
+
+Use the envelope table and exact expected values already specified in Task 5. Commit Task 3A separately:
+
+```bash
+git add plugins/codex/scripts/orchestration/budget-policy.mjs tests/orchestration-budget.test.mjs
+git commit -m "feat: define orchestration budgets"
+```
+
+### Task 3B correction
+
+In `plan-contract.mjs`, import the real functions immediately:
+
+```js
+import { deriveBudgetEnvelope, validatePlanAgainstBudget } from "./budget-policy.mjs";
+```
+
+Delete the base-plan instruction that says to define a local private budget equivalent. `normalizeOrchestrationPlan` must return the final derived envelope from its first implementation.
+
+### Task 5 correction
+
+Task 5 no longer creates `budget-policy.mjs`. It creates the scheduler and extends `tests/orchestration-budget.test.mjs` only when scheduler integration requires additional coverage.
+
+## 2. Correct Node test-runner option order
+
+Replace both focused commands in the base plan with Node's option-before-file form:
+
+```bash
+node --test --test-name-pattern="caller-owned|native-child topology" tests/runtime.test.mjs
+```
+
+```bash
+node --test --test-name-pattern="fixture records overlapping turns" tests/orchestration-worker.test.mjs
+```
+
+All later focused test commands must follow the same rule.
+
+## 3. Clarify the Phase 1 Reviewer boundary
+
+The base plan says Phase 1 excludes reviewers while also retaining `reviewer` as a package role class. Interpret the boundary as follows:
+
+- Phase 1 excludes **automatic risk-triggered integration review** and all write-integration review gates.
+- Claude may still create an explicit **read-only `reviewer` work package** in the initial DAG.
+- Such a package is scheduled and treated like any other read-only package and cannot approve or trigger Git integration.
+
+Keep `reviewer` in the allowed role-class enum.
+
+## 4. Add `optional` to the plan contract from the first schema revision
+
+Do not defer `optional` until scheduler implementation.
+
+`orchestration-plan.schema.json` must include:
+
+```json
+"optional": { "type": "boolean", "default": false }
+```
+
+`plan-contract.mjs` must normalize an omitted value to `false`. Scheduler and final-status logic then consume the already-normalized field.
+
+## 5. Add an effective-configuration read command for automatic entry
+
+The automatic-entry skill cannot assume that it knows whether orchestration is enabled. Add a deterministic configuration read surface.
+
+### CLI extension
+
+Task 12 must add:
+
+```text
+config [--cwd ] [--json]
+```
+
+It returns:
+
+```js
+{
+ workspaceRoot,
+ userConfigPath,
+ projectConfigPath,
+ effectiveConfig,
+ autoEnabled: effectiveConfig.auto.enabled,
+ autoThreshold: effectiveConfig.auto.threshold
+}
+```
+
+This operation reads files only and does not start a controller.
+
+### Skill behavior
+
+Before **automatic** orchestration, `codex-orchestration` must call:
+
+```bash
+node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/cli.mjs" config --cwd "$PWD" --json
+```
+
+Rules:
+
+- if `autoEnabled` is false, do not auto-start Multi-Codex work;
+- if the score is below `autoThreshold`, do not auto-start;
+- explicit `/codex:orchestrate` bypasses `autoEnabled` and `autoThreshold` but still obeys Phase 1 read-only and budget limits.
+
+Add these cases to `tests/orchestration-skill.test.mjs` and `tests/orchestration-runtime.test.mjs`.
+
+## 6. Make the worker-pool/controller API internally consistent
+
+The base plan lists `WorkerPool.acquire` but later describes a fake pool with a direct `run` method. Use one concrete contract.
+
+### WorkerPool public interface
+
+```js
+class WorkerPool {
+ async acquire(packageId) {}
+ async release(packageId) {}
+ async discard(packageId, reason = null) {}
+ async cancel(packageId, options = {}) {}
+ getSnapshot() {}
+ async close() {}
+}
+```
+
+`acquire(packageId)` returns the leased `OrchestrationWorker` and records the package-to-worker mapping.
+
+### Controller execution sequence
+
+```js
+const worker = await pool.acquire(packageId);
+try {
+ const execution = await worker.run(packageSpec, options);
+ await pool.release(packageId);
+ return execution;
+} catch (error) {
+ if (isTransientWorkerError(error)) {
+ await pool.discard(packageId, error.message);
+ } else {
+ await pool.release(packageId);
+ }
+ throw error;
+}
+```
+
+A transient worker/process failure must discard the dead worker before retry. The retry acquires a new or healthy idle worker.
+
+### Cancellation sequence
+
+`pool.cancel(packageId, { graceMs })` must:
+
+1. call `worker.interrupt()`;
+2. wait up to `graceMs` for the active run to settle;
+3. call `discard(packageId, "cancel grace period exceeded")` if it does not settle;
+4. release the global lease exactly once.
+
+Update Task 9 and Task 10 tests to use this contract.
+
+## 7. Enforce the active-Codex limit globally, including native children
+
+The base plan enforces native-child counts only inside one workspace pool, which does not satisfy the approved plugin-wide limit. Extend the global registry.
+
+### Global lease shape
+
+```js
+{
+ id,
+ pid,
+ workspaceKey,
+ workerId,
+ packageId,
+ acquiredAt,
+ heartbeatAt,
+ activeNativeChildren: 0
+}
+```
+
+### Required registry API
+
+```js
+export async function acquireGlobalWorkerLease(options) {}
+export async function updateGlobalWorkerLease(leaseId, patch, options = {}) {}
+export async function releaseGlobalWorkerLease(leaseId, options = {}) {}
+export async function readGlobalWorkerRegistry(options = {}) {}
+export async function getGlobalActiveCodexCount(options = {}) {}
+```
+
+The count is:
+
+```text
+sum(1 top-level Root + activeNativeChildren for every live lease)
+```
+
+On every `native-child-started` and `native-child-completed` event, the owning pool updates its global lease under the global file lock.
+
+Before accepting a new top-level lease, enforce both:
+
+- `globalTopLevelLimit` against live lease count;
+- `globalActiveCodexLimit` against the full count.
+
+When a child-start event would push the plugin-wide total over `globalActiveCodexLimit`:
+
+1. update no count for the rejected child event;
+2. interrupt the offending Root;
+3. mark that package failed with `ACTIVE_CODEX_LIMIT_EXCEEDED`;
+4. release/discard the worker through the normal controller path.
+
+Tests must use two workspace pools sharing one temporary plugin-data root and prove that child counts from one workspace constrain the other.
+
+## 8. Define native-subagent `required` degradation precisely
+
+Keep `nativeSubagents.policy` values:
+
+```text
+allowed | forbidden | required
+```
+
+Phase 1 behavior:
+
+- `forbidden`: prompt the Root not to spawn children; any observed child is a package boundary violation and interrupts the Root.
+- `allowed`: zero to `maxChildren` children are valid.
+- `required`: request at least one child when the selected model/runtime supports it, but do not fail merely because no child appears.
+
+If a `required` package completes with zero observed native children:
+
+```js
+{
+ nativeSubagentDegraded: true,
+ nativeSubagentDegradationReason: "No native child was observed; accepted Root-only execution."
+}
+```
+
+Persist this metadata in package state and expose it in status/result. This implements the approved Root-only fallback without pretending that model support was conclusively detected.
+
+## 9. Define package-result status mapping
+
+The controller must map the canonical package result as follows:
+
+| Model result | Scheduler state | Usable by dependents | Automatic retry |
+|---|---|---:|---:|
+| `completed` | `completed` | yes | no |
+| `partial` | `partial` | yes | no |
+| `blocked` | `blocked` | no | no |
+| `failed` | `failed` | no | no |
+
+Only transport/process errors classified as transient receive the one automatic retry. A model-reported `failed` or `blocked` result is not a transient worker failure.
+
+`propagateBlockedPackages` must block descendants of both `blocked` and `failed` required dependencies.
+
+## 10. Remove the duplicate per-orchestration controller file
+
+Task 4's per-orchestration layout must not contain `/controller.json`.
+
+Use only:
+
+```text
+/_controller/controller.json
+/_controller/controller.lock
+```
+
+Each orchestration state instead records controller ownership metadata:
+
+```js
+controller: {
+ instanceId,
+ pid,
+ attachedAt,
+ lastHeartbeatAt
+}
+```
+
+This metadata supports honest Phase 1 stale-state reconciliation without creating multiple controller identity files.
+
+## 11. Use a short runtime directory for Unix socket safety
+
+Durable orchestration data remains under plugin data. Controller IPC runtime files use an OS-temporary path to avoid Unix-domain socket path limits.
+
+```js
+const runtimeRoot = path.join(os.tmpdir(), "codex-orchestration-runtime");
+const runtimeDir = path.join(runtimeRoot, workspaceKey);
+```
+
+Runtime files:
+
+```text
+/controller.sock # macOS/Linux
+/controller.json
+/controller.lock
+```
+
+Windows named pipe:
+
+```text
+pipe:\\.\pipe\-codex-orchestrator
+```
+
+The durable workspace `_controller` directory may retain an informational controller snapshot, but the authoritative live endpoint and lock are in the short runtime directory.
+
+Add a test using an intentionally long `CLAUDE_PLUGIN_DATA` path and verify that the Unix socket endpoint remains below 100 characters.
+
+## 12. Add Phase 1 stale-state reconciliation
+
+Phase 1 does not resume orphaned work, but it must never leave a durable orchestration appearing live after its controller died.
+
+On controller-server startup, before accepting orchestration requests:
+
+1. list non-terminal orchestration states for the workspace;
+2. inspect `state.controller.pid` and `state.controller.instanceId`;
+3. when the prior PID is dead or the instance no longer owns the live endpoint:
+ - mark `running`, `starting`, or `cancelling` packages `failed` with code `PHASE1_CONTROLLER_LOST`;
+ - mark dependent planned/ready packages `blocked`;
+ - mark unrelated planned/ready packages `cancelled` because Phase 1 does not resume them;
+ - derive and persist the terminal orchestration status;
+ - write a final aggregate result;
+ - append a `controller-loss-finalized` event explaining the Phase 1 limitation.
+
+Do not restart those packages automatically. Full resume/reconciliation remains Phase 3.
+
+Add controller lifecycle and runtime tests for this behavior.
+
+## 13. Durable reads must not require a live controller
+
+`status` and `result` are state-store operations first.
+
+### CLI behavior
+
+- `config`: local config read, no controller.
+- `status`: read durable state directly. Probe the controller only to enrich the snapshot with live pool/process data.
+- `result`: read durable state/results directly; never start a controller.
+- `start`: ensure/start controller.
+- `cancel`: use the live controller for active work. If the orchestration has already been finalized by stale-state reconciliation, return its terminal status without starting a controller.
+
+### Companion adapter behavior
+
+The legacy companion's orchestration-aware `status` and `result` paths must call the state-store adapter directly. They must remain usable after controller exit.
+
+Add tests that shut down the controller after completion and still retrieve status/result successfully.
+
+## 14. Controller identity and heartbeats
+
+At controller startup generate:
+
+```js
+const instanceId = `controller-${process.pid}-${crypto.randomUUID()}`;
+```
+
+Whenever a controller accepts or runs an orchestration, persist its identity in `state.controller`. Refresh `lastHeartbeatAt`:
+
+- when a package starts;
+- on each normalized package milestone;
+- when a package completes;
+- during a long-running package at least once every 30 seconds.
+
+`controller/status` returns `instanceId`, PID, endpoint, active orchestration IDs, active package IDs, and worker-pool snapshot.
+
+## 15. Clarify command/server concurrency
+
+The JSONL server may handle multiple sockets concurrently, but every mutation of one orchestration must pass through the orchestration state lock. Controller methods must be idempotent for duplicate client requests:
+
+- duplicate `start` is prevented by a generated orchestration ID created only inside the state store;
+- duplicate package completion checks the package terminal state and performs no second transition;
+- duplicate cancel returns the existing cancelling/terminal snapshot;
+- duplicate release/discard cannot release a global lease twice.
+
+Add focused duplicate-completion and duplicate-cancel tests to Task 10 or Task 11.
+
+## 16. Correct the Phase 1 plan's completion claim boundary
+
+The explicit command may report only that the orchestration was **accepted/started**. It must not wait for or synthesize the final result in the same invocation.
+
+Automatic orchestration follows the same rule:
+
+1. emit compressed plan notification;
+2. start orchestration;
+3. return orchestration ID and status/result/cancel commands;
+4. do not claim package completion until durable state reports it.
+
+Claude may later call `/codex:status` or `/codex:result` in response to the user, but the command itself remains non-blocking.
+
+## 17. Corrected self-review verification additions
+
+Add these checks before Phase 1 is considered implementation-ready:
+
+```bash
+node --test --test-name-pattern="caller-owned|native-child topology" tests/runtime.test.mjs
+node --test --test-name-pattern="fixture records overlapping turns" tests/orchestration-worker.test.mjs
+```
+
+The full suite must additionally prove:
+
+- automatic entry reads the effective config and remains disabled by default;
+- explicit orchestration works while automatic entry is disabled;
+- score threshold is enforced from config;
+- worker failure discards the old worker before retry;
+- plugin-wide active-Codex count includes native children across workspaces;
+- required native children degrade honestly to Root-only execution;
+- `blocked` and `failed` model results are not retried;
+- stale running state becomes terminal after controller loss;
+- durable status/result work without a live controller;
+- long plugin-data paths do not create overlong Unix socket paths;
+- duplicate completion/cancel/release operations are idempotent.
+
+## 18. Updated spec-coverage mapping
+
+| Corrected requirement | Task location |
+|---|---|
+| Real budget policy available during first plan validation | Task 3A, Task 3B |
+| Automatic-entry feature flag actually consulted | Task 12, Task 14 |
+| Pool/controller execution contract | Task 9, Task 10 |
+| Global native-child accounting | Task 9, Task 15 |
+| Required-child Root-only fallback | Task 8, Task 13, Task 15 |
+| Canonical result-to-scheduler mapping | Task 5, Task 10 |
+| Single controller identity location | Task 4, Task 11 |
+| Short cross-platform IPC runtime path | Task 11 |
+| Honest Phase 1 controller-loss finalization | Task 10, Task 11, Task 14 |
+| Durable status/result without controller | Task 4, Task 12, Task 13 |
+| Idempotent concurrent mutations | Task 4, Task 10, Task 11 |
+
+## 19. Self-review conclusion
+
+After applying this amendment:
+
+- no placeholder or deferred substitute implementation remains in the Phase 1 plan;
+- task dependencies are ordered so each contract is implemented against its final source of truth;
+- worker, pool, controller, and state-store interfaces are mutually consistent;
+- plugin-wide limits match the approved architecture rather than being enforced only per process;
+- Phase 1 recovery behavior is honest and testable without claiming Phase 3 resume support;
+- explicit and automatic command paths have a deterministic configuration gate;
+- the plan remains strictly read-only and preserves all Phase 2/3 boundaries.
diff --git a/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1.md b/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1.md
new file mode 100644
index 000000000..72bf0bbbd
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-17-claude-native-multi-codex-phase-1.md
@@ -0,0 +1,2577 @@
+# Claude-Native Multi-Codex Orchestration Phase 1 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Deliver the Phase 1 read-only Claude-native Multi-Codex orchestration layer: explicit and opt-in automatic entry, validated DAG plans, a bounded direct App Server worker pool, durable orchestration state, structured package results, and orchestration-aware status/result/cancel commands.
+
+**Architecture:** Claude remains the semantic planner and emits a canonical orchestration plan. A workspace-scoped detached Node.js controller validates the plan, schedules read-only packages over independent long-lived direct Codex App Server workers, persists every state transition, and exposes a small JSONL IPC surface. Existing single-job rescue/review paths remain unchanged; orchestration integrates through narrow adapters in `codex-companion.mjs` and dedicated Claude Code command/skill files.
+
+**Tech Stack:** Node.js 18.18+ ESM, built-in `node:test`, Codex App Server JSONL protocol, Claude Code plugin Markdown commands/skills, JSON Schema documents with dependency-free runtime validators, filesystem-backed JSON/JSONL state, Unix sockets on macOS/Linux, named pipes on Windows.
+
+## Global Constraints
+
+- Phase 1 is strictly read-only. Every package must declare `access: "read-only"`; reject writer packages before any worker starts.
+- Phase 1 excludes writer worktrees, snapshot refs, integration branches, package commits, reviewers, and automatic Git integration.
+- Claude Root owns decomposition, package semantics, model selection, effort selection, replanning decisions, and final interpretation. The controller executes only a validated structured plan.
+- Preserve existing `/codex:rescue`, `/codex:review`, `/codex:adversarial-review`, `/codex:transfer`, review-gate, broker, job, and session behavior.
+- Top-level Codex Roots use independent direct App Server processes; do not route orchestration work through the existing serialized shared broker.
+- One worker owns at most one top-level Codex Root at a time. Native children remain owned by that Root and the same App Server process.
+- Default workspace pool size is 3; valid configured range is 1–8.
+- Default global top-level worker limit is 8; default global active-Codex limit, including observed native children, is 12.
+- Complexity Score 3–4 allows at most 2 Roots and 15 minutes; 5–7 allows 4 Roots, parallelism 3, and 30 minutes; 8–10 allows 6 Roots, parallelism 3, and 60 minutes.
+- Automatic package retry is limited to one transient execution retry. Phase 1 does not perform semantic DAG replanning.
+- Treat `Sol > Terra > Luna` as the base capability order. Reasoning effort is a separate dimension.
+- Supported plugin-facing efforts remain `none`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`; actual model/effort compatibility comes from the current App Server model catalog.
+- Package runs use `sandbox: "read-only"` and `approvalPolicy: "never"`.
+- External writes, deployment, publication, remote mutation, and credential changes are outside Phase 1 and must never be authorized by orchestration prompts.
+- Do not add runtime npm dependencies. Use Node built-ins and the existing generated App Server types.
+- Persist state under `${CLAUDE_PLUGIN_DATA}/orchestrations//` when `CLAUDE_PLUGIN_DATA` is present, otherwise under an OS temporary fallback.
+- State writes must be atomic. Event logs are append-only JSONL.
+- Detailed events stay local; chat-facing output is milestone-oriented.
+- Support macOS, Linux, and Windows paths, process termination, Unix sockets, and named pipes.
+- Every task follows TDD and ends with a focused commit.
+
+---
+
+## File and Responsibility Map
+
+### Existing files to modify
+
+| File | Responsibility after Phase 1 |
+|---|---|
+| `plugins/codex/scripts/lib/state.mjs` | Reuse a shared workspace storage-key helper; retain legacy job/config behavior unchanged. |
+| `plugins/codex/scripts/lib/broker-lock.mjs` | Delegate lock mechanics to a generic file-lock primitive without changing broker semantics. |
+| `plugins/codex/scripts/lib/codex.mjs` | Expose turn execution on a caller-owned long-lived App Server client; report Root/native-child topology. |
+| `plugins/codex/scripts/codex-companion.mjs` | Add orchestration-aware setup/status/result/cancel routing while preserving legacy jobs. |
+| `plugins/codex/scripts/lib/render.mjs` | Render combined job/orchestration status plus orchestration and package results. |
+| `plugins/codex/commands/setup.md` | Expose automatic orchestration enable/disable flags. |
+| `plugins/codex/commands/status.md` | Accept orchestration/package references and combined status. |
+| `plugins/codex/commands/result.md` | Accept orchestration/package references. |
+| `plugins/codex/commands/cancel.md` | Accept orchestration/package references. |
+| `tests/fake-codex-fixture.mjs` | Simulate concurrent App Servers, delayed turns, canonical package results, native children, and interrupts. |
+| `tests/commands.test.mjs` | Verify command and skill surfaces. |
+| `tests/runtime.test.mjs` | Protect existing single-job behavior and reusable App Server turn execution. |
+| `tests/state.test.mjs` | Verify storage-key refactor does not move legacy state. |
+| `tests/render.test.mjs` | Verify orchestration rendering. |
+| `tsconfig.app-server.json` | Type-check orchestration App Server modules. |
+| `README.md` | Document Phase 1 command, limits, read-only boundary, and configuration. |
+
+### New runtime modules
+
+| File | Single responsibility |
+|---|---|
+| `plugins/codex/scripts/lib/workspace-key.mjs` | Canonical workspace key and storage-root calculation. |
+| `plugins/codex/scripts/lib/file-lock.mjs` | Cross-process exclusive lock with stale-owner recovery. |
+| `plugins/codex/scripts/orchestration/constants.mjs` | Status enums, defaults, limits, and Phase 1 feature constants. |
+| `plugins/codex/scripts/orchestration/config.mjs` | Load, merge, validate, and patch user/project orchestration configuration. |
+| `plugins/codex/scripts/orchestration/plan-contract.mjs` | Normalize and validate canonical plans, package IDs, dependencies, cycles, and Phase 1 read-only constraints. |
+| `plugins/codex/scripts/orchestration/result-contract.mjs` | Validate and normalize package and orchestration results. |
+| `plugins/codex/scripts/orchestration/state-store.mjs` | Atomic orchestration/package state, result files, events, and reference resolution. |
+| `plugins/codex/scripts/orchestration/budget-policy.mjs` | Derive and validate the adaptive budget envelope. |
+| `plugins/codex/scripts/orchestration/scheduler.mjs` | Pure DAG state transitions and ready/blocked/final status calculation. |
+| `plugins/codex/scripts/orchestration/package-prompt.mjs` | Build bounded read-only Codex prompts and structured-output contracts. |
+| `plugins/codex/scripts/orchestration/event-router.mjs` | Normalize worker progress into package/orchestration milestones and native-child counts. |
+| `plugins/codex/scripts/orchestration/worker-runtime.mjs` | Own one direct App Server client and one active Root execution. |
+| `plugins/codex/scripts/orchestration/global-worker-registry.mjs` | Enforce cross-workspace top-level worker leases. |
+| `plugins/codex/scripts/orchestration/worker-pool.mjs` | Lazy workspace worker creation, lease/release, idle TTL, cancellation, and shutdown. |
+| `plugins/codex/scripts/orchestration/controller.mjs` | Execute validated plans, schedule packages, persist state, retry transient failures, finalize, and cancel. |
+| `plugins/codex/scripts/orchestration/ipc.mjs` | Controller endpoint creation/parsing and JSONL request helpers. |
+| `plugins/codex/scripts/orchestration/controller-lifecycle.mjs` | Probe, start, reuse, and stop the workspace controller process. |
+| `plugins/codex/scripts/orchestration/controller-client.mjs` | Typed request methods for start/status/result/cancel. |
+| `plugins/codex/scripts/orchestration/controller-server.mjs` | Detached workspace controller JSONL server. |
+| `plugins/codex/scripts/orchestration/companion-adapter.mjs` | Bridge legacy companion handlers to orchestration storage/controller. |
+| `plugins/codex/scripts/orchestration/cli.mjs` | Deterministic command-line surface used by `/codex:orchestrate` and tests. |
+
+### New schemas, commands, and skills
+
+| File | Responsibility |
+|---|---|
+| `plugins/codex/scripts/orchestration/schemas/config.schema.json` | Phase 1 configuration schema. |
+| `plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json` | Canonical plan documentation/schema. |
+| `plugins/codex/scripts/orchestration/schemas/package-result.schema.json` | Canonical package result schema passed to `turn/start`. |
+| `plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json` | Canonical aggregate result documentation/schema. |
+| `plugins/codex/commands/orchestrate.md` | Explicit Claude-root orchestration command. |
+| `plugins/codex/skills/codex-orchestration/SKILL.md` | Auto-entry, plan construction, execution, decision-point, and final-response policy. |
+| `plugins/codex/skills/codex-work-package-contract/SKILL.md` | Internal package contract. |
+| `plugins/codex/skills/codex-integration-policy/SKILL.md` | Phase 1 result interpretation; explicitly defers Git integration to Phase 2. |
+| `plugins/codex/skills/codex-orchestration-recovery/SKILL.md` | Phase 1 restart behavior; explicitly defers automatic resume to Phase 3. |
+
+### New tests
+
+| File | Coverage |
+|---|---|
+| `tests/file-lock.test.mjs` | Lock serialization and stale-owner recovery. |
+| `tests/orchestration-config.test.mjs` | Configuration precedence, validation, and patching. |
+| `tests/orchestration-contracts.test.mjs` | Plan/result schemas, cycle detection, and read-only enforcement. |
+| `tests/orchestration-state.test.mjs` | Atomic state, event logs, results, references, and listing. |
+| `tests/orchestration-scheduler.test.mjs` | DAG readiness, failure propagation, cancellation, and final statuses. |
+| `tests/orchestration-worker.test.mjs` | Long-lived direct workers, structured results, child topology, and interrupt. |
+| `tests/orchestration-pool.test.mjs` | Pool bounds, reuse, global leases, child limits, and idle close. |
+| `tests/orchestration-controller.test.mjs` | End-to-end controller scheduling with fake workers. |
+| `tests/orchestration-ipc.test.mjs` | Detached server lifecycle and request routing. |
+| `tests/orchestration-runtime.test.mjs` | Full fake-Codex command flow for start/status/result/cancel and real parallel overlap. |
+| `tests/orchestration-skill.test.mjs` | Command/skill policy, Phase 1 exclusions, and auto-entry flag. |
+
+---
+
+### Task 1: Extract Reusable Workspace Storage Keys and Generic File Locks
+
+**Files:**
+- Create: `plugins/codex/scripts/lib/workspace-key.mjs`
+- Create: `plugins/codex/scripts/lib/file-lock.mjs`
+- Modify: `plugins/codex/scripts/lib/state.mjs`
+- Modify: `plugins/codex/scripts/lib/broker-lock.mjs`
+- Create: `tests/file-lock.test.mjs`
+- Modify: `tests/state.test.mjs`
+- Test: `tests/broker-lifecycle.test.mjs`
+
+**Interfaces:**
+- Produces: `buildWorkspaceStorageKey(cwd): { workspaceRoot, canonicalWorkspaceRoot, slug, hash, key }`
+- Produces: `resolvePluginDataRoot(env?): string`
+- Produces: `withFileLock(lockFile, options, action): Promise`
+- Preserves: `resolveStateDir(cwd)` output format and `withBrokerLock(cwd, options, action)` behavior.
+
+- [ ] **Step 1: Add failing storage-key tests**
+
+Add to `tests/state.test.mjs`:
+
+```js
+import { buildWorkspaceStorageKey } from "../plugins/codex/scripts/lib/workspace-key.mjs";
+
+test("workspace storage keys are stable for the same canonical workspace", () => {
+ const workspace = makeTempDir();
+ const first = buildWorkspaceStorageKey(workspace);
+ const second = buildWorkspaceStorageKey(path.join(workspace, "."));
+
+ assert.equal(first.workspaceRoot, second.workspaceRoot);
+ assert.equal(first.key, second.key);
+ assert.match(first.key, /.+-[a-f0-9]{16}$/);
+});
+```
+
+- [ ] **Step 2: Add failing lock serialization and stale-owner tests**
+
+Create `tests/file-lock.test.mjs`:
+
+```js
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { makeTempDir } from "./helpers.mjs";
+import { withFileLock } from "../plugins/codex/scripts/lib/file-lock.mjs";
+
+test("withFileLock serializes concurrent actions", async () => {
+ const lockFile = path.join(makeTempDir(), "test.lock");
+ const order = [];
+
+ const first = withFileLock(lockFile, {}, async () => {
+ order.push("first-enter");
+ await new Promise((resolve) => setTimeout(resolve, 75));
+ order.push("first-exit");
+ });
+ const second = withFileLock(lockFile, {}, async () => {
+ order.push("second-enter");
+ order.push("second-exit");
+ });
+
+ await Promise.all([first, second]);
+ assert.deepEqual(order, ["first-enter", "first-exit", "second-enter", "second-exit"]);
+});
+
+test("withFileLock removes an abandoned lock owned by a dead process", async () => {
+ const lockFile = path.join(makeTempDir(), "test.lock");
+ fs.writeFileSync(lockFile, `999999:${Date.now() - 60000}:abandoned`, "utf8");
+
+ const value = await withFileLock(lockFile, { staleMs: 1 }, async () => "recovered");
+
+ assert.equal(value, "recovered");
+ assert.equal(fs.existsSync(lockFile), false);
+});
+```
+
+- [ ] **Step 3: Run the focused tests and confirm failure**
+
+Run:
+
+```bash
+node --test tests/state.test.mjs tests/file-lock.test.mjs
+```
+
+Expected: FAIL because `workspace-key.mjs` and `file-lock.mjs` do not exist.
+
+- [ ] **Step 4: Implement `workspace-key.mjs`**
+
+Create:
+
+```js
+import { createHash } from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+import { resolveWorkspaceRoot } from "./workspace.mjs";
+
+export const PLUGIN_DATA_ENV = "CLAUDE_PLUGIN_DATA";
+
+export function resolvePluginDataRoot(env = process.env) {
+ return env?.[PLUGIN_DATA_ENV] ? path.resolve(env[PLUGIN_DATA_ENV]) : path.join(os.tmpdir(), "codex-companion");
+}
+
+export function buildWorkspaceStorageKey(cwd) {
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
+ let canonicalWorkspaceRoot = workspaceRoot;
+ try {
+ canonicalWorkspaceRoot = fs.realpathSync.native(workspaceRoot);
+ } catch {
+ canonicalWorkspaceRoot = path.resolve(workspaceRoot);
+ }
+
+ const slugSource = path.basename(workspaceRoot) || "workspace";
+ const slug = slugSource.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
+ const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0, 16);
+ return {
+ workspaceRoot,
+ canonicalWorkspaceRoot,
+ slug,
+ hash,
+ key: `${slug}-${hash}`
+ };
+}
+```
+
+- [ ] **Step 5: Implement generic `withFileLock`**
+
+Create `plugins/codex/scripts/lib/file-lock.mjs` with these exported semantics:
+
+```js
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function isProcessAlive(pid) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (error) {
+ return error?.code !== "ESRCH";
+ }
+}
+
+function removeAbandonedLock(lockFile, staleMs) {
+ try {
+ const stat = fs.statSync(lockFile);
+ const token = fs.readFileSync(lockFile, "utf8");
+ const ownerPid = Number.parseInt(token.split(":", 1)[0], 10);
+ if (Number.isFinite(ownerPid) && isProcessAlive(ownerPid)) {
+ return false;
+ }
+ if (!Number.isFinite(ownerPid) && Date.now() - stat.mtimeMs <= staleMs) {
+ return false;
+ }
+ fs.unlinkSync(lockFile);
+ return true;
+ } catch (error) {
+ if (error?.code === "ENOENT") {
+ return true;
+ }
+ throw error;
+ }
+}
+
+export async function withFileLock(lockFile, options = {}, action) {
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
+ const timeoutMs = options.timeoutMs ?? 5000;
+ const staleMs = options.staleMs ?? 30000;
+ const retryMs = options.retryMs ?? 25;
+ const deadline = Date.now() + timeoutMs;
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
+ let fd = null;
+
+ while (fd === null) {
+ try {
+ fd = fs.openSync(lockFile, "wx");
+ fs.writeFileSync(fd, token, "utf8");
+ } catch (error) {
+ if (error?.code !== "EEXIST") {
+ throw error;
+ }
+ if (removeAbandonedLock(lockFile, staleMs)) {
+ continue;
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`Timed out waiting for lock at ${lockFile}.`);
+ }
+ await sleep(retryMs);
+ }
+ }
+
+ try {
+ return await action();
+ } finally {
+ try {
+ fs.closeSync(fd);
+ } finally {
+ try {
+ if (fs.readFileSync(lockFile, "utf8") === token) {
+ fs.unlinkSync(lockFile);
+ }
+ } catch (error) {
+ if (error?.code !== "ENOENT") {
+ throw error;
+ }
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 6: Refactor legacy state and broker lock to use the new primitives**
+
+In `state.mjs`, replace duplicated canonicalization/hash code with:
+
+```js
+import { buildWorkspaceStorageKey, resolvePluginDataRoot } from "./workspace-key.mjs";
+
+export function resolveStateDir(cwd) {
+ const { key } = buildWorkspaceStorageKey(cwd);
+ return path.join(resolvePluginDataRoot(), "state", key);
+}
+```
+
+In `broker-lock.mjs`, keep `brokerLockPath()` but replace its internal lock implementation with:
+
+```js
+import { withFileLock } from "./file-lock.mjs";
+
+export async function withBrokerLock(cwd, options, action) {
+ return withFileLock(
+ brokerLockPath(cwd),
+ {
+ timeoutMs: options?.lockTimeoutMs,
+ staleMs: options?.lockStaleMs
+ },
+ action
+ );
+}
+```
+
+- [ ] **Step 7: Run regression tests**
+
+Run:
+
+```bash
+node --test tests/file-lock.test.mjs tests/state.test.mjs tests/broker-lifecycle.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/lib/workspace-key.mjs plugins/codex/scripts/lib/file-lock.mjs plugins/codex/scripts/lib/state.mjs plugins/codex/scripts/lib/broker-lock.mjs tests/file-lock.test.mjs tests/state.test.mjs
+git commit -m "refactor: share workspace keys and file locks"
+```
+
+---
+
+### Task 2: Add Phase 1 Orchestration Configuration and Setup Flags
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/constants.mjs`
+- Create: `plugins/codex/scripts/orchestration/config.mjs`
+- Create: `plugins/codex/scripts/orchestration/schemas/config.schema.json`
+- Modify: `plugins/codex/scripts/codex-companion.mjs`
+- Modify: `plugins/codex/scripts/lib/render.mjs`
+- Modify: `plugins/codex/commands/setup.md`
+- Create: `tests/orchestration-config.test.mjs`
+- Modify: `tests/runtime.test.mjs`
+- Modify: `tests/commands.test.mjs`
+
+**Interfaces:**
+- Produces: `DEFAULT_ORCHESTRATION_CONFIG`
+- Produces: `loadOrchestrationConfig(workspaceRoot, options?): OrchestrationConfig`
+- Produces: `patchUserOrchestrationConfig(patch, options?): OrchestrationConfig`
+- Produces: `getUserConfigPath(options?): string`
+- Extends setup JSON with `orchestration: { autoEnabled, userConfigPath, projectConfigPath, effectiveConfig }`.
+
+- [ ] **Step 1: Write failing precedence and validation tests**
+
+Create `tests/orchestration-config.test.mjs` with tests that:
+
+```js
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { makeTempDir } from "./helpers.mjs";
+import {
+ DEFAULT_ORCHESTRATION_CONFIG,
+ getUserConfigPath,
+ loadOrchestrationConfig,
+ patchUserOrchestrationConfig
+} from "../plugins/codex/scripts/orchestration/config.mjs";
+
+test("project config overrides user config without dropping sibling defaults", () => {
+ const homeDir = makeTempDir();
+ const workspace = makeTempDir();
+ fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
+ fs.mkdirSync(path.join(workspace, ".claude"), { recursive: true });
+ fs.writeFileSync(
+ getUserConfigPath({ homeDir }),
+ JSON.stringify({ auto: { enabled: true }, workers: { workspacePoolSize: 2 } }),
+ "utf8"
+ );
+ fs.writeFileSync(
+ path.join(workspace, ".claude", "codex-orchestration.json"),
+ JSON.stringify({ workers: { workspacePoolSize: 4 } }),
+ "utf8"
+ );
+
+ const config = loadOrchestrationConfig(workspace, { homeDir });
+ assert.equal(config.auto.enabled, true);
+ assert.equal(config.workers.workspacePoolSize, 4);
+ assert.equal(config.workers.globalTopLevelLimit, DEFAULT_ORCHESTRATION_CONFIG.workers.globalTopLevelLimit);
+});
+
+test("invalid worker limits are rejected with a path-specific error", () => {
+ const homeDir = makeTempDir();
+ fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
+ fs.writeFileSync(getUserConfigPath({ homeDir }), JSON.stringify({ workers: { workspacePoolSize: 9 } }), "utf8");
+
+ assert.throws(
+ () => loadOrchestrationConfig(makeTempDir(), { homeDir }),
+ /workers\.workspacePoolSize must be between 1 and 8/
+ );
+});
+
+test("patchUserOrchestrationConfig preserves unrelated keys", () => {
+ const homeDir = makeTempDir();
+ fs.mkdirSync(path.join(homeDir, ".claude"), { recursive: true });
+ fs.writeFileSync(getUserConfigPath({ homeDir }), JSON.stringify({ workers: { workspacePoolSize: 2 } }), "utf8");
+
+ const result = patchUserOrchestrationConfig({ auto: { enabled: true } }, { homeDir });
+ assert.equal(result.auto.enabled, true);
+ assert.equal(result.workers.workspacePoolSize, 2);
+});
+```
+
+- [ ] **Step 2: Run the new tests and confirm failure**
+
+```bash
+node --test tests/orchestration-config.test.mjs
+```
+
+Expected: FAIL because the orchestration config modules do not exist.
+
+- [ ] **Step 3: Define Phase 1 constants**
+
+Create `constants.mjs`:
+
+```js
+export const ORCHESTRATION_STATE_VERSION = 1;
+export const ORCHESTRATION_PLAN_VERSION = 1;
+export const DEFAULT_WORKSPACE_POOL_SIZE = 3;
+export const MIN_WORKSPACE_POOL_SIZE = 1;
+export const MAX_WORKSPACE_POOL_SIZE = 8;
+export const DEFAULT_GLOBAL_TOP_LEVEL_LIMIT = 8;
+export const DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT = 12;
+export const DEFAULT_IDLE_TTL_MINUTES = 10;
+export const DEFAULT_AUTO_THRESHOLD = 5;
+export const DEFAULT_CANCEL_GRACE_MS = 10000;
+export const DEFAULT_CONTROLLER_IDLE_TTL_MS = 10 * 60 * 1000;
+export const VALID_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]);
+export const PHASE1_PACKAGE_ACCESS = "read-only";
+export const PACKAGE_TERMINAL_STATUSES = new Set(["completed", "partial", "blocked", "failed", "cancelled"]);
+export const ORCHESTRATION_TERMINAL_STATUSES = new Set([
+ "completed",
+ "completed-with-omissions",
+ "degraded",
+ "blocked",
+ "failed",
+ "cancelled"
+]);
+```
+
+- [ ] **Step 4: Add the configuration schema**
+
+Create `schemas/config.schema.json` documenting exactly:
+
+```json
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "auto": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": { "type": "boolean" },
+ "threshold": { "type": "integer", "minimum": 0, "maximum": 10 }
+ }
+ },
+ "workers": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "workspacePoolSize": { "type": "integer", "minimum": 1, "maximum": 8 },
+ "globalTopLevelLimit": { "type": "integer", "minimum": 1, "maximum": 8 },
+ "globalActiveCodexLimit": { "type": "integer", "minimum": 1, "maximum": 12 },
+ "idleTtlMinutes": { "type": "integer", "minimum": 0, "maximum": 60 }
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 5: Implement configuration loading and patching**
+
+`config.mjs` must export:
+
+```js
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+import {
+ DEFAULT_AUTO_THRESHOLD,
+ DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT,
+ DEFAULT_GLOBAL_TOP_LEVEL_LIMIT,
+ DEFAULT_IDLE_TTL_MINUTES,
+ DEFAULT_WORKSPACE_POOL_SIZE
+} from "./constants.mjs";
+
+export const DEFAULT_ORCHESTRATION_CONFIG = Object.freeze({
+ auto: { enabled: false, threshold: DEFAULT_AUTO_THRESHOLD },
+ workers: {
+ workspacePoolSize: DEFAULT_WORKSPACE_POOL_SIZE,
+ globalTopLevelLimit: DEFAULT_GLOBAL_TOP_LEVEL_LIMIT,
+ globalActiveCodexLimit: DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT,
+ idleTtlMinutes: DEFAULT_IDLE_TTL_MINUTES
+ }
+});
+
+export function getUserConfigPath(options = {}) {
+ return path.join(options.homeDir ?? os.homedir(), ".claude", "codex-orchestration.json");
+}
+
+export function getProjectConfigPath(workspaceRoot) {
+ return path.join(workspaceRoot, ".claude", "codex-orchestration.json");
+}
+```
+
+Implement a recursive object-only merge, reject arrays/unknown top-level keys, validate all numeric bounds, and write user patches through a temporary file followed by `fs.renameSync`.
+
+- [ ] **Step 6: Extend setup handling**
+
+In `codex-companion.mjs`:
+
+- add `--enable-orchestration` and `--disable-orchestration` boolean options;
+- reject enabling and disabling together;
+- call `patchUserOrchestrationConfig({ auto: { enabled: true|false } })`;
+- include the effective configuration in `buildSetupReport`;
+- add next-step text only when auto orchestration is disabled.
+
+The report shape must include:
+
+```js
+orchestration: {
+ autoEnabled: orchestrationConfig.auto.enabled,
+ userConfigPath: getUserConfigPath(),
+ projectConfigPath: getProjectConfigPath(workspaceRoot),
+ effectiveConfig: orchestrationConfig
+}
+```
+
+- [ ] **Step 7: Extend setup rendering and command documentation**
+
+Add to `renderSetupReport`:
+
+```js
+`- orchestration auto-entry: ${report.orchestration.autoEnabled ? "enabled" : "disabled"}`,
+`- orchestration user config: ${report.orchestration.userConfigPath}`,
+`- orchestration project config: ${report.orchestration.projectConfigPath}`,
+```
+
+Update `setup.md` argument hint to:
+
+```yaml
+argument-hint: '[--enable-review-gate|--disable-review-gate] [--enable-orchestration|--disable-orchestration]'
+```
+
+- [ ] **Step 8: Run focused tests**
+
+```bash
+node --test tests/orchestration-config.test.mjs tests/runtime.test.mjs tests/commands.test.mjs tests/render.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/constants.mjs plugins/codex/scripts/orchestration/config.mjs plugins/codex/scripts/orchestration/schemas/config.schema.json plugins/codex/scripts/codex-companion.mjs plugins/codex/scripts/lib/render.mjs plugins/codex/commands/setup.md tests/orchestration-config.test.mjs tests/runtime.test.mjs tests/commands.test.mjs tests/render.test.mjs
+git commit -m "feat: add orchestration configuration"
+```
+
+---
+
+### Task 3: Define Canonical Plan and Result Contracts
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json`
+- Create: `plugins/codex/scripts/orchestration/schemas/package-result.schema.json`
+- Create: `plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json`
+- Create: `plugins/codex/scripts/orchestration/plan-contract.mjs`
+- Create: `plugins/codex/scripts/orchestration/result-contract.mjs`
+- Create: `tests/orchestration-contracts.test.mjs`
+
+**Interfaces:**
+- Produces: `normalizeOrchestrationPlan(input, context): NormalizedPlan`
+- Produces: `validatePackageResult(input, packageId): NormalizedPackageResult`
+- Produces: `buildOrchestrationResult(orchestrationState): OrchestrationResult`
+- Produces: `readPackageResultSchema(): object`
+
+- [ ] **Step 1: Add a canonical valid plan fixture and failing contract tests**
+
+Create `tests/orchestration-contracts.test.mjs` with this fixture:
+
+```js
+const VALID_PLAN = {
+ version: 1,
+ objective: "Compare two independent failure hypotheses and verify the stronger explanation.",
+ complexityScore: 6,
+ requestedBy: { explicit: true, sessionId: "claude-session-1" },
+ packages: [
+ {
+ id: "pkg-cache",
+ title: "Inspect cache invalidation",
+ role: { class: "explorer", label: "cache-investigator" },
+ objective: "Determine whether stale cache state explains the regression.",
+ dependencies: [],
+ access: "read-only",
+ workspace: { mode: "shared" },
+ model: { name: "gpt-5.6-luna", effort: "high" },
+ nativeSubagents: { policy: "allowed", maxChildren: 1 },
+ acceptanceCriteria: ["Cite relevant files and commands", "Return a falsifiable conclusion"],
+ expectedOutputs: ["claims", "evidence", "residual risks"]
+ },
+ {
+ id: "pkg-race",
+ title: "Inspect race conditions",
+ role: { class: "explorer", label: "race-investigator" },
+ objective: "Determine whether a concurrency race explains the regression.",
+ dependencies: [],
+ access: "read-only",
+ workspace: { mode: "shared" },
+ model: { name: "gpt-5.6-terra", effort: "high" },
+ nativeSubagents: { policy: "forbidden", maxChildren: 0 },
+ acceptanceCriteria: ["Cite relevant files and commands", "Return a falsifiable conclusion"],
+ expectedOutputs: ["claims", "evidence", "residual risks"]
+ },
+ {
+ id: "pkg-synthesis-check",
+ title: "Verify both hypotheses",
+ role: { class: "verifier", label: "hypothesis-verifier" },
+ objective: "Compare evidence from both investigations and identify unresolved contradictions.",
+ dependencies: ["pkg-cache", "pkg-race"],
+ access: "read-only",
+ workspace: { mode: "shared" },
+ model: { name: "gpt-5.6-sol", effort: "high" },
+ nativeSubagents: { policy: "forbidden", maxChildren: 0 },
+ acceptanceCriteria: ["Explicitly compare both package results"],
+ expectedOutputs: ["claims", "evidence", "residual risks"]
+ }
+ ]
+};
+```
+
+Test:
+
+- valid normalization preserves package order;
+- duplicate package IDs fail;
+- unknown dependencies fail;
+- a cycle fails with the cycle path;
+- `access: "write"` fails with `Phase 1 only supports read-only packages`;
+- invalid effort fails;
+- package count above the score-derived cap fails;
+- result `changedFiles` must be empty in Phase 1;
+- confidence must be between 0 and 1.
+
+- [ ] **Step 2: Run contract tests and confirm failure**
+
+```bash
+node --test tests/orchestration-contracts.test.mjs
+```
+
+Expected: FAIL because contract modules and schemas are missing.
+
+- [ ] **Step 3: Write `orchestration-plan.schema.json`**
+
+The schema must require:
+
+```json
+{
+ "required": ["version", "objective", "complexityScore", "requestedBy", "packages"],
+ "properties": {
+ "version": { "const": 1 },
+ "objective": { "type": "string", "minLength": 1 },
+ "complexityScore": { "type": "integer", "minimum": 0, "maximum": 10 },
+ "requestedBy": {
+ "type": "object",
+ "required": ["explicit"],
+ "properties": {
+ "explicit": { "type": "boolean" },
+ "sessionId": { "type": ["string", "null"] }
+ }
+ },
+ "packages": { "type": "array", "minItems": 1, "maxItems": 8 }
+ }
+}
+```
+
+Each package must require the exact fields in `VALID_PLAN`, constrain role class to:
+
+```text
+planner, architect, explorer, implementer, tester, reviewer, verifier, migration-specialist, security-reviewer
+```
+
+and constrain Phase 1 access/workspace to:
+
+```json
+"access": { "const": "read-only" },
+"workspace": {
+ "type": "object",
+ "required": ["mode"],
+ "properties": { "mode": { "const": "shared" } }
+}
+```
+
+- [ ] **Step 4: Write `package-result.schema.json`**
+
+Require this shape:
+
+```json
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "packageId",
+ "status",
+ "summary",
+ "claims",
+ "evidence",
+ "changedFiles",
+ "verification",
+ "residualRisks",
+ "confidence",
+ "followUpRequests"
+ ],
+ "properties": {
+ "packageId": { "type": "string" },
+ "status": { "enum": ["completed", "partial", "blocked", "failed"] },
+ "summary": { "type": "string" },
+ "claims": { "type": "array", "items": { "type": "string" } },
+ "evidence": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["type", "description"],
+ "properties": {
+ "type": { "enum": ["file", "command", "observation"] },
+ "description": { "type": "string" },
+ "path": { "type": ["string", "null"] },
+ "lineStart": { "type": ["integer", "null"], "minimum": 1 },
+ "lineEnd": { "type": ["integer", "null"], "minimum": 1 },
+ "command": { "type": ["string", "null"] },
+ "exitCode": { "type": ["integer", "null"] }
+ }
+ }
+ },
+ "changedFiles": { "type": "array", "maxItems": 0 },
+ "verification": {
+ "type": "object",
+ "required": ["passed", "commands"],
+ "properties": {
+ "passed": { "type": "boolean" },
+ "commands": { "type": "array", "items": { "type": "string" } }
+ }
+ },
+ "residualRisks": { "type": "array", "items": { "type": "string" } },
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
+ "followUpRequests": { "type": "array", "items": { "type": "string" } }
+ }
+}
+```
+
+- [ ] **Step 5: Implement plan normalization and graph validation**
+
+`plan-contract.mjs` must:
+
+1. verify object/array/string primitives without a schema library;
+2. trim all IDs, labels, objectives, criteria, and model strings;
+3. reject unknown package IDs and duplicate IDs;
+4. reject package dependencies on themselves;
+5. run DFS cycle detection and include `pkg-a -> pkg-b -> pkg-a` in the error;
+6. reject `write` access and non-`shared` workspace modes;
+7. validate score-derived package/parallelism limits through Task 5's `deriveBudgetEnvelope` once available; until Task 5, define a local private equivalent and replace it during Task 5;
+8. return a deeply frozen normalized plan.
+
+Export:
+
+```js
+export function normalizeOrchestrationPlan(input, context = {}) {
+ // context.workspaceRoot and context.config are required by the controller.
+}
+```
+
+- [ ] **Step 6: Implement result normalization**
+
+`result-contract.mjs` must export:
+
+```js
+export function validatePackageResult(input, packageId) {
+ // Return a normalized object or throw a path-specific validation error.
+}
+
+export function buildOrchestrationResult(state) {
+ return {
+ orchestrationId: state.id,
+ status: state.status,
+ objective: state.plan.objective,
+ planRevision: state.planRevision,
+ packages: state.plan.packages.map((pkg) => ({
+ id: pkg.id,
+ title: pkg.title,
+ role: pkg.role,
+ status: state.packages[pkg.id].status,
+ result: state.packages[pkg.id].result ?? null,
+ threadId: state.packages[pkg.id].threadId ?? null,
+ nativeChildThreadIds: state.packages[pkg.id].nativeChildThreadIds ?? []
+ })),
+ omissions: state.omissions ?? [],
+ remainingWork: state.remainingWork ?? []
+ };
+}
+```
+
+Load `package-result.schema.json` once through `fs.readFileSync(new URL(...))` and export `readPackageResultSchema()`.
+
+- [ ] **Step 7: Run contract tests**
+
+```bash
+node --test tests/orchestration-contracts.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/schemas plugins/codex/scripts/orchestration/plan-contract.mjs plugins/codex/scripts/orchestration/result-contract.mjs tests/orchestration-contracts.test.mjs
+git commit -m "feat: define orchestration contracts"
+```
+
+---
+
+### Task 4: Build the Durable Orchestration State Store
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/state-store.mjs`
+- Create: `tests/orchestration-state.test.mjs`
+
+**Interfaces:**
+- Produces: `resolveOrchestrationWorkspaceDir(workspaceRoot)`
+- Produces: `createOrchestrationState(workspaceRoot, plan, context): State`
+- Produces: `loadOrchestrationState(workspaceRoot, id): State`
+- Produces: `updateOrchestrationState(workspaceRoot, id, mutate): State`
+- Produces: `listOrchestrations(workspaceRoot): StateSummary[]`
+- Produces: `resolveOrchestrationReference(workspaceRoot, reference): { kind, orchestrationId, packageId? }`
+- Produces: `writePackageResult`, `readPackageResult`, `appendOrchestrationEvent`.
+
+- [ ] **Step 1: Add failing persistence and reference tests**
+
+Create tests covering:
+
+```js
+test("createOrchestrationState writes canonical state and package files", () => {});
+test("updateOrchestrationState is atomic under concurrent writers", async () => {});
+test("appendOrchestrationEvent writes one JSON object per line", () => {});
+test("resolveOrchestrationReference accepts exact and unique prefixes", () => {});
+test("package references resolve as pkg-id within their orchestration", () => {});
+test("ambiguous orchestration prefixes are rejected", () => {});
+```
+
+The first state must contain:
+
+```js
+{
+ version: 1,
+ id: /^orch-/,
+ workspaceRoot,
+ claudeSessionId: "session-1",
+ status: "queued",
+ planRevision: 1,
+ plan,
+ packages: {
+ "pkg-cache": {
+ id: "pkg-cache",
+ status: "planned",
+ attempt: 0,
+ workerId: null,
+ threadId: null,
+ turnId: null,
+ nativeChildThreadIds: [],
+ result: null,
+ error: null
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Run tests and confirm failure**
+
+```bash
+node --test tests/orchestration-state.test.mjs
+```
+
+- [ ] **Step 3: Implement storage paths**
+
+Use:
+
+```js
+const FALLBACK_ORCHESTRATION_ROOT = path.join(os.tmpdir(), "codex-companion", "orchestrations");
+
+export function resolveOrchestrationWorkspaceDir(workspaceRoot) {
+ const { key } = buildWorkspaceStorageKey(workspaceRoot);
+ const root = process.env.CLAUDE_PLUGIN_DATA
+ ? path.join(path.resolve(process.env.CLAUDE_PLUGIN_DATA), "orchestrations")
+ : FALLBACK_ORCHESTRATION_ROOT;
+ return path.join(root, key);
+}
+```
+
+Per orchestration:
+
+```text
+//orchestration.json
+//events.jsonl
+//packages/.json
+//results/.json
+//controller.json
+```
+
+- [ ] **Step 4: Implement atomic JSON writes**
+
+Use a unique same-directory temp path and `renameSync`:
+
+```js
+function writeJsonAtomic(filePath, value) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
+ fs.renameSync(tempPath, filePath);
+}
+```
+
+Wrap read-modify-write updates with `withFileLock(path.join(orchestrationDir, "state.lock"), {}, action)`.
+
+- [ ] **Step 5: Implement creation, updates, package results, and events**
+
+IDs use:
+
+```js
+export function generateOrchestrationId(now = Date.now()) {
+ return `orch-${now.toString(36)}-${crypto.randomBytes(3).toString("hex")}`;
+}
+```
+
+`appendOrchestrationEvent` writes:
+
+```js
+{
+ timestamp: new Date().toISOString(),
+ orchestrationId,
+ packageId: event.packageId ?? null,
+ type: event.type,
+ phase: event.phase ?? null,
+ message: event.message,
+ data: event.data ?? null
+}
+```
+
+- [ ] **Step 6: Implement reference resolution**
+
+Rules:
+
+- exact orchestration ID wins;
+- a unique orchestration prefix is allowed;
+- exact package ID searches all known orchestrations in the workspace and must match one;
+- a package prefix must be unique across the workspace;
+- errors must direct the user to `/codex:status`.
+
+- [ ] **Step 7: Run state tests**
+
+```bash
+node --test tests/orchestration-state.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/state-store.mjs tests/orchestration-state.test.mjs
+git commit -m "feat: persist orchestration state"
+```
+
+---
+
+### Task 5: Implement Adaptive Budgets and the Pure DAG Scheduler
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/budget-policy.mjs`
+- Create: `plugins/codex/scripts/orchestration/scheduler.mjs`
+- Modify: `plugins/codex/scripts/orchestration/plan-contract.mjs`
+- Create: `tests/orchestration-scheduler.test.mjs`
+- Modify: `tests/orchestration-contracts.test.mjs`
+
+**Interfaces:**
+- Produces: `deriveBudgetEnvelope(complexityScore, config): BudgetEnvelope`
+- Produces: `validatePlanAgainstBudget(plan, envelope): void`
+- Produces: `createSchedulerState(plan): SchedulerState`
+- Produces: `getReadyPackageIds(state): string[]`
+- Produces state transition functions with immutable return values.
+
+- [ ] **Step 1: Write failing budget tests**
+
+Test exact envelopes:
+
+```js
+assert.deepEqual(deriveBudgetEnvelope(3, config), {
+ maxTopLevelRoots: 2,
+ workerParallelism: 2,
+ maxNativeChildrenPerRoot: 1,
+ timeoutMinutes: 15,
+ maxRetries: 1,
+ maxReplans: 0,
+ maxAdditionalPackages: 0,
+ maxConcurrentSolUltra: 2
+});
+```
+
+Score 6 returns Roots 4, parallelism 3, children 2, 30 minutes. Score 9 returns Roots 6, parallelism 3, children 3, 60 minutes. Clamp parallelism by `config.workers.workspacePoolSize` and roots by `globalTopLevelLimit`.
+
+- [ ] **Step 2: Write failing scheduler tests**
+
+Cover:
+
+```js
+- packages with no dependencies become ready;
+- dependent packages remain planned until every dependency is completed or partial;
+- failed required dependencies mark downstream packages blocked;
+- cancelling one package does not cancel independent branches;
+- all completed packages finalize as completed;
+- completed plus optional blocked packages finalize as completed-with-omissions;
+- any failed required package finalizes as degraded when another package completed;
+- all failed/blocked with no usable result finalizes as failed;
+- no transition may move a terminal package back to running.
+```
+
+Add `optional: false` to normalized packages, defaulting to false when omitted. The plan schema may document `optional` as a boolean.
+
+- [ ] **Step 3: Run tests and confirm failure**
+
+```bash
+node --test tests/orchestration-scheduler.test.mjs tests/orchestration-contracts.test.mjs
+```
+
+- [ ] **Step 4: Implement `budget-policy.mjs`**
+
+Use a table, not nested ad hoc conditionals:
+
+```js
+const ENVELOPES = [
+ { min: 0, max: 2, maxTopLevelRoots: 1, workerParallelism: 1, maxNativeChildrenPerRoot: 0, timeoutMinutes: 15 },
+ { min: 3, max: 4, maxTopLevelRoots: 2, workerParallelism: 2, maxNativeChildrenPerRoot: 1, timeoutMinutes: 15 },
+ { min: 5, max: 7, maxTopLevelRoots: 4, workerParallelism: 3, maxNativeChildrenPerRoot: 2, timeoutMinutes: 30 },
+ { min: 8, max: 10, maxTopLevelRoots: 6, workerParallelism: 3, maxNativeChildrenPerRoot: 3, timeoutMinutes: 60 }
+];
+```
+
+Return the fixed caps shown in Step 1 and clamp by configuration.
+
+- [ ] **Step 5: Implement immutable scheduler state**
+
+State shape:
+
+```js
+{
+ packages: {
+ [packageId]: {
+ id: packageId,
+ status: "planned",
+ dependencies: [...],
+ optional: false,
+ attempt: 0
+ }
+ }
+}
+```
+
+Export:
+
+```js
+export function markPackageReady(state, id) {}
+export function markPackageRunning(state, id, attempt) {}
+export function markPackageCompleted(state, id, resultStatus = "completed") {}
+export function markPackageFailed(state, id, error) {}
+export function markPackageCancelled(state, id) {}
+export function propagateBlockedPackages(state) {}
+export function deriveOrchestrationStatus(state) {}
+```
+
+Every transition validates the source status and returns a new state object.
+
+- [ ] **Step 6: Replace temporary budget validation in `plan-contract.mjs`**
+
+Import `deriveBudgetEnvelope` and `validatePlanAgainstBudget`. Store the resulting envelope in the normalized plan:
+
+```js
+return deepFreeze({
+ ...normalized,
+ budget: deriveBudgetEnvelope(normalized.complexityScore, context.config)
+});
+```
+
+- [ ] **Step 7: Run focused tests**
+
+```bash
+node --test tests/orchestration-scheduler.test.mjs tests/orchestration-contracts.test.mjs
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/budget-policy.mjs plugins/codex/scripts/orchestration/scheduler.mjs plugins/codex/scripts/orchestration/plan-contract.mjs plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json tests/orchestration-scheduler.test.mjs tests/orchestration-contracts.test.mjs
+git commit -m "feat: add orchestration DAG scheduler"
+```
+
+---
+
+### Task 6: Make Codex Turn Execution Reusable by Long-Lived Direct Workers
+
+**Files:**
+- Modify: `plugins/codex/scripts/lib/codex.mjs`
+- Modify: `plugins/codex/scripts/lib/app-server-protocol.d.ts`
+- Modify: `tests/runtime.test.mjs`
+- Modify: `tsconfig.app-server.json`
+
+**Interfaces:**
+- Produces: `runAppServerTurnWithClient(client, cwd, options): Promise`
+- Extends `TurnResult` with `threadIds`, `nativeChildThreadIds`, and `nativeChildPeak`.
+- Preserves: `runAppServerTurn(cwd, options)` behavior and output fields.
+
+- [ ] **Step 1: Add a failing test for caller-owned clients**
+
+In `tests/runtime.test.mjs`, add a test that:
+
+1. installs fake Codex;
+2. opens one `CodexAppServerClient.connect(cwd, { disableBroker: true })`;
+3. calls `runAppServerTurnWithClient` twice sequentially;
+4. asserts `appServerStarts === 1`;
+5. closes the client explicitly.
+
+Use:
+
+```js
+const client = await CodexAppServerClient.connect(repo, { disableBroker: true, env });
+try {
+ const first = await runAppServerTurnWithClient(client, repo, {
+ prompt: "first",
+ model: "gpt-5.6-luna",
+ effort: "high",
+ sandbox: "read-only"
+ });
+ const second = await runAppServerTurnWithClient(client, repo, {
+ prompt: "second",
+ model: "gpt-5.6-terra",
+ effort: "high",
+ sandbox: "read-only"
+ });
+ assert.notEqual(first.threadId, second.threadId);
+} finally {
+ await client.close();
+}
+```
+
+- [ ] **Step 2: Add a failing native-child topology test**
+
+Run fake behavior `with-subagent` and assert:
+
+```js
+assert.equal(result.threadIds.includes(result.threadId), true);
+assert.equal(result.nativeChildThreadIds.length, 1);
+assert.equal(result.nativeChildPeak, 1);
+```
+
+Also collect progress events and assert one event has:
+
+```js
+{
+ eventType: "native-child-started",
+ parentThreadId: result.threadId,
+ threadId: result.nativeChildThreadIds[0]
+}
+```
+
+- [ ] **Step 3: Run the focused runtime tests and confirm failure**
+
+```bash
+node --test tests/runtime.test.mjs --test-name-pattern "caller-owned|native-child topology"
+```
+
+- [ ] **Step 4: Extend capture state and progress metadata**
+
+In `createTurnCaptureState`, add:
+
+```js
+nativeChildThreadIds: new Set(),
+nativeChildPeak: 0,
+```
+
+On non-root `thread/started` and `turn/started`, register the child and emit:
+
+```js
+emitProgress(state.onProgress, `Native child started (${childId}).`, "investigating", {
+ eventType: "native-child-started",
+ threadId: childId,
+ parentThreadId: state.threadId,
+ agentNickname: message.params.thread.agentNickname ?? null,
+ agentRole: message.params.thread.agentRole ?? null
+});
+```
+
+On non-root `turn/completed`, emit `native-child-completed`. Update `nativeChildPeak` from `activeSubagentTurns.size`.
+
+- [ ] **Step 5: Extract `runAppServerTurnWithClient`**
+
+Move the body currently inside `withAppServer(cwd, async (client) => { ... })` into:
+
+```js
+export async function runAppServerTurnWithClient(client, cwd, options = {}) {
+ // Existing thread start/resume, model validation, captureTurn, and result building.
+}
+```
+
+Return:
+
+```js
+{
+ status,
+ threadId,
+ turnId,
+ threadIds: [...turnState.threadIds],
+ nativeChildThreadIds: [...turnState.nativeChildThreadIds],
+ nativeChildPeak: turnState.nativeChildPeak,
+ finalMessage,
+ reasoningSummary,
+ turn,
+ error,
+ stderr,
+ fileChanges,
+ touchedFiles,
+ commandExecutions
+}
+```
+
+Then keep the existing public wrapper:
+
+```js
+export async function runAppServerTurn(cwd, options = {}) {
+ const availability = getCodexAvailability(cwd);
+ if (!availability.available) {
+ throw new Error(/* existing message */);
+ }
+ return withAppServer(cwd, (client) => runAppServerTurnWithClient(client, cwd, options));
+}
+```
+
+- [ ] **Step 6: Update JSDoc/type-check scope**
+
+Add `plugins/codex/scripts/orchestration/**/*.mjs` to the `include` array in `tsconfig.app-server.json`. Do not enable `experimentalApi` globally.
+
+- [ ] **Step 7: Run runtime and build checks**
+
+```bash
+node --test tests/runtime.test.mjs
+npm run build
+```
+
+Expected: PASS with all existing runtime tests unchanged.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/lib/codex.mjs plugins/codex/scripts/lib/app-server-protocol.d.ts tests/runtime.test.mjs tsconfig.app-server.json
+git commit -m "refactor: support caller-owned App Server turns"
+```
+
+---
+
+### Task 7: Extend the Fake Codex Fixture for Parallel Orchestration
+
+**Files:**
+- Modify: `tests/fake-codex-fixture.mjs`
+- Modify: `tests/helpers.mjs`
+- Create: `tests/orchestration-worker.test.mjs`
+
+**Interfaces:**
+- Extends: `installFakeCodex(binDir, behavior, version, options?)`
+- Produces: `readFakeCodexEvents(binDir): object[]`
+- Adds behavior: `orchestration-read-only`, `orchestration-long-running`, `orchestration-transient-once`.
+
+- [ ] **Step 1: Add failing fixture tests**
+
+In `tests/orchestration-worker.test.mjs`, first test the fixture directly by spawning two direct App Servers and asserting the event log contains two `turn-started` events before either `turn-completed` event.
+
+Use per-prompt markers:
+
+```text
+pkg-a
+250
+```
+
+- [ ] **Step 2: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-worker.test.mjs --test-name-pattern "fixture records overlapping turns"
+```
+
+- [ ] **Step 3: Add append-only fake event logging**
+
+Inside the generated fake `codex` script, define:
+
+```js
+const EVENTS_PATH = path.join(path.dirname(STATE_PATH), "fake-codex-events.jsonl");
+
+function appendEvent(event) {
+ fs.appendFileSync(
+ EVENTS_PATH,
+ JSON.stringify({ timestamp: Date.now(), pid: process.pid, ...event }) + "\n",
+ "utf8"
+ );
+}
+```
+
+Record `app-server-started`, `thread-started`, `turn-started`, `turn-completed`, and `turn-interrupted`.
+
+- [ ] **Step 4: Generate canonical package results**
+
+When `turn/start.outputSchema` contains a `packageId` property, return:
+
+```js
+const packageIdMatch = prompt.match(/([^<]+)<\/orchestration_package_id>/);
+const packageId = packageIdMatch ? packageIdMatch[1].trim() : "pkg-unknown";
+const payload = JSON.stringify({
+ packageId,
+ status: "completed",
+ summary: `Completed read-only analysis for ${packageId}.`,
+ claims: [`Claim from ${packageId}`],
+ evidence: [
+ {
+ type: "observation",
+ description: `Observed repository state for ${packageId}.`,
+ path: null,
+ lineStart: null,
+ lineEnd: null,
+ command: null,
+ exitCode: null
+ }
+ ],
+ changedFiles: [],
+ verification: { passed: true, commands: [] },
+ residualRisks: [],
+ confidence: 0.8,
+ followUpRequests: []
+});
+```
+
+- [ ] **Step 5: Add delayed, interruptible, and transient-once behavior**
+
+- parse `` and delay completion;
+- store active timers in `interruptibleTurns`;
+- on `turn/interrupt`, clear the timer and emit a cancelled turn;
+- for `orchestration-transient-once`, persist a per-package attempt counter and terminate the first App Server process during the first attempt only.
+
+- [ ] **Step 6: Export event-reading helper**
+
+Outside the generated script, export:
+
+```js
+export function readFakeCodexEvents(binDir) {
+ const eventFile = path.join(binDir, "fake-codex-events.jsonl");
+ if (!fs.existsSync(eventFile)) {
+ return [];
+ }
+ return fs.readFileSync(eventFile, "utf8").split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
+}
+```
+
+- [ ] **Step 7: Run fixture tests plus existing runtime tests**
+
+```bash
+node --test tests/orchestration-worker.test.mjs tests/runtime.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add tests/fake-codex-fixture.mjs tests/helpers.mjs tests/orchestration-worker.test.mjs
+git commit -m "test: simulate parallel Codex workers"
+```
+
+---
+
+### Task 8: Implement Read-Only Package Prompts, Event Routing, and Worker Runtime
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/package-prompt.mjs`
+- Create: `plugins/codex/scripts/orchestration/event-router.mjs`
+- Create: `plugins/codex/scripts/orchestration/worker-runtime.mjs`
+- Modify: `tests/orchestration-worker.test.mjs`
+
+**Interfaces:**
+- Produces: `buildReadOnlyPackagePrompt(packageSpec, context): string`
+- Produces: `createPackageEventRouter(options): (progressEvent) => void`
+- Produces: `class OrchestrationWorker`
+- `OrchestrationWorker.run(packageSpec, options): Promise`
+- `OrchestrationWorker.interrupt(): Promise<{ attempted, interrupted }>`
+
+- [ ] **Step 1: Add failing prompt boundary tests**
+
+Assert the prompt includes:
+
+```text
+pkg-cache
+Read-only. Do not modify files...
+...
+...
+...
+allowed; maximum 1 child...
+Return one JSON object matching the supplied schema...
+```
+
+Assert it explicitly forbids push, deploy, publish, remote mutation, credential changes, and package creation outside the assigned objective.
+
+- [ ] **Step 2: Add failing worker execution tests**
+
+Tests must assert:
+
+- the worker opens one direct App Server process and reuses it for sequential packages;
+- each package receives the selected model and effort;
+- output parses through `validatePackageResult`;
+- `touchedFiles` is empty or execution fails with a Phase 1 boundary violation;
+- native child IDs and peak are returned;
+- exceeding `maxChildren` interrupts and fails the package;
+- `interrupt()` sends `turn/interrupt` to the active thread/turn.
+
+- [ ] **Step 3: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-worker.test.mjs
+```
+
+- [ ] **Step 4: Implement the package prompt builder**
+
+Use deterministic XML blocks. Do not include hidden Claude reasoning or unrelated conversation context. Include dependency result summaries only when the package depends on completed packages:
+
+```js
+export function buildReadOnlyPackagePrompt(packageSpec, context = {}) {
+ const dependencySummaries = context.dependencyResults ?? [];
+ return [
+ `${escapeXml(packageSpec.id)}`,
+ `${escapeXml(packageSpec.role.class)}: ${escapeXml(packageSpec.role.label)}`,
+ `${escapeXml(packageSpec.objective)}`,
+ `Read-only. Do not modify files, create commits, change credentials, push, publish, deploy, or mutate remote systems.`,
+ `${escapeXml(JSON.stringify(dependencySummaries))}`,
+ `${escapeXml(JSON.stringify(packageSpec.acceptanceCriteria))}`,
+ `${escapeXml(`${packageSpec.nativeSubagents.policy}; maximum ${packageSpec.nativeSubagents.maxChildren} child agents`)}`,
+ `Run only non-destructive checks needed to support the claims. Record exact commands and exit codes.`,
+ `Return exactly one JSON object matching the supplied package-result schema. changedFiles must be an empty array.`
+ ].join("\n\n");
+}
+```
+
+- [ ] **Step 5: Implement event routing**
+
+`createPackageEventRouter` receives `{ orchestrationId, packageId, maxChildren, onEvent, onLimitExceeded }` and normalizes string or object progress events into:
+
+```js
+{
+ type: "package-progress" | "native-child-started" | "native-child-completed" | "package-log",
+ phase,
+ message,
+ packageId,
+ threadId,
+ turnId,
+ childThreadId,
+ activeNativeChildren,
+ nativeChildPeak
+}
+```
+
+Call `onLimitExceeded` once when observed active children exceed `maxChildren`.
+
+- [ ] **Step 6: Implement `OrchestrationWorker`**
+
+Constructor:
+
+```js
+new OrchestrationWorker({
+ id,
+ workspaceRoot,
+ env = process.env,
+ onEvent = () => {},
+ clientFactory = (cwd, options) => CodexAppServerClient.connect(cwd, options)
+})
+```
+
+Methods:
+
+```js
+async start() {
+ this.client = await this.clientFactory(this.workspaceRoot, { disableBroker: true, env: this.env });
+}
+
+async run(packageSpec, options = {}) {
+ if (this.active) throw new Error(`Worker ${this.id} is already running a package.`);
+ // Build prompt, capture thread/turn IDs from progress, enforce child limit,
+ // call runAppServerTurnWithClient with read-only sandbox and output schema,
+ // reject touched files, parse JSON, return normalized result.
+}
+
+async interrupt() {
+ if (!this.active?.threadId || !this.active?.turnId) return { attempted: false, interrupted: false };
+ await this.client.request("turn/interrupt", {
+ threadId: this.active.threadId,
+ turnId: this.active.turnId
+ });
+ return { attempted: true, interrupted: true };
+}
+
+async close() {
+ await this.client?.close();
+ this.client = null;
+}
+```
+
+Use `parseStructuredOutput` followed by `validatePackageResult`. Preserve raw output and parse error in the failure object.
+
+- [ ] **Step 7: Run worker tests and build**
+
+```bash
+node --test tests/orchestration-worker.test.mjs
+npm run build
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/package-prompt.mjs plugins/codex/scripts/orchestration/event-router.mjs plugins/codex/scripts/orchestration/worker-runtime.mjs tests/orchestration-worker.test.mjs
+git commit -m "feat: execute read-only orchestration packages"
+```
+
+---
+
+### Task 9: Implement Global Worker Leases and the Workspace Worker Pool
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/global-worker-registry.mjs`
+- Create: `plugins/codex/scripts/orchestration/worker-pool.mjs`
+- Create: `tests/orchestration-pool.test.mjs`
+
+**Interfaces:**
+- Produces: `acquireGlobalWorkerLease(options): Promise`
+- Produces: `releaseGlobalWorkerLease(lease): Promise`
+- Produces: `class WorkerPool`
+- `WorkerPool.acquire(packageId): Promise`
+- `WorkerPool.release(worker): void`
+- `WorkerPool.cancel(packageId): Promise`
+- `WorkerPool.getSnapshot(): WorkerPoolSnapshot`
+
+- [ ] **Step 1: Write failing global registry tests**
+
+Use separate fake PIDs/lease IDs and assert:
+
+- eight leases are accepted when limit is 8;
+- the ninth is rejected with `Global Codex worker limit 8 reached`;
+- leases owned by a dead process are pruned;
+- releasing a lease allows another acquisition;
+- registry JSON remains valid under concurrent acquisitions.
+
+- [ ] **Step 2: Write failing pool tests with a fake worker factory**
+
+Fake worker:
+
+```js
+class FakeWorker {
+ constructor(id) {
+ this.id = id;
+ this.started = false;
+ this.closed = false;
+ }
+ async start() { this.started = true; }
+ async interrupt() { return { attempted: true, interrupted: true }; }
+ async close() { this.closed = true; }
+}
+```
+
+Assert:
+
+- pool lazily creates up to configured size;
+- a fourth acquire waits when size is 3;
+- releasing a worker resolves the oldest waiter;
+- idle workers are reused;
+- idle TTL closes unused workers;
+- `cancel(packageId)` targets the leased worker;
+- `close()` rejects pending waiters and closes every worker.
+
+- [ ] **Step 3: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-pool.test.mjs
+```
+
+- [ ] **Step 4: Implement global lease storage**
+
+Store under:
+
+```text
+/orchestrations/_global/workers.json
+/orchestrations/_global/workers.lock
+```
+
+Lease shape:
+
+```js
+{
+ id: `worker-${process.pid}-${crypto.randomUUID()}`,
+ pid: process.pid,
+ workspaceKey,
+ workerId,
+ acquiredAt: new Date().toISOString(),
+ heartbeatAt: new Date().toISOString()
+}
+```
+
+Use `withFileLock`. Prune leases whose PID is dead before enforcing the limit.
+
+- [ ] **Step 5: Implement `WorkerPool`**
+
+Constructor:
+
+```js
+new WorkerPool({
+ workspaceRoot,
+ size,
+ globalTopLevelLimit,
+ idleTtlMs,
+ workerFactory,
+ onEvent
+})
+```
+
+Internal maps:
+
+```js
+this.workers = new Map();
+this.idleWorkerIds = [];
+this.packageLeases = new Map();
+this.waiters = [];
+```
+
+Create workers only when no idle worker exists and `workers.size < size`. Acquire the global lease before `worker.start()`; release it if start fails.
+
+- [ ] **Step 6: Add active Codex accounting hooks**
+
+The pool receives worker events and tracks:
+
+```js
+activeTopLevelRoots = packageLeases.size;
+activeNativeChildren = sum(worker.nativeChildActive);
+activeCodex = activeTopLevelRoots + activeNativeChildren;
+```
+
+If `activeCodex` exceeds configured `globalActiveCodexLimit`, call the offending worker's `interrupt()` and emit `active-codex-limit-exceeded`.
+
+- [ ] **Step 7: Run pool tests**
+
+```bash
+node --test tests/orchestration-pool.test.mjs
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/global-worker-registry.mjs plugins/codex/scripts/orchestration/worker-pool.mjs tests/orchestration-pool.test.mjs
+git commit -m "feat: add bounded orchestration worker pool"
+```
+
+---
+
+### Task 10: Implement the Deterministic Orchestration Controller
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/controller.mjs`
+- Create: `tests/orchestration-controller.test.mjs`
+
+**Interfaces:**
+- Produces: `class OrchestrationController`
+- `start(planInput, context): Promise`
+- `status(reference): Promise`
+- `result(reference): Promise`
+- `cancel(reference): Promise`
+- `shutdown(): Promise`
+
+- [ ] **Step 1: Write failing controller tests with an injected fake pool**
+
+Create a deterministic fake pool whose `run` returns package results after controlled promises. Test:
+
+1. two independent packages start before either completes;
+2. the dependent package starts only after both dependencies complete;
+3. package events persist to `events.jsonl`;
+4. a transient worker failure retries once on a different attempt;
+5. a second transient failure marks the package failed and blocks dependents;
+6. cancelling a package interrupts only that package and blocks its dependents;
+7. cancelling the orchestration cancels all running packages and prevents queued packages from starting;
+8. final state/result is durable and can be read by a fresh controller instance.
+
+- [ ] **Step 2: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-controller.test.mjs
+```
+
+- [ ] **Step 3: Implement controller construction and plan acceptance**
+
+Constructor:
+
+```js
+new OrchestrationController({
+ workspaceRoot,
+ config,
+ pool,
+ stateStore,
+ now = () => new Date(),
+ onMilestone = () => {}
+})
+```
+
+`start`:
+
+```js
+async start(planInput, context = {}) {
+ const plan = normalizeOrchestrationPlan(planInput, {
+ workspaceRoot: this.workspaceRoot,
+ config: this.config
+ });
+ const state = await this.stateStore.create(plan, context);
+ this.runOrchestration(state.id).catch((error) => this.failControllerRun(state.id, error));
+ return this.buildSummary(state.id);
+}
+```
+
+Return immediately after the state is accepted and background execution is started.
+
+- [ ] **Step 4: Implement the scheduling loop**
+
+Maintain:
+
+```js
+this.activeRuns = new Map();
+this.runningPackages = new Map();
+```
+
+The loop must:
+
+1. load current state;
+2. derive ready packages;
+3. launch up to `plan.budget.workerParallelism` packages;
+4. wait for one package promise to settle;
+5. update scheduler and durable state;
+6. propagate blocked packages;
+7. repeat until terminal;
+8. write final aggregate result.
+
+Dependency results passed to a package contain only normalized result summaries and evidence—not raw hidden reasoning.
+
+- [ ] **Step 5: Implement package execution and transient retry**
+
+Transient errors are limited to:
+
+```js
+const TRANSIENT_CODES = new Set(["EPIPE", "ECONNRESET", "ECONNREFUSED", "ENOENT", "CODEX_WORKER_EXIT"]);
+```
+
+Retry exactly once when the error code matches or the error has `transient === true`. Increment package attempt before each run. Do not retry schema errors, model/effort validation errors, read-only boundary violations, or package-reported `failed` status.
+
+- [ ] **Step 6: Implement status/result/cancel**
+
+`status` returns orchestration summary plus package state, pool snapshot, elapsed time, and latest milestones.
+
+`result` returns `buildOrchestrationResult(state)` only for terminal orchestrations. For active orchestrations, throw:
+
+```text
+Orchestration is still running. Use /codex:status .
+```
+
+`cancel` behavior:
+
+- exact package reference: mark `cancelling`, interrupt through pool, wait up to `DEFAULT_CANCEL_GRACE_MS`, then close the worker if still active; mark package `cancelled`; propagate blocked dependents;
+- orchestration reference: mark orchestration `cancelling`, cancel every running package, mark unstarted packages `cancelled`, then finalize `cancelled`.
+
+- [ ] **Step 7: Run controller tests**
+
+```bash
+node --test tests/orchestration-controller.test.mjs
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/controller.mjs tests/orchestration-controller.test.mjs
+git commit -m "feat: add orchestration controller"
+```
+
+---
+
+### Task 11: Add Workspace Controller IPC and Lifecycle Management
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/ipc.mjs`
+- Create: `plugins/codex/scripts/orchestration/controller-lifecycle.mjs`
+- Create: `plugins/codex/scripts/orchestration/controller-client.mjs`
+- Create: `plugins/codex/scripts/orchestration/controller-server.mjs`
+- Create: `tests/orchestration-ipc.test.mjs`
+
+**Interfaces:**
+- Produces: `createControllerEndpoint(runtimeDir, platform?)`
+- Produces: `parseControllerEndpoint(endpoint)`
+- Produces: `ensureControllerServer(workspaceRoot, options): Promise`
+- Produces: `class OrchestrationControllerClient`
+- Protocol methods: `orchestration/start`, `orchestration/status`, `orchestration/result`, `orchestration/cancel`, `controller/status`, `controller/shutdown`.
+
+- [ ] **Step 1: Write endpoint tests**
+
+Assert Unix endpoint:
+
+```text
+unix:/controller.sock
+```
+
+Assert Windows endpoint starts with:
+
+```text
+pipe:\\.\pipe\-codex-orchestrator
+```
+
+- [ ] **Step 2: Write a failing detached lifecycle integration test**
+
+The test must:
+
+1. create a temporary workspace and plugin data directory;
+2. call `ensureControllerServer` twice concurrently;
+3. assert both return the same endpoint/PID;
+4. connect with `OrchestrationControllerClient` and call `controller/status`;
+5. request `controller/shutdown`;
+6. assert the process exits and runtime state is removed.
+
+- [ ] **Step 3: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-ipc.test.mjs
+```
+
+- [ ] **Step 4: Implement JSONL IPC helpers**
+
+`ipc.mjs` must provide one request per client connection for simplicity:
+
+```js
+export async function requestController(endpoint, method, params = {}, options = {}) {
+ // Connect, send { id: 1, method, params } + newline, wait for matching response,
+ // enforce timeout, close socket, convert JSON-RPC error to Error with rpcCode.
+}
+```
+
+Use `net.createConnection({ path })` for both Unix sockets and named pipes.
+
+- [ ] **Step 5: Implement controller lifecycle state**
+
+Runtime directory:
+
+```text
+/_controller/
+ controller.json
+ controller.lock
+ controller.sock (Unix only)
+```
+
+`controller.json`:
+
+```js
+{
+ version: 1,
+ pid,
+ endpoint,
+ workspaceRoot,
+ pluginVersion,
+ startedAt
+}
+```
+
+`ensureControllerServer` must run under `withFileLock(controller.lock)`, probe an existing endpoint, remove stale state, spawn detached `controller-server.mjs`, and poll until `controller/status` succeeds.
+
+- [ ] **Step 6: Implement controller client methods**
+
+```js
+export class OrchestrationControllerClient {
+ constructor(endpoint) { this.endpoint = endpoint; }
+ start(plan, context) { return requestController(this.endpoint, "orchestration/start", { plan, context }); }
+ status(reference = "") { return requestController(this.endpoint, "orchestration/status", { reference }); }
+ result(reference = "") { return requestController(this.endpoint, "orchestration/result", { reference }); }
+ cancel(reference) { return requestController(this.endpoint, "orchestration/cancel", { reference }); }
+ shutdown() { return requestController(this.endpoint, "controller/shutdown", {}); }
+}
+```
+
+- [ ] **Step 7: Implement `controller-server.mjs`**
+
+Server startup arguments:
+
+```text
+serve --workspace --endpoint --runtime-file
+```
+
+The server:
+
+- loads effective config;
+- constructs `WorkerPool` and `OrchestrationController`;
+- handles one newline-delimited request at a time per socket;
+- responds before long orchestration execution completes;
+- refuses `controller/shutdown` while active orchestrations exist unless `force: true`;
+- exits after configured controller idle TTL only when no active orchestration and no worker lease remain;
+- removes Unix socket/runtime state on clean exit.
+
+- [ ] **Step 8: Run IPC tests**
+
+```bash
+node --test tests/orchestration-ipc.test.mjs
+```
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/ipc.mjs plugins/codex/scripts/orchestration/controller-lifecycle.mjs plugins/codex/scripts/orchestration/controller-client.mjs plugins/codex/scripts/orchestration/controller-server.mjs tests/orchestration-ipc.test.mjs
+git commit -m "feat: add orchestration controller IPC"
+```
+
+---
+
+### Task 12: Add the Orchestration CLI and Legacy Companion Adapter
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/cli.mjs`
+- Create: `plugins/codex/scripts/orchestration/companion-adapter.mjs`
+- Modify: `plugins/codex/scripts/codex-companion.mjs`
+- Modify: `plugins/codex/scripts/lib/job-control.mjs`
+- Create: `tests/orchestration-runtime.test.mjs`
+- Modify: `tests/runtime.test.mjs`
+
+**Interfaces:**
+- CLI: `start --plan-file [--cwd ] [--json]`
+- CLI: `status [reference] [--cwd ] [--json]`
+- CLI: `result [reference] [--cwd ] [--json]`
+- CLI: `cancel [--cwd ] [--json]`
+- Adapter: `isOrchestrationReference`, `listOrchestrationStatus`, `getOrchestrationStatus`, `getOrchestrationResult`, `cancelOrchestration`.
+
+- [ ] **Step 1: Add failing CLI tests**
+
+Test direct CLI subprocess behavior:
+
+```bash
+node plugins/codex/scripts/orchestration/cli.mjs start --cwd --plan-file --json
+```
+
+Assert JSON includes:
+
+```js
+{
+ orchestrationId: /^orch-/,
+ status: "queued" | "running",
+ objective,
+ packageCount: 2,
+ commands: {
+ status: `/codex:status ${id}`,
+ result: `/codex:result ${id}`,
+ cancel: `/codex:cancel ${id}`
+ }
+}
+```
+
+- [ ] **Step 2: Add failing legacy companion routing tests**
+
+Test:
+
+- `codex-companion status orch-... --json` returns orchestration status;
+- `status` with no reference returns both legacy jobs and orchestration summaries;
+- `result pkg-... --json` returns the package result;
+- `cancel orch-... --json` routes to the controller;
+- unknown non-orchestration references still use legacy job errors.
+
+- [ ] **Step 3: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-runtime.test.mjs tests/runtime.test.mjs
+```
+
+- [ ] **Step 4: Implement `cli.mjs`**
+
+Use existing `parseArgs` and `readStdinIfPiped`. `start` accepts exactly one plan source:
+
+- `--plan-file`;
+- piped JSON.
+
+Reject positional natural language; Claude must create the structured plan first.
+
+`start` flow:
+
+```js
+const workspaceRoot = resolveWorkspaceRoot(cwd);
+const session = await ensureControllerServer(workspaceRoot, { env: process.env });
+const client = new OrchestrationControllerClient(session.endpoint);
+const plan = JSON.parse(planText);
+const summary = await client.start(plan, {
+ claudeSessionId: process.env.CODEX_COMPANION_SESSION_ID ?? null
+});
+```
+
+- [ ] **Step 5: Implement companion adapter**
+
+Reference detection must not rely only on prefixes because package IDs are user-defined. Resolve against durable orchestration state first; return `null` when no match so legacy job resolution can continue.
+
+```js
+export async function tryResolveOrchestrationReference(cwd, reference) {
+ try {
+ return resolveOrchestrationReference(resolveWorkspaceRoot(cwd), reference);
+ } catch (error) {
+ if (/No orchestration or package found/.test(error.message)) return null;
+ throw error;
+ }
+}
+```
+
+- [ ] **Step 6: Extend `codex-companion` handlers**
+
+`handleStatus`:
+
+- with a reference: try orchestration first, then legacy job;
+- without a reference: add `orchestrations` to `buildStatusSnapshot` output;
+- `--wait` remains legacy-job-only in Phase 1; reject `--wait` for orchestration references with a precise message.
+
+`handleResult` and `handleCancel` follow the same orchestration-first, legacy-fallback order.
+
+- [ ] **Step 7: Run routing tests**
+
+```bash
+node --test tests/orchestration-runtime.test.mjs tests/runtime.test.mjs
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/scripts/orchestration/cli.mjs plugins/codex/scripts/orchestration/companion-adapter.mjs plugins/codex/scripts/codex-companion.mjs plugins/codex/scripts/lib/job-control.mjs tests/orchestration-runtime.test.mjs tests/runtime.test.mjs
+git commit -m "feat: route orchestration commands"
+```
+
+---
+
+### Task 13: Add Orchestration Renderers and Combined Status Output
+
+**Files:**
+- Modify: `plugins/codex/scripts/lib/render.mjs`
+- Modify: `tests/render.test.mjs`
+- Modify: `tests/orchestration-runtime.test.mjs`
+
+**Interfaces:**
+- Produces: `renderOrchestrationLaunch(summary)`
+- Produces: `renderOrchestrationStatus(snapshot)`
+- Produces: `renderOrchestrationResult(result)`
+- Produces: `renderOrchestrationCancel(snapshot)`
+- Extends: `renderStatusReport(report)` with an orchestration table.
+
+- [ ] **Step 1: Add failing renderer tests**
+
+Expected status table:
+
+```markdown
+Active orchestrations:
+| Orchestration | Status | Packages | Running | Completed | Failed | Elapsed | Objective | Actions |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- |
+```
+
+Package detail table:
+
+```markdown
+| Package | Role | Model / Effort | Status | Worker | Codex Session ID | Native Children | Summary |
+```
+
+Result output must include:
+
+- orchestration ID and final status;
+- each package's complete normalized summary, claims, evidence, verification, residual risks, and confidence;
+- omissions and remaining work;
+- no reasoning summary sections.
+
+- [ ] **Step 2: Run and confirm failure**
+
+```bash
+node --test tests/render.test.mjs
+```
+
+- [ ] **Step 3: Implement orchestration renderers**
+
+`renderOrchestrationLaunch`:
+
+```text
+Multi-Codex orchestration started.
+Status:
+Packages:
+Status: /codex:status
+Result: /codex:result
+Cancel: /codex:cancel
+```
+
+`renderOrchestrationStatus` must show milestone progress but not raw command logs unless a package failed.
+
+`renderOrchestrationResult` must render evidence entries as:
+
+```text
+- [file] src/cache.mjs:42-58 — Cache generation is not invalidated after config changes.
+- [command] npm test -- cache (exit 0) — Regression test reproduced the stale value.
+```
+
+- [ ] **Step 4: Extend combined legacy status**
+
+Add an orchestration section before active jobs. Preserve the existing job table exactly when there are no orchestrations.
+
+- [ ] **Step 5: Run render and runtime tests**
+
+```bash
+node --test tests/render.test.mjs tests/orchestration-runtime.test.mjs
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add plugins/codex/scripts/lib/render.mjs tests/render.test.mjs tests/orchestration-runtime.test.mjs
+git commit -m "feat: render orchestration status and results"
+```
+
+---
+
+### Task 14: Add the Claude Command and Skill Policy Surface
+
+**Files:**
+- Create: `plugins/codex/commands/orchestrate.md`
+- Modify: `plugins/codex/commands/status.md`
+- Modify: `plugins/codex/commands/result.md`
+- Modify: `plugins/codex/commands/cancel.md`
+- Create: `plugins/codex/skills/codex-orchestration/SKILL.md`
+- Create: `plugins/codex/skills/codex-work-package-contract/SKILL.md`
+- Create: `plugins/codex/skills/codex-integration-policy/SKILL.md`
+- Create: `plugins/codex/skills/codex-orchestration-recovery/SKILL.md`
+- Create: `tests/orchestration-skill.test.mjs`
+- Modify: `tests/commands.test.mjs`
+
+**Interfaces:**
+- `/codex:orchestrate ` causes Claude Root—not a forwarding subagent—to inspect the repository, construct a plan JSON, issue the compressed plan notification, and start the controller.
+- `codex-orchestration` is the only auto-trigger orchestration skill.
+- Internal policy skills use `user-invocable: false` and narrow descriptions.
+
+- [ ] **Step 1: Add failing command/skill tests**
+
+Assert:
+
+- `orchestrate.md` exists and is included in the command file list;
+- it does not invoke `codex:codex-rescue` or any general-purpose planning subagent;
+- it requires a canonical plan file and calls `orchestration/cli.mjs start`;
+- it requires a compressed 3–6 line plan notification before start;
+- it states that local read-only execution begins without waiting for approval;
+- the root skill contains hard exclusions and the ten-point Complexity Score;
+- score 5+ prefers auto orchestration when enabled;
+- Phase 1 rejects write packages;
+- Root/native-child ownership is explicit;
+- `Sol > Terra > Luna` and effort separation are explicit;
+- internal skills are non-user-invocable;
+- recovery skill states that controller restart auto-resume is deferred to Phase 3.
+
+- [ ] **Step 2: Run and confirm failure**
+
+```bash
+node --test tests/orchestration-skill.test.mjs tests/commands.test.mjs
+```
+
+- [ ] **Step 3: Write `orchestrate.md`**
+
+Frontmatter:
+
+```yaml
+---
+description: Plan and start a Claude-managed read-only Multi-Codex orchestration
+argument-hint: ''
+allowed-tools: Read, Glob, Grep, Write, Bash(node:*), Bash(git:*)
+---
+```
+
+Required body sequence:
+
+1. Load and follow `codex-orchestration`.
+2. Inspect only enough repository context to identify package boundaries and acceptance criteria.
+3. Refuse Phase 1 writer packages and explain that write orchestration arrives in Phase 2.
+4. Write canonical JSON to a temporary file under `${TMPDIR:-/tmp}` with a collision-safe name.
+5. Show the compressed plan notification.
+6. Run:
+
+```bash
+node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/cli.mjs" start --cwd "$PWD" --plan-file ""
+```
+
+7. Delete the temporary plan file after the start command returns.
+8. Return the launch output without inventing completion claims.
+
+- [ ] **Step 4: Write the root orchestration skill**
+
+The skill must include:
+
+```yaml
+---
+name: codex-orchestration
+description: Use for an explicit Multi-Codex request or, when automatic orchestration is enabled, for repository work with multiple genuinely independent packages that meets the Complexity Score threshold
+user-invocable: false
+---
+```
+
+It must define:
+
+- hard exclusions;
+- ten scoring factors;
+- score behavior;
+- package role classes;
+- model routing;
+- adaptive budget;
+- dependency contract;
+- read-only Phase 1 restriction;
+- native child policy;
+- exact canonical plan shape;
+- compressed plan notification;
+- controller invocation;
+- evidence-based result interpretation;
+- no automatic claim that queued work is complete.
+
+- [ ] **Step 5: Write the three internal skills**
+
+`codex-work-package-contract` defines bounded objective, dependencies, role, model/effort, max children, acceptance criteria, and expected outputs.
+
+`codex-integration-policy` states that Phase 1 only interprets read-only results and must not apply patches, create branches, or merge changes.
+
+`codex-orchestration-recovery` states:
+
+- live controller reconnect is allowed;
+- durable status/result remain readable after controller exit;
+- an orchestration found `running` without a live controller is reported `failed` with a recovery limitation;
+- automatic restart/resume and orphan reconciliation are Phase 3 work.
+
+- [ ] **Step 6: Update status/result/cancel command copy**
+
+Change hints to `[job-id|orchestration-id|package-id]` and preserve existing Bash entrypoints. Explain combined status and full orchestration/package output.
+
+- [ ] **Step 7: Run skill and command tests**
+
+```bash
+node --test tests/orchestration-skill.test.mjs tests/commands.test.mjs
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add plugins/codex/commands plugins/codex/skills/codex-orchestration plugins/codex/skills/codex-work-package-contract plugins/codex/skills/codex-integration-policy plugins/codex/skills/codex-orchestration-recovery tests/orchestration-skill.test.mjs tests/commands.test.mjs
+git commit -m "feat: add Claude Multi-Codex orchestration skill"
+```
+
+---
+
+### Task 15: Prove Real Parallelism and Full Phase 1 Command Behavior
+
+**Files:**
+- Modify: `tests/orchestration-runtime.test.mjs`
+- Modify: `tests/fake-codex-fixture.mjs`
+- Modify: `tests/runtime.test.mjs`
+
+**Interfaces:**
+- No new production interface; this task is the Phase 1 integration gate.
+
+- [ ] **Step 1: Add the parallel overlap test**
+
+Start an orchestration with two independent 300 ms packages and one dependent package. Read fake events and assert:
+
+```js
+const starts = events.filter((event) => event.type === "turn-started" && ["pkg-a", "pkg-b"].includes(event.packageId));
+const completes = events.filter((event) => event.type === "turn-completed" && ["pkg-a", "pkg-b"].includes(event.packageId));
+assert.equal(starts.length, 2);
+assert.equal(completes.length, 2);
+assert.equal(Math.max(...starts.map((event) => event.timestamp)) < Math.min(...completes.map((event) => event.timestamp)), true);
+```
+
+Also assert at least two distinct App Server PIDs handled the independent packages.
+
+- [ ] **Step 2: Add event isolation assertions**
+
+Give packages distinct claims and assert neither package result contains the other's package ID, claims, thread ID, or events. Assert the dependent package receives only dependency result summaries through its prompt.
+
+- [ ] **Step 3: Add transient retry coverage**
+
+With `orchestration-transient-once`:
+
+- first attempt exits the worker;
+- controller records `package-retry`;
+- second attempt succeeds;
+- package attempt equals 2;
+- unrelated package remains on attempt 1.
+
+- [ ] **Step 4: Add package and whole-orchestration cancellation coverage**
+
+For a long-running package:
+
+```js
+await cancelPackage(pkgId);
+assert.equal(packageState.status, "cancelled");
+assert.equal(events.some((event) => event.type === "turn-interrupted"), true);
+```
+
+For whole cancellation, assert queued packages never emit `turn-started`.
+
+- [ ] **Step 5: Add malformed result and boundary violation coverage**
+
+- malformed JSON marks only that package failed;
+- a fake non-empty `changedFiles` result is rejected;
+- a fake `fileChange` App Server item is rejected even if returned JSON says `changedFiles: []`;
+- dependent packages become blocked;
+- independent packages complete.
+
+- [ ] **Step 6: Run the integration suite repeatedly**
+
+```bash
+for i in 1 2 3; do node --test tests/orchestration-runtime.test.mjs || exit 1; done
+```
+
+Expected: all three runs PASS without leaked controller or App Server processes.
+
+- [ ] **Step 7: Run the full test suite and build**
+
+```bash
+npm test
+npm run build
+git diff --check
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add tests/orchestration-runtime.test.mjs tests/fake-codex-fixture.mjs tests/runtime.test.mjs
+git commit -m "test: verify parallel Multi-Codex orchestration"
+```
+
+---
+
+### Task 16: Document Phase 1 and Finalize the Release-Ready Verification Gate
+
+**Files:**
+- Modify: `README.md`
+- Modify: `plugins/codex/CHANGELOG.md`
+- Modify: `tsconfig.app-server.json`
+- Modify: `.github/workflows/pull-request-ci.yml`
+- Modify: `tests/commands.test.mjs`
+
+**Interfaces:**
+- Documentation only; no new runtime API.
+
+- [ ] **Step 1: Add failing documentation assertions**
+
+In `tests/commands.test.mjs`, assert README includes:
+
+- `/codex:orchestrate`;
+- `read-only Phase 1`;
+- automatic entry enable/disable commands;
+- default pool size 3 and configured range 1–8;
+- top-level limit 8 and active Codex limit 12;
+- status/result/cancel examples with orchestration IDs;
+- explicit statement that writer worktrees and Git integration are Phase 2.
+
+- [ ] **Step 2: Run and confirm failure**
+
+```bash
+node --test tests/commands.test.mjs
+```
+
+- [ ] **Step 3: Update README**
+
+Add sections:
+
+```text
+Claude-Native Multi-Codex Orchestration
+Automatic Entry
+Plan and Package Contract
+Worker and Budget Limits
+Status, Results, and Cancellation
+Phase 1 Read-Only Boundary
+Configuration
+```
+
+Example explicit use:
+
+```bash
+/codex:orchestrate investigate the cache regression and independently challenge the concurrency assumptions
+/codex:status orch-...
+/codex:result orch-...
+/codex:cancel orch-...
+```
+
+Example setup:
+
+```bash
+/codex:setup --enable-orchestration
+/codex:setup --disable-orchestration
+```
+
+- [ ] **Step 4: Update changelog without changing the package version**
+
+Add an `Unreleased` section summarizing Phase 1. Do not introduce a fork or upstream release version bump in this task.
+
+- [ ] **Step 5: Expand CI to a platform matrix**
+
+Change `pull-request-ci.yml` to run Node 22 on:
+
+```yaml
+strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+runs-on: ${{ matrix.os }}
+```
+
+Use platform-neutral commands. Replace shell-specific `mkdir -p` in the `prebuild` package script before enabling Windows CI:
+
+```json
+"prebuild": "node scripts/prepare-generated-dir.mjs && codex app-server generate-ts --out plugins/codex/.generated/app-server-types"
+```
+
+Create `scripts/prepare-generated-dir.mjs`:
+
+```js
+import fs from "node:fs";
+fs.mkdirSync(new URL("../plugins/codex/.generated/app-server-types", import.meta.url), { recursive: true });
+```
+
+Add the file to the task's commit.
+
+- [ ] **Step 6: Run all local gates**
+
+```bash
+npm ci
+npm run check-version
+npm test
+npm run build
+git diff --check
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Inspect for prohibited Phase 2/3 implementation leakage**
+
+Run:
+
+```bash
+git grep -nE 'git worktree add|refs/codex-orchestration/snapshots|integration branch|danger-full-access' -- plugins/codex/scripts/orchestration
+```
+
+Expected: no production implementation of writer worktrees, snapshot refs, integration branches, or write sandbox. Documentation may name later phases, but runtime files must remain read-only.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add README.md plugins/codex/CHANGELOG.md .github/workflows/pull-request-ci.yml package.json scripts/prepare-generated-dir.mjs tests/commands.test.mjs tsconfig.app-server.json
+git commit -m "docs: document read-only Multi-Codex orchestration"
+```
+
+---
+
+## Final Phase 1 Verification Checklist
+
+- [ ] Create an isolated implementation worktree from the approved design/plan branch before changing code.
+- [ ] Run `npm ci` before the first implementation task.
+- [ ] Confirm the baseline suite passes before adding orchestration code.
+- [ ] Complete Tasks 1–16 in order; do not combine commits unless a task cannot independently pass its focused tests.
+- [ ] Confirm existing single-job broker reuse tests still pass.
+- [ ] Confirm `/codex:review`, `/codex:adversarial-review`, `/codex:rescue`, `/codex:transfer`, status/result/cancel for legacy jobs, setup, hooks, and review gate remain green.
+- [ ] Confirm two top-level Roots overlap in time and use distinct App Server PIDs.
+- [ ] Confirm a worker never runs two top-level packages simultaneously.
+- [ ] Confirm native child events are attributed to the owning package and count against the active-Codex limit.
+- [ ] Confirm model/effort validation still comes from the App Server catalog.
+- [ ] Confirm every package run uses `read-only` sandbox and returns no file changes.
+- [ ] Confirm malformed output and one package failure do not contaminate independent package results.
+- [ ] Confirm transient failure retries only the failed package and at most once.
+- [ ] Confirm package and whole-orchestration cancellation send interrupts and stop new scheduling.
+- [ ] Confirm status/result remain readable from durable state when no controller is live.
+- [ ] Confirm a stale `running` state without a live controller is reported honestly; do not claim Phase 3 recovery.
+- [ ] Confirm automatic orchestration is disabled by default and explicit `/codex:orchestrate` remains available.
+- [ ] Confirm setup flags persist only the intended user configuration patch.
+- [ ] Confirm no runtime dependency was added.
+- [ ] Confirm `npm run check-version` passes without a version bump.
+- [ ] Confirm `npm test`, `npm run build`, and `git diff --check` pass on the final tree.
+- [ ] Confirm the pull-request CI matrix passes on Ubuntu, macOS, and Windows.
+
+## Spec Coverage Audit
+
+| Approved Phase 1 requirement | Implemented by task(s) |
+|---|---|
+| `/codex:orchestrate` explicit command | 12, 14 |
+| Automatic-entry skill and feature flag | 2, 14 |
+| Plan and result schemas | 3 |
+| Deterministic controller and persistent state | 4, 10, 11 |
+| Workspace-scoped App Server worker pool | 6, 8, 9 |
+| DAG scheduler and adaptive budget | 5, 10 |
+| Model and effort routing/validation | 3, 6, 8, 14 |
+| Read-only package execution | 3, 8 |
+| Native-child event observation | 6, 8, 9 |
+| Orchestration-aware status/result/cancel | 10, 12, 13 |
+| Structured result capture and Markdown rendering | 3, 8, 13 |
+| Concurrent Root proof | 7, 15 |
+| Package-scoped failure and retry | 5, 10, 15 |
+| Bounded workers and global limits | 2, 5, 9 |
+| Durable milestone events | 4, 8, 10 |
+| Cross-platform IPC and CI | 11, 16 |
+| Existing single-job behavior preserved | 1, 6, 12, 15, 16 |
+| Phase 2/3 boundaries preserved | Global Constraints, 14, 16 |
diff --git a/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2-self-review.md b/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2-self-review.md
new file mode 100644
index 000000000..da85959d3
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2-self-review.md
@@ -0,0 +1,412 @@
+# Phase 2 Implementation Plan — Self-Review Amendments
+
+- **Status:** Normative amendment
+- **Applies to:** `2026-08-25-claude-native-multi-codex-phase-2.md`
+- **Date:** 2026-08-25
+- **Review outcome:** Implementation-ready after applying the amendments below
+
+Implementers must read the base Phase 2 plan first and then this document. Where the two documents differ, **this amendment takes precedence**.
+
+The review compared the Phase 2 plan against:
+
+- the approved orchestration design;
+- the merged Phase 1 runtime and its persistence contracts;
+- the current Codex App Server request, sandbox, and approval surfaces;
+- Git worktree, ref, index, hook, filter, and race behavior;
+- macOS, Linux, and Windows execution constraints.
+
+No unresolved product decision blocks implementation. The review did find several execution-order and safety gaps that must be corrected before coding.
+
+---
+
+## 1. Correct the write-start ordering
+
+The base plan's controller sequence captures the baseline and then acquires the write lease. That is sufficient to reject a competing controller eventually, but it leaves an avoidable interval in which the first baseline can become stale.
+
+Use this exact sequence:
+
+1. Run a non-mutating preflight and confirm that the repository appears startable.
+2. Acquire the workspace write lease.
+3. Capture the authoritative clean baseline while holding lease ownership.
+4. Immediately compare the current repository state with the captured baseline.
+5. Persist the lease and baseline in orchestration state.
+6. Create package worktrees.
+
+If steps 3–5 fail, release the lease and create no worktree or orchestration Git ref.
+
+Task 3 still owns baseline capture. Task 4 owns the lease. Task 18 must compose them in the order above.
+
+---
+
+## 2. Prohibit stale post-writer package execution
+
+Phase 2 isolated package worktrees all start from the same clean baseline. A normal DAG package therefore cannot safely assume that it sees another writer's unintegrated code.
+
+Add these plan-validation rules:
+
+- A `read-only` package may not depend directly or transitively on a `write` package in Phase 2.
+- A `write` package may depend on another writer only for evidence, ordering, or interface decisions that do not require the upstream package's concrete tree.
+- When a writer needs upstream implementation output as its compilation or editing base, Claude must collapse the work into one writer package.
+- Post-write testing belongs in controller-run package verification, integration verification, or the integration Reviewer—not in a normal package that reads the source workspace.
+
+Do not add an `integration-worktree` DAG package mode in Phase 2. Scheduled post-integration packages and dependency-tip worktrees belong to Phase 3 dynamic replanning.
+
+Task 1, Task 9, Task 18, and the orchestration skill must enforce and explain this boundary.
+
+---
+
+## 3. Add explicit workspace-preparation commands
+
+A fresh Git worktree commonly lacks ignored dependencies, generated code, virtual environments, build caches, and other machine-local prerequisites. Verification commands alone do not solve this.
+
+Extend write packages and top-level integration policy with optional preparation commands using the same argv-array contract as verification:
+
+```json
+{
+ "preparationCommands": [
+ {
+ "argv": ["npm", "ci", "--ignore-scripts", "--offline"],
+ "timeoutMs": 1200000
+ }
+ ]
+}
+```
+
+Rules:
+
+- Preparation commands execute after worktree creation and before the Codex Root starts.
+- Integration preparation commands execute after integration-worktree creation and before package cherry-picks only when the command is tree-independent; otherwise run them after integration and before verification.
+- Commands use `shell: false` and the same command-safety classifier as verification.
+- Automatic orchestration does not grant network access to preparation commands.
+- Failure is `WORKSPACE_PREPARATION_FAILED` and blocks that package before model execution.
+- Preparation output is redacted and bounded identically to verification output.
+- A plan may omit preparation commands when the checkout is already self-contained.
+
+Update Tasks 1, 6, 8, 10, 13, 18, and 20 accordingly.
+
+---
+
+## 4. Harden controller-owned Git operations
+
+Repository Git configuration can invoke hooks, filters, editors, credential prompts, fsmonitor processes, or LFS smudge behavior. Controller-owned Git must not inherit those effects silently.
+
+Every orchestration Git command must use a hardened environment and explicit configuration equivalent to:
+
+```text
+GIT_TERMINAL_PROMPT=0
+GIT_ASKPASS=
+GIT_EDITOR=:
+GIT_SEQUENCE_EDITOR=:
+GIT_MERGE_AUTOEDIT=no
+GIT_OPTIONAL_LOCKS=0 for read-only probes only
+-c core.hooksPath=
+-c core.fsmonitor=false
+-c credential.helper=
+```
+
+Additional rules:
+
+- Do not set `GIT_OPTIONAL_LOCKS=0` for mutations that require normal locking.
+- Worktree creation, cherry-pick, and any checkout-like operation must disable repository hooks.
+- The controller must not allow Git to prompt for credentials.
+- Default worktree creation sets `GIT_LFS_SKIP_SMUDGE=1` to prevent implicit network access.
+- If tracked LFS content is required for implementation or verification, fail with `LFS_CONTENT_UNAVAILABLE` and preserve an actionable diagnostic. Network-backed LFS hydration is not automatic in Phase 2.
+- Do not initialize or update submodules automatically.
+- Reject a write plan that requires changing a submodule gitlink or nested repository metadata.
+- Record the effective hardening policy in local diagnostic state without persisting secrets.
+
+Apply this to Tasks 3, 8, 11, 13, 15, 16, 17, and 20.
+
+---
+
+## 5. Keep managed worktrees outside the source repository
+
+`${CLAUDE_PLUGIN_DATA}` is user-configurable. It may point inside the repository, inside `.git`, or through a symlink back into the repository. That would contaminate status, ownership, and recursion behavior.
+
+Before creating managed paths, verify that the canonical orchestration data root and every package/integration worktree path are outside:
+
+- the source worktree;
+- the repository Git common directory;
+- every currently registered linked worktree.
+
+If the configured plugin-data root is unsafe, do not silently relocate durable state. Fail with `UNSAFE_PLUGIN_DATA_LOCATION` and tell the user which canonical paths conflict.
+
+Task 8 must test direct containment, symlink containment, case-insensitive Windows path comparison, and linked-worktree containment.
+
+---
+
+## 6. Use plumbing for package commit normalization
+
+Package normalization must not use `git commit`, `git reset --hard`, or the user's normal index. Those commands can invoke hooks, interact with signing configuration, or alter checked-out content unnecessarily.
+
+Use this mechanical sequence in the isolated package worktree:
+
+1. Audit the final tree and changed paths relative to the package base.
+2. Create a temporary index outside the worktree.
+3. Populate it from the package base with `git read-tree`.
+4. Stage the package worktree into the temporary index with `GIT_INDEX_FILE`.
+5. Write the tree using `git write-tree`.
+6. Create the normalized commit using `git commit-tree -p ` with the required message and trailers.
+7. Atomically move the package branch with `git update-ref `.
+8. Update only the package worktree/index to the normalized commit and verify a clean package worktree.
+
+The original model-created commits remain recoverable through local diagnostics until the package is accepted, but they are not integration inputs.
+
+Task 11 must test hook suppression, commit-signing configuration, expected-old-tip races, and temporary-index cleanup.
+
+---
+
+## 7. Verification and preparation commands require policy checks
+
+Argv arrays prevent shell injection but do not make an arbitrary executable safe. A plan could still request a deployment CLI, destructive script, remote mutation, or credential-bearing command.
+
+Before executing a preparation or verification command:
+
+1. Run the same deterministic command classifier used for App Server command approval.
+2. Reject known remote, publication, deployment, credential, destructive, or repository-metadata mutation commands.
+3. Reject a cwd outside the assigned worktree.
+4. Reject Git branch/ref/index/worktree mutations unless the operation is an internal fixed controller command rather than a plan command.
+5. Remove known secret values from the persisted command representation.
+
+Execution environment rules:
+
+- Start from a minimal allowlisted environment plus `PATH`, platform essentials, and explicitly configured variable names.
+- Never persist environment values.
+- Strip common cloud, package-registry, VCS, and authorization secrets unless an explicit future user-authorized mechanism supplies them.
+- Set non-interactive/CI flags where safe.
+- Network is not guaranteed to be OS-confined for arbitrary local processes; therefore command classification, secret stripping, no automatic credentials, and explicit documentation are required defense in depth.
+
+Add `UNSAFE_PLAN_COMMAND` and `WORKSPACE_PREPARATION_FAILED` to the required error-code table.
+
+Tasks 2, 6, 7, 18, and 20 must cover this behavior.
+
+---
+
+## 8. Preserve default App Server request rejection
+
+The current App Server client rejects unsupported server-initiated requests. Phase 2 must add mediation without weakening existing callers.
+
+Required client contract:
+
+- `setServerRequestHandler` is opt-in per client.
+- A client with no handler retains the existing JSON-RPC `-32601` response.
+- The writer worker installs its handler before any thread or turn request.
+- Every request receives exactly one response.
+- Handler timeout, throw, malformed decision, or worker cancellation returns a fail-closed error/decline.
+- Closing a client drains or rejects outstanding server requests deterministically.
+- Generated current App Server types are authoritative; do not hand-maintain a permanent request-shape matrix when generated types expose it.
+
+Task 7 must begin by regenerating types and inspecting the exact command, file-change, network, and permission request/decision unions supported by the installed Codex CLI.
+
+---
+
+## 9. Clarify sandbox mapping and unrestricted fallback
+
+The plan-level string `workspace-write` is plugin configuration, not the wire representation. Convert it at one narrow adapter boundary to the current App Server sandbox-policy object with:
+
+- package worktree as the primary writable root;
+- network disabled;
+- no source-worktree writable root;
+- no Git common-directory writable root;
+- no sibling package or integration worktree writable root.
+
+`danger-full-access` rules:
+
+- Default remains disabled.
+- It requires both `allowDangerFullAccess: true` and an explicit invocation.
+- Automatic orchestration may not select it.
+- The controller never falls back to it because workspace-write setup failed.
+- Its use must be visible in the compressed plan and durable result.
+
+Add `allowDirectMode: false` to default Git configuration. Direct mode requires both explicit invocation and `allowDirectMode: true`.
+
+Tasks 2, 7, 10, 17, and 19 must apply these rules.
+
+---
+
+## 10. Restrict direct mode to explicit experimental use
+
+Direct single-writer mode affects the user's visible working tree before verification completes. Ignored-file writes and arbitrary local tool side effects cannot be fully reconstructed through Git audits.
+
+Therefore:
+
+- Isolated writer mode is the shipped default and release-critical path.
+- Automatic orchestration never uses direct mode in Phase 2.
+- Direct mode requires explicit `/codex:orchestrate`, project/user `allowDirectMode: true`, a clean named branch, one writer, no concurrent packages, no mandatory Reviewer, workspace-write sandboxing, and successful preflight.
+- The compressed plan must label it `experimental direct writer`.
+- Failure/cancellation preserves visible changes and never resets them.
+- If cross-platform tests cannot prove the branch/index/tree transition safely, direct mode may remain disabled without blocking isolated-writer Phase 2 release. In that case, document it as planned but unavailable rather than silently degrading to isolated mode after the user explicitly requested direct mode.
+
+Task 17 is therefore an **optional release extension** after Tasks 1–16 and 18–20 are green. Acceptance criterion 17 applies only when direct mode is enabled in the release candidate.
+
+---
+
+## 11. Define safe final-application mechanics
+
+The base plan correctly requires fast-forward-only application with an expected-old-value guard. Use a porcelain or plumbing sequence that provides all-or-nothing safety as far as Git permits.
+
+For isolated mode, the preferred implementation is:
+
+1. Revalidate branch, `HEAD`, index tree, status, and Git-operation state.
+2. Confirm the final commit is a direct child of baseline.
+3. Invoke a fast-forward-only operation from the user's active worktree with hooks, editors, credentials, and prompts disabled.
+4. Verify `HEAD`, index tree, and clean status.
+
+Do not use `reset --hard`, force-update, checkout overwrite, stash, or clean.
+
+The implementation must include an injected race point in tests. If the branch moves before Git acquires its ref lock, application must fail without modifying the user's worktree. If an unexpected partial failure occurs after ref movement, mark `AUTO_APPLY_PARTIAL_FAILURE`, preserve diagnostics, and do not attempt an automatic rollback that could destroy user work.
+
+Add `AUTO_APPLY_PARTIAL_FAILURE` to the required error-code table.
+
+Task 16 owns this proof.
+
+---
+
+## 12. Separate controller preparation from model permissions
+
+The controller may create worktrees, refs, package commits, integration commits, and final commits. The model may not.
+
+Approval and sandbox tests must distinguish:
+
+- fixed internal controller Git operations, constructed by code;
+- plan-declared preparation/verification commands, subject to command policy;
+- model-requested commands, subject to App Server approval policy;
+- ordinary file edits inside writable roots.
+
+Never grant the writer model writable access to the Git common directory merely because the controller later needs to commit.
+
+Tasks 7, 8, 10, 11, and 13 must prove this separation.
+
+---
+
+## 13. Cancellation and controller-loss behavior
+
+Cancellation order for write orchestration:
+
+1. Persist `cancelling` before interrupting workers.
+2. Stop new scheduling.
+3. Interrupt active Roots.
+4. Wait the configured grace period.
+5. Terminate remaining worker process trees.
+6. Capture model output, Git status, actual changed files, and worktree identity.
+7. Preserve package branches/worktrees and direct-mode visible changes.
+8. Persist terminal state and aggregate result.
+9. Release the write lease.
+
+Do not normalize, integrate, review, create a final commit, or auto-apply after cancellation begins.
+
+On controller loss, a newly started controller may inspect but not mutate an unfinished write orchestration. It records `WRITE_RECOVERY_REQUIRES_PHASE_3`, preserves artifacts, and does not release or steal an orphan lease until the stale-owner safety checks in Task 4 are satisfied.
+
+Task 18 must include crash points before and after worktree creation, package commit creation, integration, and final commit creation.
+
+---
+
+## 14. Read-only regression invariants
+
+Phase 2 changes shared controller and App Server modules. Preserve these exact Phase 1 properties:
+
+- Read-only plan normalization remains valid without write fields.
+- Read-only packages use `access: "read-only"`, shared workspace, read-only sandbox, and empty canonical changed files.
+- Read-only orchestration does not require Git, a clean tree, a write lease, preparation commands, worktrees, package commits, integration, or Reviewer state.
+- Existing rescue/review/transfer and shared-broker behavior retains default server-request rejection.
+- Existing model-catalog and reasoning-effort behavior remains unchanged.
+- Read-only cancellation and transient retry semantics remain unchanged.
+
+Every task that modifies a shared module must run its existing Phase 1 focused tests in addition to its new writer tests.
+
+---
+
+## 15. Corrected implementation dependencies
+
+The base plan's numbered order remains valid with these refinements:
+
+- Task 1 defines preparation-command and post-writer dependency contracts.
+- Task 2 adds `allowDirectMode` and explicit unrestricted-mode gates.
+- Task 3 provides preflight and authoritative baseline capture primitives.
+- Task 4 provides write lease ownership.
+- Task 5 remains the ownership policy dependency for prompts, commits, integration, and application.
+- Task 6 implements one reusable safe local-command runner for both preparation and verification.
+- Task 7 implements App Server request mediation and the shared command classifier.
+- Task 8 applies hardened Git/worktree environment and safe data-root checks.
+- Task 9 defines model-facing writer/reviewer contracts.
+- Task 10 executes Roots but does not normalize commits.
+- Task 11 uses temporary-index plus `commit-tree` normalization.
+- Task 12 persists v2 state after the value objects are stable.
+- Tasks 13–16 implement integration, review, finalization, and safe application.
+- Task 17 is optional explicit direct mode and must not delay the isolated-writer release.
+- Task 18 composes the controller only after the lower-level components exist.
+- Tasks 19–20 expose and verify the complete feature.
+
+Task 18's start sequence is amended by Section 1 of this document.
+
+---
+
+## 16. Design-to-plan traceability
+
+| Approved Phase 2 deliverable | Plan coverage | Amendment |
+|---|---|---|
+| Clean-tree writer orchestration | Tasks 1–5, 8, 10, 18 | Authoritative baseline captured after lease acquisition. |
+| Writer package worktrees | Task 8 | Managed root must be outside repo/Git/worktrees; hooks/LFS hardened. |
+| Package commit normalization | Task 11 | Temporary index + `commit-tree` + expected-old `update-ref`. |
+| Integration branch | Tasks 13, 15 | Hardened Git; no semantic resolution. |
+| Dependency-ordered cherry-pick | Task 13 | Post-writer DAG packages prohibited; writer code dependencies collapsed. |
+| Conflict decision points | Tasks 13, 18, 19 | Terminal/preserved in Phase 2. |
+| Risk-triggered Reviewer | Task 14 | Post-package gate declared by Claude, not invented by runtime. |
+| Full verification | Tasks 6, 15 | Preparation added; commands policy-checked and non-shell. |
+| Final local squash commit | Task 15 | Parent fixed to baseline; plumbing audit. |
+| Safe clean-branch application | Task 16 | No reset/stash/clean; expected-old/race proof. |
+| Controller-mediated approval | Task 7 | Opt-in request handler; default remains rejection. |
+| Automatic local writes | Tasks 18–19 | Only clean isolated mode; unrestricted/direct excluded from auto-entry. |
+| macOS/Linux/Windows support | Task 20 | Git hardening, path containment, process-tree and race coverage. |
+| Preserve Phase 1 | Every shared-module task | Explicit regression invariants in Section 14. |
+| Dirty-tree/recovery deferral | Tasks 4, 18, 19 | Preserve and block with Phase 3 error; no implicit resume. |
+
+---
+
+## 17. Additional required error codes
+
+Add these to the base plan's table:
+
+| Code | Meaning |
+|---|---|
+| `WORKSPACE_PREPARATION_FAILED` | A declared local preparation command failed. |
+| `UNSAFE_PLAN_COMMAND` | A preparation/verification command violates command policy. |
+| `UNSAFE_PLUGIN_DATA_LOCATION` | Managed state/worktree root overlaps repository metadata or a source worktree. |
+| `LFS_CONTENT_UNAVAILABLE` | Required tracked LFS content cannot be hydrated without forbidden network access. |
+| `AUTO_APPLY_PARTIAL_FAILURE` | Unexpected failure occurred after application began; no destructive rollback attempted. |
+
+---
+
+## 18. Final self-review verdict
+
+The amended plan is sufficiently detailed for implementation.
+
+The decisive safety structure is:
+
+```text
+clean preflight
+→ exclusive write lease
+→ authoritative baseline
+→ isolated worktrees
+→ workspace-write App Server sandbox
+→ fail-closed approvals
+→ Git-derived ownership audit
+→ controller-run verification
+→ controller-normalized package commits
+→ deterministic integration
+→ mandatory risk review
+→ final single-parent commit
+→ revalidated fast-forward or preservation
+```
+
+The implementation must not claim Phase 2 completion until:
+
+- isolated one-writer and two-writer flows pass on all three operating systems;
+- the current generated App Server approval and sandbox types are exercised;
+- external and out-of-root requests are denied;
+- package and final commits are independently audited;
+- an injected user-branch race leaves user work intact;
+- Phase 1 and all legacy single-job paths remain green;
+- the authenticated disposable-repository smoke suite passes.
+
+No dirty-tree, automatic recovery, dynamic repair-package, or semantic conflict-resolution behavior may be inferred from the existence of preserved worktrees. Those remain Phase 3.
\ No newline at end of file
diff --git a/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2.md b/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2.md
new file mode 100644
index 000000000..4d8c3a205
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-25-claude-native-multi-codex-phase-2.md
@@ -0,0 +1,1705 @@
+# Claude-Native Multi-Codex Orchestration Phase 2 Implementation Plan
+
+> **For agentic workers:** Implement this plan task-by-task with test-first changes and focused commits. Do not start a later task while an earlier task's focused tests are red. The final implementation must preserve all Phase 1 behavior and remain mergeable as one coherent feature.
+
+- **Status:** Approved implementation plan
+- **Date:** 2026-08-25
+- **Target repository:** `eureka-pd/codex-plugin-cc`
+- **Base:** Phase 1 merged at `3444f60f4a124b2b24df654e5df6787b8478ab8d`
+- **Scope:** Clean-tree local writer orchestration only
+
+**Goal:** Extend the Phase 1 read-only Multi-Codex runtime with safe clean-tree writer execution, isolated package worktrees, controller-owned package commits, deterministic integration, independent review, full verification, one final squash commit, and conditional fast-forward application to an unchanged clean user branch.
+
+**Architecture:** Claude Root still owns decomposition, ownership, risk declarations, reviewer policy, and final judgment. The detached Node.js controller owns mechanical write execution: workspace write leases, clean-tree baseline capture, package worktree lifecycle, App Server approval mediation, ownership auditing, package commit normalization, dependency-ordered integration, deterministic verification, reviewer execution, final commit creation, and safe application. Phase 2 uses the existing Phase 1 controller, scheduler, worker slots, state store, and command surfaces rather than adding a second orchestration runtime.
+
+**Tech Stack:** Node.js 18.18+ ESM, built-in `node:test`, Git CLI invoked with argv arrays and `shell: false`, Codex App Server JSONL v2 protocol, workspace-write sandbox policies, server-initiated approval requests, filesystem-backed JSON/JSONL state, Unix sockets on macOS/Linux, named pipes on Windows.
+
+---
+
+## 1. Phase 2 Boundaries
+
+### Included
+
+- Git repositories whose user branch, index, and working tree are clean at orchestration start.
+- One or more `access: "write"` work packages.
+- Isolated package branches and worktrees for every multi-writer plan.
+- Isolated worktree execution as the default even for one writer.
+- A tightly gated direct single-writer optimization implemented only after the isolated path is complete.
+- Controller-mediated command and file-change approvals.
+- Controller-owned package commit normalization.
+- Declared file ownership and changed-file auditing.
+- Controller-run package and integration verification commands.
+- Dependency-ordered package commit integration.
+- Mechanical conflict detection and explicit decision-point records.
+- Risk-triggered independent integration review.
+- A single final commit whose parent is the orchestration base commit.
+- Automatic fast-forward application only when the user branch is unchanged and clean.
+- Preservation of package branches, worktrees, integration state, and final commit when automatic application is unsafe.
+
+### Explicitly deferred to Phase 3
+
+- Starting write orchestration from a dirty working tree.
+- Hidden snapshot refs representing staged, unstaged, and untracked user work.
+- Automatic recovery or resume of an interrupted write orchestration.
+- Reattaching to orphaned write workers or blindly continuing an abandoned worktree.
+- Dynamic DAG revision, repair-package insertion, or semantic conflict-resolution packages.
+- Automatic retention pruning and stale worktree deletion.
+- Cross-session write resumption.
+- Replaying previously approved external actions.
+
+### Never allowed automatically
+
+- `git push`, force-push, remote branch deletion, PR mutation, release publication, deployment, cloud mutation, remote database mutation, credential changes, purchases, or destructive writes outside the package worktree.
+- Semantic merge-conflict resolution invented by the controller.
+- Silent reset, stash, checkout, clean, or index rewrite of the user's active worktree to make orchestration possible.
+- Automatic fallback from a restricted writer sandbox to unrestricted local access.
+
+---
+
+## 2. Normative Implementation Decisions
+
+These decisions resolve ambiguities between the approved design and the concrete Phase 1 implementation.
+
+### 2.1 Preserve plan version 1
+
+`ORCHESTRATION_PLAN_VERSION` remains `1`. Phase 2 extends the existing contract in a backward-compatible direction:
+
+- Phase 1 read-only plans remain valid without new fields.
+- Write packages require the new ownership, workspace, verification, and integration fields.
+- Persisted Phase 1 orchestration records remain readable.
+
+Do not invalidate existing read-only callers merely to distinguish the implementation phase.
+
+### 2.2 Use state version 2 with read migration
+
+`ORCHESTRATION_STATE_VERSION` becomes `2`. `loadOrchestrationState` must normalize version-1 records in memory by adding absent write/integration fields with safe read-only defaults. It must not rewrite old state merely because it was read.
+
+### 2.3 Isolated writer mode is the correctness path
+
+Every writer uses an isolated package worktree unless the strict direct-mode predicate in Task 16 succeeds. A single writer does not automatically imply direct execution.
+
+### 2.4 All isolated writers start from the same immutable base
+
+Phase 2 writer worktrees start from the clean orchestration-start `HEAD` commit. Package dependencies define scheduling and integration order, but a downstream writer does not automatically receive an upstream writer's unintegrated tree.
+
+If a package requires another writer's concrete code as its implementation base, Claude must combine them into one writer package in Phase 2. Dependency-tip worktrees and dynamic repair packages are Phase 3.
+
+### 2.5 Integration Reviewer is a post-package gate
+
+The integration Reviewer is declared under the plan's top-level `integration.reviewer` policy. It is not a normal DAG package and does not compete with implementation packages for dependency scheduling. The controller may execute it only according to the pre-authorized plan policy and deterministic trigger rules.
+
+Claude still chooses its role label, model, effort, and native-child policy in the plan. The controller does not invent a semantic reviewer configuration.
+
+### 2.6 Verification commands are argv arrays
+
+Package and integration verification commands use arrays, not shell strings:
+
+```json
+{
+ "argv": ["npm", "test", "--", "auth"],
+ "timeoutMs": 900000
+}
+```
+
+The controller executes them with `shell: false`. Shell pipelines, redirects, command substitution, and chained commands are not supported in Phase 2. Claude must split them into multiple commands.
+
+### 2.7 Prefer workspace-write sandboxing
+
+Writer turns use a workspace-write sandbox rooted at the package worktree with network access disabled. The controller must not automatically fall back to `dangerFullAccess`.
+
+An explicit project/user configuration may permit unrestricted writer access, but automatic orchestration must still fail closed when the active App Server or managed requirements cannot enforce the selected mode. The initial shipped default is workspace-write.
+
+### 2.8 The controller owns Git commits
+
+A Codex Root may leave uncommitted changes or create one or more local commits inside its isolated package branch. The controller always audits the final tree and normalizes it to exactly one package commit. Model-reported commit SHAs are advisory and never canonical.
+
+### 2.9 Conflict decision points are terminal in Phase 2
+
+A semantic cherry-pick conflict, reviewer `revise`/`reject`, ownership violation, unsafe user-branch movement, or failed required integration verification preserves artifacts and ends the orchestration as `blocked`, `degraded`, or `failed` with a structured decision point. Automatic resume and repair-package insertion arrive in Phase 3.
+
+### 2.10 Final application is fast-forward only
+
+The final squash commit is created with the orchestration base commit as its sole parent. Automatic application to the user branch uses a fast-forward-only operation after revalidating the original branch, `HEAD`, index, and clean worktree. If any check fails, the final commit is preserved but not applied.
+
+---
+
+## 3. Extended Plan Contract
+
+A read-only Phase 1 package remains unchanged. A write-capable plan adds the following conceptual fields.
+
+```json
+{
+ "version": 1,
+ "objective": "Implement and verify the authentication change",
+ "complexityScore": 7,
+ "requestedBy": {
+ "explicit": true,
+ "sessionId": "claude-session-id"
+ },
+ "riskTags": ["authentication", "public-api"],
+ "integration": {
+ "enabled": true,
+ "verificationCommands": [
+ {
+ "argv": ["npm", "test"],
+ "timeoutMs": 1200000
+ },
+ {
+ "argv": ["npm", "run", "build"],
+ "timeoutMs": 1200000
+ }
+ ],
+ "reviewer": {
+ "mode": "auto",
+ "role": {
+ "class": "reviewer",
+ "label": "integration-reviewer"
+ },
+ "model": {
+ "name": "gpt-5.6-sol",
+ "effort": "max"
+ },
+ "nativeSubagents": {
+ "policy": "allowed",
+ "maxChildren": 1
+ }
+ },
+ "finalCommit": {
+ "mode": "squash",
+ "autoApply": true,
+ "subject": "feat: implement authentication change"
+ }
+ },
+ "packages": [
+ {
+ "id": "pkg-auth-backend",
+ "title": "Implement authentication backend",
+ "role": {
+ "class": "implementer",
+ "label": "authentication-backend-implementer"
+ },
+ "objective": "Implement the backend change without altering unrelated APIs.",
+ "dependencies": [],
+ "optional": false,
+ "access": "write",
+ "ownership": {
+ "files": ["src/auth/**", "tests/auth/**"],
+ "interfaces": ["auth-session-contract"]
+ },
+ "workspace": {
+ "mode": "isolated-worktree",
+ "base": "orchestration-head"
+ },
+ "model": {
+ "name": "gpt-5.6-terra",
+ "effort": "high"
+ },
+ "nativeSubagents": {
+ "policy": "allowed",
+ "maxChildren": 1
+ },
+ "acceptanceCriteria": [
+ "Existing auth tests pass.",
+ "Failure-path tests cover the new behavior."
+ ],
+ "verificationCommands": [
+ {
+ "argv": ["npm", "test", "--", "auth"],
+ "timeoutMs": 600000
+ }
+ ],
+ "riskTags": ["authentication"],
+ "expectedOutputs": [
+ "changed files",
+ "normalized package commit",
+ "verification evidence",
+ "residual risks"
+ ]
+ }
+ ]
+}
+```
+
+### 3.1 Package access and workspace rules
+
+| Access | Allowed workspace mode | Phase 2 behavior |
+|---|---|---|
+| `read-only` | `shared` | Existing Phase 1 behavior. |
+| `write` | `isolated-worktree` | Default writer path. |
+| `write` | `direct` | Strictly gated single-writer optimization from Task 16. |
+
+Rules:
+
+- A write package requires non-empty `ownership.files`.
+- Ownership paths are repository-relative and use `/` separators.
+- Absolute paths, `..`, empty path segments, `.git`, and unsupported glob syntax are rejected.
+- A write package requires at least one package verification command unless the plan explicitly sets `verificationWaiver` with a non-empty reason. Automatic orchestration may not use a waiver.
+- Any plan with a writer requires `integration.enabled: true`.
+- `integration.finalCommit.mode` is `squash` in Phase 2.
+- A read-only-only plan may omit `riskTags` and `integration` and remains a Phase 1 plan.
+
+### 3.2 Supported ownership glob grammar
+
+Implement a dependency-free constrained matcher:
+
+- exact path: `src/auth/session.ts`
+- recursive directory: `src/auth/**`
+- single-segment wildcard: `src/*/index.ts`
+- suffix wildcard inside one segment: `tests/auth/*.test.ts`
+
+Reject braces, extglobs, negation, character classes, backtracking constructs, absolute paths, and patterns containing `..`. The limited grammar keeps matching deterministic and cross-platform.
+
+### 3.3 Risk tags
+
+Built-in tags:
+
+- `security`
+- `authentication`
+- `authorization`
+- `concurrency`
+- `data-loss`
+- `migration`
+- `rollback`
+- `public-api`
+- `schema`
+- `protocol`
+- `build-system`
+- `dependency`
+
+Unknown non-empty tags may be preserved for forward compatibility, but built-in reviewer triggers rely only on the built-in set.
+
+---
+
+## 4. Write and Integration State
+
+### 4.1 Orchestration write metadata
+
+```js
+write: {
+ enabled: true,
+ mode: "isolated" | "direct",
+ lease: {
+ orchestrationId,
+ controllerInstanceId,
+ pid,
+ acquiredAt,
+ heartbeatAt
+ },
+ baseline: {
+ repositoryRoot,
+ gitCommonDir,
+ branch,
+ detached,
+ head,
+ indexTree,
+ statusPorcelainV2,
+ capturedAt
+ }
+}
+```
+
+### 4.2 Package write metadata
+
+```js
+workspace: {
+ mode: "isolated-worktree" | "direct",
+ path,
+ branch,
+ baseCommit,
+ createdAt,
+ preserved: false
+},
+writeResult: {
+ packageCommit,
+ changedFiles,
+ ownershipAudit,
+ verification,
+ modelReportedChangedFiles,
+ normalizedAt
+}
+```
+
+### 4.3 Integration metadata
+
+```js
+integration: {
+ status: "pending" | "preparing" | "integrating" | "verifying" |
+ "reviewing" | "approved" | "blocked" | "failed" | "applied" | "preserved",
+ branch,
+ worktreePath,
+ baseCommit,
+ integratedPackages: [],
+ skippedPackages: [],
+ conflicts: [],
+ verification: null,
+ reviewer: null,
+ finalCommit: null,
+ application: {
+ requested: true,
+ eligible: false,
+ applied: false,
+ reason: null
+ },
+ decisionPoint: null,
+ startedAt: null,
+ completedAt: null
+}
+```
+
+### 4.4 Orchestration lifecycle
+
+```text
+queued
+ → running
+ → integrating
+ → completed | completed-with-omissions
+
+running/integrating
+ → blocked | degraded | failed | cancelled
+```
+
+A write orchestration does not become `completed` merely because all packages are terminal. The controller must finish integration, required verification, reviewer gates, final commit creation, and application/preservation classification first.
+
+---
+
+## 5. File and Responsibility Map
+
+### Existing files to modify
+
+| File | Phase 2 responsibility |
+|---|---|
+| `plugins/codex/scripts/lib/app-server.mjs` | Add pluggable handling for server-initiated approval requests without changing default rejection behavior. |
+| `plugins/codex/scripts/lib/app-server-protocol.d.ts` | Add server-request and current sandbox/approval request typing needed by writer workers. |
+| `plugins/codex/scripts/lib/codex.mjs` | Accept caller-provided approval policy and turn-level sandbox policy; preserve read-only defaults. |
+| `plugins/codex/scripts/lib/git.mjs` | Export or delegate safe low-level Git helpers while preserving review behavior. |
+| `plugins/codex/scripts/orchestration/constants.mjs` | Add write, integration, reviewer, risk, and state constants. |
+| `plugins/codex/scripts/orchestration/config.mjs` | Add Git, safety, verification, and write-mode configuration. |
+| `plugins/codex/scripts/orchestration/plan-contract.mjs` | Normalize read/write packages, ownership, commands, risk tags, and integration policy. |
+| `plugins/codex/scripts/orchestration/result-contract.mjs` | Separate model-reported write results from controller-canonical results and render integration results. |
+| `plugins/codex/scripts/orchestration/state-store.mjs` | Persist state version 2, write baseline, worktrees, package commits, integration, reviewer, and decisions. |
+| `plugins/codex/scripts/orchestration/package-prompt.mjs` | Build read-only, writer, and integration-review prompts. |
+| `plugins/codex/scripts/orchestration/package-worker.mjs` | Execute read-only or write packages in the supplied workspace with approval mediation and sandbox policy. |
+| `plugins/codex/scripts/orchestration/worker-pool.mjs` | Support per-execution workspace roots, access modes, sandbox policy, and approval context. |
+| `plugins/codex/scripts/orchestration/controller.mjs` | Acquire write leases, prepare workspaces, normalize commits, integrate, review, verify, apply, preserve, and cancel. |
+| `plugins/codex/scripts/orchestration/controller-server.mjs` | Expose integration/application status and preserve write state on shutdown. |
+| `plugins/codex/scripts/orchestration/controller-client.mjs` | Add typed internal operations required by integration and tests. |
+| `plugins/codex/scripts/orchestration/cli.mjs` | Render write/integration state and expose deterministic internal inspect/apply surfaces. |
+| `plugins/codex/scripts/codex-companion.mjs` | Preserve routing while showing write/integration milestones and results. |
+| `plugins/codex/scripts/lib/render.mjs` | Render worktree, commit, reviewer, verification, application, and decision-point information. |
+| `plugins/codex/skills/codex-orchestration/SKILL.md` | Allow clean-tree local write plans and require ownership/risk/integration policy. |
+| `plugins/codex/skills/codex-work-package-contract/SKILL.md` | Define writer boundaries, verification, and commit ownership. |
+| `plugins/codex/skills/codex-integration-policy/SKILL.md` | Replace the Phase 1 prohibition with Phase 2 integration and reviewer policy. |
+| `plugins/codex/skills/codex-orchestration-recovery/SKILL.md` | State that interrupted write runs preserve artifacts and do not auto-resume until Phase 3. |
+| `plugins/codex/commands/orchestrate.md` | Document clean-tree write behavior and compressed plan fields. |
+| `README.md` | Document writer safety, worktrees, reviewer gates, final commit, and Phase 3 exclusions. |
+| `tsconfig.app-server.json` | Type-check all new writer and integration modules. |
+
+### New runtime modules
+
+| File | Single responsibility |
+|---|---|
+| `plugins/codex/scripts/orchestration/git-state.mjs` | Capture and compare clean user-worktree baseline state and detect in-progress Git operations. |
+| `plugins/codex/scripts/orchestration/workspace-write-lease.mjs` | Enforce one active write orchestration per repository across controller processes. |
+| `plugins/codex/scripts/orchestration/ownership-policy.mjs` | Validate ownership patterns, detect package overlap, and audit actual changed files. |
+| `plugins/codex/scripts/orchestration/approval-policy.mjs` | Classify App Server command/file/permission requests and return fail-closed decisions. |
+| `plugins/codex/scripts/orchestration/verification-runner.mjs` | Execute declared argv commands with timeout, bounded logs, redaction, and process-tree termination. |
+| `plugins/codex/scripts/orchestration/worktree-manager.mjs` | Create, validate, preserve, and remove package/integration worktrees and refs. |
+| `plugins/codex/scripts/orchestration/package-commit.mjs` | Audit and normalize one writer package to one atomic commit with trailers. |
+| `plugins/codex/scripts/orchestration/integration-manager.mjs` | Create integration state, cherry-pick package commits, detect conflicts, verify, create final commit, and apply/preserve. |
+| `plugins/codex/scripts/orchestration/reviewer-policy.mjs` | Decide whether review is required and validate reviewer verdicts. |
+| `plugins/codex/scripts/orchestration/redaction.mjs` | Redact approval, command, and verification logs before durable persistence. |
+
+### Schemas to modify or add
+
+| File | Responsibility |
+|---|---|
+| `plugins/codex/scripts/orchestration/schemas/config.schema.json` | Full Phase 2 configuration schema. |
+| `plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json` | Read/write package, ownership, risk, and integration policy schema. |
+| `plugins/codex/scripts/orchestration/schemas/package-result.schema.json` | Model-reported package result schema permitting writer change claims. |
+| `plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json` | Canonical aggregate result including integration/application. |
+| `plugins/codex/scripts/orchestration/schemas/reviewer-result.schema.json` | `approve | revise | reject` integration Reviewer result. |
+| `plugins/codex/scripts/orchestration/schemas/decision-point.schema.json` | Conflict, review, verification, ownership, and application decision records. |
+
+### New focused tests
+
+| File | Coverage |
+|---|---|
+| `tests/orchestration-write-contracts.test.mjs` | Backward-compatible read plans and strict write-plan validation. |
+| `tests/orchestration-git-state.test.mjs` | Clean baseline, Git-operation detection, and branch/head/index comparisons. |
+| `tests/orchestration-write-lease.test.mjs` | Cross-process single-writer lease and stale-owner behavior. |
+| `tests/orchestration-ownership.test.mjs` | Pattern grammar, overlap detection, and changed-file audit. |
+| `tests/orchestration-approval.test.mjs` | Command/file/network/permission decisions and redaction. |
+| `tests/orchestration-verification.test.mjs` | Argv execution, timeout, cancellation, and bounded logs. |
+| `tests/orchestration-worktree.test.mjs` | Package/integration worktrees, branch naming, and cross-platform cleanup. |
+| `tests/orchestration-package-commit.test.mjs` | Uncommitted, single-commit, multi-commit, ownership violation, and trailers. |
+| `tests/orchestration-integration.test.mjs` | Topological integration, omissions, conflicts, verification, final commit, and application. |
+| `tests/orchestration-reviewer.test.mjs` | Trigger policy, Reviewer schema, and blocking verdicts. |
+| `tests/orchestration-write-runtime.test.mjs` | Full fake-App-Server one-writer, two-writer, denial, cancellation, and no-auto-apply flows. |
+
+---
+
+# Implementation Tasks
+
+## Task 1: Lock Phase 1 Regression Behavior and Add Write-Plan Contract Tests
+
+**Files:**
+- Modify: `tests/orchestration-contracts.test.mjs`
+- Create: `tests/orchestration-write-contracts.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/constants.mjs`
+- Modify: `plugins/codex/scripts/orchestration/plan-contract.mjs`
+- Modify: `plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json`
+
+**Required outcomes:**
+
+- Existing read-only plan normalization remains byte-for-byte equivalent for existing fields.
+- Plan version remains `1`.
+- Write packages are rejected until all required write fields are present.
+- Ownership overlap is validated before any controller or worker starts.
+- Any writer requires top-level integration policy.
+
+- [ ] Add regression tests proving the current Phase 1 sample still normalizes and freezes correctly.
+- [ ] Add failing tests for `access: "write"`, `ownership`, `workspace.mode`, package verification commands, `riskTags`, and `integration`.
+- [ ] Add failing tests for absolute ownership paths, `..`, `.git/**`, unsupported glob syntax, duplicate command entries, shell-string commands, direct mode with multiple writers, and missing integration policy.
+- [ ] Add failing tests proving a read-only plan may still omit all Phase 2 fields.
+- [ ] Implement normalization helpers for command specs, ownership, risk tags, reviewer policy, and final-commit policy.
+- [ ] Keep Phase 1's model, effort, native-child, dependency, optional, and budget validations unchanged.
+- [ ] Freeze normalized plans recursively.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-contracts.test.mjs tests/orchestration-write-contracts.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: extend orchestration plan contracts for writers
+```
+
+---
+
+## Task 2: Expand Configuration Without Changing Existing Defaults Unexpectedly
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/config.mjs`
+- Modify: `plugins/codex/scripts/orchestration/schemas/config.schema.json`
+- Modify: `tests/orchestration-config.test.mjs`
+- Modify: `plugins/codex/commands/setup.md`
+
+**Default configuration added in Phase 2:**
+
+```json
+{
+ "git": {
+ "writerMode": "isolated",
+ "finalCommitMode": "squash",
+ "autoApplyToCleanBranch": true,
+ "preserveSuccessfulWorktrees": false
+ },
+ "safety": {
+ "writeSandbox": "workspace-write",
+ "allowDangerFullAccess": false,
+ "allowWriterNetwork": false,
+ "externalActions": "deny"
+ },
+ "verification": {
+ "defaultCommandTimeoutMs": 900000,
+ "maxCommandTimeoutMs": 3600000,
+ "maxLogBytes": 1048576
+ }
+}
+```
+
+Rules:
+
+- `writeSandbox` accepts `workspace-write` and `danger-full-access`.
+- `danger-full-access` is invalid unless `allowDangerFullAccess` is true.
+- Automatic orchestration may not enable writer network access.
+- `externalActions` is fixed to `deny` in Phase 2.
+- Unknown keys still fail validation.
+
+- [ ] Add precedence tests for user and project write configuration.
+- [ ] Add range and enum tests.
+- [ ] Prove loading the old `{ auto, workers }` config returns all new defaults.
+- [ ] Implement validation and atomic patching.
+- [ ] Update setup documentation; do not add a broad "disable safety" flag.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-config.test.mjs tests/commands.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: add writer orchestration configuration
+```
+
+---
+
+## Task 3: Capture a Clean Git Baseline and Detect Unsupported Repository States
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/git-state.mjs`
+- Create: `tests/orchestration-git-state.test.mjs`
+- Modify: `plugins/codex/scripts/lib/git.mjs`
+
+**Interfaces:**
+
+```js
+export function captureCleanGitBaseline(workspaceRoot) {}
+export function compareGitBaseline(workspaceRoot, baseline) {}
+export function detectInProgressGitOperation(workspaceRoot) {}
+export function assertWriteOrchestrationStartable(workspaceRoot) {}
+```
+
+`captureCleanGitBaseline` records:
+
+- canonical repository root;
+- Git common directory;
+- current branch or detached state;
+- `HEAD` commit;
+- index tree (`git write-tree` on an already clean index);
+- `git status --porcelain=v2 --untracked-files=all` output;
+- active merge/rebase/cherry-pick/revert/bisect operation;
+- capture timestamp.
+
+Start rules:
+
+- write orchestration requires a Git repository;
+- worktree/index must be clean;
+- no in-progress Git operation;
+- `HEAD` must resolve to a commit;
+- detached `HEAD` may run isolated writers but is never auto-applied;
+- non-Git write orchestration remains unsupported in Phase 2.
+
+- [ ] Add failing tests for clean branch, dirty tracked file, staged file, untracked file, detached HEAD, unborn branch, merge state, rebase state, and concurrent HEAD movement.
+- [ ] Implement all Git calls through argv arrays with `shell: false`.
+- [ ] Return structured differences rather than one opaque Boolean.
+- [ ] Preserve existing review helpers in `lib/git.mjs`.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-git-state.test.mjs tests/git.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: capture clean writer orchestration baselines
+```
+
+---
+
+## Task 4: Enforce One Write Orchestration Per Repository
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/workspace-write-lease.mjs`
+- Create: `tests/orchestration-write-lease.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/controller-lifecycle.mjs`
+- Modify: `plugins/codex/scripts/orchestration/state-store.mjs`
+
+**Interfaces:**
+
+```js
+export async function acquireWorkspaceWriteLease(options) {}
+export async function heartbeatWorkspaceWriteLease(lease, options) {}
+export async function releaseWorkspaceWriteLease(lease, options) {}
+export function readWorkspaceWriteLease(workspaceRoot, options) {}
+```
+
+The lease contains the repository canonical path, orchestration ID, controller instance ID, PID, start time, and heartbeat. Acquisition is protected by the existing generic file lock.
+
+Phase 2 stale behavior is fail-closed:
+
+- If the owner PID is alive, reject the second writer orchestration.
+- If the owner PID is dead but an unfinished write orchestration or preserved worktree exists, reject automatic replacement and report the orphan.
+- Only a dead lease with no unfinished writer state may be removed automatically.
+
+- [ ] Add cross-process contention tests.
+- [ ] Add stale-live, stale-dead-safe, and stale-dead-orphan tests.
+- [ ] Add controller shutdown tests proving a completed/cancelled run releases the lease.
+- [ ] Do not hold a filesystem lock for the entire orchestration; persist a lease and heartbeat instead.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-write-lease.test.mjs tests/orchestration-state.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: serialize workspace writer orchestrations
+```
+
+---
+
+## Task 5: Implement Deterministic Ownership Validation
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/ownership-policy.mjs`
+- Create: `tests/orchestration-ownership.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/plan-contract.mjs`
+
+**Interfaces:**
+
+```js
+export function normalizeOwnershipPattern(pattern) {}
+export function matchesOwnershipPattern(relativePath, pattern) {}
+export function validatePackageOwnership(packages) {}
+export function auditChangedFiles(changedFiles, ownership) {}
+```
+
+Required behavior:
+
+- Normalize path separators to `/` only for repository-relative comparison.
+- Reject absolute paths, drive roots, UNC paths, `.git`, parent traversal, and unsupported glob syntax.
+- Detect overlapping write ownership before execution.
+- Allow explicit overlap only when both packages declare the same `sharedOwnershipGroup` and are transitively ordered; otherwise reject.
+- `auditChangedFiles` returns allowed, disallowed, unmatched patterns, and a Boolean pass/fail.
+- Changed submodule gitlinks, `.gitmodules`, and nested repository metadata are disallowed in Phase 2 unless an exact file ownership entry explicitly permits `.gitmodules`; `.git` is never permitted.
+
+- [ ] Add path-separator tests on Windows-style inputs.
+- [ ] Add exact, recursive, segment wildcard, and suffix wildcard matching tests.
+- [ ] Add overlap tests across independent and ordered packages.
+- [ ] Add audit tests for created, modified, renamed, and deleted files.
+- [ ] Integrate ownership validation into plan normalization.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-ownership.test.mjs tests/orchestration-write-contracts.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: enforce writer package ownership
+```
+
+---
+
+## Task 6: Add Redaction and Deterministic Verification Execution
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/redaction.mjs`
+- Create: `plugins/codex/scripts/orchestration/verification-runner.mjs`
+- Create: `tests/orchestration-verification.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/state-store.mjs`
+- Modify: `plugins/codex/scripts/lib/process.mjs`
+
+**Interfaces:**
+
+```js
+export function redactSensitiveText(text, options = {}) {}
+export async function runVerificationCommands(commands, options) {}
+```
+
+Verification requirements:
+
+- Execute `argv[0]` with the remaining argv entries and `shell: false`.
+- Use the package or integration worktree as `cwd`.
+- Reject empty argv and embedded NUL bytes.
+- Apply per-command and global maximum timeouts.
+- Terminate the full process tree on timeout or cancellation.
+- Capture exit code, signal, duration, stdout/stderr byte counts, truncated/redacted excerpts, and optional full local log path.
+- Do not persist complete environment values.
+- Redact common authorization headers, API-key/token patterns, and configured secret environment values.
+- A required verification command passes only on exit code zero.
+
+- [ ] Add success, nonzero exit, timeout, cancellation, large output, and redaction tests.
+- [ ] Add a test proving shell metacharacters remain literal argv text.
+- [ ] Add Windows process-tree coverage through injected platform/process helpers.
+- [ ] Persist only redacted bounded excerpts in state; full logs stay in a local file with mode `0600` where supported.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-verification.test.mjs tests/process.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: run deterministic orchestration verification
+```
+
+---
+
+## Task 7: Support App Server Approval Mediation
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/approval-policy.mjs`
+- Create: `tests/orchestration-approval.test.mjs`
+- Modify: `plugins/codex/scripts/lib/app-server.mjs`
+- Modify: `plugins/codex/scripts/lib/app-server-protocol.d.ts`
+- Modify: `plugins/codex/scripts/lib/codex.mjs`
+- Modify: `tests/fake-codex-fixture.mjs`
+- Modify: `tests/runtime.test.mjs`
+
+**App Server client change:**
+
+Add a server-request handler whose default behavior remains the current JSON-RPC `-32601` rejection.
+
+```js
+client.setServerRequestHandler(async (request) => {
+ return { result: decisionPayload };
+});
+```
+
+The handler must respond exactly once and must handle rejected/throwing callbacks by returning a structured JSON-RPC error without crashing the process.
+
+**Approval policy behavior:**
+
+- Command approvals inspect `commandActions` when available, otherwise the command preview and cwd.
+- File-change approvals are accepted only for the active package worktree root.
+- Network approval context is declined by default.
+- Permission requests grant only the requested filesystem subset inside the package worktree and no network permission.
+- Unknown server requests are declined or rejected fail-closed.
+- Commands with cwd outside the package worktree are declined.
+- Deny remote/destructive command families including `git push`, remote branch deletion, `gh`, deployment CLIs, publication commands, credential mutation, and destructive filesystem operations outside the worktree.
+- Local Git inspection, compilation, testing, and repository-local editing may be accepted when bounded to the worktree.
+- Approval decisions and reasons are redacted and appended to orchestration events.
+
+**Sandbox execution change:**
+
+`runAppServerTurnWithClient` accepts explicit thread approval policy and turn-level sandbox policy while preserving `approvalPolicy: "never"` and read-only defaults for existing callers.
+
+- [ ] Add fake server-request tests for command, file-change, network, permission, unknown, duplicate, and late-resolved requests.
+- [ ] Add regression tests proving review/rescue behavior still rejects unsupported server requests by default.
+- [ ] Add tests for workspace-write sandbox parameters and no automatic danger-full-access fallback.
+- [ ] Type-check against freshly generated current App Server types.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-approval.test.mjs tests/runtime.test.mjs
+npm run build
+```
+
+**Commit:**
+
+```text
+feat: mediate writer app-server approvals
+```
+
+---
+
+## Task 8: Create and Validate Isolated Package Worktrees
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/worktree-manager.mjs`
+- Create: `tests/orchestration-worktree.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/state-store.mjs`
+
+**Interfaces:**
+
+```js
+export function buildPackageBranchName(orchestrationId, packageId) {}
+export function buildIntegrationBranchName(orchestrationId) {}
+export async function createPackageWorktree(options) {}
+export async function createIntegrationWorktree(options) {}
+export function inspectManagedWorktree(options) {}
+export async function removeManagedWorktree(options) {}
+```
+
+Paths:
+
+```text
+${CLAUDE_PLUGIN_DATA}/orchestrations///worktrees/packages/
+${CLAUDE_PLUGIN_DATA}/orchestrations///worktrees/integration
+```
+
+Refs/branches:
+
+```text
+codex-orchestration//package/
+codex-orchestration//integration
+refs/codex-orchestration/final/
+```
+
+Use sanitized short components plus stable hashes to stay within Windows path limits and avoid collisions.
+
+Required behavior:
+
+- Create branches from the exact baseline commit.
+- Never reuse a non-empty path or a branch pointing at an unexpected commit.
+- Verify the created worktree belongs to the original repository common directory.
+- Reject symlinked managed roots that escape plugin data storage.
+- Keep package and integration worktrees isolated from one another.
+- Removal is best-effort only for successful applied runs; failed, blocked, or cancelled runs are preserved in Phase 2.
+- Never run `git worktree prune` globally as an automatic cleanup shortcut.
+
+- [ ] Add one- and two-worktree tests.
+- [ ] Add existing-path, branch-collision, wrong-repository, symlink-escape, and long-path tests.
+- [ ] Add Windows-compatible branch/path quoting tests.
+- [ ] Verify the user's active branch, HEAD, index, and status are unchanged by isolated worktree creation.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-worktree.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: create isolated writer worktrees
+```
+
+---
+
+## Task 9: Build Writer Prompts and Write-Aware Result Contracts
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/package-prompt.mjs`
+- Modify: `plugins/codex/scripts/orchestration/result-contract.mjs`
+- Modify: `plugins/codex/scripts/orchestration/schemas/package-result.schema.json`
+- Create: `tests/orchestration-write-result.test.mjs`
+- Modify: `tests/orchestration-contracts.test.mjs`
+
+**Writer prompt requirements:**
+
+- State the exact worktree root and package ID.
+- Include objective, dependencies, ownership files/interfaces, acceptance criteria, risk tags, and expected outputs.
+- Prohibit remote mutation, credentials, deployment, publication, work outside ownership, and edits outside the worktree.
+- State that the controller owns final Git commit normalization.
+- Tell the Root not to alter branches, remotes, worktrees, Git config, hooks, or repository metadata.
+- Require a canonical JSON result with model-reported changed files, claims, evidence, verification observations, residual risks, and follow-up requests.
+- Make clear that the controller independently audits changed files and reruns declared verification.
+
+**Result normalization:**
+
+- Read-only package results still require empty `changedFiles`.
+- Write package model results may report changed files.
+- Model-reported paths are normalized and compared later with controller-observed paths.
+- Canonical controller result adds `packageCommit`, actual `changedFiles`, `ownershipAudit`, and `controllerVerification`.
+- A mismatch between model-reported and actual changed files is a blocking audit failure, not silently corrected.
+
+- [ ] Add read-only regression tests.
+- [ ] Add valid write result tests.
+- [ ] Add malformed paths, duplicate paths, missing evidence, wrong package ID, and claimed/actual mismatch tests.
+- [ ] Keep raw model output available only for local diagnosis with redaction.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-contracts.test.mjs tests/orchestration-write-result.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: define writer prompts and results
+```
+
+---
+
+## Task 10: Execute Write Packages in Their Assigned Workspaces
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/package-worker.mjs`
+- Modify: `plugins/codex/scripts/orchestration/worker-pool.mjs`
+- Modify: `tests/orchestration-runtime.test.mjs`
+- Create: `tests/orchestration-write-runtime.test.mjs`
+- Modify: `tests/fake-codex-fixture.mjs`
+
+**Worker request shape:**
+
+```js
+{
+ orchestrationId,
+ sourceWorkspaceRoot,
+ executionWorkspaceRoot,
+ packageSpec,
+ dependencyResults,
+ access,
+ sandboxPolicy,
+ approvalContext,
+ resultKind
+}
+```
+
+Required behavior:
+
+- Read-only packages continue using the source workspace and read-only sandbox.
+- Write packages use the package worktree as `cwd` and execution workspace.
+- Worker pool global accounting remains keyed to the source repository, not each generated worktree.
+- Approval handler is installed before thread/turn start.
+- Writer network access remains disabled.
+- Progress and native-child accounting remain unchanged.
+- The worker returns model result and App Server file/command observations; it does not create the canonical package commit.
+- Worker process exit, timeout, and cancellation remain package-scoped.
+
+Fake Codex behaviors must include:
+
+- requesting command approval;
+- requesting file-change approval;
+- writing an owned file after acceptance;
+- attempting a denied external command;
+- attempting an out-of-worktree cwd;
+- leaving uncommitted changes;
+- creating multiple commits;
+- returning a valid write result;
+- interruption while files are modified.
+
+- [ ] Add isolated writer success and denial tests.
+- [ ] Add a regression test proving two read-only Roots still overlap on separate PIDs.
+- [ ] Add two writer Roots modifying separate worktrees concurrently.
+- [ ] Add no-network and out-of-root denial tests.
+- [ ] Add Windows fake binary wrappers and path behavior.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-write-runtime.test.mjs tests/orchestration-runtime.test.mjs
+npm run build
+```
+
+**Commit:**
+
+```text
+feat: execute write packages in isolated worktrees
+```
+
+---
+
+## Task 11: Normalize Every Writer to One Audited Package Commit
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/package-commit.mjs`
+- Create: `tests/orchestration-package-commit.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/state-store.mjs`
+
+**Interfaces:**
+
+```js
+export async function normalizePackageCommit(options) {}
+export function inspectPackageCommit(options) {}
+```
+
+Normalization algorithm for isolated worktrees:
+
+1. Verify package branch and worktree identity.
+2. Verify the branch descends from exactly the package base commit.
+3. Reject merge commits, submodule metadata changes, branch changes, and commits that include unrelated history.
+4. Collect all committed and uncommitted final-tree changes relative to the package base.
+5. Reject an empty change set when the package claims implementation completion, unless `allowEmpty` is explicitly true.
+6. Audit every changed path against ownership.
+7. Compare model-reported changed files with actual changed files.
+8. Run package verification commands in the final tree.
+9. Reset only the isolated package branch to the base while preserving the final tree.
+10. Create exactly one commit using the user's effective Git identity.
+11. Include trailers:
+
+```text
+Codex-Orchestration-Id:
+Codex-Package-Id:
+```
+
+12. Reinspect the created commit and persist canonical metadata.
+
+Do not rewrite the user's active branch or index.
+
+Required result:
+
+```js
+{
+ commit,
+ baseCommit,
+ changedFiles,
+ ownershipAudit,
+ verification,
+ originalCommitCount,
+ normalized: true
+}
+```
+
+- [ ] Test uncommitted changes, one commit, multiple commits, merge commit rejection, wrong base, empty package, owned rename/delete, out-of-scope change, claimed-file mismatch, failed verification, and trailers.
+- [ ] Test Git identity absence with an actionable blocked result; do not invent a fake user identity.
+- [ ] Prove normalization changes only the package branch/worktree.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-package-commit.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: normalize writer package commits
+```
+
+---
+
+## Task 12: Persist Phase 2 State and Migrate Phase 1 Reads
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/constants.mjs`
+- Modify: `plugins/codex/scripts/orchestration/state-store.mjs`
+- Modify: `plugins/codex/scripts/orchestration/result-contract.mjs`
+- Modify: `plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json`
+- Modify: `tests/orchestration-state.test.mjs`
+
+Required behavior:
+
+- New records use state version 2.
+- Version-1 records load with `write.enabled: false` and `integration.status: "not-required"` in memory.
+- Write baseline, lease identity, package workspace, commit, ownership audit, verification, integration, reviewer, final commit, application, and decision point are durable.
+- State updates remain atomic and workspace scoped.
+- Events remain append-only and redacted.
+- Status/result reference resolution remains compatible.
+- Canonical orchestration result includes preserved artifact paths only when local disclosure is safe and useful.
+
+- [ ] Add v1 load tests using fixture JSON.
+- [ ] Add v2 round-trip tests.
+- [ ] Add concurrent package/integration update tests.
+- [ ] Add result rendering tests for applied, preserved, blocked conflict, reviewer reject, and failed verification.
+- [ ] Do not auto-rewrite v1 state during read.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-state.test.mjs tests/render.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: persist writer orchestration state
+```
+
+---
+
+## Task 13: Integrate Package Commits Deterministically
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/integration-manager.mjs`
+- Create: `tests/orchestration-integration.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/scheduler.mjs`
+
+**Interfaces:**
+
+```js
+export async function prepareIntegration(options) {}
+export async function integratePackageCommits(options) {}
+export function buildIntegrationOrder(plan, packageStates) {}
+export function buildConflictDecisionPoint(options) {}
+```
+
+Integration rules:
+
+- Start the integration branch/worktree from the exact baseline commit.
+- Include only `completed` or accepted `partial` writer packages whose dependencies are usable.
+- Exclude failed, blocked, or cancelled optional packages and record omissions.
+- Refuse to integrate when a required writer is absent.
+- Use stable topological order; ties follow plan order and then package ID.
+- Cherry-pick normalized package commits one by one.
+- After each cherry-pick, record package ID, source commit, integration commit, and resulting tree.
+- Automatically accept only conflict-free Git operations.
+- On conflict, abort the active cherry-pick while preserving conflict diagnostics and set a structured decision point containing package IDs, files, stages, ownership, and available evidence.
+- Do not attempt a strategy-option merge or semantic resolution.
+
+Phase 2 decision behavior:
+
+- A conflict makes the orchestration `blocked`.
+- Package branches and the integration worktree are preserved.
+- Result output explains that Phase 3 will add repair/resume behavior.
+
+- [ ] Add independent-order tests.
+- [ ] Add dependency-order tests.
+- [ ] Add optional omission and required failure tests.
+- [ ] Add clean overlapping textual edits that Git can merge.
+- [ ] Add true conflict tests and verify no user-branch mutation.
+- [ ] Add deterministic rerun tests proving the same inputs create the same integrated tree.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-integration.test.mjs tests/orchestration-scheduler.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: integrate package commits deterministically
+```
+
+---
+
+## Task 14: Add Risk-Triggered Integration Review
+
+**Files:**
+- Create: `plugins/codex/scripts/orchestration/reviewer-policy.mjs`
+- Create: `plugins/codex/scripts/orchestration/schemas/reviewer-result.schema.json`
+- Create: `tests/orchestration-reviewer.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/package-prompt.mjs`
+- Modify: `plugins/codex/scripts/orchestration/package-worker.mjs`
+- Modify: `plugins/codex/scripts/orchestration/worker-pool.mjs`
+
+**Interfaces:**
+
+```js
+export function evaluateReviewerRequirement(context) {}
+export function validateReviewerResult(input) {}
+```
+
+Automatic review triggers:
+
+- two or more integrated writer packages;
+- any built-in high-risk tag;
+- any declared interface ownership;
+- model-reported confidence below configured threshold;
+- a writer required transient retry or model escalation;
+- package evidence is incomplete;
+- integration changed a public schema/protocol path declared by the plan;
+- Claude set reviewer mode `required`.
+
+Reviewer modes:
+
+- `disabled`: valid only when no mandatory trigger exists; explicit orchestration only.
+- `auto`: run when a trigger exists.
+- `required`: always run.
+
+Reviewer execution:
+
+- Read-only sandbox in the integration worktree.
+- Review the complete `base..integration` diff, package results, ownership, and controller verification.
+- No file changes, command mutation, or external action.
+- Canonical result:
+
+```json
+{
+ "verdict": "approve",
+ "summary": "...",
+ "blockingFindings": [],
+ "nonBlockingFindings": [],
+ "evidence": [],
+ "recommendedResolution": []
+}
+```
+
+Behavior:
+
+- `approve` permits final verification/commit.
+- `revise` or `reject` blocks automatic integration in Phase 2 and records a decision point.
+- Malformed output fails closed.
+- Claude may report the preserved integration result but may not silently reinterpret a blocking verdict as approval.
+
+- [ ] Add trigger matrix tests.
+- [ ] Add reviewer-disabled invalid-plan tests.
+- [ ] Add approve/revise/reject/malformed output tests.
+- [ ] Add read-only boundary tests proving reviewer edits fail.
+- [ ] Add model/effort routing tests preserving `Sol > Terra > Luna` tier semantics.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-reviewer.test.mjs tests/model-policy.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: gate integration with independent review
+```
+
+---
+
+## Task 15: Run Full Verification and Create One Final Commit
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/integration-manager.mjs`
+- Modify: `tests/orchestration-integration.test.mjs`
+- Modify: `plugins/codex/scripts/orchestration/result-contract.mjs`
+
+Finalization sequence:
+
+1. Ensure package integration is complete and conflict-free.
+2. Run top-level integration verification commands.
+3. Run Reviewer when required.
+4. Read the final integration tree.
+5. Create one final commit whose sole parent is the original baseline commit.
+6. Use the plan's final subject and a generated body summarizing included package IDs.
+7. Include trailers:
+
+```text
+Codex-Orchestration-Id:
+Codex-Packages:
+```
+
+8. Store the commit at `refs/codex-orchestration/final/`.
+9. Reinspect parent, tree, message, and changed files.
+
+Do not create the final commit when:
+
+- required package integration is incomplete;
+- required verification failed;
+- Reviewer did not approve;
+- ownership audit failed;
+- conflict/decision point exists.
+
+- [ ] Add full-verification pass/fail/timeout tests.
+- [ ] Add final commit parent/tree/trailer tests.
+- [ ] Prove package integration commits are not parents of the final squash commit.
+- [ ] Prove final commit creation does not move the user branch.
+- [ ] Add deterministic content tests excluding timestamps from commit-message semantics; commit SHA may vary with commit time, but tree and parent must be stable.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-integration.test.mjs tests/orchestration-verification.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: create verified orchestration squash commits
+```
+
+---
+
+## Task 16: Apply the Final Commit Only to an Unchanged Clean User Branch
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/integration-manager.mjs`
+- Modify: `plugins/codex/scripts/orchestration/git-state.mjs`
+- Modify: `tests/orchestration-integration.test.mjs`
+
+Eligibility predicate:
+
+- Plan requests `autoApply` and config permits it.
+- Baseline was on a named branch.
+- Current branch equals baseline branch.
+- Current `HEAD` equals baseline `HEAD`.
+- Current index tree equals baseline index tree.
+- Current porcelain-v2 status is empty.
+- No Git operation is in progress.
+- Final commit parent equals baseline `HEAD`.
+- Final diff stays inside the union of accepted writer ownership.
+- All required packages, verification, and review gates passed.
+
+Application:
+
+- Acquire/revalidate the workspace write lease.
+- Run a fast-forward-only update from the user's active worktree.
+- Verify resulting `HEAD` equals the final commit and status is clean.
+- On any precondition failure, perform no branch/index/worktree mutation and mark application `preserved` with the exact reason.
+- On unexpected Git failure, preserve the final ref and mark integration `blocked`; do not retry with reset or force.
+
+- [ ] Add successful fast-forward test.
+- [ ] Add tests for branch switch, HEAD movement, staged change, unstaged change, untracked file, detached HEAD, Git operation, ownership mismatch, and config-disabled auto-apply.
+- [ ] Add a race test that changes `HEAD` after eligibility check but before ref update; use Git's expected-old-value semantics or an equivalent atomic guard.
+- [ ] Prove no unsafe case mutates the user's branch or index.
+
+**Focused verification:**
+
+```bash
+node --test --test-name-pattern="auto-apply|preserves final commit|HEAD movement" tests/orchestration-integration.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: safely apply final orchestration commits
+```
+
+---
+
+## Task 17: Add the Strict Direct Single-Writer Optimization
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/worktree-manager.mjs`
+- Modify: `plugins/codex/scripts/orchestration/package-commit.mjs`
+- Modify: `plugins/codex/scripts/orchestration/controller.mjs`
+- Modify: `tests/orchestration-package-commit.test.mjs`
+- Modify: `tests/orchestration-write-runtime.test.mjs`
+
+Direct mode is eligible only when:
+
+- exactly one write package exists;
+- no other package runs concurrently with the writer;
+- baseline branch is named and clean;
+- Reviewer is not mandatory;
+- the plan explicitly requests `workspace.mode: "direct"` or config chooses direct mode;
+- package ownership excludes `.gitmodules` and repository metadata;
+- approval policy denies Git index/branch/commit operations initiated by the model;
+- automatic final application is enabled.
+
+Direct-mode algorithm:
+
+1. Revalidate baseline immediately before starting the writer.
+2. Run the writer in the user's worktree with workspace-write sandboxing.
+3. Do not permit model-initiated `git add`, `git commit`, `git reset`, branch, worktree, or remote operations.
+4. Audit the resulting working-tree changes.
+5. Build the package/final tree through a temporary index based on baseline `HEAD`; do not use the user's index for staging.
+6. Run package and integration verification against the visible user worktree.
+7. Create the final commit with `git commit-tree` without moving `HEAD`.
+8. Revalidate that user-visible file content exactly matches the final tree, the branch and `HEAD` remain baseline values, and the user index remains unchanged.
+9. Atomically update the branch ref from baseline to final commit and update the index to the final tree.
+10. Verify clean status.
+
+Failure behavior:
+
+- Never reset or discard visible writer changes.
+- If verification, audit, or finalization fails, leave the user worktree dirty and report exact files and preserved commit/ref state.
+- Cancellation leaves partial visible changes and marks the result accordingly.
+
+- [ ] Add direct success test.
+- [ ] Add model-staging denial test.
+- [ ] Add concurrent user edit and HEAD movement tests.
+- [ ] Add verification failure and cancellation tests proving changes are preserved, not reset.
+- [ ] Add temporary-index tests proving the original index fingerprint is unchanged until final successful application.
+- [ ] Keep isolated mode as default after direct mode exists.
+
+**Focused verification:**
+
+```bash
+node --test --test-name-pattern="direct writer" tests/orchestration-package-commit.test.mjs tests/orchestration-write-runtime.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: add gated direct single-writer mode
+```
+
+---
+
+## Task 18: Integrate Write Phases into the Controller State Machine
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/controller.mjs`
+- Modify: `plugins/codex/scripts/orchestration/controller-server.mjs`
+- Modify: `plugins/codex/scripts/orchestration/controller-client.mjs`
+- Create or modify: `tests/orchestration-controller.test.mjs`
+- Modify: `tests/orchestration-runtime.test.mjs`
+- Modify: `tests/orchestration-write-runtime.test.mjs`
+
+Controller sequence for a write plan:
+
+1. Normalize plan.
+2. Capture clean Git baseline.
+3. Acquire workspace write lease.
+4. Persist write state before creating worktrees.
+5. Prepare package workspaces.
+6. Run Phase 1 scheduler for packages.
+7. Normalize each successful writer package commit before marking it integration-usable.
+8. Propagate package dependency failures.
+9. When package execution is terminal, enter `integrating` rather than finalizing.
+10. Prepare integration worktree and integrate commits.
+11. Run integration verification.
+12. Run Reviewer when required.
+13. Create final commit.
+14. Apply or preserve.
+15. Persist aggregate result and release lease.
+
+Read-only plans follow the existing Phase 1 path with no Git baseline, write lease, worktrees, or integration state.
+
+Cancellation:
+
+- Stop scheduling.
+- Interrupt active Roots.
+- Preserve writer worktrees and branches unless final application already completed.
+- Direct mode leaves visible changes.
+- Never auto-apply after cancellation.
+- Release the write lease only after durable terminal state is written.
+
+Controller-loss behavior in Phase 2:
+
+- On startup, unfinished write orchestration state is not automatically resumed.
+- Mark it `blocked` with reason `WRITE_RECOVERY_REQUIRES_PHASE_3` if the owning controller is gone.
+- Preserve all refs/worktrees and report them.
+- Read-only Phase 1 loss behavior remains unchanged.
+
+- [ ] Add one-writer isolated end-to-end test.
+- [ ] Add two-writer parallel execution and deterministic integration test.
+- [ ] Add optional writer failure/omission test.
+- [ ] Add required writer failure test.
+- [ ] Add reviewer reject, verification failure, conflict, user-HEAD movement, and cancellation tests.
+- [ ] Add controller-loss preservation test.
+- [ ] Add read-only regression test proving no Git commands/worktrees are introduced.
+
+**Focused verification:**
+
+```bash
+node --test tests/orchestration-controller.test.mjs tests/orchestration-write-runtime.test.mjs tests/orchestration-runtime.test.mjs
+```
+
+**Commit:**
+
+```text
+feat: orchestrate writer integration lifecycle
+```
+
+---
+
+## Task 19: Update CLI, Rendering, Commands, and Skills
+
+**Files:**
+- Modify: `plugins/codex/scripts/orchestration/cli.mjs`
+- Modify: `plugins/codex/scripts/codex-companion.mjs`
+- Modify: `plugins/codex/scripts/lib/render.mjs`
+- Modify: `plugins/codex/commands/orchestrate.md`
+- Modify: `plugins/codex/commands/status.md`
+- Modify: `plugins/codex/commands/result.md`
+- Modify: `plugins/codex/commands/cancel.md`
+- Modify: `plugins/codex/skills/codex-orchestration/SKILL.md`
+- Modify: `plugins/codex/skills/codex-work-package-contract/SKILL.md`
+- Modify: `plugins/codex/skills/codex-integration-policy/SKILL.md`
+- Modify: `plugins/codex/skills/codex-orchestration-recovery/SKILL.md`
+- Modify: `tests/commands.test.mjs`
+- Modify: `tests/render.test.mjs`
+- Modify: `tests/orchestration-skill.test.mjs`
+
+Status output must expose:
+
+- read-only versus write orchestration;
+- baseline branch/HEAD;
+- package worktree mode and path summary;
+- package commit and ownership/verification status;
+- integration status and included/omitted packages;
+- conflict or other decision point;
+- Reviewer requirement and verdict;
+- final commit;
+- application eligibility/result;
+- preserved artifacts and Phase 3 recovery limitation.
+
+Result output must clearly distinguish:
+
+- **applied:** final commit is on the user's branch;
+- **preserved:** final commit is ready but user branch was not changed;
+- **blocked:** conflict, reviewer, audit, or verification prevented finalization;
+- **degraded:** objective completed with permitted omissions;
+- **failed/cancelled:** no automatic integration occurred.
+
+Skill requirements:
+
+- Automatic write orchestration is allowed only when orchestration auto-entry is enabled and the user tree is clean.
+- Claude must declare ownership, risk tags, package verification, integration verification, Reviewer policy, and final commit subject.
+- Claude must collapse writers that require each other's unintegrated code.
+- Claude must not promise Phase 3 recovery.
+- Claude must report blocking Reviewer findings and preserved artifacts.
+- External actions always require a separate explicit user authorization and are not executed by Phase 2.
+
+Do not add a large user-facing command vocabulary. A local final commit that was preserved may be applied later only after an explicit user request and a fresh safety check; the internal CLI may expose `apply --json`, but no automatic retry is performed by status/result.
+
+- [ ] Add command surface tests.
+- [ ] Add compressed plan examples for one and multiple writers.
+- [ ] Add render snapshots for applied, preserved, blocked, degraded, and cancelled write runs.
+- [ ] Remove the Phase 1 text that says all orchestration is strictly read-only.
+- [ ] Preserve the Phase 1 read-only safety explanation as a supported mode.
+
+**Focused verification:**
+
+```bash
+node --test tests/commands.test.mjs tests/render.test.mjs tests/orchestration-skill.test.mjs
+```
+
+**Commit:**
+
+```text
+docs: expose clean-tree writer orchestration
+```
+
+---
+
+## Task 20: Cross-Platform Hardening and Release Verification
+
+**Files:**
+- Modify: `.github/workflows/pull-request-ci.yml`
+- Modify: `package.json` only if adding a dedicated smoke script is necessary
+- Modify: `tsconfig.app-server.json`
+- Modify: `README.md`
+- Add or modify tests from prior tasks
+
+### CI matrix
+
+The existing Ubuntu, macOS, and Windows matrix remains mandatory. Add focused worktree/integration coverage to the normal `npm test` suite; do not hide it behind a platform-specific optional script.
+
+### Cross-platform requirements
+
+- Git paths are argv entries, never shell-concatenated strings.
+- Repository-relative ownership paths use `/`; OS paths use `path` APIs.
+- Named pipes and Unix sockets remain unchanged.
+- Worktree paths stay below conservative Windows path-length budgets.
+- Process-tree timeout/cancellation works on Windows.
+- Symlink tests skip only when the runner cannot create symlinks and must report the skip.
+- File mode assertions are conditional on filesystem capability.
+- Atomic state writes and final application are verified on all platforms.
+
+### Real Codex smoke suite
+
+Provide a documented manual or authenticated CI smoke path for:
+
+1. one isolated writer;
+2. two parallel isolated writers;
+3. denied external command;
+4. integration verification;
+5. Reviewer approve;
+6. Reviewer reject;
+7. final commit preserved after user `HEAD` movement;
+8. successful clean-branch fast-forward;
+9. direct single-writer mode;
+10. cancellation with preserved changes.
+
+The smoke suite must run in a disposable repository. It must never target the plugin's own active development checkout.
+
+### Final merge gate
+
+Run from a clean checkout:
+
+```bash
+npm ci
+npm run check-version
+npm test
+npm run build
+git diff --check
+```
+
+Then verify:
+
+```bash
+# Phase 1 read-only regression
+node --test tests/orchestration-runtime.test.mjs
+
+# Phase 2 write path
+node --test tests/orchestration-write-runtime.test.mjs \
+ tests/orchestration-worktree.test.mjs \
+ tests/orchestration-package-commit.test.mjs \
+ tests/orchestration-integration.test.mjs \
+ tests/orchestration-approval.test.mjs
+```
+
+Required release evidence:
+
+- all tests pass on Ubuntu, macOS, and Windows;
+- two writer Roots overlap in time while modifying different worktrees;
+- no package contaminates another package's tree/result;
+- external command approval is denied and audited;
+- final commit is a single child of baseline;
+- unsafe auto-apply conditions leave the user branch unchanged;
+- existing review/rescue/transfer/broker flows remain green;
+- no dirty-tree snapshot ref or automatic write recovery has leaked into Phase 2.
+
+**Commit:**
+
+```text
+test: verify clean-tree writer orchestration
+```
+
+---
+
+## 6. Required Error Codes
+
+Use stable machine-readable codes in state/events/results where applicable:
+
+| Code | Meaning |
+|---|---|
+| `WRITE_REQUIRES_GIT` | Writer plan started outside a Git repository. |
+| `WRITE_REQUIRES_CLEAN_TREE` | User branch/index/worktree is dirty. |
+| `GIT_OPERATION_IN_PROGRESS` | Merge/rebase/cherry-pick/revert/bisect is active. |
+| `WRITE_LEASE_CONFLICT` | Another write orchestration owns the repository. |
+| `WRITE_ORPHAN_REQUIRES_RECOVERY` | Dead owner left unfinished write artifacts. |
+| `INVALID_OWNERSHIP_PATTERN` | Ownership path grammar is unsafe or unsupported. |
+| `OWNERSHIP_OVERLAP` | Writer packages overlap without valid ordering/group. |
+| `OWNERSHIP_VIOLATION` | Actual changes exceed declared ownership. |
+| `MODEL_CHANGED_FILES_MISMATCH` | Model-reported and actual changed-file sets differ. |
+| `APPROVAL_DENIED` | Requested command/file/permission action was denied. |
+| `EXTERNAL_ACTION_DENIED` | Remote or consequential action was attempted. |
+| `WRITER_SANDBOX_UNAVAILABLE` | Required workspace-write policy cannot be established. |
+| `PACKAGE_VERIFICATION_FAILED` | Controller-run package verification failed. |
+| `PACKAGE_COMMIT_NORMALIZATION_FAILED` | Final package tree could not be normalized safely. |
+| `INTEGRATION_CONFLICT` | Cherry-pick produced a semantic conflict. |
+| `INTEGRATION_VERIFICATION_FAILED` | Full integration verification failed. |
+| `REVIEWER_REVISE` | Reviewer requires changes. |
+| `REVIEWER_REJECT` | Reviewer rejected integration. |
+| `FINAL_COMMIT_INVALID` | Final parent/tree/message audit failed. |
+| `AUTO_APPLY_PRECONDITION_FAILED` | User branch was not safely applicable. |
+| `AUTO_APPLY_RACE` | Baseline changed during application. |
+| `WRITE_RECOVERY_REQUIRES_PHASE_3` | Interrupted write orchestration cannot auto-resume. |
+
+---
+
+## 7. Security Invariants
+
+The implementation is not complete unless all invariants are enforced by code and tests.
+
+1. A write Root can write only in its assigned execution worktree under the selected sandbox policy.
+2. The controller never auto-approves network access in Phase 2.
+3. The controller never authorizes external mutation commands.
+4. Git orchestration commands use `shell: false` and fixed argv construction.
+5. Verification commands are plan-declared argv arrays and never shell-evaluated.
+6. Actual changed files are derived from Git, not trusted from the model.
+7. Every changed file must match declared ownership.
+8. Package commits are controller-normalized and independently reinspected.
+9. The integration manager never resolves semantic conflicts automatically.
+10. The final commit's parent must equal the captured baseline commit.
+11. Automatic application is fast-forward only and guarded by expected-old-value checks.
+12. Failure or cancellation never resets the user's visible work to hide partial changes.
+13. Read-only orchestration remains read-only and never acquires a write lease or creates a worktree.
+14. Server-request handling defaults to rejection for callers that do not explicitly install an approval policy.
+15. Durable logs are redacted and bounded.
+16. Controller loss preserves write artifacts and fails closed until Phase 3 recovery exists.
+
+---
+
+## 8. Phase 2 Acceptance Criteria
+
+Phase 2 is complete only when all of the following are demonstrated with fresh evidence:
+
+1. Existing Phase 1 read-only orchestration passes unchanged.
+2. A write plan cannot start from a dirty tree or active Git operation.
+3. Two writer packages execute concurrently in different worktrees from the same base commit.
+4. A writer cannot modify another package's worktree or files outside ownership.
+5. Model-initiated external commands are denied and recorded.
+6. Uncommitted, single-commit, and multi-commit writer output normalize to one package commit.
+7. Package verification is controller-run and blocks invalid commits.
+8. Required package commits integrate in deterministic topological order.
+9. Optional failed packages may be omitted only when dependency/objective rules permit it.
+10. Semantic Git conflicts produce a structured blocking decision point with no invented resolution.
+11. Mandatory risk triggers run a Sol Reviewer according to the plan.
+12. Reviewer `revise` or `reject` prevents final automatic application.
+13. Full integration verification must pass before final commit creation.
+14. The final commit is one child of the original baseline and contains the integration tree.
+15. An unchanged clean user branch fast-forwards to the final commit.
+16. Branch switch, HEAD movement, or any dirty state preserves the final commit without touching the user branch.
+17. Direct single-writer mode never uses the user's index as a temporary staging area and never discards failed changes.
+18. Cancellation preserves package/direct changes and never auto-applies.
+19. A lost controller does not auto-resume write work in Phase 2.
+20. The complete suite and type-check build pass on Ubuntu, macOS, and Windows.
+21. A disposable authenticated smoke test proves real App Server approval, writer, integration, Reviewer, and application behavior.
+
+---
+
+## 9. Recommended Implementation Sequence
+
+Execute commits in this exact dependency order:
+
+1. `feat: extend orchestration plan contracts for writers`
+2. `feat: add writer orchestration configuration`
+3. `feat: capture clean writer orchestration baselines`
+4. `feat: serialize workspace writer orchestrations`
+5. `feat: enforce writer package ownership`
+6. `feat: run deterministic orchestration verification`
+7. `feat: mediate writer app-server approvals`
+8. `feat: create isolated writer worktrees`
+9. `feat: define writer prompts and results`
+10. `feat: execute write packages in isolated worktrees`
+11. `feat: normalize writer package commits`
+12. `feat: persist writer orchestration state`
+13. `feat: integrate package commits deterministically`
+14. `feat: gate integration with independent review`
+15. `feat: create verified orchestration squash commits`
+16. `feat: safely apply final orchestration commits`
+17. `feat: add gated direct single-writer mode`
+18. `feat: orchestrate writer integration lifecycle`
+19. `docs: expose clean-tree writer orchestration`
+20. `test: verify clean-tree writer orchestration`
+
+Do not squash these during development. Preserve focused commits until the final reviewed pull request; the repository owner may squash on merge.
diff --git a/docs/superpowers/specs/2026-08-17-claude-native-multi-codex-orchestration-design.md b/docs/superpowers/specs/2026-08-17-claude-native-multi-codex-orchestration-design.md
new file mode 100644
index 000000000..e179c623a
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-17-claude-native-multi-codex-orchestration-design.md
@@ -0,0 +1,1640 @@
+# Claude-Native Multi-Codex Orchestration Layer
+
+- **Status:** Approved design
+- **Date:** 2026-08-17
+- **Target repository:** `eureka-pd/codex-plugin-cc`
+- **Primary owner:** Claude Root Agent
+- **Runtime substrate:** Codex App Server
+- **Initial model policy:** GPT-5.6 Sol, Terra, and Luna
+
+## 1. Executive summary
+
+This design extends the Claude Code Codex plugin from a single delegated Codex task into a Claude-native, multi-Codex orchestration layer.
+
+Claude remains the root orchestrator and owns semantic decisions:
+
+- whether orchestration is appropriate;
+- decomposition into bounded work packages;
+- dependency and execution planning;
+- model, reasoning-effort, access, and workspace assignment;
+- replanning, escalation, and reviewer placement;
+- integration judgment and final user-facing conclusions.
+
+A deterministic Node.js runtime controller owns mechanical execution:
+
+- a bounded Codex App Server worker pool;
+- top-level Codex thread and turn lifecycle;
+- notification routing and result capture;
+- DAG scheduling and package state transitions;
+- timeout, interruption, retry, and cancellation;
+- persistent orchestration state and restart recovery;
+- Git snapshots, package worktrees, integration branches, and cleanup.
+
+Each top-level Codex Root receives one bounded work package. A Codex Root may use native Codex child agents when the model supports them and the package budget permits it. Claude observes those child-agent events but does not bypass the Codex parent to steer the children directly.
+
+The feature is hybrid at every important boundary:
+
+- explicit `/codex:orchestrate` invocation plus opt-in automatic entry;
+- read-only agents sharing safe state plus isolated writer worktrees;
+- an initial DAG plus limited dynamic replanning;
+- role-based model defaults plus dynamic escalation;
+- evidence-based Claude integration plus risk-triggered Sol review;
+- in-plugin deployment plus an internal boundary suitable for later extraction.
+
+## 2. Problem statement
+
+The current plugin is optimized for a single Codex delegation or review at a time. Its rescue subagent is deliberately a thin forwarder, and the shared broker serializes active streaming operations through a single active request/stream owner. This is appropriate for one task, but it prevents Claude from acting as a genuine root orchestrator over several independent Codex Roots.
+
+Complex repository work commonly contains independent concerns that benefit from parallel execution:
+
+- architecture analysis and implementation;
+- frontend and backend work;
+- migration design and rollback analysis;
+- implementation and independent verification;
+- competing hypotheses in an unclear debugging problem;
+- separate security, concurrency, and regression reviews.
+
+Delegating all of these to one Codex thread creates several failure modes:
+
+- unnecessary serial latency;
+- one model tier and effort applied to heterogeneous work;
+- weak isolation between parallel writers;
+- context dilution in long-running tasks;
+- a single failure blocking unrelated progress;
+- no explicit dependency graph or evidence contract;
+- no durable orchestration-level recovery state.
+
+The desired system must allow Claude to coordinate several Codex Roots without surrendering top-level intent, without allowing unbounded agent growth, and without exposing the user's active branch to incomplete parallel changes.
+
+## 3. Goals
+
+The system shall:
+
+1. Allow Claude to start multiple top-level Codex Roots for one user task.
+2. Preserve Claude as the owner of decomposition, scheduling intent, replanning, and final judgment.
+3. Execute independent top-level Codex Roots concurrently through a bounded App Server worker pool.
+4. Allow each Codex Root to use native child agents inside its assigned package when supported.
+5. Support both explicit orchestration and opt-in automatic orchestration.
+6. Allow local write-capable orchestration to start automatically when enabled.
+7. Keep external, destructive, publication, deployment, and remote mutation actions behind explicit user approval.
+8. Isolate multiple writers through Git worktrees and package branches.
+9. Preserve a stable snapshot when the user's working tree is dirty without modifying the user's index, branch, or working tree.
+10. Integrate verified package commits on an isolated integration branch before touching the user's branch.
+11. Validate results through structured evidence, executable verification, and risk-triggered independent review.
+12. Persist orchestration state sufficiently to recover from Claude Code reloads, controller restarts, and worker failures.
+13. Bound agent count, model tier, retries, replans, and elapsed time through an adaptive budget envelope.
+14. Extend existing `status`, `result`, and `cancel` surfaces rather than creating a large command vocabulary.
+15. Remain testable on macOS, Linux, and Windows.
+16. Preserve module boundaries that allow upstream contribution in smaller pull requests.
+
+## 4. Non-goals
+
+The initial system does not aim to:
+
+- replace Claude with a Codex lead agent;
+- let Claude directly control native Codex child agents owned by a Codex Root;
+- provide shared hidden reasoning or shared mutable memory between Claude and Codex;
+- distribute workers across multiple machines;
+- create an unbounded autonomous software-development loop;
+- automatically push, open or merge pull requests, publish packages, or deploy systems;
+- guarantee perfect confinement while using `danger-full-access`;
+- become a general-purpose workflow engine unrelated to Codex delegation;
+- make every repository task use multiple agents;
+- expose every controller operation as a separate slash command;
+- require users to commit or stash their existing work before orchestration.
+
+## 5. Terminology
+
+| Term | Definition |
+|---|---|
+| **Orchestration** | One Claude-managed multi-Codex execution created for a user objective. |
+| **Claude Root** | The Claude Code agent that owns semantic planning and final integration judgment. |
+| **Work package** | A bounded top-level task with explicit dependencies, ownership, access, acceptance criteria, and result contract. |
+| **Codex Root** | One top-level Codex thread assigned to one work package and owned directly by Claude orchestration. |
+| **Native child** | A Codex subagent spawned and owned by a Codex Root through native multi-agent tools. |
+| **Worker** | One long-lived Codex App Server process, one controller client, and one active top-level Codex Root lease. |
+| **Workspace** | A Git repository root or non-Git working directory in which orchestration operates. |
+| **Package branch** | A temporary branch containing one writer package's normalized atomic commit. |
+| **Integration branch** | A temporary branch where package commits are combined, conflicts are resolved, and verification is executed. |
+| **Snapshot ref** | An internal Git ref representing the user's complete working-tree content at orchestration start. |
+| **Decision point** | A state requiring Claude semantic judgment, such as a structural failure, conflict, or reviewer rejection. |
+
+## 6. Decision summary
+
+The approved design decisions are:
+
+| Concern | Decision |
+|---|---|
+| Invocation | Hybrid: explicit command plus opt-in automatic entry. |
+| Automatic local writes | Allowed after compressed plan notification. |
+| Workspace isolation | Hybrid: safe readers may share; multiple writers use worktrees. |
+| Topology | Claude-managed top-level Codex Roots; Codex-managed native children. |
+| Model routing | Role defaults plus Luna → Terra → Sol escalation. |
+| App Server allocation | Bounded workspace-scoped worker pool. |
+| Scheduling | Initial DAG plus limited dynamic replanning. |
+| Integration | Evidence-based Claude judgment plus risk-triggered Reviewer. |
+| Persistence | Durable control plane with scoped recovery. |
+| Commands | Add `/codex:orchestrate`; extend `status`, `result`, and `cancel`. |
+| User notification | Compressed plan notification, then immediate start. |
+| Auto-entry | Hard exclusions plus Complexity Score. |
+| Budget | Complexity-adaptive envelope with absolute caps. |
+| Failure handling | Package-scoped circuit breaker and conditional partial integration. |
+| Git integration | Package branches → integration branch → review → squash. |
+| Execution ownership | Claude judgment plus deterministic runtime controller. |
+| Deployment boundary | In-plugin modules with an independent application boundary. |
+| Implementation | Three phases: read-only, clean-tree writers, recovery/dirty-tree. |
+| Skill layout | One root orchestration skill plus internal policy skills. |
+| Configuration | Dedicated user/project JSON configuration with schema. |
+| Auto-enable | Explicit one-time enablement; explicit command always available. |
+| Worker reuse | Workspace pool, global cap, ten-minute idle TTL. |
+| Native-agent fallback | A required-but-unavailable native child policy degrades to Root-only execution. |
+| Dirty tree | Hidden snapshot commit using a temporary index. |
+| Final commit | One local squash commit when automatic application is safe. |
+| Commit identity | User Git identity, orchestration/package trailers. |
+| Safety | App Server approval mediation, command deny rules, prompt policy, and audit. |
+| Progress output | Milestone-oriented chat output; detailed local event log. |
+| Cancellation | Interrupt, grace period, process-tree termination, partial-state recovery. |
+| Retention | Time-bounded metadata, logs, refs, and failed worktrees. |
+| Result format | Canonical JSON plus Markdown renderer. |
+| Platforms | macOS, Linux, and Windows from the first complete release. |
+| Upstream strategy | Complete in the fork while preserving small PR boundaries. |
+
+## 7. Architectural principles
+
+### 7.1 Claude owns meaning; the controller owns mechanics
+
+The controller shall never invent or materially reinterpret the plan. It validates and executes structured instructions. Claude decides what work exists, whether results are sufficient, and how conflicts are resolved.
+
+### 7.2 Top-level parallelism must correspond to real independence
+
+A high Complexity Score alone does not justify parallel writers. Claude must identify independent package ownership, stable interfaces, or a read-only comparison purpose. If several tasks modify the same semantic core, the plan shall collapse to one writer plus one or more read-only reviewers.
+
+### 7.3 Every package is independently understandable
+
+A package must state:
+
+- what it is expected to accomplish;
+- what it may read and write;
+- what other packages it depends on;
+- how completion will be verified;
+- what evidence it must return;
+- which files, APIs, schemas, or domains it owns.
+
+### 7.4 Evidence outranks self-reported confidence
+
+A package's confidence value is advisory. Acceptance depends on repository evidence, command results, diff inspection, contract compatibility, and reviewer findings.
+
+### 7.5 Failures are isolated before they are escalated
+
+A failed package blocks only its dependent subgraph unless the package is required for the overall objective. Independent branches continue within the approved budget.
+
+### 7.6 User work is never silently rewritten to enable orchestration
+
+The system does not stash, reset, commit, or modify the user's index merely to create a stable base. It creates an internal snapshot ref with a temporary index.
+
+### 7.7 Automatic local action does not imply automatic external action
+
+Local repository edits, tests, builds, and static analysis may start automatically. Push, publication, deployment, remote data mutation, credential changes, and destructive external actions require explicit user approval.
+
+## 8. System architecture
+
+```mermaid
+flowchart TD
+ U[User request] --> C[Claude Root]
+ C --> S[codex-orchestration skill]
+ S -->|structured plan| RC[Runtime Controller]
+ RC --> PS[Persistent State Store]
+ RC --> SCH[DAG Scheduler]
+ RC --> WM[Workspace and Integration Manager]
+ SCH --> WP[Workspace-scoped Worker Pool]
+ WP --> W1[Worker 1 / App Server]
+ WP --> W2[Worker 2 / App Server]
+ WP --> W3[Worker 3 / App Server]
+ W1 --> R1[Codex Root A]
+ W2 --> R2[Codex Root B]
+ W3 --> R3[Codex Root C]
+ R1 --> C1[Native children]
+ R2 --> C2[Native children]
+ R3 --> C3[Native children]
+ W1 --> ER[Event Router]
+ W2 --> ER
+ W3 --> ER
+ ER --> PS
+ PS --> C
+ WM --> IB[Integration branch]
+ IB --> RV[Risk-triggered Reviewer]
+ RV --> C
+ C --> F[Final integration decision]
+```
+
+### 8.1 Separation from the existing single-job path
+
+The existing `codex-companion` and serialized shared broker remain responsible for single rescue and review jobs. Multi-Codex orchestration uses a separate controller and dedicated App Server worker pool.
+
+Shared implementation utilities may include:
+
+- generated App Server protocol types;
+- model-catalog access;
+- process-tree management;
+- filesystem helpers;
+- structured-output parsing;
+- rendering and redaction utilities.
+
+The following remain separate:
+
+- broker ownership;
+- orchestration state;
+- DAG scheduling;
+- worker leasing;
+- workspace snapshots;
+- package branch management;
+- recovery and integration state.
+
+This avoids weakening the stable single-job behavior while the orchestration runtime matures.
+
+## 9. Invocation and user experience
+
+### 9.1 Explicit invocation
+
+```text
+/codex:orchestrate
+```
+
+Explicit invocation is always available, even when automatic orchestration is disabled.
+
+### 9.2 Automatic invocation
+
+Automatic invocation is controlled by:
+
+```text
+/codex:setup --enable-orchestration
+/codex:setup --disable-orchestration
+```
+
+The setting applies to Claude's automatic selection only. When enabled, Claude may automatically start read-only or write-capable local orchestration if the task passes the entry policy.
+
+### 9.3 Compressed plan notification
+
+Claude does not wait for approval before local execution. It emits a short plan and starts immediately.
+
+Example:
+
+```text
+Starting Multi-Codex orchestration orch-20260817-a31f.
+
+- Architecture: Sol / high / read-only
+- Backend: Terra / high / isolated writer
+- Regression verification: Luna / medium / snapshot reader
+- Parallelism: 3; base budget: 30 minutes
+- External actions: none
+```
+
+A replan emits only the delta:
+
+```text
+Plan revision 2:
+- Split persistence from the backend package.
+- Escalated persistence to Terra / max.
+- Package count changed from 3 to 4.
+```
+
+### 9.4 Command surface
+
+New command:
+
+```text
+/codex:orchestrate
+```
+
+Extended commands:
+
+```text
+/codex:status [orchestration-id|package-id|job-id]
+/codex:result [orchestration-id|package-id|job-id]
+/codex:cancel [orchestration-id|package-id|job-id]
+```
+
+Existing single-agent command:
+
+```text
+/codex:rescue
+```
+
+Maintenance commands:
+
+```text
+/codex:setup --enable-orchestration
+/codex:setup --disable-orchestration
+/codex:setup --prune-orchestrations
+/codex:setup --prune-orchestrations --all
+```
+
+No `/codex:status --watch` mode is included in the initial scope.
+
+## 10. Automatic-entry policy
+
+### 10.1 Hard exclusions
+
+Claude shall normally avoid Multi-Codex orchestration when any of these conditions applies:
+
+- a one-file local change has an obvious solution;
+- root cause and fix are already established;
+- the task is a single command or a narrow information lookup;
+- no independent work packages can be identified;
+- orchestration overhead is likely larger than the work;
+- all plausible writers must edit the same semantic core;
+- the user explicitly requests a single-agent execution.
+
+### 10.2 Complexity Score
+
+Claude assigns one point for each applicable condition:
+
+1. Two or more independent work packages exist.
+2. The task spans multiple modules, layers, or services.
+3. Architecture or design judgment is material.
+4. The root cause is unclear.
+5. Competing implementation approaches require comparison.
+6. The change warrants an independent reviewer.
+7. Implementation and testing can run independently.
+8. A single Codex execution is expected to be long-running.
+9. A previous single-agent attempt failed.
+10. The task affects security, concurrency, migration, data loss, or another high-risk area.
+
+### 10.3 Score interpretation
+
+| Score | Default behavior |
+|---:|---|
+| 0–2 | Claude handles directly or uses one `/codex:rescue`. |
+| 3–4 | Claude may orchestrate up to two top-level Roots. |
+| 5–7 | Automatic Multi-Codex orchestration is preferred. |
+| 8–10 | Include a Sol architecture/plan-validation or Reviewer package. |
+
+A `planner` package at high complexity may validate a bounded technical plan, but Claude retains ownership of the orchestration DAG and final plan.
+
+## 11. Skill architecture
+
+### 11.1 `codex-orchestration`
+
+Path:
+
+```text
+plugins/codex/skills/codex-orchestration/SKILL.md
+```
+
+This is the only orchestration skill intended for automatic model invocation. It defines:
+
+- entry and exclusion rules;
+- Complexity Score calculation;
+- plan and package creation;
+- role and model routing;
+- compressed user notification;
+- controller invocation;
+- decision-point handling;
+- replan and escalation rules;
+- final integration and response requirements.
+
+The skill must explicitly prohibit delegating top-level semantic planning to a Codex Root.
+
+### 11.2 `codex-work-package-contract`
+
+Path:
+
+```text
+plugins/codex/skills/codex-work-package-contract/SKILL.md
+```
+
+Internal, non-user-invocable policy for:
+
+- bounded objective formulation;
+- dependencies and ownership;
+- read/write scope;
+- acceptance criteria;
+- structured output requirements;
+- native-child policy;
+- timeout and retry budget.
+
+### 11.3 `codex-integration-policy`
+
+Path:
+
+```text
+plugins/codex/skills/codex-integration-policy/SKILL.md
+```
+
+Internal policy for:
+
+- package commit validation;
+- integration ordering;
+- conflict and overlap decisions;
+- reviewer placement;
+- verification sufficiency;
+- final squash and user-branch application.
+
+### 11.4 `codex-orchestration-recovery`
+
+Path:
+
+```text
+plugins/codex/skills/codex-orchestration-recovery/SKILL.md
+```
+
+Internal policy for:
+
+- interrupted controller or worker state;
+- partial package changes;
+- existing package and integration branches;
+- same-session automatic recovery;
+- new-session resume choices;
+- safe cleanup and retention.
+
+### 11.5 No LLM orchestrator subagent
+
+There is no separate Claude orchestrator subagent. Claude Root reads the orchestration skill and retains direct ownership of planning and integration. The runtime controller is deterministic, not an LLM.
+
+## 12. Work-package contract
+
+A package must validate against a JSON Schema equivalent to the following conceptual shape:
+
+```yaml
+id: pkg-backend-auth
+title: Implement authentication backend
+
+role:
+ class: implementer
+ label: authentication-backend-implementer
+
+objective: >
+ Implement the authentication backend while preserving the public API
+ and current session semantics.
+
+dependencies:
+ - pkg-auth-contract
+
+required: true
+access: write
+
+ownership:
+ files:
+ - src/auth/**
+ - tests/auth/**
+ interfaces:
+ - auth-session-contract
+
+workspace:
+ mode: isolated-worktree
+ base: orchestration-snapshot
+
+model:
+ name: gpt-5.6-terra
+ effort: high
+ escalation:
+ - model: gpt-5.6-terra
+ effort: max
+ - model: gpt-5.6-sol
+ effort: high
+
+native_subagents:
+ policy: allowed
+ max_children: 2
+
+acceptance_criteria:
+ - Existing authentication tests pass.
+ - New failure-path tests are added.
+ - Public API signatures remain compatible.
+
+verification_commands:
+ - npm test -- auth
+ - npm run typecheck
+
+expected_outputs:
+ - normalized package commit
+ - changed-file list
+ - verification evidence
+ - residual risks
+
+timeout:
+ soft_minutes: 20
+ hard_minutes: 30
+
+retry:
+ automatic_attempts: 1
+```
+
+### 12.1 Built-in role classes
+
+The runtime recognizes these capability classes:
+
+- `planner`
+- `architect`
+- `explorer`
+- `implementer`
+- `tester`
+- `reviewer`
+- `verifier`
+- `migration-specialist`
+- `security-reviewer`
+
+Claude may use a more specific label, but the package must map to one built-in class.
+
+### 12.2 Ownership rules
+
+- Two write packages may not own the same file glob unless the overlap is explicitly declared and scheduled sequentially.
+- Interface ownership is separate from file ownership. A package changing a public interface must declare all dependent packages.
+- Read-only packages may inspect any repository content unless the plan explicitly restricts them.
+- The controller validates obvious ownership overlaps before execution; Claude resolves semantic overlaps.
+
+## 13. Result contract
+
+Each Codex Root returns canonical JSON. Markdown output is rendered from this JSON rather than treated as the canonical record.
+
+```yaml
+package_id: pkg-backend-auth
+status: completed
+summary: Authentication backend implemented and verified.
+
+claims:
+ - Session rotation preserves existing API behavior.
+
+evidence:
+ - kind: source
+ path: src/auth/session.ts
+ line_start: 42
+ line_end: 118
+ - kind: command
+ command: npm test -- auth
+ exit_code: 0
+
+changed_files:
+ - src/auth/session.ts
+ - tests/auth/session.test.ts
+
+commits:
+ - abc1234
+
+verification:
+ passed: true
+ commands:
+ - command: npm test -- auth
+ exit_code: 0
+ - command: npm run typecheck
+ exit_code: 0
+
+residual_risks:
+ - External identity-provider timeout behavior remains untested.
+
+confidence: 0.88
+follow_up_requests: []
+```
+
+Permitted package statuses:
+
+- `completed`
+- `partial`
+- `blocked`
+- `failed`
+- `cancelled`
+
+The controller rejects malformed results and preserves the raw final message for diagnosis.
+
+## 14. Model and reasoning-effort routing
+
+### 14.1 Capability ordering
+
+The initial policy treats:
+
+```text
+Sol > Terra > Luna
+```
+
+as the base capability ordering. Reasoning effort is a separate inference-budget dimension and does not reverse the base tier ordering.
+
+### 14.2 Role defaults
+
+| Work type | Default routing |
+|---|---|
+| Bounded exploration | Luna / medium or high |
+| Repetitive verification | Luna / medium |
+| Routine implementation | Terra / high |
+| Complex implementation or debugging | Terra / max |
+| Architecture and integration analysis | Sol / high or max |
+| High-risk independent review | Sol / max |
+| Repeated failure, architectural conflict, or security deadlock | Sol / ultra when advertised |
+
+### 14.3 Selection precedence
+
+```text
+Explicit user selection
+> package-specific plan selection
+> project orchestration configuration
+> user orchestration configuration
+> role default
+> Codex runtime default
+```
+
+### 14.4 Escalation
+
+- First deterministic failure: steer the same Root with concrete failure evidence when the thread remains healthy.
+- Repeated deterministic failure: raise effort or tier according to the package escalation list.
+- Conflicting results: create or activate a Sol Reviewer.
+- Structural failure: Claude replans rather than only increasing model capability.
+- Simplified follow-up work may be reassigned to a lower tier.
+
+### 14.5 Model-catalog authority
+
+The controller shall use the current App Server model catalog to validate known model/effort combinations. It shall not hardcode a permanent per-model effort matrix. Custom providers and unknown future model names are passed through unless configuration explicitly restricts them.
+
+## 15. Native Codex child agents
+
+Each package declares:
+
+```yaml
+native_subagents:
+ policy: allowed | forbidden | required
+ max_children: 2
+```
+
+Rules:
+
+1. The selected model must advertise native multi-agent support.
+2. The package and total orchestration budget must permit child agents.
+3. Native children remain in the same App Server worker as the parent Codex Root.
+4. The parent Root owns `spawn`, `send_input`, `wait`, `resume`, and `close` behavior.
+5. Claude observes lifecycle, status, evidence, and usage events but does not directly steer a child owned by the Root.
+6. Native children count toward the global active-Codex cap.
+7. If policy is `required` but the capability is unavailable, execution degrades to Root-only mode and records the degradation. It does not fail solely for that reason.
+
+## 16. Adaptive budget envelope
+
+### 16.1 Default envelope
+
+| Complexity Score | Top-level Roots | Worker concurrency | Native children per Root | Base elapsed-time budget |
+|---:|---:|---:|---:|---:|
+| 3–4 | 2 | 2 | 1 | 15 minutes |
+| 5–7 | 4 | 3 | 2 | 30 minutes |
+| 8–10 | 6 | 3 | 3 | 60 minutes |
+
+### 16.2 Absolute limits
+
+- Maximum top-level Roots per orchestration: **8**
+- Maximum concurrently active Codex Roots and children across the plugin: **12**
+- Automatic package retry attempts: **1**
+- Automatic DAG replans: **2**
+- Automatically added packages beyond the initial plan: **2**
+- Concurrent Sol/Ultra top-level Roots: **2**
+- Configurable workspace worker pool: **1–8**, default **3**
+
+### 16.3 Budget-pressure behavior
+
+When approaching the envelope, Claude and the controller shall prefer:
+
+1. removing duplicate or low-value packages;
+2. reusing evidence already produced;
+3. converting parallel tasks to sequential tasks;
+4. reducing effort or model tier for bounded follow-ups;
+5. cancelling optional packages;
+6. pausing and reporting remaining work when the required objective cannot be completed safely.
+
+Budget pressure must never silently expand the absolute limits.
+
+## 17. DAG planning and scheduling
+
+### 17.1 Initial DAG
+
+Claude creates an initial plan before starting workers. The plan contains all currently known work packages, dependencies, required/optional status, ownership, and acceptance criteria.
+
+### 17.2 Limited dynamic replanning
+
+A Codex Root may request additional investigation or identify a missing dependency. It cannot create top-level packages directly. Claude evaluates the request and may:
+
+- add a package;
+- split a package;
+- replace a package;
+- cancel an optional package;
+- change dependencies;
+- change model, effort, or workspace mode.
+
+The controller applies a new validated plan revision.
+
+### 17.3 Package state machine
+
+```mermaid
+stateDiagram-v2
+ [*] --> Planned
+ Planned --> Ready: dependencies satisfied
+ Ready --> Queued: accepted by scheduler
+ Queued --> Running: worker lease acquired
+ Running --> Completed
+ Running --> Partial
+ Running --> Blocked
+ Running --> Failed
+ Running --> Cancelling
+ Running --> Steering
+ Steering --> Running
+ Failed --> Retrying: transient or approved retry
+ Retrying --> Running
+ Planned --> Superseded: replan replaces package
+ Ready --> Superseded
+ Queued --> Superseded
+ Cancelling --> Cancelled
+ Partial --> [*]
+ Completed --> [*]
+ Blocked --> [*]
+ Failed --> [*]
+ Superseded --> [*]
+ Cancelled --> [*]
+```
+
+### 17.4 Scheduler behavior
+
+- Only packages whose dependencies satisfy the plan may become ready.
+- Required dependency failure blocks dependent packages.
+- Optional dependency omission is allowed only if the dependent package contract explicitly permits it.
+- Worker allocation respects workspace locks, access mode, model limits, and the global active-Codex cap.
+- One write orchestration may be active per workspace. Read-only orchestration may coexist when it does not depend on mutable user-tree state.
+- Completed packages are not rerun merely because more improvement might be possible.
+
+## 18. Worker-pool design
+
+### 18.1 Worker definition
+
+A worker contains:
+
+- one spawned `codex app-server` process;
+- one initialized App Server client;
+- one event-router channel;
+- one current top-level Root lease;
+- health and heartbeat state;
+- workspace identity and runtime identity;
+- accumulated stderr and diagnostic information.
+
+### 18.2 Pool scope
+
+- Pools are workspace-scoped.
+- Default pool size is three workers.
+- Workers may be reused between packages in the same workspace.
+- A worker is restarted before moving to another workspace.
+- Idle workers exit after ten minutes.
+- Plugin version, Codex CLI version, provider identity, or incompatible configuration changes invalidate idle workers.
+
+### 18.3 One Root per worker
+
+One worker executes at most one top-level Codex Root at a time. Native children of that Root remain inside the same worker. This avoids the current broker's single-stream global bottleneck without requiring general multi-client stream multiplexing in the first implementation.
+
+### 18.4 Worker states
+
+- `starting`
+- `idle`
+- `leased`
+- `draining`
+- `unhealthy`
+- `stopped`
+
+### 18.5 Health behavior
+
+- Heartbeat failure triggers an App Server probe.
+- A healthy active turn receives a grace period after soft timeout.
+- A dead process invalidates only the leased package.
+- The package is retried only when policy permits.
+- Other workers and packages continue.
+
+## 19. Event routing and capture
+
+The controller maintains a capture state per top-level Root:
+
+```text
+rootThreadId
+knownThreadIds
+threadTurnIds
+threadLabels
+pendingCollaborations
+activeNativeChildTurns
+finalRootAnswer
+reasoningSummaries
+commandExecutions
+fileChanges
+verificationEvents
+completionState
+```
+
+Routing rules:
+
+- App Server notifications are routed by worker, thread ID, and turn ID.
+- Child thread IDs are registered from collaboration events and thread metadata.
+- Root and child messages are stored separately.
+- The package result is the Root's canonical final result, not the last child message.
+- A Root is not considered drained while tracked collaboration calls or child turns remain active.
+- Detailed events are appended to `events.jsonl` after redaction.
+- Chat output receives only milestones.
+
+## 20. Runtime controller boundary
+
+### 20.1 Module layout
+
+```text
+plugins/codex/scripts/orchestration/
+├─ cli.mjs
+├─ controller.mjs
+├─ controller-server.mjs
+├─ controller-client.mjs
+├─ planner-contract.mjs
+├─ scheduler.mjs
+├─ budget-manager.mjs
+├─ worker-pool.mjs
+├─ worker-runtime.mjs
+├─ event-router.mjs
+├─ state-store.mjs
+├─ approval-policy.mjs
+├─ workspace-manager.mjs
+├─ snapshot-manager.mjs
+├─ integration-manager.mjs
+├─ reviewer-policy.mjs
+├─ recovery.mjs
+├─ retention.mjs
+└─ schemas/
+ ├─ orchestration-plan.schema.json
+ ├─ work-package.schema.json
+ ├─ package-result.schema.json
+ └─ orchestration-result.schema.json
+```
+
+### 20.2 Controller CLI
+
+The deterministic controller exposes commands equivalent to:
+
+```text
+codex-orchestrator start --plan-file
+codex-orchestrator status --json
+codex-orchestrator wait --until decision-point
+codex-orchestrator steer --prompt-file
+codex-orchestrator retry
+codex-orchestrator replan --plan-file
+codex-orchestrator integrate
+codex-orchestrator cancel
+codex-orchestrator recover
+codex-orchestrator prune [--all]
+```
+
+The actual binary remains a plugin-local Node.js entry point. The command name above describes the application boundary, not a separately installed package.
+
+### 20.3 Controller responsibilities
+
+The controller may:
+
+- validate schema and state transitions;
+- allocate workers;
+- execute known retries;
+- enforce limits;
+- persist events and results;
+- interrupt or terminate workers;
+- create snapshots, worktrees, refs, and integration branches;
+- perform conflict-free Git operations;
+- pause at semantic decision points.
+
+The controller may not:
+
+- invent new work packages;
+- reinterpret user intent;
+- choose a semantic conflict resolution;
+- waive acceptance criteria;
+- accept reviewer blocking findings;
+- authorize external actions.
+
+## 21. Persistent control plane
+
+### 21.1 Directory layout
+
+```text
+${CLAUDE_PLUGIN_DATA}/orchestrations/
+└─ /
+ └─ /
+ ├─ orchestration.json
+ ├─ plan.json
+ ├─ packages/
+ │ └─ .json
+ ├─ workers/
+ │ └─ .json
+ ├─ results/
+ │ └─ .json
+ ├─ integration.json
+ └─ events.jsonl
+```
+
+### 21.2 Orchestration state
+
+```yaml
+orchestration_id: orch-20260817-a31f
+claude_session_id: session-id
+workspace_root: /repo
+status: running
+plan_revision: 1
+complexity_score: 7
+budget: {}
+packages: []
+worker_leases: []
+snapshot_ref: refs/codex-orchestration/snapshots/orch-20260817-a31f
+integration_branch: codex-orchestration/orch-20260817-a31f/integration
+verification_state: {}
+created_at: 2026-08-17T00:00:00Z
+updated_at: 2026-08-17T00:00:00Z
+```
+
+### 21.3 Orchestration statuses
+
+- `planning`
+- `running`
+- `paused`
+- `integrating`
+- `completed`
+- `completed-with-omissions`
+- `degraded`
+- `blocked`
+- `failed`
+- `cancelled`
+
+### 21.4 Persistence guarantees
+
+- JSON state writes use a temporary file plus atomic rename.
+- Event logs are append-only and line-delimited.
+- A workspace-scoped lock protects controller mutation.
+- Worker leases contain a heartbeat and process identity.
+- No complete environment dump is persisted.
+- Prompts and raw outputs are retained only within configured retention limits and are redacted before log persistence.
+
+## 22. Workspace strategy
+
+### 22.1 Read-only orchestration
+
+A clean, read-only-only orchestration may share the current working tree because no package can mutate it.
+
+When any writer exists, or when the user tree is dirty, read-only packages shall use the orchestration snapshot or integration worktree to obtain stable input.
+
+### 22.2 One writer
+
+A single writer may use the current working tree only when all conditions hold:
+
+- the user branch and working tree are clean;
+- no parallel package requires a stable pre-write snapshot;
+- no integration branch is needed for another writer;
+- the plan explicitly allows direct mode.
+
+Otherwise, the writer uses an isolated worktree.
+
+### 22.3 Multiple writers
+
+Two or more writer packages always use isolated package branches and worktrees. They share the same snapshot base unless a dependency requires a later integration revision.
+
+### 22.4 Non-Git directories
+
+Non-Git directories support:
+
+- read-only orchestration;
+- one writer package.
+
+They do not support multi-writer package branches or integration-branch automation. The controller does not create an implicit Git repository.
+
+## 23. Dirty-tree snapshot design
+
+The snapshot must represent the complete visible working-tree content without modifying the user's branch or index.
+
+### 23.1 Snapshot algorithm
+
+1. Record the original `HEAD`, branch, index fingerprint, and working-tree status.
+2. Create a temporary index file.
+3. Populate the temporary index from `HEAD` with `git read-tree`.
+4. Run `git add -A` against the user's working tree with `GIT_INDEX_FILE` pointing to the temporary index.
+5. Write the resulting tree with `git write-tree`.
+6. Create an internal commit with `git commit-tree`, parented to the original `HEAD`.
+7. Store it at:
+
+```text
+refs/codex-orchestration/snapshots/
+```
+
+8. Delete the temporary index.
+9. Verify that the user's branch, index fingerprint, and working-tree status remain unchanged.
+
+### 23.2 Captured content
+
+The snapshot includes:
+
+- staged tracked changes;
+- unstaged tracked changes;
+- tracked deletions;
+- non-ignored untracked files;
+- file modes;
+- symbolic links;
+- Git attributes and clean-filter behavior as applied by the repository.
+
+The snapshot commit represents combined file content; staged-versus-unstaged distinctions are preserved separately in orchestration metadata for diagnostics, not as separate trees.
+
+### 23.3 Safety behavior
+
+Snapshot creation fails closed if the user's index or working tree changes during capture. Claude may retry after reporting that the workspace changed concurrently.
+
+## 24. Writer normalization and package commits
+
+A writer worktree must end in a normalized package commit.
+
+- If the Codex Root creates exactly one valid package commit, the controller keeps it.
+- If the Root creates multiple commits, the controller squashes them on the package branch after validating the final tree.
+- If the Root leaves only working-tree changes, the controller creates one package commit after verifying scope and required evidence.
+- If changes exceed declared ownership, the package is marked for Claude review and is not integrated automatically.
+
+Package commits use the user's configured Git identity and include:
+
+```text
+Codex-Orchestration-Id:
+Codex-Package-Id:
+```
+
+Model, effort, prompt, thread ID, turn ID, and usage metadata remain in local orchestration records rather than commit trailers.
+
+## 25. Integration branch
+
+### 25.1 Integration flow
+
+```text
+package branches
+ ↓ normalized atomic commits
+integration branch
+ ↓ dependency-ordered cherry-pick
+conflict and contract checks
+ ↓
+full verification
+ ↓
+risk-triggered Reviewer
+ ↓
+final local squash commit
+```
+
+### 25.2 Cherry-pick order
+
+Required packages are integrated in topological order. Independent packages use a stable deterministic order based on plan order and package ID.
+
+### 25.3 Conflict handling
+
+The integration manager may automatically handle only mechanical cases that preserve identical content. Semantic conflicts pause integration and create a decision point containing:
+
+- conflicting package IDs;
+- files and hunks;
+- ownership declarations;
+- package claims and verification evidence;
+- available resolution strategies.
+
+Claude may then:
+
+- choose one package;
+- request a repair package;
+- replan ownership;
+- provide a structured resolution patch;
+- reject the combined result.
+
+The controller never invents a semantic merge.
+
+### 25.4 Automatic application to the user branch
+
+A final squash commit may be applied automatically only when all conditions hold:
+
+- the user's branch `HEAD` has not changed since orchestration start;
+- the user branch, index, and working tree are clean;
+- all required packages completed successfully;
+- optional omissions do not violate the objective;
+- full verification passed;
+- every required Reviewer returned `approve`;
+- no external action is included;
+- the integration diff stays within approved repository scope.
+
+If any condition fails, the integration branch and result are preserved, but the user branch is not modified.
+
+## 26. Reviewer policy
+
+### 26.1 Automatic Reviewer triggers
+
+A read-only Reviewer package is created when any condition applies:
+
+- two or more writer packages are integrated;
+- package claims conflict;
+- confidence is below configured threshold;
+- evidence is incomplete despite passing tests;
+- security, authentication, concurrency, data loss, migration, rollback, or public protocol behavior changes;
+- a public API, schema, or protocol changes;
+- completion required escalation after failure;
+- integration required semantic conflict resolution;
+- Claude cannot confidently determine correctness.
+
+### 26.2 Reviewer routing
+
+| Risk | Default Reviewer |
+|---|---|
+| Normal multi-writer integration | Sol / high |
+| High-risk behavior | Sol / max |
+| Security, architecture conflict, or repeated failure | Sol / ultra when available |
+
+### 26.3 Reviewer result
+
+```yaml
+verdict: approve | revise | reject
+blocking_findings: []
+non_blocking_findings: []
+evidence: []
+recommended_resolution: []
+```
+
+A `revise` verdict normally creates a bounded repair package. A `reject` verdict prevents automatic integration. Claude remains the final arbiter but may not silently ignore blocking findings; it must resolve or explicitly report them.
+
+## 27. Failure handling
+
+### 27.1 Package-scoped circuit breaker
+
+- An independent package failure does not stop unrelated packages.
+- A failed dependency blocks its downstream subgraph.
+- A failed required package moves the orchestration to `degraded` while recovery is attempted.
+- An optional package may be omitted only when the objective remains satisfied.
+
+### 27.2 Failure classes
+
+| Class | Definition | Default response |
+|---|---|---|
+| `transient` | Process exit, temporary connection loss, recoverable timeout | Retry once, possibly on a new worker. |
+| `deterministic` | Test failure, implementation defect, contract mismatch | Steer with evidence, then escalate or repackage. |
+| `structural` | Invalid decomposition, cyclic dependency, ownership collision | Pause for Claude replan; supersede affected packages. |
+| `external-blocked` | Missing credential, required user input, remote dependency | Do not retry automatically; request only the needed input. |
+
+### 27.3 Timeouts
+
+- Activity continues within the package budget while progress events arrive.
+- A quiet period triggers a worker health probe.
+- A healthy active turn receives a soft-timeout notice and grace period.
+- A dead or unreachable worker is terminated and the package is classified as transient.
+- Hard timeout sends `turn/interrupt`, waits for cancellation grace, then terminates the worker process tree.
+- Partial changes, commits, messages, and logs are recovered before final package classification.
+
+### 27.4 Conditional partial integration
+
+A successful package may be integrated despite another package's failure only when:
+
+- there is no file, API, schema, or semantic dependency on the failed package;
+- its own acceptance criteria are independently satisfied;
+- relevant integration verification passes;
+- Claude or a Reviewer determines that omission does not create a misleading or incomplete result.
+
+## 28. Cancellation
+
+Cancellation is staged:
+
+1. Mark the target package or orchestration `cancelling`.
+2. Stop scheduling new dependent work.
+3. Send `turn/interrupt` to active Roots.
+4. Wait ten seconds for graceful completion.
+5. Terminate remaining worker process trees.
+6. Recover partial diff, commits, structured output, and logs.
+7. Exclude cancelled packages from integration.
+8. Preserve failed or cancelled package branches and worktrees for 24 hours.
+
+Cancelling the orchestration never automatically merges the integration branch.
+
+## 29. Safety and approval mediation
+
+### 29.1 Allowed automatic local actions
+
+- repository-local file edits;
+- local tests, builds, linters, type checks, and static analysis;
+- Git operations inside orchestration refs and worktrees;
+- local service invocation needed for verification;
+- read-only external research or package metadata lookup when permitted by the active environment.
+
+### 29.2 Actions requiring explicit user approval
+
+- `git push`, force-push, or remote branch deletion;
+- pull-request creation, update, merge, or closure;
+- release, package, image, or artifact publication;
+- deployment or infrastructure mutation;
+- remote database or service mutation;
+- credential, secret, account, or permission changes;
+- destructive changes outside the repository;
+- purchases or other consequential external transactions.
+
+### 29.3 Controller approval policy
+
+Where supported by the active App Server protocol, workers use controller-mediated approval requests. The controller:
+
+- automatically approves known local verification and orchestration-internal Git commands;
+- automatically denies known external mutation commands without an authorization token from Claude's user-approved action;
+- records redacted approval decisions in the event log;
+- fails closed for external mutation when approval mediation is unavailable.
+
+### 29.4 `danger-full-access` limitation
+
+Write packages may use `danger-full-access`, as approved for this plugin. This is not a complete security boundary. Defense in depth includes:
+
+- explicit package prompts prohibiting external and destructive actions;
+- approval mediation for recognized commands;
+- declared file and workspace ownership;
+- pre/post workspace audits;
+- touched-file validation;
+- secret redaction;
+- no automatic external authorization.
+
+The design does not claim that every possible shell or filesystem side effect can be prevented under unrestricted local access.
+
+## 30. Recovery
+
+### 30.1 Same Claude session
+
+Temporary disconnection, plugin reload, or controller restart triggers automatic recovery:
+
+- reacquire workspace lock;
+- read persisted orchestration and lease state;
+- probe active worker processes;
+- reconnect or mark workers unhealthy;
+- reconcile thread and turn status;
+- recover package results and partial Git state;
+- resume ready scheduling when safe.
+
+### 30.2 New Claude session
+
+When a new session discovers unfinished work, it displays a compact summary and asks the user to choose:
+
+- resume execution;
+- stop and preserve results;
+- retrieve available results without resuming.
+
+Local write packages are resumed only after validating branch, snapshot, worktree, and existing diff state. Previously approved external actions are never automatically replayed.
+
+### 30.3 Orphaned package state
+
+If a worker disappears but its worktree contains changes:
+
+1. freeze the package state;
+2. inspect existing commits and diff;
+3. validate file ownership;
+4. preserve evidence;
+5. let Claude decide whether to accept partial work, create a recovery package, or discard it.
+
+A replacement Root is not started blindly over the same worktree.
+
+## 31. Hooks
+
+### 31.1 SessionStart
+
+- detect unfinished orchestrations associated with the workspace;
+- automatically reconnect work owned by the same Claude session;
+- summarize unfinished work from another session;
+- export controller state locations needed by commands.
+
+### 31.2 SessionEnd
+
+- flush state and events;
+- renew or release controller leases appropriately;
+- do not automatically terminate long-running orchestration;
+- preserve enough state for later recovery.
+
+### 31.3 Stop Review Gate
+
+The existing Stop Review Gate remains active for ordinary Claude work. During orchestration integration, the orchestration Reviewer policy is authoritative and the Stop hook skips duplicate review to avoid recursive Claude/Codex review loops.
+
+## 32. Configuration
+
+### 32.1 Files
+
+User configuration:
+
+```text
+~/.claude/codex-orchestration.json
+```
+
+Project configuration:
+
+```text
+/.claude/codex-orchestration.json
+```
+
+### 32.2 Precedence
+
+```text
+Explicit command option
+> project configuration
+> user configuration
+> plugin default
+```
+
+### 32.3 Default configuration
+
+```json
+{
+ "$schema": "https://raw.githubusercontent.com/eureka-pd/codex-plugin-cc/main/plugins/codex/schemas/codex-orchestration.schema.json",
+ "auto": {
+ "enabled": false,
+ "threshold": 5
+ },
+ "workers": {
+ "workspacePoolSize": 3,
+ "globalTopLevelLimit": 8,
+ "globalActiveCodexLimit": 12,
+ "idleTtlMinutes": 10
+ },
+ "budget": {
+ "automaticRetriesPerPackage": 1,
+ "automaticReplans": 2,
+ "automaticAdditionalPackages": 2,
+ "concurrentSolUltraRoots": 2
+ },
+ "git": {
+ "finalCommitMode": "squash",
+ "autoApplyToCleanBranch": true
+ },
+ "safety": {
+ "writeSandbox": "danger-full-access",
+ "externalActions": "require-user-approval"
+ },
+ "retention": {
+ "metadataDays": 30,
+ "eventLogDays": 7,
+ "failedWorktreeHours": 24,
+ "gitRefDays": 7
+ }
+}
+```
+
+The shipped JSON Schema validates ranges, enums, required fields, and unknown properties.
+
+## 33. Status and result rendering
+
+### 33.1 Milestone chat output
+
+Claude shows:
+
+- compressed initial plan;
+- package start and completion;
+- retries and escalation;
+- plan revisions;
+- blocked states;
+- integration start;
+- Reviewer verdict;
+- final verification and orchestration status.
+
+It does not stream every command, reasoning summary, child message, or file-change event into chat.
+
+### 33.2 `/codex:status`
+
+Status for an orchestration includes:
+
+- orchestration status and plan revision;
+- elapsed time and budget;
+- dependency graph summary;
+- package role, model, effort, state, worker, and thread;
+- active native-child count;
+- integration and verification state;
+- current decision point;
+- retention paths for detailed logs.
+
+### 33.3 `/codex:result`
+
+Result includes:
+
+- final orchestration summary;
+- package outcomes and omissions;
+- evidence and verification summary;
+- Reviewer findings;
+- integration branch and commit;
+- whether the user branch was updated;
+- residual risks and remaining work.
+
+Canonical JSON remains available with `--json`.
+
+## 34. Retention and cleanup
+
+Default retention:
+
+| Artifact | Retention |
+|---|---:|
+| Completed orchestration metadata | 30 days |
+| Detailed event and command logs | 7 days |
+| Successful package worktrees | Removed after verified final application |
+| Failed or cancelled package worktrees | 24 hours |
+| Snapshot refs | 7 days |
+| Package and integration refs | 7 days |
+
+Pruning commands:
+
+```text
+/codex:setup --prune-orchestrations
+/codex:setup --prune-orchestrations --all
+```
+
+Secret redaction covers:
+
+- common API-key and token formats;
+- authorization headers;
+- secret-like environment-variable values;
+- known credential-file content;
+- controller approval payloads containing sensitive values.
+
+## 35. Cross-platform requirements
+
+The complete feature supports macOS, Linux, and Windows.
+
+Platform-specific tests cover:
+
+- Unix sockets and Windows named pipes;
+- process-tree termination;
+- path quoting and escaping;
+- Git worktree behavior;
+- temporary-index and atomic-rename behavior;
+- symbolic links and file modes where supported;
+- temporary-directory semantics;
+- filesystem permission differences;
+- detached process cleanup.
+
+A platform that cannot safely provide a requested capability must degrade explicitly rather than silently changing workspace or safety semantics.
+
+## 36. Phased implementation
+
+### Phase 1: Read-only Multi-Codex
+
+Deliver:
+
+- `/codex:orchestrate` explicit command;
+- automatic-entry skill and feature flag;
+- plan and result schemas;
+- deterministic controller and persistent state;
+- workspace-scoped App Server worker pool;
+- DAG scheduler and adaptive budget;
+- model and effort routing;
+- read-only package execution;
+- native-child event observation;
+- orchestration-aware `status`, `result`, and `cancel`;
+- structured result capture and Markdown rendering.
+
+Phase 1 excludes writer worktrees and automatic Git integration.
+
+### Phase 2: Clean-tree writer orchestration
+
+Deliver:
+
+- writer package worktrees;
+- package commit normalization;
+- integration branch;
+- dependency-ordered cherry-pick;
+- conflict decision points;
+- risk-triggered Reviewer;
+- full verification;
+- final local squash commit;
+- automatic application to an unchanged clean user branch;
+- controller-mediated command approval.
+
+### Phase 3: Recovery and dirty-tree support
+
+Deliver:
+
+- hidden snapshot refs through temporary indexes;
+- dirty-tree read and writer orchestration;
+- controller crash recovery;
+- worker reconnection and orphan reconciliation;
+- partial package recovery;
+- limited dynamic replanning;
+- retention and pruning;
+- cross-platform hardening and real restart/resume tests.
+
+## 37. Testing strategy
+
+### 37.1 Unit tests
+
+- plan and package schema validation;
+- DAG cycle and missing-dependency detection;
+- package ownership overlap detection;
+- scheduler state transitions;
+- Complexity Score and adaptive budget;
+- role/model/effort routing;
+- retry and replan caps;
+- Reviewer trigger policy;
+- result normalization;
+- approval deny/allow policy;
+- secret redaction;
+- retention calculations.
+
+### 37.2 Fake App Server integration
+
+- two and three workers running concurrently;
+- response and notification isolation by worker/thread/turn;
+- Root plus native-child event topology;
+- child-drain completion;
+- malformed structured output;
+- transient worker failure and one retry;
+- soft and hard timeout;
+- interruption and forced process termination;
+- controller restart and lease reconciliation;
+- whole-orchestration and package cancellation;
+- global active-Codex cap.
+
+### 37.3 Git integration tests
+
+- clean repository and direct single-writer mode;
+- dirty-tree snapshot without index mutation;
+- untracked files, deletions, modes, and symlinks;
+- multiple package worktrees;
+- package commit normalization;
+- dependency-ordered integration;
+- overlapping ownership rejection;
+- clean cherry-pick;
+- semantic conflict decision point;
+- failed package exclusion;
+- optional omission;
+- user `HEAD` movement during execution;
+- dirty user branch at final application;
+- final squash commit and trailers;
+- ref and worktree retention cleanup.
+
+### 37.4 Real Codex smoke tests
+
+Release-candidate testing includes:
+
+- two read-only top-level Roots in parallel;
+- one writer;
+- two isolated writers;
+- a Root using native children;
+- model/effort selection for Sol, Terra, and Luna;
+- cancellation;
+- worker crash and retry;
+- controller restart and resume;
+- Reviewer and integration flow.
+
+Real smoke tests are not required on unauthenticated public pull-request CI.
+
+### 37.5 Merge gate
+
+```text
+npm test
+npm run build
+git diff --check
+macOS/Linux/Windows CI
+```
+
+A release candidate additionally requires the real Codex smoke suite.
+
+## 38. Acceptance criteria
+
+The feature is complete when all of the following are demonstrated:
+
+1. Claude can create a valid orchestration plan with at least two independent packages.
+2. Two top-level Codex Roots execute concurrently through different App Server workers.
+3. Package events cannot contaminate another package's result or status.
+4. The controller enforces workspace and global agent caps.
+5. A transient worker failure affects only its package and can retry once.
+6. A structural failure pauses for Claude replan rather than blindly retrying.
+7. Native-child events are attributed to the correct Root and count against budget.
+8. Multiple writers operate in isolated worktrees based on the same stable snapshot.
+9. Dirty-tree snapshot creation does not change the user's branch, index, or working tree.
+10. Package commits are normalized, scoped, and traceable through trailers.
+11. The integration branch excludes failed packages and detects semantic conflicts.
+12. A risk-triggered Reviewer can block automatic integration.
+13. The user branch is modified only when all automatic-application conditions hold.
+14. External mutation commands cannot proceed without explicit user approval.
+15. Controller restart recovery preserves valid work and does not duplicate active packages.
+16. `status`, `result`, and `cancel` work for orchestration and package identifiers.
+17. Detailed local logs are redacted and pruned according to policy.
+18. The complete test suite passes on macOS, Linux, and Windows.
+
+## 39. Risks and mitigations
+
+| Risk | Mitigation |
+|---|---|
+| Excessive agent usage | Complexity gate, adaptive envelope, absolute caps, no unbounded replan. |
+| Conflicting writers | Declared ownership, isolated worktrees, integration branch, semantic decision points. |
+| Stale or orphaned workers | Heartbeats, leases, probes, process identity, scoped retry. |
+| Cross-package event leakage | Dedicated App Server worker per top-level Root and thread-aware capture state. |
+| Incomplete Root result | Canonical schema validation, raw-output retention, evidence checks. |
+| User branch changes during execution | Start-state fingerprint and final automatic-application checks. |
+| Dirty-tree data loss | Temporary-index snapshot and post-capture invariants. |
+| External side effect under unrestricted access | Approval mediation, command policy, prompt boundary, audit, no implicit authorization. |
+| Duplicate Stop review loops | Skip Stop Review Gate during orchestration integration. |
+| Controller complexity | Independent module boundary, phased delivery, deterministic state machines. |
+| Upstream review burden | Small separable PR boundaries and no dependency on fork-only release metadata. |
+
+## 40. Upstream contribution strategy
+
+The fork may deliver the integrated feature, but implementation commits and module boundaries should permit these upstream pull requests:
+
+1. **App Server worker abstraction and concurrent event isolation**
+2. **Orchestration state model, schemas, and DAG scheduler**
+3. **`/codex:orchestrate` skill/command and status/result/cancel extensions**
+4. **Writer worktrees and integration branch**
+5. **Recovery, adaptive budget, approval mediation, and risk Reviewer**
+
+Version bumps, fork installation instructions, and fork-specific release metadata shall not be mixed into upstream feature pull requests.
+
+## 41. Design consistency review
+
+This specification has no unresolved placeholders. The following boundaries are explicit:
+
+- Claude owns semantic orchestration; no LLM subagent replaces it.
+- One worker owns one top-level Root at a time.
+- Native children remain parent-owned.
+- Automatic local writes are allowed only after plan notification and within configured safety policy.
+- External actions remain user-authorized.
+- Multiple writers are isolated and integrated before touching the user branch.
+- Dirty user state is snapshotted without mutation.
+- Budget, retry, replan, and retention limits are finite.
+- Full implementation is phased without changing the final architecture.
+
+No additional design decision is required before producing the implementation plan.
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0c919c3db..baa27e8b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@openai/codex-plugin-cc",
- "version": "1.0.6",
+ "version": "1.0.7-eureka.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openai/codex-plugin-cc",
- "version": "1.0.6",
+ "version": "1.0.7-eureka.2",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^25.5.0",
diff --git a/package.json b/package.json
index b1d984d1a..545c047d8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@openai/codex-plugin-cc",
- "version": "1.0.6",
+ "version": "1.0.7-eureka.2",
"private": true,
"type": "module",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
@@ -11,7 +11,7 @@
"scripts": {
"bump-version": "node scripts/bump-version.mjs",
"check-version": "node scripts/bump-version.mjs --check",
- "prebuild": "mkdir -p plugins/codex/.generated/app-server-types && codex app-server generate-ts --out plugins/codex/.generated/app-server-types",
+ "prebuild": "node scripts/prepare-generated-dir.mjs && codex app-server generate-ts --out plugins/codex/.generated/app-server-types",
"build": "tsc -p tsconfig.app-server.json",
"test": "node --test tests/*.test.mjs"
},
diff --git a/plugins/codex/.claude-plugin/plugin.json b/plugins/codex/.claude-plugin/plugin.json
index e91e5238c..bddaf2605 100644
--- a/plugins/codex/.claude-plugin/plugin.json
+++ b/plugins/codex/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "codex",
- "version": "1.0.6",
+ "version": "1.0.7-eureka.2",
"description": "Use Codex from Claude Code to review code or delegate tasks.",
"author": {
"name": "OpenAI"
diff --git a/plugins/codex/CHANGELOG.md b/plugins/codex/CHANGELOG.md
index d647561bb..26d9b7da1 100644
--- a/plugins/codex/CHANGELOG.md
+++ b/plugins/codex/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## Unreleased
+
+- Add read-only Claude-native Multi-Codex orchestration with durable status, results, cancellation, adaptive budgets, and bounded parallel workers.
+
+## 1.0.7-eureka.2
+
+- Added GPT-5.6 Sol, Terra, and Luna model/effort support based on the current Codex model catalog.
+- Added `max` and `ultra` transport support for task and review flows.
+- Refreshed stale shared brokers when the plugin or Codex CLI runtime changes while preserving active work and cancellation.
+- Added model and effort selection to normal and adversarial review commands.
+- Replaced generation-pinned rescue guidance with the version-neutral `codex-prompting` skill.
+- Removed the deprecated generation-specific prompting alias and examples.
+- Removed the obsolete lowest reasoning-effort alias from commands and runtime validation.
+- Remapped the `spark` alias to `gpt-5.6-luna`.
+- Identified this fork build separately from upstream plugin releases.
+
## 1.0.0
- Initial version of the Codex plugin for Claude Code
diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md
index 7009ec86a..7d0306d1a 100644
--- a/plugins/codex/agents/codex-rescue.md
+++ b/plugins/codex/agents/codex-rescue.md
@@ -5,7 +5,7 @@ model: sonnet
tools: Bash
skills:
- codex-cli-runtime
- - gpt-5-4-prompting
+ - codex-prompting
---
You are a thin forwarding wrapper around the Codex companion task runtime.
@@ -22,14 +22,16 @@ Forwarding rules:
- Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task ...`.
- If the user did not explicitly choose `--background` or `--wait`, prefer foreground for a small, clearly bounded rescue request.
- If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution.
-- You may use the `gpt-5-4-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it.
+- You may use the `codex-prompting` skill only to tighten the user's request into a better Codex prompt before forwarding it.
- Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work beyond shaping the forwarded prompt text.
- Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`. This subagent only forwards to `task`.
- Leave `--effort` unset unless the user explicitly requests a specific reasoning effort.
- Leave model unset by default. Only add `--model` when the user explicitly asks for a specific model.
-- If the user asks for `spark`, map that to `--model gpt-5.3-codex-spark`.
-- If the user asks for a concrete model name such as `gpt-5.4-mini`, pass it through with `--model`.
+- Treat the GPT-5.6 capability tiers as `Sol > Terra > Luna`; reasoning effort is a separate inference-budget dimension and does not reverse that base ordering.
+- If the user explicitly asks for Sol, Terra, or Luna, pass the corresponding full model name: `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`.
+- If the user asks for `spark`, map that to `--model gpt-5.6-luna`.
+- If the user asks for a concrete model name such as `gpt-5.6-terra`, pass it through with `--model`.
- Treat `--effort ` and `--model ` as runtime controls and do not include them in the task text you pass through.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
- Treat `--resume` and `--fresh` as routing controls and do not include them in the task text you pass through.
diff --git a/plugins/codex/commands/adversarial-review.md b/plugins/codex/commands/adversarial-review.md
index da440ab4d..1f5e7ac2c 100644
--- a/plugins/codex/commands/adversarial-review.md
+++ b/plugins/codex/commands/adversarial-review.md
@@ -1,6 +1,6 @@
---
description: Run a Codex review that challenges the implementation approach and design choices
-argument-hint: '[--wait|--background] [--base ][] [--scope auto|working-tree|branch] [focus ...]'
+argument-hint: '[--wait|--background] [--base ][] [--scope auto|working-tree|branch] [--model ] [--effort ] [focus ...]'
disable-model-invocation: true
allowed-tools: Read, Glob, Grep, Bash(node:*), Bash(git:*), AskUserQuestion
---
@@ -36,6 +36,7 @@ Execution mode rules:
Argument handling:
- Preserve the user's arguments exactly.
+- `--model` and `--effort` select the Codex runtime and must not become part of the focus text.
- Do not strip `--wait` or `--background` yourself.
- Do not weaken the adversarial framing or rewrite the user's focus text.
- The companion script parses `--wait` and `--background`, but Claude Code's `Bash(..., run_in_background: true)` is what actually detaches the run.
diff --git a/plugins/codex/commands/cancel.md b/plugins/codex/commands/cancel.md
index a1472b836..ffb87a639 100644
--- a/plugins/codex/commands/cancel.md
+++ b/plugins/codex/commands/cancel.md
@@ -5,4 +5,7 @@ disable-model-invocation: true
allowed-tools: Bash(node:*)
---
-!`node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" cancel "$ARGUMENTS"`
+!`node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/dispatch.mjs" cancel "$ARGUMENTS"`
+
+
+The reference may identify a legacy job, an entire orchestration, or one package.
diff --git a/plugins/codex/commands/orchestrate.md b/plugins/codex/commands/orchestrate.md
new file mode 100644
index 000000000..b7fee4402
--- /dev/null
+++ b/plugins/codex/commands/orchestrate.md
@@ -0,0 +1,23 @@
+
+---
+description: Plan and start a Claude-managed read-only Multi-Codex orchestration
+argument-hint: ''
+allowed-tools: Read, Glob, Grep, Write, Bash(node:*), Bash(git:*)
+---
+
+Use the `codex-orchestration` skill as the binding policy.
+
+Inspect only enough repository context to identify genuinely independent read-only work packages, their dependencies, models, efforts, and acceptance criteria. Phase 1 rejects writer packages.
+
+Create canonical plan JSON in a collision-safe temporary file. Before execution, show a compressed 3–6 line plan including package roles, model/effort, parallelism, budget, and the fact that no external actions are authorized. Do not wait for approval for this local read-only run.
+
+Start it with:
+
+```bash
+node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/cli.mjs" start --cwd "$PWD" --plan-file ""
+```
+
+Delete the temporary plan file after the command returns. Report only that the orchestration was accepted or started; never claim queued work has completed. Preserve the orchestration ID and status/result/cancel commands verbatim.
+
+User objective:
+$ARGUMENTS
diff --git a/plugins/codex/commands/rescue.md b/plugins/codex/commands/rescue.md
index 56de9555d..d8acfbc80 100644
--- a/plugins/codex/commands/rescue.md
+++ b/plugins/codex/commands/rescue.md
@@ -1,6 +1,6 @@
---
description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent
-argument-hint: "[--background|--wait] [--resume|--fresh] [--model ] [--effort ] [what Codex should investigate, solve, or continue]"
+argument-hint: "[--background|--wait] [--resume|--fresh] [--model ] [--effort ] [what Codex should investigate, solve, or continue]"
allowed-tools: Bash(node:*), AskUserQuestion, Agent
---
@@ -43,7 +43,7 @@ Operating rules:
- Do not paraphrase, summarize, rewrite, or add commentary before or after it.
- Do not ask the subagent to inspect files, monitor progress, poll `/codex:status`, fetch `/codex:result`, call `/codex:cancel`, summarize output, or do follow-up work of its own.
- Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort.
-- Leave the model unset unless the user explicitly asks for one. If they ask for `spark`, map it to `gpt-5.3-codex-spark`.
+- Leave the model unset unless the user explicitly asks for one. If they ask for `spark`, map it to `gpt-5.6-luna`.
- Leave `--resume` and `--fresh` in the forwarded request. The subagent handles that routing when it builds the `task` command.
- If the helper reports that Codex is missing or unauthenticated, stop and tell the user to run `/codex:setup`.
- If the user did not supply a request, ask what Codex should investigate or fix.
diff --git a/plugins/codex/commands/result.md b/plugins/codex/commands/result.md
index 3abc2d931..812458fab 100644
--- a/plugins/codex/commands/result.md
+++ b/plugins/codex/commands/result.md
@@ -5,7 +5,7 @@ disable-model-invocation: true
allowed-tools: Bash(node:*)
---
-!`node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" result "$ARGUMENTS"`
+!`node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/dispatch.mjs" result "$ARGUMENTS"`
Present the full command output to the user. Do not summarize or condense it. Preserve all details including:
- Job ID and status
@@ -13,3 +13,6 @@ Present the full command output to the user. Do not summarize or condense it. Pr
- File paths and line numbers exactly as reported
- Any error messages or parse errors
- Follow-up commands such as `/codex:status ` and `/codex:review`
+
+
+For orchestration and package references, preserve the canonical result, evidence, verification, residual risks, and lifecycle metadata.
diff --git a/plugins/codex/commands/review.md b/plugins/codex/commands/review.md
index fb70a4876..6d0002bf3 100644
--- a/plugins/codex/commands/review.md
+++ b/plugins/codex/commands/review.md
@@ -1,6 +1,6 @@
---
description: Run a Codex code review against local git state
-argument-hint: '[--wait|--background] [--base ][] [--scope auto|working-tree|branch]'
+argument-hint: '[--wait|--background] [--base ][] [--scope auto|working-tree|branch] [--model ] [--effort ]'
disable-model-invocation: true
allowed-tools: Read, Glob, Grep, Bash(node:*), Bash(git:*), AskUserQuestion
---
@@ -33,6 +33,7 @@ Execution mode rules:
Argument handling:
- Preserve the user's arguments exactly.
+- `--model` and `--effort` select the Codex runtime for the review and are not focus text.
- Do not strip `--wait` or `--background` yourself.
- Do not add extra review instructions or rewrite the user's intent.
- The companion script parses `--wait` and `--background`, but Claude Code's `Bash(..., run_in_background: true)` is what actually detaches the run.
diff --git a/plugins/codex/commands/setup.md b/plugins/codex/commands/setup.md
index fb33a150a..5bf901cae 100644
--- a/plugins/codex/commands/setup.md
+++ b/plugins/codex/commands/setup.md
@@ -1,13 +1,13 @@
---
description: Check whether the local Codex CLI is ready and optionally toggle the stop-time review gate
-argument-hint: '[--enable-review-gate|--disable-review-gate]'
+argument-hint: '[--enable-review-gate|--disable-review-gate] [--enable-orchestration|--disable-orchestration]'
allowed-tools: Bash(node:*), Bash(npm:*), AskUserQuestion
---
Run:
```bash
-node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" setup --json $ARGUMENTS
+node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/setup-dispatch.mjs" --json $ARGUMENTS
```
If the result says Codex is unavailable and npm is available:
@@ -25,7 +25,7 @@ npm install -g @openai/codex
- Then rerun:
```bash
-node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" setup --json $ARGUMENTS
+node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/setup-dispatch.mjs" --json $ARGUMENTS
```
If Codex is already installed or npm is unavailable:
@@ -35,3 +35,6 @@ Output rules:
- Present the final setup output to the user.
- If installation was skipped, present the original setup output.
- If Codex is installed but not authenticated, preserve the guidance to run `!codex login`.
+
+
+Automatic orchestration is disabled by default. `/codex:setup --enable-orchestration` enables Claude's automatic Complexity Score entry policy; explicit `/codex:orchestrate` remains available while it is disabled.
diff --git a/plugins/codex/commands/status.md b/plugins/codex/commands/status.md
index 8f70663d1..dbbd9404a 100644
--- a/plugins/codex/commands/status.md
+++ b/plugins/codex/commands/status.md
@@ -5,7 +5,7 @@ disable-model-invocation: true
allowed-tools: Bash(node:*)
---
-!`node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" status "$ARGUMENTS"`
+!`node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/dispatch.mjs" status "$ARGUMENTS"`
If the user did not pass a job ID:
- Render the command output as a single Markdown table for the current and past runs in this session.
@@ -15,3 +15,6 @@ If the user did not pass a job ID:
If the user did pass a job ID:
- Present the full command output to the user.
- Do not summarize or condense it.
+
+
+The reference may also identify a Multi-Codex orchestration or package. Without a reference, append orchestration status when present.
diff --git a/plugins/codex/scripts/app-server-broker.mjs b/plugins/codex/scripts/app-server-broker.mjs
index 1954274fe..3a9851d51 100644
--- a/plugins/codex/scripts/app-server-broker.mjs
+++ b/plugins/codex/scripts/app-server-broker.mjs
@@ -158,6 +158,13 @@ async function main() {
}
if (message.id !== undefined && message.method === "broker/shutdown") {
+ if (activeRequestSocket || activeStreamSocket) {
+ send(socket, {
+ id: message.id,
+ error: buildJsonRpcError(BROKER_BUSY_RPC_CODE, "Shared Codex broker is busy.")
+ });
+ continue;
+ }
send(socket, { id: message.id, result: {} });
await shutdown(server);
process.exit(0);
diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs
index 83df468ad..9c13b7fd1 100644
--- a/plugins/codex/scripts/codex-companion.mjs
+++ b/plugins/codex/scripts/codex-companion.mjs
@@ -8,19 +8,17 @@ import { fileURLToPath } from "node:url";
import { parseArgs, splitRawArgumentString } from "./lib/args.mjs";
import {
- buildPersistentTaskThreadName,
- DEFAULT_CONTINUE_PROMPT,
- findLatestTaskThread,
- getCodexAuthStatus,
- getCodexAvailability,
- getSessionRuntimeStatus,
- importExternalAgentSession,
- interruptAppServerTurn,
- parseStructuredOutput,
- readOutputSchema,
- runAppServerReview,
- runAppServerTurn
- } from "./lib/codex.mjs";
+ findLatestTaskThread,
+ getCodexAuthStatus,
+ getCodexAvailability,
+ getSessionRuntimeStatus,
+ importExternalAgentSession,
+ interruptAppServerTurn,
+ runAppServerReview,
+ runAppServerTurn
+} from "./lib/codex.mjs";
+import { parseStructuredOutput, readOutputSchema } from "./lib/structured-output.mjs";
+import { buildPersistentTaskThreadName, DEFAULT_CONTINUE_PROMPT } from "./lib/task-thread.mjs";
import { resolveClaudeSessionPath } from "./lib/claude-session-transfer.mjs";
import { readStdinIfPiped } from "./lib/fs.mjs";
import { collectReviewContext, ensureGitRepository, resolveReviewTarget } from "./lib/git.mjs";
@@ -68,8 +66,9 @@ const ROOT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
const REVIEW_SCHEMA = path.join(ROOT_DIR, "schemas", "review-output.schema.json");
const DEFAULT_STATUS_WAIT_TIMEOUT_MS = 240000;
const DEFAULT_STATUS_POLL_INTERVAL_MS = 2000;
-const VALID_REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
-const MODEL_ALIASES = new Map([["spark", "gpt-5.3-codex-spark"]]);
+const REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max", "ultra"];
+const VALID_REASONING_EFFORTS = new Set(REASONING_EFFORTS);
+const MODEL_ALIASES = new Map([["spark", "gpt-5.6-luna"]]);
const STOP_REVIEW_TASK_MARKER = "Run a stop-gate review of the previous Claude turn.";
function printUsage() {
@@ -77,9 +76,9 @@ function printUsage() {
[
"Usage:",
" node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]",
- " node scripts/codex-companion.mjs review [--wait|--background] [--base ][] [--scope ]",
- " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base ][] [--scope ] [focus text]",
- " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]",
+ " node scripts/codex-companion.mjs review [--wait|--background] [--base ][] [--scope ] [--model ] [--effort ]",
+ " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base ][] [--scope ] [--model ] [--effort ] [focus text]",
+ " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]",
" node scripts/codex-companion.mjs transfer [--source ] [--json]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
@@ -121,7 +120,7 @@ function normalizeReasoningEffort(effort) {
}
if (!VALID_REASONING_EFFORTS.has(normalized)) {
throw new Error(
- `Unsupported reasoning effort "${effort}". Use one of: none, minimal, low, medium, high, xhigh.`
+ `Unsupported reasoning effort "${effort}". Use one of: ${REASONING_EFFORTS.join(", ")}.`
);
}
return normalized;
@@ -370,6 +369,7 @@ async function executeReviewRun(request) {
const result = await runAppServerReview(request.cwd, {
target: reviewTarget,
model: request.model,
+ effort: request.effort,
onProgress: request.onProgress
});
const payload = {
@@ -411,6 +411,7 @@ async function executeReviewRun(request) {
const result = await runAppServerTurn(context.repoRoot, {
prompt,
model: request.model,
+ effort: request.effort,
sandbox: "read-only",
outputSchema: readOutputSchema(REVIEW_SCHEMA),
onProgress: request.onProgress
@@ -488,7 +489,7 @@ async function executeTaskRun(request) {
defaultPrompt: resumeThreadId ? DEFAULT_CONTINUE_PROMPT : "",
model: request.model,
effort: request.effort,
- sandbox: request.write ? "workspace-write" : "read-only",
+ sandbox: request.write ? "danger-full-access" : "read-only",
onProgress: request.onProgress,
persistThread: true,
threadName: resumeThreadId ? null : buildPersistentTaskThreadName(request.prompt || DEFAULT_CONTINUE_PROMPT)
@@ -711,7 +712,7 @@ function enqueueBackgroundTask(cwd, job, request) {
async function handleReviewCommand(argv, config) {
const { options, positionals } = parseCommandInput(argv, {
- valueOptions: ["base", "scope", "model", "cwd"],
+ valueOptions: ["base", "scope", "model", "effort", "cwd"],
booleanOptions: ["json", "background", "wait"],
aliasMap: {
m: "model"
@@ -721,6 +722,8 @@ async function handleReviewCommand(argv, config) {
const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const focusText = positionals.join(" ").trim();
+ const model = normalizeRequestedModel(options.model);
+ const effort = normalizeReasoningEffort(options.effort);
const target = resolveReviewTarget(cwd, {
base: options.base,
scope: options.scope
@@ -743,7 +746,8 @@ async function handleReviewCommand(argv, config) {
cwd,
base: options.base,
scope: options.scope,
- model: options.model,
+ model,
+ effort,
focusText,
reviewName: config.reviewName,
onProgress: progress
diff --git a/plugins/codex/scripts/lib/app-server-protocol.d.ts b/plugins/codex/scripts/lib/app-server-protocol.d.ts
index f61a4588e..e66d19a6b 100644
--- a/plugins/codex/scripts/lib/app-server-protocol.d.ts
+++ b/plugins/codex/scripts/lib/app-server-protocol.d.ts
@@ -8,6 +8,8 @@ import type {
import type {
ExternalAgentConfigImportParams,
ExternalAgentConfigImportResponse,
+ ConfigReadParams,
+ ConfigReadResponse,
ReviewStartParams,
ReviewStartResponse,
ReviewTarget,
@@ -15,6 +17,8 @@ import type {
ThreadItem,
ThreadListParams,
ThreadListResponse,
+ ModelListParams,
+ ModelListResponse,
ThreadResumeParams as RawThreadResumeParams,
ThreadResumeResponse,
ThreadSetNameParams,
@@ -58,11 +62,13 @@ export interface CodexAppServerClientOptions {
export interface AppServerMethodMap {
initialize: { params: InitializeParams; result: InitializeResponse };
+ "config/read": { params: ConfigReadParams; result: ConfigReadResponse };
"externalAgentConfig/import": { params: ExternalAgentConfigImportParams; result: ExternalAgentConfigImportResponse };
"thread/start": { params: ThreadStartParams; result: ThreadStartResponse };
"thread/resume": { params: ThreadResumeParams; result: ThreadResumeResponse };
"thread/name/set": { params: ThreadSetNameParams; result: ThreadSetNameResponse };
"thread/list": { params: ThreadListParams; result: ThreadListResponse };
+ "model/list": { params: ModelListParams; result: ModelListResponse };
"review/start": { params: ReviewStartParams; result: ReviewStartResponse };
"turn/start": { params: TurnStartParams; result: TurnStartResponse };
"turn/interrupt": { params: TurnInterruptParams; result: TurnInterruptResponse };
diff --git a/plugins/codex/scripts/lib/app-server.mjs b/plugins/codex/scripts/lib/app-server.mjs
index 72b30a764..af88bc15b 100644
--- a/plugins/codex/scripts/lib/app-server.mjs
+++ b/plugins/codex/scripts/lib/app-server.mjs
@@ -13,7 +13,7 @@ import process from "node:process";
import { spawn } from "node:child_process";
import readline from "node:readline";
import { parseBrokerEndpoint } from "./broker-endpoint.mjs";
-import { ensureBrokerSession, loadBrokerSession } from "./broker-lifecycle.mjs";
+import { ensureBrokerSession, loadReusableBrokerSession } from "./broker-lifecycle.mjs";
import { terminateProcessTree } from "./process.mjs";
const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url);
@@ -335,13 +335,17 @@ class BrokerCodexAppServerClient extends AppServerClientBase {
export class CodexAppServerClient {
static async connect(cwd, options = {}) {
let brokerEndpoint = null;
+ const brokerOptions = {
+ env: options.env,
+ allowBusyStaleBroker: options.allowBusyStaleBroker
+ };
if (!options.disableBroker) {
brokerEndpoint = options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV] ?? null;
if (!brokerEndpoint && options.reuseExistingBroker) {
- brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null;
+ brokerEndpoint = (await loadReusableBrokerSession(cwd, brokerOptions))?.endpoint ?? null;
}
if (!brokerEndpoint && !options.reuseExistingBroker) {
- const brokerSession = await ensureBrokerSession(cwd, { env: options.env });
+ const brokerSession = await ensureBrokerSession(cwd, brokerOptions);
brokerEndpoint = brokerSession?.endpoint ?? null;
}
}
diff --git a/plugins/codex/scripts/lib/broker-endpoint.mjs b/plugins/codex/scripts/lib/broker-endpoint.mjs
index 8abdcc71a..78795b87d 100644
--- a/plugins/codex/scripts/lib/broker-endpoint.mjs
+++ b/plugins/codex/scripts/lib/broker-endpoint.mjs
@@ -13,7 +13,9 @@ export function createBrokerEndpoint(sessionDir, platform = process.platform) {
return `pipe:\\\\.\\pipe\\${pipeName}`;
}
- return `unix:${path.join(sessionDir, "broker.sock")}`;
+ // Honor the requested target platform instead of the host running the test
+ // or packaging step. This keeps Unix endpoints slash-based on Windows CI.
+ return `unix:${path.posix.join(String(sessionDir).replace(/\\/g, "/"), "broker.sock")}`;
}
export function parseBrokerEndpoint(endpoint) {
diff --git a/plugins/codex/scripts/lib/broker-lifecycle.mjs b/plugins/codex/scripts/lib/broker-lifecycle.mjs
index ef763819c..ee7fcf529 100644
--- a/plugins/codex/scripts/lib/broker-lifecycle.mjs
+++ b/plugins/codex/scripts/lib/broker-lifecycle.mjs
@@ -6,11 +6,31 @@ import process from "node:process";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createBrokerEndpoint, parseBrokerEndpoint } from "./broker-endpoint.mjs";
+import { withBrokerLock } from "./broker-lock.mjs";
+import { probeBroker } from "./broker-probe.mjs";
+import { binaryAvailable, terminateProcessTree } from "./process.mjs";
import { resolveStateDir } from "./state.mjs";
export const PID_FILE_ENV = "CODEX_COMPANION_APP_SERVER_PID_FILE";
export const LOG_FILE_ENV = "CODEX_COMPANION_APP_SERVER_LOG_FILE";
const BROKER_STATE_FILE = "broker.json";
+const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url);
+const PLUGIN_MANIFEST = JSON.parse(fs.readFileSync(PLUGIN_MANIFEST_URL, "utf8"));
+
+export function resolveBrokerRuntimeIdentity(cwd, env = process.env) {
+ const codex = binaryAvailable("codex", ["--version"], { cwd, env });
+ return {
+ pluginVersion: PLUGIN_MANIFEST.version ?? "0.0.0",
+ codexVersion: codex.available ? codex.detail : null
+ };
+}
+
+export function isBrokerRuntimeCurrent(session, runtime) {
+ return (
+ session?.runtime?.pluginVersion === runtime.pluginVersion &&
+ session?.runtime?.codexVersion === runtime.codexVersion
+ );
+}
export function createBrokerSessionDir(prefix = "cxc-") {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -41,18 +61,29 @@ export async function waitForBrokerEndpoint(endpoint, timeoutMs = 2000) {
}
export async function sendBrokerShutdown(endpoint) {
- await new Promise((resolve) => {
+ return new Promise((resolve) => {
const socket = connectToEndpoint(endpoint);
+ let buffer = "";
socket.setEncoding("utf8");
socket.on("connect", () => {
socket.write(`${JSON.stringify({ id: 1, method: "broker/shutdown", params: {} })}\n`);
});
- socket.on("data", () => {
+ socket.on("data", (chunk) => {
+ buffer += chunk;
+ const newlineIndex = buffer.indexOf("\n");
+ if (newlineIndex === -1) {
+ return;
+ }
+ const line = buffer.slice(0, newlineIndex);
socket.end();
- resolve();
+ try {
+ resolve(!JSON.parse(line.trim()).error);
+ } catch {
+ resolve(false);
+ }
});
- socket.on("error", resolve);
- socket.on("close", resolve);
+ socket.on("error", () => resolve(false));
+ socket.on("close", () => resolve(false));
});
}
@@ -110,64 +141,105 @@ async function isBrokerEndpointReady(endpoint) {
}
}
-export async function ensureBrokerSession(cwd, options = {}) {
+function teardownExistingBroker(cwd, existing, killProcess) {
+ teardownBrokerSession({
+ endpoint: existing.endpoint ?? null,
+ pidFile: existing.pidFile ?? null,
+ logFile: existing.logFile ?? null,
+ sessionDir: existing.sessionDir ?? null,
+ pid: existing.pid ?? null,
+ killProcess
+ });
+ clearBrokerSession(cwd);
+}
+
+async function loadReusableBrokerSessionUnlocked(cwd, options = {}) {
const existing = loadBrokerSession(cwd);
- if (existing && (await isBrokerEndpointReady(existing.endpoint))) {
+ const runtime = resolveBrokerRuntimeIdentity(cwd, options.env);
+ if (
+ existing &&
+ isBrokerRuntimeCurrent(existing, runtime) &&
+ (await isBrokerEndpointReady(existing.endpoint))
+ ) {
return existing;
}
if (existing) {
- teardownBrokerSession({
- endpoint: existing.endpoint ?? null,
- pidFile: existing.pidFile ?? null,
- logFile: existing.logFile ?? null,
- sessionDir: existing.sessionDir ?? null,
- pid: existing.pid ?? null,
- killProcess: options.killProcess ?? null
- });
- clearBrokerSession(cwd);
+ if (await isBrokerEndpointReady(existing.endpoint)) {
+ const brokerStatus = await probeBroker(existing.endpoint, cwd);
+ if (brokerStatus === "busy" && options.allowBusyStaleBroker) {
+ return existing;
+ }
+ if (brokerStatus !== "idle") {
+ options.deferBrokerReplacement = true;
+ return null;
+ }
+ if (!(await sendBrokerShutdown(existing.endpoint))) {
+ options.deferBrokerReplacement = true;
+ return null;
+ }
+ }
+ teardownExistingBroker(cwd, existing, options.killProcess ?? terminateProcessTree);
}
- const sessionDir = createBrokerSessionDir();
- const endpointFactory = options.createBrokerEndpoint ?? createBrokerEndpoint;
- const endpoint = endpointFactory(sessionDir, options.platform);
- const pidFile = path.join(sessionDir, "broker.pid");
- const logFile = path.join(sessionDir, "broker.log");
- const scriptPath =
- options.scriptPath ??
- fileURLToPath(new URL("../app-server-broker.mjs", import.meta.url));
-
- const child = spawnBrokerProcess({
- scriptPath,
- cwd,
- endpoint,
- pidFile,
- logFile,
- env: options.env ?? process.env
- });
+ return null;
+}
+
+export async function loadReusableBrokerSession(cwd, options = {}) {
+ return withBrokerLock(cwd, options, () => loadReusableBrokerSessionUnlocked(cwd, options));
+}
+
+export async function ensureBrokerSession(cwd, options = {}) {
+ return withBrokerLock(cwd, options, async () => {
+ const existing = await loadReusableBrokerSessionUnlocked(cwd, options);
+ if (existing || options.deferBrokerReplacement) {
+ return existing;
+ }
+
+ const runtime = resolveBrokerRuntimeIdentity(cwd, options.env);
- const ready = await waitForBrokerEndpoint(endpoint, options.timeoutMs ?? 2000);
- if (!ready) {
- teardownBrokerSession({
+ const sessionDir = createBrokerSessionDir();
+ const endpointFactory = options.createBrokerEndpoint ?? createBrokerEndpoint;
+ const endpoint = endpointFactory(sessionDir, options.platform);
+ const pidFile = path.join(sessionDir, "broker.pid");
+ const logFile = path.join(sessionDir, "broker.log");
+ const scriptPath =
+ options.scriptPath ??
+ fileURLToPath(new URL("../app-server-broker.mjs", import.meta.url));
+
+ const child = spawnBrokerProcess({
+ scriptPath,
+ cwd,
endpoint,
pidFile,
logFile,
- sessionDir,
- pid: child.pid ?? null,
- killProcess: options.killProcess ?? null
+ env: options.env ?? process.env
});
- return null;
- }
- const session = {
- endpoint,
- pidFile,
- logFile,
- sessionDir,
- pid: child.pid ?? null
- };
- saveBrokerSession(cwd, session);
- return session;
+ const ready = await waitForBrokerEndpoint(endpoint, options.timeoutMs ?? 2000);
+ if (!ready) {
+ teardownBrokerSession({
+ endpoint,
+ pidFile,
+ logFile,
+ sessionDir,
+ pid: child.pid ?? null,
+ killProcess: options.killProcess ?? terminateProcessTree
+ });
+ return null;
+ }
+
+ const session = {
+ endpoint,
+ pidFile,
+ logFile,
+ sessionDir,
+ pid: child.pid ?? null,
+ runtime
+ };
+ saveBrokerSession(cwd, session);
+ return session;
+ });
}
export function teardownBrokerSession({ endpoint = null, pidFile, logFile, sessionDir = null, pid = null, killProcess = null }) {
diff --git a/plugins/codex/scripts/lib/broker-lock.mjs b/plugins/codex/scripts/lib/broker-lock.mjs
new file mode 100644
index 000000000..842c2a84e
--- /dev/null
+++ b/plugins/codex/scripts/lib/broker-lock.mjs
@@ -0,0 +1,100 @@
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+
+import { resolveStateDir } from "./state.mjs";
+
+const BROKER_LOCK_FILE = "broker.lock";
+const BROKER_LOCK_TIMEOUT_MS = 5000;
+const BROKER_LOCK_STALE_MS = 30000;
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function brokerLockPath(cwd) {
+ const stateDir = resolveStateDir(cwd);
+ fs.mkdirSync(stateDir, { recursive: true });
+ return path.join(stateDir, BROKER_LOCK_FILE);
+}
+
+function isProcessAlive(pid) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (error) {
+ return error?.code !== "ESRCH";
+ }
+}
+
+function removeAbandonedBrokerLock(lockFile, staleMs) {
+ try {
+ const stat = fs.statSync(lockFile);
+ const ownerPid = Number.parseInt(fs.readFileSync(lockFile, "utf8").split(":", 1)[0], 10);
+ if (Number.isFinite(ownerPid) && isProcessAlive(ownerPid)) {
+ return false;
+ }
+ if (!Number.isFinite(ownerPid) && Date.now() - stat.mtimeMs <= staleMs) {
+ return false;
+ }
+ fs.unlinkSync(lockFile);
+ return true;
+ } catch (error) {
+ if (error?.code === "ENOENT") {
+ return true;
+ }
+ throw error;
+ }
+}
+
+async function acquireBrokerLock(cwd, options = {}) {
+ const lockFile = brokerLockPath(cwd);
+ const timeoutMs = options.lockTimeoutMs ?? BROKER_LOCK_TIMEOUT_MS;
+ const staleMs = options.lockStaleMs ?? BROKER_LOCK_STALE_MS;
+ const deadline = Date.now() + timeoutMs;
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
+
+ while (true) {
+ try {
+ const fd = fs.openSync(lockFile, "wx");
+ fs.writeFileSync(fd, token, "utf8");
+ return { fd, lockFile, token };
+ } catch (error) {
+ if (error?.code !== "EEXIST") {
+ throw error;
+ }
+ if (removeAbandonedBrokerLock(lockFile, staleMs)) {
+ continue;
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`Timed out waiting for the shared Codex broker lock at ${lockFile}.`);
+ }
+ await sleep(25);
+ }
+ }
+}
+
+function releaseBrokerLock(lock) {
+ try {
+ fs.closeSync(lock.fd);
+ } finally {
+ try {
+ if (fs.readFileSync(lock.lockFile, "utf8") === lock.token) {
+ fs.unlinkSync(lock.lockFile);
+ }
+ } catch (error) {
+ if (error?.code !== "ENOENT") {
+ throw error;
+ }
+ }
+ }
+}
+
+export async function withBrokerLock(cwd, options, action) {
+ const lock = await acquireBrokerLock(cwd, options);
+ try {
+ return await action();
+ } finally {
+ releaseBrokerLock(lock);
+ }
+}
diff --git a/plugins/codex/scripts/lib/broker-probe.mjs b/plugins/codex/scripts/lib/broker-probe.mjs
new file mode 100644
index 000000000..4cebb26ec
--- /dev/null
+++ b/plugins/codex/scripts/lib/broker-probe.mjs
@@ -0,0 +1,91 @@
+import net from "node:net";
+import { parseBrokerEndpoint } from "./broker-endpoint.mjs";
+
+const BROKER_BUSY_RPC_CODE = -32001;
+const BROKER_PROBE_TIMEOUT_MS = 500;
+
+function connectToEndpoint(endpoint) {
+ const target = parseBrokerEndpoint(endpoint);
+ return net.createConnection({ path: target.path });
+}
+
+function sendJsonLine(socket, message) {
+ socket.write(`${JSON.stringify(message)}\n`);
+}
+
+function handleProbeMessage(message, phase, socket, cwd) {
+ if (message.error) {
+ return { status: message.error.code === BROKER_BUSY_RPC_CODE ? "busy" : "unknown" };
+ }
+ if (phase === "initialize" && message.id === 1) {
+ sendJsonLine(socket, { method: "initialized", params: {} });
+ sendJsonLine(socket, { id: 2, method: "thread/list", params: { cwd, limit: 1 } });
+ return { phase: "probe" };
+ }
+ if (phase === "probe" && message.id === 2) {
+ return { status: "idle" };
+ }
+ return {};
+}
+
+function consumeProbeData(buffer, chunk, phase, socket, cwd, finish) {
+ buffer += chunk;
+ let newlineIndex = buffer.indexOf("\n");
+ while (newlineIndex !== -1) {
+ const line = buffer.slice(0, newlineIndex);
+ buffer = buffer.slice(newlineIndex + 1);
+ newlineIndex = buffer.indexOf("\n");
+ if (!line.trim()) {
+ continue;
+ }
+ let message;
+ try {
+ message = JSON.parse(line);
+ } catch {
+ finish("unknown");
+ return { buffer, phase };
+ }
+ const next = handleProbeMessage(message, phase, socket, cwd);
+ if (next.status) {
+ finish(next.status);
+ return { buffer, phase };
+ }
+ phase = next.phase ?? phase;
+ }
+ return { buffer, phase };
+}
+
+export async function probeBroker(endpoint, cwd, timeoutMs = BROKER_PROBE_TIMEOUT_MS) {
+ return new Promise((resolve) => {
+ const socket = connectToEndpoint(endpoint);
+ let buffer = "";
+ let phase = "initialize";
+ let settled = false;
+ const timer = setTimeout(() => finish("unknown"), timeoutMs);
+
+ function finish(status) {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ clearTimeout(timer);
+ socket.destroy();
+ resolve(status);
+ }
+
+ socket.setEncoding("utf8");
+ socket.on("connect", () => sendJsonLine(socket, {
+ id: 1,
+ method: "initialize",
+ params: {
+ clientInfo: { title: "Codex Plugin Broker Probe", name: "Claude Code", version: "0.0.0" },
+ capabilities: { experimentalApi: false, requestAttestation: false }
+ }
+ }));
+ socket.on("data", (chunk) => {
+ ({ buffer, phase } = consumeProbeData(buffer, chunk, phase, socket, cwd, finish));
+ });
+ socket.on("error", () => finish("unknown"));
+ socket.on("close", () => finish("unknown"));
+ });
+}
diff --git a/plugins/codex/scripts/lib/claude-session-transfer.mjs b/plugins/codex/scripts/lib/claude-session-transfer.mjs
index eea0aeba2..1c66e4d09 100644
--- a/plugins/codex/scripts/lib/claude-session-transfer.mjs
+++ b/plugins/codex/scripts/lib/claude-session-transfer.mjs
@@ -5,25 +5,34 @@ import path from "node:path";
import { ensureAbsolutePath } from "./fs.mjs";
export const TRANSCRIPT_PATH_ENV = "CODEX_COMPANION_TRANSCRIPT_PATH";
-const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
-function resolveUserPath(cwd, value) {
+function resolveHomeDir(env = process.env) {
+ const configured = env.HOME || env.USERPROFILE;
+ return configured ? path.resolve(configured) : os.homedir();
+}
+
+function resolveUserPath(cwd, value, homeDir) {
if (value === "~") {
- return os.homedir();
+ return homeDir;
}
- if (String(value).startsWith("~/")) {
- return path.join(os.homedir(), String(value).slice(2));
+ if (/^~[\\/]/.test(String(value))) {
+ return path.join(homeDir, String(value).slice(2));
}
return ensureAbsolutePath(cwd, value);
}
export function resolveClaudeSessionPath(cwd, options = {}) {
- const requestedPath = options.source || process.env[TRANSCRIPT_PATH_ENV];
+ const env = options.env ?? process.env;
+ const requestedPath = options.source || env[TRANSCRIPT_PATH_ENV];
if (!requestedPath) {
- throw new Error("Could not identify the current Claude transcript. Retry with --source .");
+ throw new Error(
+ "Could not identify the current Claude transcript. Retry with --source ."
+ );
}
- const sourcePath = resolveUserPath(cwd, requestedPath);
+ const homeDir = resolveHomeDir(env);
+ const projectsDir = path.join(homeDir, ".claude", "projects");
+ const sourcePath = resolveUserPath(cwd, requestedPath, homeDir);
if (path.extname(sourcePath) !== ".jsonl") {
throw new Error(`Claude session source must be a JSONL file: ${sourcePath}`);
}
@@ -32,13 +41,19 @@ export function resolveClaudeSessionPath(cwd, options = {}) {
let projects;
try {
source = fs.realpathSync(sourcePath);
- projects = fs.realpathSync(CLAUDE_PROJECTS_DIR);
+ projects = fs.realpathSync(projectsDir);
} catch {
throw new Error(`Claude session file not found: ${sourcePath}`);
}
+
const relative = path.relative(projects, source);
- if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
- throw new Error(`Codex can import Claude sessions only from ${CLAUDE_PROJECTS_DIR}: ${source}`);
+ if (
+ relative === ""
+ || relative === ".."
+ || relative.startsWith(`..${path.sep}`)
+ || path.isAbsolute(relative)
+ ) {
+ throw new Error(`Codex can import Claude sessions only from ${projectsDir}: ${source}`);
}
return source;
}
diff --git a/plugins/codex/scripts/lib/codex.mjs b/plugins/codex/scripts/lib/codex.mjs
index fead00cc4..ba1116671 100644
--- a/plugins/codex/scripts/lib/codex.mjs
+++ b/plugins/codex/scripts/lib/codex.mjs
@@ -23,6 +23,7 @@
* finalAnswerSeen: boolean,
* pendingCollaborations: Set,
* activeSubagentTurns: Set,
+ * nativeChildPeak: number,
* completionTimer: ReturnType | null,
* lastAgentMessage: string,
* reviewText: string,
@@ -43,11 +44,10 @@ import { readJsonFile } from "./fs.mjs";
import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs";
import { loadBrokerSession } from "./broker-lifecycle.mjs";
import { binaryAvailable } from "./process.mjs";
+import { validateExplicitReasoningSelection, validateReasoningSelection } from "./model-catalog.mjs";
+import { TASK_THREAD_PREFIX } from "./task-thread.mjs";
const SERVICE_NAME = "claude_code_codex_plugin";
-const TASK_THREAD_PREFIX = "Codex Companion Task";
-const DEFAULT_CONTINUE_PROMPT =
- "Continue from the current thread state. Pick the next highest-value step and follow through until the task is resolved.";
const EXTERNAL_AGENT_IMPORT_COMPLETED = "externalAgentConfig/import/completed";
const EXTERNAL_AGENT_IMPORT_TIMEOUT_MS = 2 * 60 * 1000;
@@ -66,6 +66,7 @@ function buildThreadParams(cwd, options = {}) {
model: options.model ?? null,
approvalPolicy: options.approvalPolicy ?? "never",
sandbox: options.sandbox ?? "read-only",
+ config: options.effort ? { model_reasoning_effort: options.effort } : null,
serviceName: SERVICE_NAME,
ephemeral: options.ephemeral ?? true
};
@@ -104,11 +105,6 @@ function looksLikeVerificationCommand(command) {
);
}
-function buildTaskThreadName(prompt) {
- const excerpt = shorten(prompt, 56);
- return excerpt ? `${TASK_THREAD_PREFIX}: ${excerpt}` : TASK_THREAD_PREFIX;
-}
-
function extractThreadId(message) {
return message?.params?.threadId ?? null;
}
@@ -324,6 +320,7 @@ function createTurnCaptureState(threadId, options = {}) {
finalAnswerSeen: false,
pendingCollaborations: new Set(),
activeSubagentTurns: new Set(),
+ nativeChildPeak: 0,
completionTimer: null,
lastAgentMessage: "",
reviewText: "",
@@ -507,6 +504,7 @@ function applyTurnNotification(state, message) {
state.threadTurnIds.set(message.params.threadId, message.params.turn.id);
if ((message.params.threadId ?? null) !== state.threadId) {
state.activeSubagentTurns.add(message.params.threadId);
+ state.nativeChildPeak = Math.max(state.nativeChildPeak, state.activeSubagentTurns.size);
}
emitProgress(
state.onProgress,
@@ -979,7 +977,10 @@ export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
let client = null;
try {
- client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true });
+ client = await CodexAppServerClient.connect(cwd, {
+ reuseExistingBroker: true,
+ allowBusyStaleBroker: true
+ });
await client.request("turn/interrupt", { threadId, turnId });
return {
attempted: true,
@@ -1006,14 +1007,21 @@ export async function runAppServerReview(cwd, options = {}) {
}
return withAppServer(cwd, async (client) => {
+ await validateExplicitReasoningSelection(client, cwd, options);
emitProgress(options.onProgress, "Starting Codex review thread.", "starting");
const thread = await startThread(client, cwd, {
model: options.model,
+ effort: options.effort,
sandbox: "read-only",
ephemeral: true,
threadName: options.threadName
});
const sourceThreadId = thread.thread.id;
+ await validateReasoningSelection(client, {
+ model: options.model ?? thread.model,
+ effort: options.effort ?? thread.reasoningEffort,
+ modelProvider: thread.modelProvider
+ });
emitProgress(options.onProgress, `Thread ready (${sourceThreadId}).`, "starting", {
threadId: sourceThreadId
});
@@ -1092,14 +1100,20 @@ export async function importExternalAgentSession(cwd, options = {}) {
});
}
-export async function runAppServerTurn(cwd, options = {}) {
+export async function runAppServerTurnWithClient(client, cwd, options = {}) {
const availability = getCodexAvailability(cwd);
if (!availability.available) {
throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/codex:setup`.");
}
- return withAppServer(cwd, async (client) => {
let threadId;
+ let threadSelection;
+
+ if (!options.resumeThreadId) {
+ await validateExplicitReasoningSelection(client, cwd, options, {
+ includeInherited: options.persistThread === true
+ });
+ }
if (options.resumeThreadId) {
emitProgress(options.onProgress, `Resuming thread ${options.resumeThreadId}.`, "starting");
@@ -1109,6 +1123,7 @@ export async function runAppServerTurn(cwd, options = {}) {
ephemeral: false
});
threadId = response.thread.id;
+ threadSelection = response;
} else {
emitProgress(options.onProgress, "Starting Codex task thread.", "starting");
const response = await startThread(client, cwd, {
@@ -1118,8 +1133,15 @@ export async function runAppServerTurn(cwd, options = {}) {
threadName: options.persistThread ? options.threadName : options.threadName ?? null
});
threadId = response.thread.id;
+ threadSelection = response;
}
+ await validateReasoningSelection(client, {
+ model: options.model ?? threadSelection.model,
+ effort: options.effort ?? threadSelection.reasoningEffort,
+ modelProvider: threadSelection.modelProvider
+ });
+
emitProgress(options.onProgress, `Thread ready (${threadId}).`, "starting", {
threadId
});
@@ -1147,6 +1169,9 @@ export async function runAppServerTurn(cwd, options = {}) {
status: buildResultStatus(turnState),
threadId,
turnId: turnState.turnId,
+ threadIds: [...turnState.threadIds],
+ nativeChildThreadIds: [...turnState.threadIds].filter((candidate) => candidate !== threadId),
+ nativeChildPeak: turnState.nativeChildPeak,
finalMessage: turnState.lastAgentMessage,
reasoningSummary: turnState.reasoningSummary,
turn: turnState.finalTurn,
@@ -1156,9 +1181,17 @@ export async function runAppServerTurn(cwd, options = {}) {
touchedFiles: collectTouchedFiles(turnState.fileChanges),
commandExecutions: turnState.commandExecutions
};
- });
+
}
+export async function runAppServerTurn(cwd, options = {}) {
+ const availability = getCodexAvailability(cwd);
+ if (!availability.available) {
+ throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/codex:setup`.");
+ }
+
+ return withAppServer(cwd, (client) => runAppServerTurnWithClient(client, cwd, options));
+}
export async function findLatestTaskThread(cwd) {
const availability = getCodexAvailability(cwd);
if (!availability.available) {
@@ -1180,40 +1213,3 @@ export async function findLatestTaskThread(cwd) {
);
});
}
-
-export function buildPersistentTaskThreadName(prompt) {
- return buildTaskThreadName(prompt);
-}
-
-export function parseStructuredOutput(rawOutput, fallback = {}) {
- if (!rawOutput) {
- return {
- parsed: null,
- parseError: fallback.failureMessage ?? "Codex did not return a final structured message.",
- rawOutput: rawOutput ?? "",
- ...fallback
- };
- }
-
- try {
- return {
- parsed: JSON.parse(rawOutput),
- parseError: null,
- rawOutput,
- ...fallback
- };
- } catch (error) {
- return {
- parsed: null,
- parseError: error.message,
- rawOutput,
- ...fallback
- };
- }
-}
-
-export function readOutputSchema(schemaPath) {
- return readJsonFile(schemaPath);
-}
-
-export { DEFAULT_CONTINUE_PROMPT, TASK_THREAD_PREFIX };
diff --git a/plugins/codex/scripts/lib/model-catalog.mjs b/plugins/codex/scripts/lib/model-catalog.mjs
new file mode 100644
index 000000000..441e52ee7
--- /dev/null
+++ b/plugins/codex/scripts/lib/model-catalog.mjs
@@ -0,0 +1,92 @@
+function isUnsupportedMethodError(error) {
+ if (error?.rpcCode === -32601) {
+ return true;
+ }
+ return /unknown (variant|method)|unsupported method|method not found/i.test(
+ String(error?.message ?? error ?? "")
+ );
+}
+
+async function readModelCatalog(client) {
+ const models = [];
+ let cursor = null;
+
+ try {
+ do {
+ const response = await client.request("model/list", {
+ cursor,
+ limit: 100,
+ includeHidden: true
+ });
+ models.push(...(response.data ?? []));
+ cursor = response.nextCursor ?? null;
+ } while (cursor);
+ } catch (error) {
+ if (isUnsupportedMethodError(error)) {
+ return null;
+ }
+ throw error;
+ }
+
+ return models;
+}
+
+function supportedEfforts(model) {
+ return (model.supportedReasoningEfforts ?? [])
+ .map((option) => String(option.reasoningEffort ?? "").trim().toLowerCase())
+ .filter(Boolean);
+}
+
+export async function validateReasoningSelection(client, selection = {}) {
+ const modelName = String(selection.model ?? "").trim();
+ const effort = String(selection.effort ?? "").trim().toLowerCase();
+ const provider = String(selection.modelProvider ?? "").trim().toLowerCase();
+ if (!effort || provider !== "openai") {
+ return;
+ }
+
+ const catalog = await readModelCatalog(client);
+ if (!catalog) {
+ return;
+ }
+
+ const model = modelName
+ ? catalog.find((candidate) => candidate.model === modelName || candidate.id === modelName)
+ : catalog.find((candidate) => candidate.isDefault === true);
+ if (!model) {
+ return;
+ }
+ const selectedModelName = model.model ?? model.id ?? modelName;
+
+ const efforts = supportedEfforts(model);
+ if (efforts.length === 0 || efforts.includes(effort)) {
+ return;
+ }
+
+ throw new Error(
+ `Reasoning effort "${effort}" is not supported by model "${selectedModelName}". Supported efforts: ${efforts.join(", ")}.`
+ );
+}
+
+export async function validateExplicitReasoningSelection(client, cwd, selection = {}, options = {}) {
+ if (!selection.model && !selection.effort && !options.includeInherited) {
+ return;
+ }
+
+ let config;
+ try {
+ const response = await client.request("config/read", { cwd, includeLayers: false });
+ config = response.config ?? {};
+ } catch (error) {
+ if (isUnsupportedMethodError(error)) {
+ return;
+ }
+ throw error;
+ }
+
+ await validateReasoningSelection(client, {
+ model: selection.model ?? config.model,
+ effort: selection.effort ?? config.model_reasoning_effort,
+ modelProvider: config.model_provider ?? "openai"
+ });
+}
diff --git a/plugins/codex/scripts/lib/process.mjs b/plugins/codex/scripts/lib/process.mjs
index dd8fc3751..58e6d9ecf 100644
--- a/plugins/codex/scripts/lib/process.mjs
+++ b/plugins/codex/scripts/lib/process.mjs
@@ -54,6 +54,28 @@ function looksLikeMissingProcessMessage(text) {
return /not found|no running instance|cannot find|does not exist|no such process/i.test(text);
}
+function terminateDirectly(pid, killImpl, taskkillResult = null) {
+ try {
+ killImpl(pid, "SIGTERM");
+ return {
+ attempted: true,
+ delivered: true,
+ method: "kill",
+ ...(taskkillResult ? { result: taskkillResult } : {})
+ };
+ } catch (error) {
+ if (error?.code === "ESRCH") {
+ return {
+ attempted: true,
+ delivered: false,
+ method: "kill",
+ ...(taskkillResult ? { result: taskkillResult } : {})
+ };
+ }
+ throw error;
+ }
+}
+
export function terminateProcessTree(pid, options = {}) {
if (!Number.isFinite(pid)) {
return { attempted: false, delivered: false, method: null };
@@ -78,23 +100,15 @@ export function terminateProcessTree(pid, options = {}) {
return { attempted: true, delivered: false, method: "taskkill", result };
}
- if (result.error?.code === "ENOENT") {
- try {
- killImpl(pid);
- return { attempted: true, delivered: true, method: "kill" };
- } catch (error) {
- if (error?.code === "ESRCH") {
- return { attempted: true, delivered: false, method: "kill" };
- }
- throw error;
- }
- }
-
- if (result.error) {
+ if (result.error && result.error.code !== "ENOENT") {
throw result.error;
}
- throw new Error(formatCommandFailure(result));
+ // Windows taskkill can report a partial tree failure even after the root
+ // process is terminated, especially for console-wrapper children. A direct
+ // process kill is the reliable best-effort fallback and must not turn a
+ // successful cancellation into a command failure.
+ return terminateDirectly(pid, killImpl, result);
}
try {
diff --git a/plugins/codex/scripts/lib/structured-output.mjs b/plugins/codex/scripts/lib/structured-output.mjs
new file mode 100644
index 000000000..317c915ba
--- /dev/null
+++ b/plugins/codex/scripts/lib/structured-output.mjs
@@ -0,0 +1,32 @@
+import { readJsonFile } from "./fs.mjs";
+
+export function parseStructuredOutput(rawOutput, fallback = {}) {
+ if (!rawOutput) {
+ return {
+ parsed: null,
+ parseError: fallback.failureMessage ?? "Codex did not return a final structured message.",
+ rawOutput: rawOutput ?? "",
+ ...fallback
+ };
+ }
+
+ try {
+ return {
+ parsed: JSON.parse(rawOutput),
+ parseError: null,
+ rawOutput,
+ ...fallback
+ };
+ } catch (error) {
+ return {
+ parsed: null,
+ parseError: error.message,
+ rawOutput,
+ ...fallback
+ };
+ }
+}
+
+export function readOutputSchema(schemaPath) {
+ return readJsonFile(schemaPath);
+}
diff --git a/plugins/codex/scripts/lib/task-thread.mjs b/plugins/codex/scripts/lib/task-thread.mjs
new file mode 100644
index 000000000..f0f01e095
--- /dev/null
+++ b/plugins/codex/scripts/lib/task-thread.mjs
@@ -0,0 +1,16 @@
+export const TASK_THREAD_PREFIX = "Codex Companion Task";
+export const DEFAULT_CONTINUE_PROMPT =
+ "Continue from the current thread state. Pick the next highest-value step and follow through until the task is resolved.";
+
+function shorten(text, limit) {
+ const normalized = String(text ?? "").trim().replace(/\s+/g, " ");
+ if (!normalized || normalized.length <= limit) {
+ return normalized;
+ }
+ return `${normalized.slice(0, limit - 3)}...`;
+}
+
+export function buildPersistentTaskThreadName(prompt) {
+ const excerpt = shorten(prompt, 56);
+ return excerpt ? `${TASK_THREAD_PREFIX}: ${excerpt}` : TASK_THREAD_PREFIX;
+}
diff --git a/plugins/codex/scripts/orchestration/budget-policy.mjs b/plugins/codex/scripts/orchestration/budget-policy.mjs
new file mode 100644
index 000000000..86b2aa3f3
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/budget-policy.mjs
@@ -0,0 +1,35 @@
+
+const ENVELOPES = [
+ { min: 0, max: 2, maxTopLevelRoots: 1, workerParallelism: 1, maxNativeChildrenPerRoot: 0, timeoutMinutes: 15 },
+ { min: 3, max: 4, maxTopLevelRoots: 2, workerParallelism: 2, maxNativeChildrenPerRoot: 1, timeoutMinutes: 15 },
+ { min: 5, max: 7, maxTopLevelRoots: 4, workerParallelism: 3, maxNativeChildrenPerRoot: 2, timeoutMinutes: 30 },
+ { min: 8, max: 10, maxTopLevelRoots: 6, workerParallelism: 3, maxNativeChildrenPerRoot: 3, timeoutMinutes: 60 }
+];
+
+export function deriveBudgetEnvelope(complexityScore, config) {
+ if (!Number.isInteger(complexityScore) || complexityScore < 0 || complexityScore > 10) {
+ throw new Error("complexityScore must be an integer between 0 and 10.");
+ }
+ const base = ENVELOPES.find((entry) => complexityScore >= entry.min && complexityScore <= entry.max);
+ return {
+ maxTopLevelRoots: Math.min(base.maxTopLevelRoots, config.workers.globalTopLevelLimit),
+ workerParallelism: Math.min(base.workerParallelism, config.workers.workspacePoolSize),
+ maxNativeChildrenPerRoot: base.maxNativeChildrenPerRoot,
+ timeoutMinutes: base.timeoutMinutes,
+ maxRetries: 1,
+ maxReplans: 0,
+ maxAdditionalPackages: 0,
+ maxConcurrentSolUltra: 2
+ };
+}
+
+export function validatePlanAgainstBudget(plan, envelope) {
+ if (plan.packages.length > envelope.maxTopLevelRoots) {
+ throw new Error(`Plan contains ${plan.packages.length} packages but the budget permits ${envelope.maxTopLevelRoots}.`);
+ }
+ for (const pkg of plan.packages) {
+ if (pkg.nativeSubagents.maxChildren > envelope.maxNativeChildrenPerRoot) {
+ throw new Error(`${pkg.id}.nativeSubagents.maxChildren exceeds the budget limit ${envelope.maxNativeChildrenPerRoot}.`);
+ }
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/cli.mjs b/plugins/codex/scripts/orchestration/cli.mjs
new file mode 100644
index 000000000..155d3f16b
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/cli.mjs
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+import fs from "node:fs";
+import path from "node:path";
+
+import { parseArgs } from "../lib/args.mjs";
+import { resolveWorkspaceRoot } from "../lib/workspace.mjs";
+import { getProjectConfigPath, getUserConfigPath, loadOrchestrationConfig } from "./config.mjs";
+import { OrchestrationControllerClient } from "./controller-client.mjs";
+import { ensureControllerServer } from "./controller-lifecycle.mjs";
+import { buildOrchestrationResult } from "./result-contract.mjs";
+import { isTerminalState, listOrchestrations, loadOrchestrationState, resolveOrchestrationReference } from "./state-store.mjs";
+
+function output(value, json) { process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : render(value)); }
+function render(value) {
+ if (value?.orchestrationId && value?.packageCount != null) return `Multi-Codex orchestration ${value.orchestrationId} accepted.\nStatus: ${value.status}\nPackages: ${value.packageCount}\nStatus: /codex:status ${value.orchestrationId}\nResult: /codex:result ${value.orchestrationId}\nCancel: /codex:cancel ${value.orchestrationId}\n`;
+ return `${JSON.stringify(value, null, 2)}\n`;
+}
+function readPiped() { if (process.stdin.isTTY) return ""; return fs.readFileSync(0, "utf8"); }
+function durableStatus(cwd, reference) {
+ if (!reference) return { workspaceRoot: cwd, orchestrations: listOrchestrations(cwd) };
+ const resolved = resolveOrchestrationReference(cwd, reference); const state = loadOrchestrationState(cwd, resolved.orchestrationId);
+ return resolved.kind === "package" ? { orchestrationId: state.id, packageSpec: state.plan.packages.find((pkg) => pkg.id === resolved.packageId), package: state.packages[resolved.packageId] } : state;
+}
+function durableResult(cwd, reference) {
+ const resolved = resolveOrchestrationReference(cwd, reference); const state = loadOrchestrationState(cwd, resolved.orchestrationId);
+ if (resolved.kind === "package") return state.packages[resolved.packageId].result;
+ if (!isTerminalState(state)) throw new Error(`Orchestration ${state.id} is still running. Use /codex:status ${state.id}.`);
+ return buildOrchestrationResult(state);
+}
+async function main() {
+ const [command, ...argv] = process.argv.slice(2);
+ const { options, positionals } = parseArgs(argv, { valueOptions: ["cwd", "plan-file"], booleanOptions: ["json"] });
+ const cwd = resolveWorkspaceRoot(options.cwd ? path.resolve(options.cwd) : process.cwd());
+ if (command === "config") {
+ const effectiveConfig = loadOrchestrationConfig(cwd);
+ output({ workspaceRoot: cwd, userConfigPath: getUserConfigPath(), projectConfigPath: getProjectConfigPath(cwd), effectiveConfig, autoEnabled: effectiveConfig.auto.enabled, autoThreshold: effectiveConfig.auto.threshold }, options.json); return;
+ }
+ if (command === "start") {
+ const planText = options["plan-file"] ? fs.readFileSync(path.resolve(options["plan-file"]), "utf8") : readPiped();
+ if (!planText.trim()) throw new Error("start requires --plan-file or piped plan JSON.");
+ const session = await ensureControllerServer(cwd); const client = new OrchestrationControllerClient(session.endpoint);
+ const summary = await client.start(JSON.parse(planText), { claudeSessionId: process.env.CODEX_COMPANION_SESSION_ID ?? null });
+ output({ ...summary, commands: { status: `/codex:status ${summary.orchestrationId}`, result: `/codex:result ${summary.orchestrationId}`, cancel: `/codex:cancel ${summary.orchestrationId}` } }, options.json); return;
+ }
+ const reference = positionals[0] ?? "";
+ if (command === "status") { output(durableStatus(cwd, reference), options.json); return; }
+ if (command === "result") { if (!reference) throw new Error("result requires an orchestration or package reference."); output(durableResult(cwd, reference), options.json); return; }
+ if (command === "cancel") {
+ if (!reference) throw new Error("cancel requires an orchestration or package reference.");
+ const state = durableStatus(cwd, reference); const orchestrationState = state.package ? loadOrchestrationState(cwd, state.orchestrationId) : state;
+ if (isTerminalState(orchestrationState)) { output(orchestrationState, options.json); return; }
+ const session = await ensureControllerServer(cwd); const client = new OrchestrationControllerClient(session.endpoint); output(await client.cancel(reference), options.json); return;
+ }
+ throw new Error("Usage: cli.mjs ...");
+}
+main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
diff --git a/plugins/codex/scripts/orchestration/config.mjs b/plugins/codex/scripts/orchestration/config.mjs
new file mode 100644
index 000000000..d6304d879
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/config.mjs
@@ -0,0 +1,98 @@
+
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import crypto from "node:crypto";
+
+import {
+ DEFAULT_AUTO_THRESHOLD,
+ DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT,
+ DEFAULT_GLOBAL_TOP_LEVEL_LIMIT,
+ DEFAULT_IDLE_TTL_MINUTES,
+ DEFAULT_WORKSPACE_POOL_SIZE
+} from "./constants.mjs";
+
+export const DEFAULT_ORCHESTRATION_CONFIG = Object.freeze({
+ auto: Object.freeze({ enabled: false, threshold: DEFAULT_AUTO_THRESHOLD }),
+ workers: Object.freeze({
+ workspacePoolSize: DEFAULT_WORKSPACE_POOL_SIZE,
+ globalTopLevelLimit: DEFAULT_GLOBAL_TOP_LEVEL_LIMIT,
+ globalActiveCodexLimit: DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT,
+ idleTtlMinutes: DEFAULT_IDLE_TTL_MINUTES
+ })
+});
+
+export function getUserConfigPath(options = {}) {
+ return path.join(options.homeDir ?? os.homedir(), ".claude", "codex-orchestration.json");
+}
+
+export function getProjectConfigPath(workspaceRoot) {
+ return path.join(workspaceRoot, ".claude", "codex-orchestration.json");
+}
+
+function isPlainObject(value) {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+}
+
+function mergeObjects(base, patch) {
+ const result = { ...base };
+ for (const [key, value] of Object.entries(patch ?? {})) {
+ result[key] = isPlainObject(value) && isPlainObject(base?.[key]) ? mergeObjects(base[key], value) : value;
+ }
+ return result;
+}
+
+function readConfig(filePath) {
+ if (!fs.existsSync(filePath)) return {};
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
+ if (!isPlainObject(parsed)) throw new Error(`${filePath} must contain a JSON object.`);
+ return parsed;
+}
+
+function integerIn(value, min, max, label) {
+ if (!Number.isInteger(value) || value < min || value > max) {
+ throw new Error(`${label} must be between ${min} and ${max}.`);
+ }
+}
+
+export function validateOrchestrationConfig(config) {
+ for (const key of Object.keys(config)) {
+ if (!new Set(["auto", "workers"]).has(key)) throw new Error(`Unknown orchestration configuration key: ${key}`);
+ }
+ if (!isPlainObject(config.auto) || typeof config.auto.enabled !== "boolean") {
+ throw new Error("auto.enabled must be a boolean.");
+ }
+ integerIn(config.auto.threshold, 0, 10, "auto.threshold");
+ if (!isPlainObject(config.workers)) throw new Error("workers must be an object.");
+ integerIn(config.workers.workspacePoolSize, 1, 8, "workers.workspacePoolSize");
+ integerIn(config.workers.globalTopLevelLimit, 1, 8, "workers.globalTopLevelLimit");
+ integerIn(config.workers.globalActiveCodexLimit, 1, 12, "workers.globalActiveCodexLimit");
+ integerIn(config.workers.idleTtlMinutes, 0, 60, "workers.idleTtlMinutes");
+ return config;
+}
+
+export function loadOrchestrationConfig(workspaceRoot, options = {}) {
+ const userPath = getUserConfigPath(options);
+ const projectPath = getProjectConfigPath(workspaceRoot);
+ const config = mergeObjects(
+ mergeObjects(DEFAULT_ORCHESTRATION_CONFIG, readConfig(userPath)),
+ readConfig(projectPath)
+ );
+ return validateOrchestrationConfig(config);
+}
+
+function writeJsonAtomic(filePath, value) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
+ fs.renameSync(tempPath, filePath);
+}
+
+export function patchUserOrchestrationConfig(patch, options = {}) {
+ const filePath = getUserConfigPath(options);
+ const existing = readConfig(filePath);
+ const mergedUser = mergeObjects(existing, patch);
+ const effective = validateOrchestrationConfig(mergeObjects(DEFAULT_ORCHESTRATION_CONFIG, mergedUser));
+ writeJsonAtomic(filePath, mergedUser);
+ return effective;
+}
diff --git a/plugins/codex/scripts/orchestration/constants.mjs b/plugins/codex/scripts/orchestration/constants.mjs
new file mode 100644
index 000000000..cfb6af684
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/constants.mjs
@@ -0,0 +1,20 @@
+
+export const ORCHESTRATION_STATE_VERSION = 1;
+export const ORCHESTRATION_PLAN_VERSION = 1;
+export const DEFAULT_WORKSPACE_POOL_SIZE = 3;
+export const MIN_WORKSPACE_POOL_SIZE = 1;
+export const MAX_WORKSPACE_POOL_SIZE = 8;
+export const DEFAULT_GLOBAL_TOP_LEVEL_LIMIT = 8;
+export const DEFAULT_GLOBAL_ACTIVE_CODEX_LIMIT = 12;
+export const DEFAULT_IDLE_TTL_MINUTES = 10;
+export const DEFAULT_AUTO_THRESHOLD = 5;
+export const DEFAULT_CANCEL_GRACE_MS = 10_000;
+export const VALID_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]);
+export const ROLE_CLASSES = new Set([
+ "planner", "architect", "explorer", "implementer", "tester", "reviewer", "verifier",
+ "migration-specialist", "security-reviewer"
+]);
+export const PACKAGE_TERMINAL_STATUSES = new Set(["completed", "partial", "blocked", "failed", "cancelled"]);
+export const ORCHESTRATION_TERMINAL_STATUSES = new Set([
+ "completed", "completed-with-omissions", "degraded", "blocked", "failed", "cancelled"
+]);
diff --git a/plugins/codex/scripts/orchestration/controller-client.mjs b/plugins/codex/scripts/orchestration/controller-client.mjs
new file mode 100644
index 000000000..f5ce353ad
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/controller-client.mjs
@@ -0,0 +1,11 @@
+
+import { requestController } from "./ipc.mjs";
+export class OrchestrationControllerClient {
+ constructor(endpoint) { this.endpoint = endpoint; }
+ start(plan, context) { return requestController(this.endpoint, "orchestration/start", { plan, context }); }
+ status(reference = "") { return requestController(this.endpoint, "orchestration/status", { reference }); }
+ result(reference = "") { return requestController(this.endpoint, "orchestration/result", { reference }); }
+ cancel(reference) { return requestController(this.endpoint, "orchestration/cancel", { reference }); }
+ controllerStatus() { return requestController(this.endpoint, "controller/status", {}); }
+ shutdown(force = false) { return requestController(this.endpoint, "controller/shutdown", { force }); }
+}
diff --git a/plugins/codex/scripts/orchestration/controller-lifecycle.mjs b/plugins/codex/scripts/orchestration/controller-lifecycle.mjs
new file mode 100644
index 000000000..c4feaef24
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/controller-lifecycle.mjs
@@ -0,0 +1,98 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { spawn } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+import { createControllerEndpoint, parseControllerEndpoint } from "./ipc.mjs";
+import { OrchestrationControllerClient } from "./controller-client.mjs";
+import { withFileLock } from "./file-lock.mjs";
+
+function key(workspaceRoot) {
+ return crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 16);
+}
+
+function runtimeDir(workspaceRoot) {
+ return path.join(os.tmpdir(), "codex-orchestration-runtime", key(workspaceRoot));
+}
+
+function alive(pid) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (error) {
+ return error?.code !== "ESRCH";
+ }
+}
+
+function read(file) {
+ try {
+ return JSON.parse(fs.readFileSync(file, "utf8"));
+ } catch {
+ return null;
+ }
+}
+
+async function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export async function ensureControllerServer(workspaceRoot, options = {}) {
+ const dir = runtimeDir(workspaceRoot);
+ fs.mkdirSync(dir, { recursive: true });
+ const runtimeFile = path.join(dir, "controller.json");
+ const lockFile = path.join(dir, "controller.lock");
+
+ return withFileLock(lockFile, {}, async () => {
+ const existing = read(runtimeFile);
+ if (existing && alive(existing.pid)) {
+ try {
+ await new OrchestrationControllerClient(existing.endpoint).controllerStatus();
+ return existing;
+ } catch {}
+ }
+
+ fs.rmSync(runtimeFile, { force: true });
+ if (existing?.endpoint?.startsWith("unix:")) {
+ fs.rmSync(parseControllerEndpoint(existing.endpoint).path, { force: true });
+ }
+
+ const endpoint = createControllerEndpoint(key(workspaceRoot));
+ const scriptPath = fileURLToPath(new URL("./controller-server.mjs", import.meta.url));
+ const child = spawn(
+ process.execPath,
+ [
+ scriptPath,
+ "serve",
+ "--workspace",
+ workspaceRoot,
+ "--endpoint",
+ endpoint,
+ "--runtime-file",
+ runtimeFile
+ ],
+ {
+ cwd: workspaceRoot,
+ env: options.env ?? process.env,
+ detached: true,
+ stdio: "ignore",
+ windowsHide: true
+ }
+ );
+ child.unref();
+
+ const deadline = Date.now() + 10_000;
+ while (Date.now() < deadline) {
+ const state = read(runtimeFile);
+ if (state) {
+ try {
+ await new OrchestrationControllerClient(endpoint).controllerStatus();
+ return state;
+ } catch {}
+ }
+ await sleep(100);
+ }
+ throw new Error("Timed out starting the Multi-Codex orchestration controller.");
+ });
+}
diff --git a/plugins/codex/scripts/orchestration/controller-server.mjs b/plugins/codex/scripts/orchestration/controller-server.mjs
new file mode 100644
index 000000000..2789af4dc
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/controller-server.mjs
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+import crypto from "node:crypto";
+import fs from "node:fs";
+import net from "node:net";
+import path from "node:path";
+
+import { loadOrchestrationConfig } from "./config.mjs";
+import { OrchestrationController } from "./controller.mjs";
+import { parseControllerEndpoint } from "./ipc.mjs";
+import { WorkerPool } from "./worker-pool.mjs";
+import { reconcilePhase1ControllerLoss } from "./recovery.mjs";
+
+function args(argv) { const result = {}; for (let i = 0; i < argv.length; i += 2) result[argv[i].replace(/^--/, "")] = argv[i + 1]; return result; }
+function send(socket, message) { socket.write(`${JSON.stringify(message)}\n`); }
+async function main() {
+ if (process.argv[2] !== "serve") throw new Error("controller-server.mjs serve --workspace --endpoint --runtime-file ");
+ const options = args(process.argv.slice(3)); const workspaceRoot = path.resolve(options.workspace); const endpoint = options.endpoint; const runtimeFile = options["runtime-file"];
+ const identity = { instanceId: `controller-${process.pid}-${crypto.randomUUID()}`, pid: process.pid, endpoint, workspaceRoot, startedAt: new Date().toISOString() };
+ fs.mkdirSync(path.dirname(runtimeFile), { recursive: true }); fs.writeFileSync(runtimeFile, `${JSON.stringify(identity, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
+ const config = loadOrchestrationConfig(workspaceRoot);
+ const pool = new WorkerPool({ workspaceRoot, size: config.workers.workspacePoolSize, globalTopLevelLimit: config.workers.globalTopLevelLimit, globalActiveCodexLimit: config.workers.globalActiveCodexLimit, onEvent: () => {} });
+ const controller = new OrchestrationController({ workspaceRoot, config, pool, controllerIdentity: identity });
+ await reconcilePhase1ControllerLoss(workspaceRoot, identity);
+ const target = parseControllerEndpoint(endpoint); if (target.kind === "unix") { fs.mkdirSync(path.dirname(target.path), { recursive: true }); fs.rmSync(target.path, { force: true }); }
+ const server = net.createServer((socket) => {
+ socket.setEncoding("utf8"); let buffer = "";
+ socket.on("data", async (chunk) => {
+ buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return;
+ const line = buffer.slice(0, index); buffer = buffer.slice(index + 1); let message;
+ try {
+ message = JSON.parse(line); let result;
+ switch (message.method) {
+ case "orchestration/start": result = await controller.start(message.params.plan, message.params.context); break;
+ case "orchestration/status": result = controller.status(message.params.reference); break;
+ case "orchestration/result": result = controller.result(message.params.reference); break;
+ case "orchestration/cancel": result = await controller.cancel(message.params.reference); break;
+ case "controller/status": result = { ...identity, pool: pool.getSnapshot(), activeOrchestrationIds: [...controller.activeRuns.keys()] }; break;
+ case "controller/shutdown": if (controller.activeRuns.size && !message.params.force) throw new Error("Controller has active orchestrations."); await controller.shutdown(); result = {}; send(socket, { id: message.id, result }); server.close(() => process.exit(0)); return;
+ default: throw Object.assign(new Error(`Unknown method: ${message.method}`), { rpcCode: -32601 });
+ }
+ send(socket, { id: message.id, result });
+ } catch (error) { send(socket, { id: message?.id ?? null, error: { code: error.rpcCode ?? -32000, message: error.message } }); }
+ });
+ });
+ const cleanup = async () => { await controller.shutdown().catch(() => {}); fs.rmSync(runtimeFile, { force: true }); if (target.kind === "unix") fs.rmSync(target.path, { force: true }); };
+ process.on("SIGTERM", async () => { await cleanup(); process.exit(0); }); process.on("SIGINT", async () => { await cleanup(); process.exit(0); });
+ server.listen(target.path);
+}
+main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exit(1); });
diff --git a/plugins/codex/scripts/orchestration/controller.mjs b/plugins/codex/scripts/orchestration/controller.mjs
new file mode 100644
index 000000000..8749f69ac
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/controller.mjs
@@ -0,0 +1,410 @@
+import {
+ createSchedulerState,
+ deriveOrchestrationStatus,
+ getReadyPackageIds,
+ markPackageCancelled,
+ markPackageCompleted,
+ markPackageFailed,
+ markPackageReady,
+ markPackageRunning,
+ propagateBlockedPackages
+} from "./scheduler.mjs";
+import { normalizeOrchestrationPlan } from "./plan-contract.mjs";
+import { buildOrchestrationResult } from "./result-contract.mjs";
+import {
+ appendOrchestrationEvent,
+ createOrchestrationState,
+ isTerminalState,
+ listOrchestrations,
+ loadOrchestrationState,
+ resolveOrchestrationReference,
+ updateOrchestrationState,
+ writePackageResult
+} from "./state-store.mjs";
+import { isTransientWorkerError } from "./worker-pool.mjs";
+
+function now() {
+ return new Date().toISOString();
+}
+
+function schedulerFromState(state) {
+ const scheduler = createSchedulerState(state.plan);
+ for (const [id, pkg] of Object.entries(state.packages)) {
+ scheduler.packages[id] = {
+ ...scheduler.packages[id],
+ status: pkg.status,
+ attempt: pkg.attempt,
+ error: pkg.error
+ };
+ }
+ return scheduler;
+}
+
+function applyScheduler(state, scheduler) {
+ for (const [id, pkg] of Object.entries(scheduler.packages)) {
+ state.packages[id] = {
+ ...state.packages[id],
+ status: pkg.status,
+ attempt: pkg.attempt,
+ error: pkg.error
+ };
+ }
+ return state;
+}
+
+export class OrchestrationController {
+ constructor(options) {
+ this.workspaceRoot = options.workspaceRoot;
+ this.config = options.config;
+ this.pool = options.pool;
+ this.controllerIdentity = options.controllerIdentity ?? null;
+ this.activeRuns = new Map();
+ this.runningPackages = new Map();
+ this.onMilestone = options.onMilestone ?? (() => {});
+ }
+
+ async start(planInput, context = {}) {
+ const plan = normalizeOrchestrationPlan(planInput, {
+ workspaceRoot: this.workspaceRoot,
+ config: this.config
+ });
+ const state = await createOrchestrationState(this.workspaceRoot, plan, {
+ ...context,
+ controller: this.controllerIdentity
+ });
+ const promise = this.runOrchestration(state.id)
+ .catch((error) => this.failControllerRun(state.id, error))
+ .finally(() => this.activeRuns.delete(state.id));
+ this.activeRuns.set(state.id, promise);
+ return {
+ orchestrationId: state.id,
+ status: "queued",
+ objective: plan.objective,
+ packageCount: plan.packages.length
+ };
+ }
+
+ async mutate(id, callback) {
+ return updateOrchestrationState(this.workspaceRoot, id, callback);
+ }
+
+ async runOrchestration(id) {
+ await this.mutate(id, (state) => {
+ state.status = "running";
+ state.startedAt = state.startedAt ?? now();
+ return state;
+ });
+
+ const budgetState = loadOrchestrationState(this.workspaceRoot, id);
+ const deadline = Date.now() + budgetState.plan.budget.timeoutMinutes * 60_000;
+
+ while (true) {
+ let state = loadOrchestrationState(this.workspaceRoot, id);
+ if (Date.now() >= deadline) {
+ await this.cancel(id, { reason: "Orchestration time budget exceeded." });
+ return;
+ }
+ if (isTerminalState(state) || state.status === "cancelling") break;
+
+ let scheduler = propagateBlockedPackages(schedulerFromState(state));
+ for (const packageId of getReadyPackageIds(scheduler)) {
+ scheduler = markPackageReady(scheduler, packageId);
+ }
+ state = await this.mutate(id, (current) => applyScheduler(current, scheduler));
+
+ const activeKeys = [...this.runningPackages.keys()].filter((key) => key.startsWith(`${id}:`));
+ const capacity = Math.max(0, state.plan.budget.workerParallelism - activeKeys.length);
+ const ready = Object.entries(state.packages)
+ .filter(
+ ([packageId, pkg]) =>
+ pkg.status === "ready" && !this.runningPackages.has(`${id}:${packageId}`)
+ )
+ .slice(0, capacity)
+ .map(([packageId]) => packageId);
+
+ for (const packageId of ready) this.launchPackage(id, packageId);
+
+ state = loadOrchestrationState(this.workspaceRoot, id);
+ if (
+ Object.values(state.packages).every((pkg) =>
+ ["completed", "partial", "blocked", "failed", "cancelled"].includes(pkg.status)
+ )
+ ) {
+ await this.finalize(id);
+ break;
+ }
+
+ const running = [...this.runningPackages.entries()]
+ .filter(([key]) => key.startsWith(`${id}:`))
+ .map(([, promise]) => promise);
+ if (running.length === 0) {
+ await this.finalize(id);
+ break;
+ }
+
+ await Promise.race([
+ Promise.race(running),
+ new Promise((resolve) =>
+ setTimeout(resolve, Math.min(1000, Math.max(10, deadline - Date.now())))
+ )
+ ]);
+ }
+ }
+
+ launchPackage(id, packageId) {
+ const key = `${id}:${packageId}`;
+ if (this.runningPackages.has(key)) return;
+ const promise = this.executePackage(id, packageId)
+ .finally(() => this.runningPackages.delete(key));
+ this.runningPackages.set(key, promise);
+ }
+
+ async executePackage(id, packageId) {
+ const state = loadOrchestrationState(this.workspaceRoot, id);
+ const spec = state.plan.packages.find((pkg) => pkg.id === packageId);
+ const attempt = (state.packages[packageId].attempt ?? 0) + 1;
+
+ await this.mutate(id, (current) => {
+ current.packages[packageId].attempt = attempt;
+ return current;
+ });
+
+ const dependencyResults = spec.dependencies
+ .map((dependencyId) => ({
+ packageId: dependencyId,
+ result: loadOrchestrationState(this.workspaceRoot, id).packages[dependencyId].result
+ }))
+ .filter((entry) => entry.result);
+
+ try {
+ const execution = await this.pool.execute(id, spec, dependencyResults, {
+ timeoutMinutes: state.plan.budget.timeoutMinutes,
+ onStarted: async ({ workerId, pid }) => {
+ await this.mutate(id, (current) => {
+ if (
+ current.status === "cancelling"
+ || current.packages[packageId].status === "cancelled"
+ || current.packages[packageId].status === "cancelling"
+ ) {
+ throw Object.assign(
+ new Error(`Package ${packageId} was cancelled before worker activation.`),
+ { code: "PACKAGE_CANCELLED" }
+ );
+ }
+
+ let scheduler = schedulerFromState(current);
+ scheduler = markPackageRunning(scheduler, packageId, attempt);
+ applyScheduler(current, scheduler);
+ Object.assign(current.packages[packageId], {
+ workerId,
+ pid,
+ startedAt: current.packages[packageId].startedAt ?? now()
+ });
+ return current;
+ });
+
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ packageId,
+ type: "package-started",
+ phase: "running",
+ message: spec.title,
+ data: { attempt }
+ });
+ }
+ });
+
+ const result = execution.packageResult;
+ writePackageResult(this.workspaceRoot, id, packageId, result);
+ await this.mutate(id, (current) => {
+ let scheduler = schedulerFromState(current);
+ scheduler = markPackageCompleted(scheduler, packageId, result.status);
+ applyScheduler(current, scheduler);
+ Object.assign(current.packages[packageId], {
+ result,
+ workerId: execution.workerId,
+ pid: null,
+ threadId: execution.threadId ?? null,
+ turnId: execution.turnId ?? null,
+ nativeChildThreadIds: execution.nativeChildThreadIds ?? [],
+ completedAt: now(),
+ nativeSubagentDegraded:
+ spec.nativeSubagents.policy === "required" && (execution.nativeChildPeak ?? 0) === 0,
+ nativeSubagentDegradationReason:
+ spec.nativeSubagents.policy === "required" && (execution.nativeChildPeak ?? 0) === 0
+ ? "No native child was observed; accepted Root-only execution."
+ : null
+ });
+ return current;
+ });
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ packageId,
+ type: "package-completed",
+ phase: result.status,
+ message: result.summary
+ });
+ } catch (error) {
+ const latest = loadOrchestrationState(this.workspaceRoot, id);
+ if (
+ latest.status === "cancelling"
+ || latest.packages[packageId].status === "cancelled"
+ || latest.packages[packageId].status === "cancelling"
+ || error?.code === "PACKAGE_CANCELLED"
+ ) {
+ return;
+ }
+
+ if (isTransientWorkerError(error) && attempt <= state.plan.budget.maxRetries) {
+ await this.mutate(id, (current) => {
+ current.packages[packageId].status = "ready";
+ current.packages[packageId].error = error.message;
+ current.packages[packageId].pid = null;
+ return current;
+ });
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ packageId,
+ type: "package-retry",
+ phase: "queued",
+ message: error.message,
+ data: { nextAttempt: attempt + 1 }
+ });
+ return;
+ }
+
+ await this.mutate(id, (current) => {
+ let scheduler = schedulerFromState(current);
+ scheduler = markPackageFailed(scheduler, packageId, error.message);
+ scheduler = propagateBlockedPackages(scheduler);
+ applyScheduler(current, scheduler);
+ current.packages[packageId].pid = null;
+ current.packages[packageId].completedAt = now();
+ return current;
+ });
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ packageId,
+ type: "package-failed",
+ phase: "failed",
+ message: error.message,
+ data: { code: error.code ?? null }
+ });
+ }
+ }
+
+ async finalize(id) {
+ const state = await this.mutate(id, (current) => {
+ const scheduler = propagateBlockedPackages(schedulerFromState(current));
+ applyScheduler(current, scheduler);
+ current.status = current.status === "cancelling"
+ ? "cancelled"
+ : deriveOrchestrationStatus(scheduler);
+ current.completedAt = now();
+ return current;
+ });
+ writePackageResult(this.workspaceRoot, id, "_orchestration", buildOrchestrationResult(state));
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ type: "orchestration-completed",
+ phase: state.status,
+ message: state.status
+ });
+ this.onMilestone({
+ orchestrationId: id,
+ type: "orchestration-completed",
+ status: state.status
+ });
+ }
+
+ async failControllerRun(id, error) {
+ await this.mutate(id, (state) => {
+ state.status = "failed";
+ state.completedAt = now();
+ state.error = error.message;
+ return state;
+ });
+ appendOrchestrationEvent(this.workspaceRoot, id, {
+ type: "controller-failed",
+ phase: "failed",
+ message: error.message
+ });
+ }
+
+ status(reference = "") {
+ if (!reference) {
+ return {
+ workspaceRoot: this.workspaceRoot,
+ orchestrations: listOrchestrations(this.workspaceRoot),
+ pool: this.pool.getSnapshot()
+ };
+ }
+ const resolved = resolveOrchestrationReference(this.workspaceRoot, reference);
+ const state = loadOrchestrationState(this.workspaceRoot, resolved.orchestrationId);
+ return resolved.kind === "package"
+ ? {
+ orchestrationId: state.id,
+ package: state.packages[resolved.packageId],
+ packageSpec: state.plan.packages.find((pkg) => pkg.id === resolved.packageId)
+ }
+ : { ...state, pool: this.pool.getSnapshot() };
+ }
+
+ result(reference = "") {
+ const resolved = resolveOrchestrationReference(this.workspaceRoot, reference);
+ const state = loadOrchestrationState(this.workspaceRoot, resolved.orchestrationId);
+ if (resolved.kind === "package") return state.packages[resolved.packageId].result;
+ if (!isTerminalState(state)) {
+ throw new Error(`Orchestration ${state.id} is still running. Use /codex:status ${state.id}.`);
+ }
+ return buildOrchestrationResult(state);
+ }
+
+ async cancel(reference, options = {}) {
+ const resolved = resolveOrchestrationReference(this.workspaceRoot, reference);
+ const state = loadOrchestrationState(this.workspaceRoot, resolved.orchestrationId);
+ if (isTerminalState(state)) return state;
+
+ if (resolved.kind === "package") {
+ await this.mutate(state.id, (current) => {
+ const pkg = current.packages[resolved.packageId];
+ if (!["completed", "partial", "blocked", "failed", "cancelled"].includes(pkg.status)) {
+ pkg.status = "cancelling";
+ }
+ return current;
+ });
+ await this.pool.cancel(resolved.packageId, { graceMs: options.graceMs });
+ return this.mutate(state.id, (current) => {
+ let scheduler = schedulerFromState(current);
+ scheduler = markPackageCancelled(scheduler, resolved.packageId);
+ scheduler = propagateBlockedPackages(scheduler);
+ applyScheduler(current, scheduler);
+ return current;
+ });
+ }
+
+ await this.mutate(state.id, (current) => {
+ current.status = "cancelling";
+ for (const pkg of Object.values(current.packages)) {
+ if (pkg.status === "running") pkg.status = "cancelling";
+ }
+ return current;
+ });
+
+ const activePackageIds = this.pool.getSnapshot().active.map((entry) => entry.packageId);
+ for (const packageId of activePackageIds) {
+ await this.pool.cancel(packageId, { graceMs: options.graceMs });
+ }
+
+ await this.mutate(state.id, (current) => {
+ let scheduler = schedulerFromState(current);
+ for (const packageId of Object.keys(current.packages)) {
+ scheduler = markPackageCancelled(scheduler, packageId);
+ }
+ applyScheduler(current, scheduler);
+ return current;
+ });
+
+ await this.finalize(state.id);
+ return loadOrchestrationState(this.workspaceRoot, state.id);
+ }
+
+ async shutdown() {
+ await this.pool.close();
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/dispatch.mjs b/plugins/codex/scripts/orchestration/dispatch.mjs
new file mode 100644
index 000000000..4c0b5504f
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/dispatch.mjs
@@ -0,0 +1,18 @@
+#!/usr/bin/env node
+import { spawnSync } from "node:child_process";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { resolveWorkspaceRoot } from "../lib/workspace.mjs";
+import { listOrchestrations, resolveOrchestrationReference } from "./state-store.mjs";
+
+const ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
+function run(script, args) { const result = spawnSync(process.execPath, [script, ...args], { cwd: process.cwd(), env: process.env, encoding: "utf8" }); process.stdout.write(result.stdout ?? ""); process.stderr.write(result.stderr ?? ""); process.exitCode = result.status ?? 1; }
+function isOrchestrationRef(reference) { if (!reference) return false; try { resolveOrchestrationReference(resolveWorkspaceRoot(process.cwd()), reference); return true; } catch { return false; } }
+const [command, ...args] = process.argv.slice(2); const reference = args.find((arg) => !arg.startsWith("--")) ?? "";
+const orchestrationCli = path.join(ROOT, "orchestration", "cli.mjs"); const companion = path.join(ROOT, "codex-companion.mjs");
+if (command === "status" && !reference) {
+ run(companion, ["status", ...args]);
+ if (listOrchestrations(resolveWorkspaceRoot(process.cwd())).length) run(orchestrationCli, ["status", ...args]);
+} else if (isOrchestrationRef(reference)) run(orchestrationCli, [command, ...args]);
+else run(companion, [command, ...args]);
diff --git a/plugins/codex/scripts/orchestration/file-lock.mjs b/plugins/codex/scripts/orchestration/file-lock.mjs
new file mode 100644
index 000000000..22922f793
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/file-lock.mjs
@@ -0,0 +1,40 @@
+
+import fs from "node:fs";
+import path from "node:path";
+
+function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
+function processAlive(pid) {
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; }
+}
+function removeStale(lockFile, staleMs) {
+ try {
+ const stat = fs.statSync(lockFile);
+ const pid = Number.parseInt(fs.readFileSync(lockFile, "utf8").split(":", 1)[0], 10);
+ if (Number.isFinite(pid) && processAlive(pid)) return false;
+ if (!Number.isFinite(pid) && Date.now() - stat.mtimeMs <= staleMs) return false;
+ fs.unlinkSync(lockFile); return true;
+ } catch (error) { if (error?.code === "ENOENT") return true; throw error; }
+}
+export async function withFileLock(lockFile, options = {}, action) {
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
+ const deadline = Date.now() + (options.timeoutMs ?? 5000);
+ const staleMs = options.staleMs ?? 30_000;
+ const token = `${process.pid}:${Date.now()}:${Math.random()}`;
+ let fd = null;
+ while (fd === null) {
+ try { fd = fs.openSync(lockFile, "wx", 0o600); fs.writeFileSync(fd, token, "utf8"); }
+ catch (error) {
+ if (error?.code !== "EEXIST") throw error;
+ if (removeStale(lockFile, staleMs)) continue;
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for lock at ${lockFile}.`);
+ await sleep(options.retryMs ?? 25);
+ }
+ }
+ try { return await action(); }
+ finally {
+ try { fs.closeSync(fd); } finally {
+ try { if (fs.readFileSync(lockFile, "utf8") === token) fs.unlinkSync(lockFile); }
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
+ }
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/global-worker-registry.mjs b/plugins/codex/scripts/orchestration/global-worker-registry.mjs
new file mode 100644
index 000000000..1ff673edc
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/global-worker-registry.mjs
@@ -0,0 +1,60 @@
+
+import crypto from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+import { withFileLock } from "./file-lock.mjs";
+
+function root(options = {}) {
+ const pluginData = options.pluginDataDir ?? process.env.CLAUDE_PLUGIN_DATA;
+ return pluginData ? path.join(path.resolve(pluginData), "orchestrations", "_global") : path.join(os.tmpdir(), "codex-companion", "orchestrations", "_global");
+}
+function registryFile(options) { return path.join(root(options), "workers.json"); }
+function lockFile(options) { return path.join(root(options), "workers.lock"); }
+function alive(pid) { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } }
+function read(options) {
+ const file = registryFile(options);
+ if (!fs.existsSync(file)) return { leases: [] };
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return { leases: [] }; }
+}
+function write(options, value) {
+ const file = registryFile(options); fs.mkdirSync(path.dirname(file), { recursive: true });
+ const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); fs.renameSync(temp, file);
+}
+function prune(registry) { return { leases: (registry.leases ?? []).filter((lease) => alive(lease.pid)) }; }
+export async function acquireGlobalWorkerLease(options) {
+ return withFileLock(lockFile(options), {}, async () => {
+ const registry = prune(read(options));
+ const topLevelLimit = options.globalTopLevelLimit ?? 8;
+ const activeLimit = options.globalActiveCodexLimit ?? 12;
+ const activeCount = registry.leases.reduce((sum, lease) => sum + 1 + (lease.activeNativeChildren ?? 0), 0);
+ if (registry.leases.length >= topLevelLimit) throw Object.assign(new Error(`Global Codex worker limit ${topLevelLimit} reached.`), { code: "GLOBAL_WORKER_LIMIT" });
+ if (activeCount + 1 > activeLimit) throw Object.assign(new Error(`Global active Codex limit ${activeLimit} reached.`), { code: "ACTIVE_CODEX_LIMIT_EXCEEDED" });
+ const lease = {
+ id: `worker-${process.pid}-${crypto.randomUUID()}`, pid: process.pid, workspaceKey: options.workspaceKey,
+ workerId: options.workerId, packageId: options.packageId, acquiredAt: new Date().toISOString(),
+ heartbeatAt: new Date().toISOString(), activeNativeChildren: 0
+ };
+ registry.leases.push(lease); write(options, registry); return lease;
+ });
+}
+export async function updateGlobalWorkerLease(leaseId, patch, options = {}) {
+ return withFileLock(lockFile(options), {}, async () => {
+ const registry = prune(read(options));
+ const index = registry.leases.findIndex((lease) => lease.id === leaseId);
+ if (index === -1) return null;
+ const next = { ...registry.leases[index], ...patch, heartbeatAt: new Date().toISOString() };
+ const projected = registry.leases.reduce((sum, lease, current) => sum + 1 + (current === index ? next.activeNativeChildren ?? 0 : lease.activeNativeChildren ?? 0), 0);
+ if (projected > (options.globalActiveCodexLimit ?? 12)) throw Object.assign(new Error(`Global active Codex limit ${options.globalActiveCodexLimit ?? 12} exceeded.`), { code: "ACTIVE_CODEX_LIMIT_EXCEEDED" });
+ registry.leases[index] = next; write(options, registry); return next;
+ });
+}
+export async function releaseGlobalWorkerLease(leaseId, options = {}) {
+ return withFileLock(lockFile(options), {}, async () => {
+ const registry = prune(read(options)); registry.leases = registry.leases.filter((lease) => lease.id !== leaseId); write(options, registry);
+ });
+}
+export async function readGlobalWorkerRegistry(options = {}) { return prune(read(options)); }
+export async function getGlobalActiveCodexCount(options = {}) { return (await readGlobalWorkerRegistry(options)).leases.reduce((sum, lease) => sum + 1 + (lease.activeNativeChildren ?? 0), 0); }
diff --git a/plugins/codex/scripts/orchestration/ipc.mjs b/plugins/codex/scripts/orchestration/ipc.mjs
new file mode 100644
index 000000000..54cd55fd1
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/ipc.mjs
@@ -0,0 +1,25 @@
+
+import net from "node:net";
+import path from "node:path";
+import os from "node:os";
+
+export function createControllerEndpoint(workspaceKey, platform = process.platform) {
+ if (platform === "win32") return `pipe:\\\\.\\pipe\\${workspaceKey}-codex-orchestrator`;
+ return `unix:${path.join(os.tmpdir(), "codex-orchestration-runtime", workspaceKey, "controller.sock")}`;
+}
+export function parseControllerEndpoint(endpoint) {
+ if (endpoint.startsWith("unix:")) return { kind: "unix", path: endpoint.slice(5) };
+ if (endpoint.startsWith("pipe:")) return { kind: "pipe", path: endpoint.slice(5) };
+ throw new Error(`Unsupported controller endpoint: ${endpoint}`);
+}
+export function requestController(endpoint, method, params = {}, options = {}) {
+ const target = parseControllerEndpoint(endpoint);
+ return new Promise((resolve, reject) => {
+ const socket = net.createConnection({ path: target.path }); let buffer = "";
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error(`Controller request timed out: ${method}`)); }, options.timeoutMs ?? 10_000);
+ socket.setEncoding("utf8");
+ socket.on("connect", () => socket.write(`${JSON.stringify({ id: 1, method, params })}\n`));
+ socket.on("data", (chunk) => { buffer += chunk; const index = buffer.indexOf("\n"); if (index < 0) return; clearTimeout(timer); socket.end(); const message = JSON.parse(buffer.slice(0, index)); if (message.error) reject(Object.assign(new Error(message.error.message), { rpcCode: message.error.code })); else resolve(message.result); });
+ socket.on("error", (error) => { clearTimeout(timer); reject(error); });
+ });
+}
diff --git a/plugins/codex/scripts/orchestration/package-prompt.mjs b/plugins/codex/scripts/orchestration/package-prompt.mjs
new file mode 100644
index 000000000..1243d56a7
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/package-prompt.mjs
@@ -0,0 +1,15 @@
+
+function xml(value) { return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); }
+export function buildReadOnlyPackagePrompt(packageSpec, context = {}) {
+ return [
+ `${xml(packageSpec.id)}`,
+ `${xml(packageSpec.role.class)}: ${xml(packageSpec.role.label)}`,
+ `${xml(packageSpec.objective)}`,
+ `Read-only. Do not modify files, create commits, change credentials, push, publish, deploy, or mutate remote systems.`,
+ `${xml(JSON.stringify(context.dependencyResults ?? []))}`,
+ `${xml(JSON.stringify(packageSpec.acceptanceCriteria))}`,
+ `${xml(`${packageSpec.nativeSubagents.policy}; maximum ${packageSpec.nativeSubagents.maxChildren} child agents`)}`,
+ `Run only non-destructive checks needed to support claims. Record exact commands and exit codes.`,
+ `Return exactly one JSON object matching the supplied package-result schema. changedFiles must be an empty array.`
+ ].join("\n\n");
+}
diff --git a/plugins/codex/scripts/orchestration/package-worker.mjs b/plugins/codex/scripts/orchestration/package-worker.mjs
new file mode 100644
index 000000000..252187b53
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/package-worker.mjs
@@ -0,0 +1,176 @@
+#!/usr/bin/env node
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import crypto from "node:crypto";
+
+import { CodexAppServerClient } from "../lib/app-server.mjs";
+import { runAppServerTurnWithClient } from "../lib/codex.mjs";
+import { parseStructuredOutput } from "../lib/structured-output.mjs";
+import { buildReadOnlyPackagePrompt } from "./package-prompt.mjs";
+import { readPackageResultSchema, validatePackageResult } from "./result-contract.mjs";
+
+function send(value) {
+ process.stdout.write(`${JSON.stringify(value)}\n`);
+}
+
+function fail(error) {
+ send({
+ type: "error",
+ error: {
+ message: error instanceof Error ? error.message : String(error),
+ code: error?.code ?? null,
+ transient: Boolean(error?.transient)
+ }
+ });
+ process.stdin.removeAllListeners("data");
+ process.stdin.destroy();
+ process.exitCode = 1;
+}
+
+async function main() {
+ const specFile = process.argv[2];
+ if (!specFile) throw new Error("Usage: package-worker.mjs ");
+
+ const request = JSON.parse(fs.readFileSync(specFile, "utf8"));
+ const invalidEndpoint = process.platform === "win32"
+ ? `pipe:\\\\.\\pipe\\codex-orchestration-direct-${process.pid}-${crypto.randomUUID()}`
+ : `unix:${path.join(os.tmpdir(), `codex-orchestration-direct-${process.pid}-${crypto.randomUUID()}.sock`)}`;
+ process.env.CODEX_COMPANION_APP_SERVER_ENDPOINT = invalidEndpoint;
+
+ let threadId = null;
+ let turnId = null;
+ let nativeChildren = 0;
+ let nativeChildPeak = 0;
+ let boundaryError = null;
+ const childThreadIds = new Set();
+ const client = await CodexAppServerClient.connect(request.workspaceRoot, {
+ disableBroker: true,
+ env: process.env
+ });
+
+ let interruptRequested = false;
+ let interruptSent = false;
+ const sendInterruptIfReady = () => {
+ if (!interruptRequested || interruptSent || !threadId || !turnId) return;
+ interruptSent = true;
+ client.request("turn/interrupt", { threadId, turnId }).catch(() => {
+ interruptSent = false;
+ });
+ };
+
+ process.stdin.setEncoding("utf8");
+ let controlBuffer = "";
+ process.stdin.on("data", (chunk) => {
+ controlBuffer += chunk;
+ let index = controlBuffer.indexOf("\n");
+ while (index !== -1) {
+ const line = controlBuffer.slice(0, index);
+ controlBuffer = controlBuffer.slice(index + 1);
+ index = controlBuffer.indexOf("\n");
+ try {
+ const message = JSON.parse(line);
+ if (message.type === "interrupt") {
+ interruptRequested = true;
+ sendInterruptIfReady();
+ }
+ } catch {}
+ }
+ });
+
+ const result = await runAppServerTurnWithClient(client, request.workspaceRoot, {
+ prompt: buildReadOnlyPackagePrompt(request.packageSpec, {
+ dependencyResults: request.dependencyResults
+ }),
+ model: request.packageSpec.model.name,
+ effort: request.packageSpec.model.effort,
+ sandbox: "read-only",
+ outputSchema: readPackageResultSchema(),
+ onProgress(event) {
+ const normalized = typeof event === "string" ? { message: event, phase: null } : event;
+ threadId = normalized.threadId ?? threadId;
+ turnId = normalized.turnId ?? turnId;
+ sendInterruptIfReady();
+
+ const message = String(normalized.message ?? "");
+ if (/Starting subagent|Native child started/i.test(message)) {
+ nativeChildren += 1;
+ if (normalized.threadId && normalized.threadId !== threadId) {
+ childThreadIds.add(normalized.threadId);
+ }
+ }
+ if (/Subagent .* completed|Native child completed/i.test(message)) {
+ nativeChildren = Math.max(0, nativeChildren - 1);
+ }
+ nativeChildPeak = Math.max(nativeChildPeak, nativeChildren);
+
+ if (request.packageSpec.nativeSubagents.policy === "forbidden" && nativeChildren > 0) {
+ boundaryError = Object.assign(
+ new Error("Native children are forbidden for this package."),
+ { code: "NATIVE_CHILD_POLICY_VIOLATION" }
+ );
+ }
+ if (nativeChildren > request.packageSpec.nativeSubagents.maxChildren) {
+ boundaryError = Object.assign(
+ new Error(`Native child limit ${request.packageSpec.nativeSubagents.maxChildren} exceeded.`),
+ { code: "NATIVE_CHILD_LIMIT_EXCEEDED" }
+ );
+ }
+
+ send({
+ type: "progress",
+ event: {
+ ...normalized,
+ threadId,
+ turnId,
+ activeNativeChildren: nativeChildren,
+ nativeChildPeak
+ }
+ });
+ }
+ });
+
+ await client.close().catch(() => {});
+ if (boundaryError) throw boundaryError;
+ if (result.status !== 0) {
+ const resultErrorMessage = result.error instanceof Error ? result.error.message : null;
+ throw Object.assign(
+ new Error(resultErrorMessage ?? result.stderr ?? "Codex package failed."),
+ { code: "CODEX_PACKAGE_FAILED" }
+ );
+ }
+ if ((result.touchedFiles ?? []).length > 0 || (result.fileChanges ?? []).length > 0) {
+ throw Object.assign(
+ new Error("Phase 1 read-only boundary violation: Codex reported file changes."),
+ { code: "READ_ONLY_BOUNDARY_VIOLATION" }
+ );
+ }
+
+ const parsed = parseStructuredOutput(result.finalMessage, {
+ status: result.status,
+ failureMessage: result.stderr
+ });
+ if (!parsed.parsed) {
+ throw Object.assign(
+ new Error(`Codex package returned invalid JSON: ${parsed.parseError}`),
+ { code: "PACKAGE_RESULT_PARSE_ERROR" }
+ );
+ }
+
+ const packageResult = validatePackageResult(parsed.parsed, request.packageSpec.id);
+ send({
+ type: "result",
+ payload: {
+ packageResult,
+ threadId: result.threadId ?? threadId,
+ turnId: result.turnId ?? turnId,
+ nativeChildPeak,
+ nativeChildThreadIds: result.nativeChildThreadIds ?? [...childThreadIds]
+ }
+ });
+
+ process.stdin.removeAllListeners("data");
+ process.stdin.destroy();
+}
+
+main().catch(fail);
diff --git a/plugins/codex/scripts/orchestration/plan-contract.mjs b/plugins/codex/scripts/orchestration/plan-contract.mjs
new file mode 100644
index 000000000..dabdfb4d7
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/plan-contract.mjs
@@ -0,0 +1,113 @@
+
+import {
+ ORCHESTRATION_PLAN_VERSION,
+ ROLE_CLASSES,
+ VALID_EFFORTS
+} from "./constants.mjs";
+import { deriveBudgetEnvelope, validatePlanAgainstBudget } from "./budget-policy.mjs";
+
+function object(value, label) {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
+ return value;
+}
+function text(value, label) {
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string.`);
+ return value.trim();
+}
+function stringArray(value, label) {
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
+ return value.map((entry, index) => text(entry, `${label}[${index}]`));
+}
+function deepFreeze(value) {
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
+ Object.freeze(value);
+ for (const child of Object.values(value)) deepFreeze(child);
+ }
+ return value;
+}
+
+function normalizePackage(input, index) {
+ const pkg = object(input, `packages[${index}]`);
+ const id = text(pkg.id, `packages[${index}].id`);
+ const role = object(pkg.role, `${id}.role`);
+ const roleClass = text(role.class, `${id}.role.class`);
+ if (!ROLE_CLASSES.has(roleClass)) throw new Error(`${id}.role.class is not supported: ${roleClass}`);
+ if (pkg.access !== "read-only") throw new Error("Phase 1 only supports read-only packages.");
+ if (object(pkg.workspace, `${id}.workspace`).mode !== "shared") throw new Error(`${id}.workspace.mode must be shared in Phase 1.`);
+ const model = object(pkg.model, `${id}.model`);
+ const effort = text(model.effort, `${id}.model.effort`).toLowerCase();
+ if (!VALID_EFFORTS.has(effort)) throw new Error(`${id}.model.effort is not supported: ${effort}`);
+ const nativeSubagents = object(pkg.nativeSubagents, `${id}.nativeSubagents`);
+ const policy = text(nativeSubagents.policy, `${id}.nativeSubagents.policy`);
+ if (!new Set(["allowed", "forbidden", "required"]).has(policy)) throw new Error(`${id}.nativeSubagents.policy is invalid.`);
+ if (!Number.isInteger(nativeSubagents.maxChildren) || nativeSubagents.maxChildren < 0) {
+ throw new Error(`${id}.nativeSubagents.maxChildren must be a non-negative integer.`);
+ }
+ if (pkg.optional !== undefined && typeof pkg.optional !== "boolean") {
+ throw new Error(`${id}.optional must be a boolean.`);
+ }
+ return {
+ id,
+ title: text(pkg.title, `${id}.title`),
+ role: { class: roleClass, label: text(role.label, `${id}.role.label`) },
+ objective: text(pkg.objective, `${id}.objective`),
+ dependencies: stringArray(pkg.dependencies ?? [], `${id}.dependencies`),
+ optional: pkg.optional ?? false,
+ access: "read-only",
+ workspace: { mode: "shared" },
+ model: { name: text(model.name, `${id}.model.name`), effort },
+ nativeSubagents: { policy, maxChildren: nativeSubagents.maxChildren },
+ acceptanceCriteria: stringArray(pkg.acceptanceCriteria, `${id}.acceptanceCriteria`),
+ expectedOutputs: stringArray(pkg.expectedOutputs, `${id}.expectedOutputs`)
+ };
+}
+
+function validateGraph(packages) {
+ const byId = new Map(packages.map((pkg) => [pkg.id, pkg]));
+ if (byId.size !== packages.length) throw new Error("Package IDs must be unique.");
+ for (const pkg of packages) {
+ for (const dependency of pkg.dependencies) {
+ if (!byId.has(dependency)) throw new Error(`${pkg.id} depends on unknown package ${dependency}.`);
+ if (dependency === pkg.id) throw new Error(`${pkg.id} cannot depend on itself.`);
+ }
+ }
+ const visiting = new Set();
+ const visited = new Set();
+ const stack = [];
+ const visit = (id) => {
+ if (visiting.has(id)) {
+ const start = stack.indexOf(id);
+ throw new Error(`Package dependency cycle: ${[...stack.slice(start), id].join(" -> ")}`);
+ }
+ if (visited.has(id)) return;
+ visiting.add(id); stack.push(id);
+ for (const dep of byId.get(id).dependencies) visit(dep);
+ stack.pop(); visiting.delete(id); visited.add(id);
+ };
+ for (const pkg of packages) visit(pkg.id);
+}
+
+export function normalizeOrchestrationPlan(input, context = {}) {
+ const plan = object(input, "plan");
+ if (plan.version !== ORCHESTRATION_PLAN_VERSION) throw new Error(`plan.version must be ${ORCHESTRATION_PLAN_VERSION}.`);
+ if (!Number.isInteger(plan.complexityScore) || plan.complexityScore < 0 || plan.complexityScore > 10) {
+ throw new Error("complexityScore must be an integer between 0 and 10.");
+ }
+ const packages = (Array.isArray(plan.packages) ? plan.packages : (() => { throw new Error("packages must be an array."); })())
+ .map(normalizePackage);
+ if (packages.length === 0) throw new Error("At least one package is required.");
+ validateGraph(packages);
+ const normalized = {
+ version: ORCHESTRATION_PLAN_VERSION,
+ objective: text(plan.objective, "objective"),
+ complexityScore: plan.complexityScore,
+ requestedBy: {
+ explicit: Boolean(object(plan.requestedBy, "requestedBy").explicit),
+ sessionId: plan.requestedBy.sessionId == null ? null : text(plan.requestedBy.sessionId, "requestedBy.sessionId")
+ },
+ packages
+ };
+ const budget = deriveBudgetEnvelope(normalized.complexityScore, context.config);
+ validatePlanAgainstBudget(normalized, budget);
+ return deepFreeze({ ...normalized, budget });
+}
diff --git a/plugins/codex/scripts/orchestration/recovery.mjs b/plugins/codex/scripts/orchestration/recovery.mjs
new file mode 100644
index 000000000..91d4538e4
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/recovery.mjs
@@ -0,0 +1,52 @@
+
+import { buildOrchestrationResult } from "./result-contract.mjs";
+import { appendOrchestrationEvent, isTerminalState, listOrchestrations, updateOrchestrationState, writePackageResult } from "./state-store.mjs";
+
+function alive(pid) {
+ try { process.kill(pid, 0); return true; }
+ catch (error) { return error?.code !== "ESRCH"; }
+}
+function descendants(plan, roots) {
+ const blocked = new Set();
+ let changed = true;
+ while (changed) {
+ changed = false;
+ for (const pkg of plan.packages) {
+ if (!blocked.has(pkg.id) && pkg.dependencies.some((dep) => roots.has(dep) || blocked.has(dep))) {
+ blocked.add(pkg.id); changed = true;
+ }
+ }
+ }
+ return blocked;
+}
+export async function reconcilePhase1ControllerLoss(workspaceRoot, identity) {
+ for (const state of listOrchestrations(workspaceRoot)) {
+ if (isTerminalState(state)) continue;
+ const previous = state.controller;
+ if (!previous?.pid || previous.instanceId === identity.instanceId || alive(previous.pid)) continue;
+ const failedRoots = new Set(Object.entries(state.packages).filter(([, pkg]) => ["running", "cancelling"].includes(pkg.status)).map(([id]) => id));
+ const blocked = descendants(state.plan, failedRoots);
+ const finalized = await updateOrchestrationState(workspaceRoot, state.id, (current) => {
+ for (const [id, pkg] of Object.entries(current.packages)) {
+ if (failedRoots.has(id)) {
+ pkg.status = "failed"; pkg.error = "PHASE1_CONTROLLER_LOST: the owning controller exited."; pkg.completedAt = new Date().toISOString();
+ } else if (blocked.has(id)) {
+ pkg.status = "blocked"; pkg.error = "Dependency was lost with the previous controller."; pkg.completedAt = new Date().toISOString();
+ } else if (["planned", "ready", "queued"].includes(pkg.status)) {
+ pkg.status = "cancelled"; pkg.error = "Phase 1 does not automatically resume orphaned packages."; pkg.completedAt = new Date().toISOString();
+ }
+ }
+ const usable = Object.values(current.packages).some((pkg) => ["completed", "partial"].includes(pkg.status));
+ current.status = usable ? "degraded" : "failed";
+ current.completedAt = new Date().toISOString();
+ current.controller = identity;
+ return current;
+ });
+ writePackageResult(workspaceRoot, state.id, "_orchestration", buildOrchestrationResult(finalized));
+ appendOrchestrationEvent(workspaceRoot, state.id, {
+ type: "controller-loss-finalized",
+ phase: finalized.status,
+ message: "Previous controller was lost; Phase 1 finalized the orchestration without automatic resume."
+ });
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/result-contract.mjs b/plugins/codex/scripts/orchestration/result-contract.mjs
new file mode 100644
index 000000000..2120c5db0
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/result-contract.mjs
@@ -0,0 +1,77 @@
+
+import fs from "node:fs";
+
+const SCHEMA_URL = new URL("./schemas/package-result.schema.json", import.meta.url);
+let cachedSchema = null;
+
+function arrayOfStrings(value, label) {
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${label} must be an array of strings.`);
+ return value.map((entry) => entry.trim()).filter(Boolean);
+}
+
+export function readPackageResultSchema() {
+ cachedSchema ??= JSON.parse(fs.readFileSync(SCHEMA_URL, "utf8"));
+ return cachedSchema;
+}
+
+export function validatePackageResult(input, packageId) {
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Package result must be an object.");
+ if (input.packageId !== packageId) throw new Error(`Package result ID ${input.packageId ?? ""} does not match ${packageId}.`);
+ if (!new Set(["completed", "partial", "blocked", "failed"]).has(input.status)) throw new Error(`${packageId}.status is invalid.`);
+ if (typeof input.summary !== "string" || !input.summary.trim()) throw new Error(`${packageId}.summary is required.`);
+ if (!Array.isArray(input.changedFiles) || input.changedFiles.length !== 0) throw new Error("Phase 1 package results must contain an empty changedFiles array.");
+ if (typeof input.confidence !== "number" || input.confidence < 0 || input.confidence > 1) throw new Error(`${packageId}.confidence must be between 0 and 1.`);
+ if (!input.verification || typeof input.verification.passed !== "boolean") throw new Error(`${packageId}.verification.passed must be a boolean.`);
+ const evidence = Array.isArray(input.evidence) ? input.evidence.map((entry, index) => {
+ if (!entry || typeof entry !== "object" || !new Set(["file", "command", "observation"]).has(entry.type)) {
+ throw new Error(`${packageId}.evidence[${index}] is invalid.`);
+ }
+ return {
+ type: entry.type,
+ description: String(entry.description ?? "").trim(),
+ path: entry.path ?? null,
+ lineStart: entry.lineStart ?? null,
+ lineEnd: entry.lineEnd ?? null,
+ command: entry.command ?? null,
+ exitCode: entry.exitCode ?? null
+ };
+ }) : (() => { throw new Error(`${packageId}.evidence must be an array.`); })();
+ return {
+ packageId,
+ status: input.status,
+ summary: input.summary.trim(),
+ claims: arrayOfStrings(input.claims, `${packageId}.claims`),
+ evidence,
+ changedFiles: [],
+ verification: { passed: input.verification.passed, commands: arrayOfStrings(input.verification.commands, `${packageId}.verification.commands`) },
+ residualRisks: arrayOfStrings(input.residualRisks, `${packageId}.residualRisks`),
+ confidence: input.confidence,
+ followUpRequests: arrayOfStrings(input.followUpRequests, `${packageId}.followUpRequests`)
+ };
+}
+
+export function buildOrchestrationResult(state) {
+ return {
+ orchestrationId: state.id,
+ status: state.status,
+ objective: state.plan.objective,
+ planRevision: state.planRevision,
+ packages: state.plan.packages.map((pkg) => {
+ const current = state.packages[pkg.id];
+ return {
+ id: pkg.id,
+ title: pkg.title,
+ role: pkg.role,
+ model: pkg.model,
+ status: current.status,
+ result: current.result ?? null,
+ threadId: current.threadId ?? null,
+ nativeChildThreadIds: current.nativeChildThreadIds ?? [],
+ nativeSubagentDegraded: Boolean(current.nativeSubagentDegraded),
+ nativeSubagentDegradationReason: current.nativeSubagentDegradationReason ?? null
+ };
+ }),
+ omissions: state.omissions ?? [],
+ remainingWork: state.remainingWork ?? []
+ };
+}
diff --git a/plugins/codex/scripts/orchestration/scheduler.mjs b/plugins/codex/scripts/orchestration/scheduler.mjs
new file mode 100644
index 000000000..a785cf785
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/scheduler.mjs
@@ -0,0 +1,63 @@
+
+import { PACKAGE_TERMINAL_STATUSES } from "./constants.mjs";
+
+function copy(state) {
+ return { packages: Object.fromEntries(Object.entries(state.packages).map(([id, value]) => [id, { ...value, dependencies: [...value.dependencies] }])) };
+}
+function transition(state, id, allowed, patch) {
+ const current = state.packages[id];
+ if (!current) throw new Error(`Unknown package ${id}.`);
+ if (!allowed.includes(current.status)) throw new Error(`Cannot transition ${id} from ${current.status}.`);
+ const next = copy(state); next.packages[id] = { ...next.packages[id], ...patch }; return next;
+}
+
+export function createSchedulerState(plan) {
+ return {
+ packages: Object.fromEntries(plan.packages.map((pkg) => [pkg.id, {
+ id: pkg.id, status: "planned", dependencies: [...pkg.dependencies], optional: pkg.optional, attempt: 0, error: null
+ }]))
+ };
+}
+export function getReadyPackageIds(state) {
+ return Object.values(state.packages).filter((pkg) => pkg.status === "planned" && pkg.dependencies.every((id) => {
+ const status = state.packages[id].status; return status === "completed" || status === "partial";
+ })).map((pkg) => pkg.id);
+}
+export function markPackageReady(state, id) { return transition(state, id, ["planned"], { status: "ready" }); }
+export function markPackageRunning(state, id, attempt) { return transition(state, id, ["ready"], { status: "running", attempt }); }
+export function markPackageCompleted(state, id, resultStatus = "completed") {
+ if (!new Set(["completed", "partial", "blocked", "failed"]).has(resultStatus)) throw new Error(`Invalid result status ${resultStatus}.`);
+ return transition(state, id, ["running"], { status: resultStatus });
+}
+export function markPackageFailed(state, id, error) { return transition(state, id, ["running", "ready", "planned"], { status: "failed", error: String(error ?? "failed") }); }
+export function markPackageCancelled(state, id) {
+ const current = state.packages[id];
+ if (PACKAGE_TERMINAL_STATUSES.has(current.status)) return state;
+ return transition(state, id, ["planned", "ready", "running", "cancelling"], { status: "cancelled" });
+}
+export function propagateBlockedPackages(state) {
+ let next = state; let changed = true;
+ while (changed) {
+ changed = false;
+ for (const pkg of Object.values(next.packages)) {
+ if (!["planned", "ready"].includes(pkg.status)) continue;
+ if (pkg.dependencies.some((id) => ["failed", "blocked", "cancelled"].includes(next.packages[id].status))) {
+ next = transition(next, pkg.id, [pkg.status], { status: "blocked", error: "Required dependency did not complete." });
+ changed = true;
+ }
+ }
+ }
+ return next;
+}
+export function deriveOrchestrationStatus(state) {
+ const values = Object.values(state.packages);
+ if (values.some((pkg) => !PACKAGE_TERMINAL_STATUSES.has(pkg.status))) return "running";
+ const required = values.filter((pkg) => !pkg.optional);
+ const usable = values.filter((pkg) => ["completed", "partial"].includes(pkg.status));
+ if (required.every((pkg) => ["completed", "partial"].includes(pkg.status))) {
+ return values.some((pkg) => pkg.optional && !["completed", "partial"].includes(pkg.status)) ? "completed-with-omissions" : "completed";
+ }
+ if (usable.length > 0) return "degraded";
+ if (required.some((pkg) => pkg.status === "blocked") && required.every((pkg) => ["blocked", "cancelled"].includes(pkg.status))) return "blocked";
+ return values.every((pkg) => pkg.status === "cancelled") ? "cancelled" : "failed";
+}
diff --git a/plugins/codex/scripts/orchestration/schemas/config.schema.json b/plugins/codex/scripts/orchestration/schemas/config.schema.json
new file mode 100644
index 000000000..15b032724
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/schemas/config.schema.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "auto": {
+ "type": "object"
+ },
+ "workers": {
+ "type": "object"
+ }
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json b/plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json
new file mode 100644
index 000000000..f122450cf
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/schemas/orchestration-plan.schema.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "required": [
+ "version",
+ "objective",
+ "complexityScore",
+ "requestedBy",
+ "packages"
+ ]
+}
diff --git a/plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json b/plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json
new file mode 100644
index 000000000..8b83c14cc
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/schemas/orchestration-result.schema.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "required": [
+ "orchestrationId",
+ "status",
+ "objective",
+ "packages"
+ ]
+}
diff --git a/plugins/codex/scripts/orchestration/schemas/package-result.schema.json b/plugins/codex/scripts/orchestration/schemas/package-result.schema.json
new file mode 100644
index 000000000..f78284190
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/schemas/package-result.schema.json
@@ -0,0 +1,66 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "packageId",
+ "status",
+ "summary",
+ "claims",
+ "evidence",
+ "changedFiles",
+ "verification",
+ "residualRisks",
+ "confidence",
+ "followUpRequests"
+ ],
+ "properties": {
+ "packageId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "completed",
+ "partial",
+ "blocked",
+ "failed"
+ ]
+ },
+ "summary": {
+ "type": "string"
+ },
+ "claims": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "evidence": {
+ "type": "array"
+ },
+ "changedFiles": {
+ "type": "array",
+ "maxItems": 0
+ },
+ "verification": {
+ "type": "object"
+ },
+ "residualRisks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "confidence": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "followUpRequests": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+}
diff --git a/plugins/codex/scripts/orchestration/setup-dispatch.mjs b/plugins/codex/scripts/orchestration/setup-dispatch.mjs
new file mode 100644
index 000000000..de8279b18
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/setup-dispatch.mjs
@@ -0,0 +1,15 @@
+#!/usr/bin/env node
+import { spawnSync } from "node:child_process";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { patchUserOrchestrationConfig } from "./config.mjs";
+const ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
+const args = process.argv.slice(2); const enable = args.includes("--enable-orchestration"); const disable = args.includes("--disable-orchestration");
+if (enable && disable) throw new Error("Choose either --enable-orchestration or --disable-orchestration.");
+if (enable || disable) patchUserOrchestrationConfig({ auto: { enabled: enable } });
+const forwarded = args.filter((arg) => !["--enable-orchestration", "--disable-orchestration"].includes(arg));
+const result = spawnSync(process.execPath, [path.join(ROOT, "codex-companion.mjs"), "setup", ...forwarded], { cwd: process.cwd(), env: process.env, encoding: "utf8" });
+process.stdout.write(result.stdout ?? ""); process.stderr.write(result.stderr ?? "");
+if (enable || disable) process.stdout.write(`Orchestration auto-entry: ${enable ? "enabled" : "disabled"}.\n`);
+process.exitCode = result.status ?? 1;
diff --git a/plugins/codex/scripts/orchestration/state-store.mjs b/plugins/codex/scripts/orchestration/state-store.mjs
new file mode 100644
index 000000000..17fe530a8
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/state-store.mjs
@@ -0,0 +1,92 @@
+
+import crypto from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+import { resolveWorkspaceRoot } from "../lib/workspace.mjs";
+import { ORCHESTRATION_STATE_VERSION, ORCHESTRATION_TERMINAL_STATUSES } from "./constants.mjs";
+import { withFileLock } from "./file-lock.mjs";
+
+function workspaceKey(workspaceRoot) {
+ const canonical = (() => { try { return fs.realpathSync.native(workspaceRoot); } catch { return path.resolve(workspaceRoot); } })();
+ const slug = (path.basename(workspaceRoot) || "workspace").replace(/[^A-Za-z0-9._-]+/g, "-");
+ return `${slug}-${crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 16)}`;
+}
+export function resolveOrchestrationWorkspaceDir(cwd, options = {}) {
+ const root = options.pluginDataDir ?? process.env.CLAUDE_PLUGIN_DATA;
+ const base = root ? path.join(path.resolve(root), "orchestrations") : path.join(os.tmpdir(), "codex-companion", "orchestrations");
+ return path.join(base, workspaceKey(resolveWorkspaceRoot(cwd)));
+}
+function orchDir(cwd, id, options) { return path.join(resolveOrchestrationWorkspaceDir(cwd, options), id); }
+function stateFile(cwd, id, options) { return path.join(orchDir(cwd, id, options), "orchestration.json"); }
+function writeAtomic(filePath, value) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ const temp = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
+ fs.renameSync(temp, filePath);
+}
+export function generateOrchestrationId(now = Date.now()) { return `orch-${now.toString(36)}-${crypto.randomBytes(3).toString("hex")}`; }
+export function loadOrchestrationState(cwd, id, options = {}) {
+ const file = stateFile(cwd, id, options);
+ if (!fs.existsSync(file)) throw new Error(`No orchestration found for "${id}". Run /codex:status.`);
+ return JSON.parse(fs.readFileSync(file, "utf8"));
+}
+export async function createOrchestrationState(cwd, plan, context = {}, options = {}) {
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
+ const id = context.id ?? generateOrchestrationId();
+ const now = new Date().toISOString();
+ const state = {
+ version: ORCHESTRATION_STATE_VERSION, id, workspaceRoot, claudeSessionId: context.claudeSessionId ?? null,
+ status: "queued", planRevision: 1, plan, createdAt: now, updatedAt: now, startedAt: null, completedAt: null,
+ controller: context.controller ?? null, packages: Object.fromEntries(plan.packages.map((pkg) => [pkg.id, {
+ id: pkg.id, status: "planned", attempt: 0, workerId: null, pid: null, threadId: null, turnId: null,
+ nativeChildThreadIds: [], nativeSubagentDegraded: false, nativeSubagentDegradationReason: null,
+ result: null, error: null, startedAt: null, completedAt: null
+ }])), omissions: [], remainingWork: []
+ };
+ writeAtomic(stateFile(cwd, id, options), state);
+ appendOrchestrationEvent(cwd, id, { type: "orchestration-created", message: plan.objective }, options);
+ return state;
+}
+export async function updateOrchestrationState(cwd, id, mutate, options = {}) {
+ const directory = orchDir(cwd, id, options);
+ return withFileLock(path.join(directory, "state.lock"), {}, async () => {
+ const state = loadOrchestrationState(cwd, id, options);
+ const result = await mutate(state) ?? state;
+ result.updatedAt = new Date().toISOString();
+ writeAtomic(stateFile(cwd, id, options), result);
+ return result;
+ });
+}
+export function appendOrchestrationEvent(cwd, id, event, options = {}) {
+ const file = path.join(orchDir(cwd, id, options), "events.jsonl");
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.appendFileSync(file, `${JSON.stringify({ timestamp: new Date().toISOString(), orchestrationId: id, packageId: event.packageId ?? null, type: event.type, phase: event.phase ?? null, message: event.message ?? "", data: event.data ?? null })}\n`, { encoding: "utf8", mode: 0o600 });
+}
+export function writePackageResult(cwd, id, packageId, result, options = {}) {
+ const file = path.join(orchDir(cwd, id, options), "results", `${packageId}.json`); writeAtomic(file, result); return file;
+}
+export function readPackageResult(cwd, id, packageId, options = {}) {
+ const file = path.join(orchDir(cwd, id, options), "results", `${packageId}.json`); return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null;
+}
+export function listOrchestrations(cwd, options = {}) {
+ const directory = resolveOrchestrationWorkspaceDir(cwd, options);
+ if (!fs.existsSync(directory)) return [];
+ return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("orch-"))
+ .map((entry) => { try { return loadOrchestrationState(cwd, entry.name, options); } catch { return null; } }).filter(Boolean)
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
+}
+export function resolveOrchestrationReference(cwd, reference, options = {}) {
+ const states = listOrchestrations(cwd, options);
+ const exact = states.find((state) => state.id === reference);
+ if (exact) return { kind: "orchestration", orchestrationId: exact.id };
+ const orchMatches = states.filter((state) => state.id.startsWith(reference));
+ if (orchMatches.length === 1) return { kind: "orchestration", orchestrationId: orchMatches[0].id };
+ const packageMatches = [];
+ for (const state of states) for (const packageId of Object.keys(state.packages)) if (packageId === reference || packageId.startsWith(reference)) packageMatches.push({ kind: "package", orchestrationId: state.id, packageId });
+ if (packageMatches.length === 1) return packageMatches[0];
+ if (orchMatches.length > 1 || packageMatches.length > 1) throw new Error(`Reference "${reference}" is ambiguous. Run /codex:status.`);
+ throw new Error(`No orchestration or package found for "${reference}". Run /codex:status.`);
+}
+export function isTerminalState(state) { return ORCHESTRATION_TERMINAL_STATUSES.has(state.status); }
diff --git a/plugins/codex/scripts/orchestration/worker-pool.mjs b/plugins/codex/scripts/orchestration/worker-pool.mjs
new file mode 100644
index 000000000..f7ddce273
--- /dev/null
+++ b/plugins/codex/scripts/orchestration/worker-pool.mjs
@@ -0,0 +1,243 @@
+import crypto from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { spawn } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+import { terminateProcessTree } from "../lib/process.mjs";
+import {
+ acquireGlobalWorkerLease,
+ releaseGlobalWorkerLease,
+ updateGlobalWorkerLease
+} from "./global-worker-registry.mjs";
+
+function workspaceKey(value) {
+ return crypto.createHash("sha256").update(path.resolve(value)).digest("hex").slice(0, 16);
+}
+
+function transientError(error) {
+ return ["EPIPE", "ECONNRESET", "ECONNREFUSED", "ENOENT", "CODEX_WORKER_EXIT"].includes(error?.code)
+ || error?.transient === true;
+}
+
+export class WorkerPool {
+ constructor(options) {
+ this.workspaceRoot = options.workspaceRoot;
+ this.size = options.size;
+ this.globalTopLevelLimit = options.globalTopLevelLimit ?? 8;
+ this.globalActiveCodexLimit = options.globalActiveCodexLimit ?? 12;
+ this.pluginDataDir = options.pluginDataDir;
+ this.onEvent = options.onEvent ?? (() => {});
+ this.active = new Map();
+ this.waiters = [];
+ this.availableSlots = this.size;
+ }
+
+ async waitForSlot() {
+ if (this.availableSlots > 0) {
+ this.availableSlots -= 1;
+ return;
+ }
+ await new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
+ }
+
+ releaseSlot() {
+ const waiter = this.waiters.shift();
+ if (waiter) waiter.resolve();
+ else this.availableSlots = Math.min(this.size, this.availableSlots + 1);
+ }
+
+ async execute(orchestrationId, packageSpec, dependencyResults, options = {}) {
+ await this.waitForSlot();
+ const workerId = `root-${crypto.randomUUID()}`;
+ let lease;
+ try {
+ lease = await acquireGlobalWorkerLease({
+ pluginDataDir: this.pluginDataDir,
+ workspaceKey: workspaceKey(this.workspaceRoot),
+ workerId,
+ packageId: packageSpec.id,
+ globalTopLevelLimit: this.globalTopLevelLimit,
+ globalActiveCodexLimit: this.globalActiveCodexLimit
+ });
+ } catch (error) {
+ this.releaseSlot();
+ throw error;
+ }
+
+ const requestFile = path.join(
+ os.tmpdir(),
+ `codex-orchestration-${process.pid}-${crypto.randomUUID()}.json`
+ );
+ fs.writeFileSync(
+ requestFile,
+ JSON.stringify({
+ workspaceRoot: this.workspaceRoot,
+ orchestrationId,
+ packageSpec,
+ dependencyResults
+ }),
+ { encoding: "utf8", mode: 0o600 }
+ );
+
+ const script = new URL("./package-worker.mjs", import.meta.url);
+ const child = spawn(process.execPath, [fileURLToPath(script), requestFile], {
+ cwd: this.workspaceRoot,
+ env: process.env,
+ stdio: ["pipe", "pipe", "pipe"],
+ windowsHide: true
+ });
+
+ let resolveExit;
+ const exitPromise = new Promise((resolve) => {
+ resolveExit = resolve;
+ });
+ const record = {
+ workerId,
+ child,
+ lease,
+ packageId: packageSpec.id,
+ settled: false,
+ nativeChildren: 0,
+ exitPromise
+ };
+ child.once("exit", () => resolveExit());
+ this.active.set(packageSpec.id, record);
+
+ let stderr = "";
+ let buffer = "";
+ /** @type {Record | null} */
+ let finalPayload = null;
+ /** @type {{ message?: string, code?: string, transient?: boolean } | null} */
+ let reportedError = null;
+
+ child.stdout.setEncoding("utf8");
+ child.stderr.setEncoding("utf8");
+ child.stderr.on("data", (chunk) => {
+ stderr += chunk;
+ });
+ child.stdout.on("data", (chunk) => {
+ buffer += chunk;
+ let index = buffer.indexOf("\n");
+ while (index !== -1) {
+ const line = buffer.slice(0, index);
+ buffer = buffer.slice(index + 1);
+ index = buffer.indexOf("\n");
+ if (!line.trim()) continue;
+ try {
+ const message = JSON.parse(line);
+ if (message.type === "progress") {
+ const nextChildren = message.event?.activeNativeChildren ?? 0;
+ if (nextChildren !== record.nativeChildren) {
+ record.nativeChildren = nextChildren;
+ updateGlobalWorkerLease(
+ lease.id,
+ { activeNativeChildren: nextChildren },
+ {
+ pluginDataDir: this.pluginDataDir,
+ globalActiveCodexLimit: this.globalActiveCodexLimit
+ }
+ ).catch((error) => {
+ if (error.code === "ACTIVE_CODEX_LIMIT_EXCEEDED") this.cancel(packageSpec.id);
+ });
+ }
+ this.onEvent({
+ orchestrationId,
+ packageId: packageSpec.id,
+ type: "package-progress",
+ ...message.event
+ });
+ } else if (message.type === "result") {
+ finalPayload = message.payload;
+ } else if (message.type === "error") {
+ reportedError = message.error;
+ }
+ } catch (error) {
+ reportedError = {
+ message: `Invalid worker JSON: ${error.message}`,
+ code: "WORKER_PROTOCOL_ERROR"
+ };
+ }
+ }
+ });
+
+ const hardTimeoutMs = Math.max(1000, (options.timeoutMinutes ?? 15) * 60_000);
+ const timeout = setTimeout(() => {
+ reportedError = {
+ message: `Package ${packageSpec.id} exceeded ${options.timeoutMinutes ?? 15} minutes.`,
+ code: "PACKAGE_TIMEOUT"
+ };
+ terminateProcessTree(child.pid ?? Number.NaN);
+ }, hardTimeoutMs);
+ timeout.unref?.();
+
+ try {
+ try {
+ await options.onStarted?.({ workerId, pid: child.pid });
+ } catch (error) {
+ terminateProcessTree(child.pid ?? Number.NaN);
+ throw error;
+ }
+
+ const code = await new Promise((resolve, reject) => {
+ child.once("error", reject);
+ child.once("exit", resolve);
+ });
+ if (code !== 0 || !finalPayload) {
+ throw Object.assign(
+ new Error(
+ reportedError?.message
+ ?? stderr.trim()
+ ?? `Package worker exited with code ${code}.`
+ ),
+ {
+ code: reportedError?.code ?? "CODEX_WORKER_EXIT",
+ transient: reportedError?.transient ?? (reportedError?.code == null)
+ }
+ );
+ }
+ return { ...(finalPayload ?? {}), workerId, pid: child.pid };
+ } finally {
+ clearTimeout(timeout);
+ record.settled = true;
+ this.active.delete(packageSpec.id);
+ fs.rmSync(requestFile, { force: true });
+ await releaseGlobalWorkerLease(lease.id, { pluginDataDir: this.pluginDataDir });
+ this.releaseSlot();
+ }
+ }
+
+ async cancel(packageId, options = {}) {
+ const record = this.active.get(packageId);
+ if (!record) return { attempted: false, interrupted: false };
+
+ record.child.stdin?.write(`${JSON.stringify({ type: "interrupt" })}\n`);
+ const settled = await Promise.race([
+ record.exitPromise.then(() => true),
+ new Promise((resolve) => setTimeout(() => resolve(false), options.graceMs ?? 1000))
+ ]);
+ if (!settled) terminateProcessTree(record.child.pid ?? Number.NaN);
+ return { attempted: true, interrupted: settled };
+ }
+
+ getSnapshot() {
+ return {
+ size: this.size,
+ active: [...this.active.values()].map((entry) => ({
+ packageId: entry.packageId,
+ workerId: entry.workerId,
+ pid: entry.child.pid,
+ nativeChildren: entry.nativeChildren
+ })),
+ queued: this.waiters.length
+ };
+ }
+
+ async close() {
+ for (const packageId of [...this.active.keys()]) await this.cancel(packageId);
+ for (const waiter of this.waiters.splice(0)) waiter.reject(new Error("Worker pool closed."));
+ }
+}
+
+export { transientError as isTransientWorkerError };
diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md
index 0e91bfb50..108f6be24 100644
--- a/plugins/codex/skills/codex-cli-runtime/SKILL.md
+++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md
@@ -16,23 +16,26 @@ Execution rules:
- Prefer the helper over hand-rolled `git`, direct Codex CLI strings, or any other Bash activity.
- Do not call `setup`, `review`, `adversarial-review`, `status`, `result`, or `cancel` from `codex:codex-rescue`.
- Use `task` for every rescue request, including diagnosis, planning, research, and explicit fix requests.
-- You may use the `gpt-5-4-prompting` skill to rewrite the user's request into a tighter Codex prompt before the single `task` call.
+- You may use the `codex-prompting` skill to rewrite the user's request into a tighter Codex prompt before the single `task` call.
- That prompt drafting is the only Claude-side work allowed. Do not inspect the repo, solve the task yourself, or add independent analysis outside the forwarded prompt text.
- Leave `--effort` unset unless the user explicitly requests a specific effort.
- Leave model unset by default. Add `--model` only when the user explicitly asks for one.
-- Map `spark` to `--model gpt-5.3-codex-spark`.
+- Treat `Sol > Terra > Luna` as the base GPT-5.6 capability ordering. Reasoning effort is a separate inference-budget dimension.
+- Map an explicit natural-language request for Sol, Terra, or Luna to `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`.
+- Map `spark` to `--model gpt-5.6-luna`.
- Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
Command selection:
- Use exactly one `task` invocation per rescue handoff.
- If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text.
-- If the forwarded request includes `--model`, normalize `spark` to `gpt-5.3-codex-spark` and pass it through to `task`.
+- If the forwarded request includes `--model`, normalize `spark` to `gpt-5.6-luna` and pass it through to `task`.
- If the forwarded request includes `--effort`, pass it through to `task`.
- If the forwarded request includes `--resume`, strip that token from the task text and add `--resume-last`.
- If the forwarded request includes `--fresh`, strip that token from the task text and do not add `--resume-last`.
- `--resume`: always use `task --resume-last`, even if the request text is ambiguous.
- `--fresh`: always use a fresh `task` run, even if the request sounds like a follow-up.
-- `--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`.
+- `--effort`: accepted transport values are `none`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`.
+- The runtime checks requested model/effort combinations against the current Codex model catalog. Do not hardcode per-model effort support in the forwarding agent.
- `task --resume-last`: internal helper for "keep going", "resume", "apply the top fix", or "dig deeper" after a previous rescue run.
Safety rules:
diff --git a/plugins/codex/skills/codex-integration-policy/SKILL.md b/plugins/codex/skills/codex-integration-policy/SKILL.md
new file mode 100644
index 000000000..36c5e60c4
--- /dev/null
+++ b/plugins/codex/skills/codex-integration-policy/SKILL.md
@@ -0,0 +1,7 @@
+
+---
+name: codex-integration-policy
+description: Internal policy for interpreting Phase 1 Multi-Codex results
+user-invocable: false
+---
+Phase 1 integrates conclusions only. Compare claims against file, command, and observation evidence; preserve contradictions and residual risks. Do not apply patches, create integration branches, cherry-pick, merge, or create commits. Writer worktrees and Git integration are Phase 2.
diff --git a/plugins/codex/skills/codex-orchestration-recovery/SKILL.md b/plugins/codex/skills/codex-orchestration-recovery/SKILL.md
new file mode 100644
index 000000000..1d207c79d
--- /dev/null
+++ b/plugins/codex/skills/codex-orchestration-recovery/SKILL.md
@@ -0,0 +1,7 @@
+
+---
+name: codex-orchestration-recovery
+description: Internal Phase 1 recovery and durable-result policy
+user-invocable: false
+---
+Durable status and terminal results remain readable without a live controller. A live controller may be reconnected to for status or cancellation. Phase 1 does not automatically resume orphaned packages; report controller loss honestly and preserve available results. Automatic restart/resume and orphan reconciliation are Phase 3.
diff --git a/plugins/codex/skills/codex-orchestration/SKILL.md b/plugins/codex/skills/codex-orchestration/SKILL.md
new file mode 100644
index 000000000..6dd2bf556
--- /dev/null
+++ b/plugins/codex/skills/codex-orchestration/SKILL.md
@@ -0,0 +1,26 @@
+
+---
+name: codex-orchestration
+description: Use for an explicit Multi-Codex request or, when automatic orchestration is enabled, for repository work with multiple genuinely independent packages that meets the Complexity Score threshold
+user-invocable: false
+---
+
+# Claude-native Multi-Codex orchestration
+
+Claude Root owns decomposition, the top-level DAG, model/effort routing, and final interpretation. Do not delegate those decisions to a Codex lead. Each top-level Codex Root receives one bounded package; native children remain owned by that Root.
+
+Before automatic entry, run `node "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration/cli.mjs" config --cwd "$PWD" --json`. Do not auto-start when `autoEnabled` is false or the score is below `autoThreshold`. Explicit `/codex:orchestrate` bypasses those two entry checks but not Phase 1 restrictions or budgets.
+
+Hard exclusions: a one-file obvious fix; a known root cause and fix; a single command or narrow lookup; no independent packages; orchestration overhead exceeds the work; every writer would touch the same semantic core; user asks for one agent.
+
+Complexity Score, one point each: two independent packages; multiple modules/layers/services; material architecture judgment; unclear root cause; competing approaches; independent review warranted; implementation and verification can be separated; long single-agent run; previous single-agent failure; security/concurrency/migration/data-loss risk.
+
+0–2: direct work or one rescue. 3–4: at most two Roots. 5–7: prefer orchestration when enabled. 8–10: include a Sol architecture, plan-validation, or reviewer package.
+
+Model defaults: Luna for bounded exploration and repetitive verification; Terra for routine implementation-quality analysis; Sol for architecture, integration judgment, ambiguity, or adversarial review. Base capability is `Sol > Terra > Luna`; reasoning effort is a separate inference-budget dimension.
+
+Phase 1 is strictly read-only: `access` is `read-only`, workspace mode is `shared`, sandbox is read-only, changedFiles must be empty, and no package may push, publish, deploy, change credentials, or mutate a remote system. Automatic local write orchestration begins in Phase 2.
+
+Canonical plan fields: version 1, objective, complexityScore, requestedBy `{ explicit, sessionId }`, and packages containing id, title, role `{ class, label }`, objective, dependencies, optional boolean, access, workspace, model `{ name, effort }`, nativeSubagents `{ policy, maxChildren }`, acceptanceCriteria, expectedOutputs.
+
+Show a compressed 3–6 line plan and start immediately. Treat the launch response as acceptance only. Use `/codex:status`, `/codex:result`, and `/codex:cancel` for lifecycle. Integrate conclusions from evidence and verification, never from confidence alone.
diff --git a/plugins/codex/skills/codex-prompting/SKILL.md b/plugins/codex/skills/codex-prompting/SKILL.md
new file mode 100644
index 000000000..6ffa11802
--- /dev/null
+++ b/plugins/codex/skills/codex-prompting/SKILL.md
@@ -0,0 +1,63 @@
+---
+name: codex-prompting
+description: Version-neutral guidance for composing Codex prompts for coding, review, diagnosis, planning, and research tasks
+user-invocable: false
+---
+
+# Codex Prompting
+
+Use this skill only when `codex:codex-rescue` needs to make a user's request easier for Codex to execute. It shapes the prompt; it does not inspect the repository, solve the task, or choose a model unless the user explicitly requested one.
+
+## Core guidance
+
+- Favor lean, outcome-first prompts. State the goal, relevant context, hard constraints, approval boundaries, success criteria, and required output—once each.
+- Prefer one coherent task per Codex run. Split unrelated jobs into separate runs.
+- Preserve the user's terminology and intent. Do not broaden scope while "improving" the prompt.
+- Tell Codex what completion means and which non-destructive verification it must perform.
+- Add grounding requirements when unsupported guesses would damage correctness.
+- Use XML blocks only when they make boundaries clearer. Do not wrap every sentence or repeat the same policy in multiple blocks.
+- Prefer a better task contract over generic instructions such as "think harder" or gratuitously raising reasoning effort.
+
+## Autonomy and approval
+
+Define autonomy and approval boundaries compactly:
+
+- For explanation, review, diagnosis, planning, or research, inspect the relevant material and report the result without editing unless edits were requested.
+- For an explicit build, change, or fix request, make the in-scope local changes and run relevant non-destructive checks without asking routine questions.
+- Stop before external writes, destructive actions, purchases, credential changes, publication, or a material expansion of scope unless the user already authorized them.
+- Ask only when a missing fact materially changes correctness, safety, or an irreversible action.
+
+## GPT-5.6 tiers and effort
+
+- Treat `Sol > Terra > Luna` as the base capability ordering: Sol is the frontier tier, Terra balances capability and cost, and Luna targets efficient high-volume work.
+- Reasoning effort is a separate inference-budget dimension. Do not treat a higher effort on a lower tier as reversing the underlying capability ordering.
+- Do not silently route models in this skill. The rescue agent leaves the model unset unless the user explicitly selects one, so Codex configuration remains authoritative.
+- When explaining an explicit selection, use Sol for the hardest quality-first work, Terra for balanced everyday implementation and review, and Luna for bounded or high-volume work.
+- Leave effort unset unless the user requested it. The companion runtime and current Codex model catalog are the source of truth for supported model/effort combinations.
+- Improve scope and verification before escalating effort. Reserve the highest settings for tasks whose measured quality benefit justifies additional latency and usage.
+
+## Prompt shape
+
+Start with the smallest useful shape:
+
+- ``: the concrete job and expected end state.
+- ``: observable completion criteria.
+- ``: what may proceed locally and what requires approval.
+- ``: checks required before finalizing.
+- ``: evidence rules for review, diagnosis, or research.
+- ``: only when the final structure matters.
+
+For a follow-up on the same persistent Codex thread, send only the delta instruction unless the goal or constraints changed materially.
+
+## Assembly checklist
+
+1. Preserve the user's task and scope.
+2. Remove duplicated or purely motivational instructions.
+3. Add explicit completion and verification criteria where needed.
+4. Add one compact approval boundary for write-capable work.
+5. Keep claims grounded in repository or tool evidence.
+6. Leave model and effort untouched unless explicitly requested.
+
+Reusable blocks live in [references/prompt-blocks.md](references/prompt-blocks.md).
+Concrete templates live in [references/codex-prompt-recipes.md](references/codex-prompt-recipes.md).
+Common failure modes live in [references/codex-prompt-antipatterns.md](references/codex-prompt-antipatterns.md).
diff --git a/plugins/codex/skills/codex-prompting/references/codex-prompt-antipatterns.md b/plugins/codex/skills/codex-prompting/references/codex-prompt-antipatterns.md
new file mode 100644
index 000000000..317778a9e
--- /dev/null
+++ b/plugins/codex/skills/codex-prompting/references/codex-prompt-antipatterns.md
@@ -0,0 +1,55 @@
+# Codex Prompt Anti-Patterns
+
+## Repeating the same rule
+
+Bad: repeat approval, scope, or verification rules in several sections.
+
+Better: state each policy once in the most relevant block.
+
+## Prescribing every step
+
+Bad: enumerate a long procedure when the desired outcome and constraints are sufficient.
+
+Better: specify the goal, hard constraints, success criteria, and required evidence; let Codex choose routine implementation steps.
+
+## Vague completion
+
+Bad: `Look into this and report back.`
+
+Better:
+
+```xml
+
+Identify the root cause, cite evidence, and state the smallest safe next step.
+
+```
+
+## Generic reasoning nudges
+
+Bad: `Think harder. Be extremely smart.`
+
+Better:
+
+```xml
+
+Check the result against observed evidence and the task requirements before finalizing.
+
+```
+
+## Silent scope expansion
+
+Bad: turn a diagnosis request into an implementation or broad refactor.
+
+Better: preserve the requested action boundary and require explicit authorization for a material expansion of scope.
+
+## Mixing unrelated work
+
+Bad: combine review, implementation, documentation, and roadmap creation in one rescue run.
+
+Better: keep one coherent objective per run and use a follow-up turn or separate task for independent work.
+
+## Hardcoding model behavior
+
+Bad: assume a specific model or effort is available because it was supported by one Codex release.
+
+Better: leave model and effort unset unless requested and let the companion validate explicit combinations against the current model catalog.
diff --git a/plugins/codex/skills/codex-prompting/references/codex-prompt-recipes.md b/plugins/codex/skills/codex-prompting/references/codex-prompt-recipes.md
new file mode 100644
index 000000000..628c352e3
--- /dev/null
+++ b/plugins/codex/skills/codex-prompting/references/codex-prompt-recipes.md
@@ -0,0 +1,97 @@
+# Codex Prompt Recipes
+
+Use these as starting points. Keep only the blocks that the task actually needs.
+
+## Diagnosis
+
+```xml
+
+Diagnose why the specified test, command, or behavior is failing in this repository.
+Identify the root cause from repository and tool evidence.
+
+
+
+The root cause, supporting evidence, and smallest safe next step are explicit.
+
+
+
+Do not guess missing repository facts. Label hypotheses until evidence confirms them.
+
+
+
+Check that the proposed root cause explains the observed failure and relevant surrounding behavior.
+
+```
+
+## Narrow fix
+
+```xml
+
+Implement the smallest safe fix for the identified issue while preserving behavior outside the failing path.
+
+
+
+The fix is applied, relevant tests or checks pass, and residual risks are reported.
+
+
+
+Make the requested in-scope local edits and run non-destructive validation without asking first.
+Stop before external or destructive actions.
+
+
+
+Avoid unrelated refactors or cleanup.
+
+```
+
+## Adversarial analysis
+
+```xml
+
+Challenge this implementation or design for material correctness, regression, reliability, security, and rollback risks.
+
+
+
+Tie every finding to repository or tool evidence. Separate facts from inference.
+
+
+
+Return actionable findings in severity order, with evidence and a specific mitigation for each.
+
+
+
+Check second-order failures, empty states, concurrency, retries, stale state, and rollback paths before finalizing.
+
+```
+
+## Research or recommendation
+
+```xml
+
+Research the available options and recommend the best path for the stated decision.
+
+
+
+Separate observed facts, reasoned inference, and unresolved questions. Prefer primary sources.
+
+
+
+Return the recommendation first, then decisive evidence, tradeoffs, and conditions that would change it.
+
+```
+
+## Prompt repair
+
+```xml
+
+Diagnose why the supplied prompt underperforms and produce the smallest revision that addresses the demonstrated failure modes.
+
+
+
+The failure modes are traced to specific prompt clauses and the revised prompt removes contradiction, duplication, or missing boundaries.
+
+
+
+Check that the revision preserves the original intent and does not add unnecessary instructions.
+
+```
diff --git a/plugins/codex/skills/codex-prompting/references/prompt-blocks.md b/plugins/codex/skills/codex-prompting/references/prompt-blocks.md
new file mode 100644
index 000000000..c8636882a
--- /dev/null
+++ b/plugins/codex/skills/codex-prompting/references/prompt-blocks.md
@@ -0,0 +1,73 @@
+# Prompt Blocks
+
+Use only the blocks that materially clarify the task. A lean prompt with three precise blocks is usually better than a long template containing every block.
+
+## `task`
+
+```xml
+
+Describe the concrete job, the relevant repository or failure context, and the expected end state.
+
+```
+
+## `done_when`
+
+```xml
+
+List the observable conditions that must be true before the task is complete.
+
+```
+
+## `action_policy`
+
+```xml
+
+For requested local changes, edit only in-scope files and run relevant non-destructive checks without asking first.
+Require confirmation before external writes, destructive actions, credential changes, publication, or material scope expansion.
+
+```
+
+## `verification`
+
+```xml
+
+Verify the result against the task requirements and the changed files or tool outputs before finalizing.
+If a check fails, fix the issue and rerun the relevant check.
+
+```
+
+## `grounding`
+
+```xml
+
+Ground factual claims in repository content or tool evidence.
+Label hypotheses and unresolved uncertainty explicitly.
+
+```
+
+## `output_contract`
+
+```xml
+
+Return the requested structure with the highest-value findings or decisions first.
+Include all required evidence and verification results without repeated recap.
+
+```
+
+## `missing_context`
+
+```xml
+
+Retrieve missing local context with available tools.
+Ask only when a missing fact materially changes correctness, safety, or an irreversible action.
+
+```
+
+## `scope_safety`
+
+```xml
+
+Keep changes tightly scoped to the request.
+Avoid unrelated refactors, renames, or cleanup unless required for correctness.
+
+```
diff --git a/plugins/codex/skills/codex-work-package-contract/SKILL.md b/plugins/codex/skills/codex-work-package-contract/SKILL.md
new file mode 100644
index 000000000..ee8cf5880
--- /dev/null
+++ b/plugins/codex/skills/codex-work-package-contract/SKILL.md
@@ -0,0 +1,7 @@
+
+---
+name: codex-work-package-contract
+description: Internal contract for bounded top-level Codex work packages
+user-invocable: false
+---
+A package must have one objective, explicit dependencies, one role class/label, a model and effort, a read-only access boundary, native-child policy and cap, observable acceptance criteria, and required evidence outputs. Package IDs are unique. Dependencies must exist and the DAG must be acyclic. Phase 1 packages may not modify files.
diff --git a/plugins/codex/skills/gpt-5-4-prompting/SKILL.md b/plugins/codex/skills/gpt-5-4-prompting/SKILL.md
deleted file mode 100644
index 16669d92d..000000000
--- a/plugins/codex/skills/gpt-5-4-prompting/SKILL.md
+++ /dev/null
@@ -1,54 +0,0 @@
----
-name: gpt-5-4-prompting
-description: Internal guidance for composing Codex and GPT-5.4 prompts for coding, review, diagnosis, and research tasks inside the Codex Claude Code plugin
-user-invocable: false
----
-
-# GPT-5.4 Prompting
-
-Use this skill when `codex:codex-rescue` needs to ask Codex or another GPT-5.4-based workflow for help.
-
-Prompt Codex like an operator, not a collaborator. Keep prompts compact and block-structured with XML tags. State the task, the output contract, the follow-through defaults, and the small set of extra constraints that matter.
-
-Core rules:
-- Prefer one clear task per Codex run. Split unrelated asks into separate runs.
-- Tell Codex what done looks like. Do not assume it will infer the desired end state.
-- Add explicit grounding and verification rules for any task where unsupported guesses would hurt quality.
-- Prefer better prompt contracts over raising reasoning or adding long natural-language explanations.
-- Use XML tags consistently so the prompt has stable internal structure.
-
-Default prompt recipe:
-- ``: the concrete job and the relevant repository or failure context.
-- `` or ``: exact shape, ordering, and brevity requirements.
-- ``: what Codex should do by default instead of asking routine questions.
-- `` or ``: required for debugging, implementation, or risky fixes.
-- `` or ``: required for review, research, or anything that could drift into unsupported claims.
-
-When to add blocks:
-- Coding or debugging: add `completeness_contract`, `verification_loop`, and `missing_context_gating`.
-- Review or adversarial review: add `grounding_rules`, `structured_output_contract`, and `dig_deeper_nudge`.
-- Research or recommendation tasks: add `research_mode` and `citation_rules`.
-- Write-capable tasks: add `action_safety` so Codex stays narrow and avoids unrelated refactors.
-
-How to choose prompt shape:
-- Use built-in `review` or `adversarial-review` commands when the job is reviewing local git changes. Those prompts already carry the review contract.
-- Use `task` when the task is diagnosis, planning, research, or implementation and you need to control the prompt more directly.
-- Use `task --resume-last` for follow-up instructions on the same Codex thread. Send only the delta instruction instead of restating the whole prompt unless the direction changed materially.
-
-Working rules:
-- Prefer explicit prompt contracts over vague nudges.
-- Use stable XML tag names that match the block names from the reference file.
-- Do not raise reasoning or complexity first. Tighten the prompt and verification rules before escalating.
-- Ask Codex for brief, outcome-based progress updates only when the task is long-running or tool-heavy.
-- Keep claims anchored to observed evidence. If something is a hypothesis, say so.
-
-Prompt assembly checklist:
-1. Define the exact task and scope in ``.
-2. Choose the smallest output contract that still makes the answer easy to use.
-3. Decide whether Codex should keep going by default or stop for missing high-risk details.
-4. Add verification, grounding, and safety tags only where the task needs them.
-5. Remove redundant instructions before sending the prompt.
-
-Reusable blocks live in [references/prompt-blocks.md](references/prompt-blocks.md).
-Concrete end-to-end templates live in [references/codex-prompt-recipes.md](references/codex-prompt-recipes.md).
-Common failure modes to avoid live in [references/codex-prompt-antipatterns.md](references/codex-prompt-antipatterns.md).
diff --git a/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-antipatterns.md b/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-antipatterns.md
deleted file mode 100644
index 10a44d6b8..000000000
--- a/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-antipatterns.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# Codex Prompt Anti-Patterns
-
-Avoid these when prompting Codex or GPT-5.4.
-
-## Vague task framing
-
-Bad:
-
-```text
-Take a look at this and let me know what you think.
-```
-
-Better:
-
-```xml
-
-Review this change for material correctness and regression risks.
-
-```
-
-## Missing output contract
-
-Bad:
-
-```text
-Investigate and report back.
-```
-
-Better:
-
-```xml
-
-Return:
-1. root cause
-2. evidence
-3. smallest safe next step
-
-```
-
-## No follow-through default
-
-Bad:
-
-```text
-Debug this failure.
-```
-
-Better:
-
-```xml
-
-Keep going until you have enough evidence to identify the root cause confidently.
-
-```
-
-## Asking for more reasoning instead of a better contract
-
-Bad:
-
-```text
-Think harder and be very smart.
-```
-
-Better:
-
-```xml
-
-Before finalizing, verify that the answer matches the observed evidence and task requirements.
-
-```
-
-## Mixing unrelated jobs into one run
-
-Bad:
-
-```text
-Review this diff, fix the bug you find, update the docs, and suggest a roadmap.
-```
-
-Better:
-- Run review first.
-- Run a separate fix prompt if needed.
-- Use a third run for docs or roadmap work.
-
-## Unsupported certainty
-
-Bad:
-
-```text
-Tell me exactly why production failed.
-```
-
-Better:
-
-```xml
-
-Ground every claim in the provided context or tool outputs.
-If a point is an inference, label it clearly.
-
-```
diff --git a/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-recipes.md b/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-recipes.md
deleted file mode 100644
index 7711de201..000000000
--- a/plugins/codex/skills/gpt-5-4-prompting/references/codex-prompt-recipes.md
+++ /dev/null
@@ -1,150 +0,0 @@
-# Codex Prompt Recipes
-
-Use these as starting templates for Codex task prompts or other Codex/GPT-5.4 prompt construction.
-Copy the smallest recipe that fits the task, then trim anything you do not need.
-In `codex:codex-rescue`, run diagnosis and fix-oriented recipes in write mode by default unless the user explicitly asked for read-only behavior.
-
-## Diagnosis
-
-```xml
-
-Diagnose why the failing test or command is breaking in this repository.
-Use the available repository context and tools to identify the most likely root cause.
-
-
-
-Return a compact diagnosis with:
-1. most likely root cause
-2. evidence
-3. smallest safe next step
-
-
-
-Keep going until you have enough evidence to identify the root cause confidently.
-Only stop to ask questions when a missing detail changes correctness materially.
-
-
-
-Before finalizing, verify that the proposed root cause matches the observed evidence.
-
-
-
-Do not guess missing repository facts.
-If required context is absent, state exactly what remains unknown.
-
-```
-
-## Narrow Fix
-
-```xml
-
-Implement the smallest safe fix for the identified issue in this repository.
-Preserve existing behavior outside the failing path.
-
-
-
-Return:
-1. summary of the fix
-2. touched files
-3. verification performed
-4. residual risks or follow-ups
-
-
-
-Default to the most reasonable low-risk interpretation and keep going.
-
-
-
-Resolve the task fully before stopping.
-Do not stop after identifying the issue without applying the fix.
-
-
-
-Before finalizing, verify that the fix matches the task requirements and that the changed code is coherent.
-
-
-
-Keep changes tightly scoped to the stated task.
-Avoid unrelated refactors or cleanup.
-
-```
-
-## Root-Cause Review
-
-```xml
-
-Analyze this change for the most likely correctness or regression issues.
-Focus on the provided repository context only.
-
-
-
-Return:
-1. findings ordered by severity
-2. supporting evidence for each finding
-3. brief next steps
-
-
-
-Ground every claim in the repository context or tool outputs.
-If a point is an inference, label it clearly.
-
-
-
-Check for second-order failures, empty-state handling, retries, stale state, and rollback paths before finalizing.
-
-
-
-Before finalizing, verify that each finding is material and actionable.
-
-```
-
-## Research Or Recommendation
-
-```xml
-
-Research the available options and recommend the best path for this task.
-
-
-
-Return:
-1. observed facts
-2. reasoned recommendation
-3. tradeoffs
-4. open questions
-
-
-
-Separate observed facts, reasoned inferences, and open questions.
-Prefer breadth first, then go deeper only where the evidence changes the recommendation.
-
-
-
-Back important claims with explicit references to the sources you inspected.
-Prefer primary sources.
-
-```
-
-## Prompt-Patching
-
-```xml
-
-Diagnose why this existing prompt is underperforming and propose the smallest high-leverage changes to improve it for Codex or GPT-5.4.
-
-
-
-Return:
-1. failure modes
-2. root causes in the current prompt
-3. a revised prompt
-4. why the revision should work better
-
-
-
-Base your diagnosis on the prompt text and the failure examples provided.
-Do not invent failure modes that are not supported by the examples.
-
-
-
-Before finalizing, make sure the revised prompt resolves the cited failure modes without adding contradictory instructions.
-
-```
diff --git a/plugins/codex/skills/gpt-5-4-prompting/references/prompt-blocks.md b/plugins/codex/skills/gpt-5-4-prompting/references/prompt-blocks.md
deleted file mode 100644
index cbf669400..000000000
--- a/plugins/codex/skills/gpt-5-4-prompting/references/prompt-blocks.md
+++ /dev/null
@@ -1,172 +0,0 @@
-# Prompt Blocks
-
-Use these blocks selectively when composing Codex or GPT-5.4 prompts.
-Wrap each block in the XML tag shown in its heading.
-
-## Core Wrapper
-
-### `task`
-
-Use in nearly every prompt.
-
-```xml
-
-Describe the concrete job, the relevant repository or failure context, and the expected end state.
-
-```
-
-## Output and Format
-
-### `structured_output_contract`
-
-Use when the response shape matters.
-
-```xml
-
-Return exactly the requested output shape and nothing else.
-Keep the answer compact.
-Put the highest-value findings or decisions first.
-
-```
-
-### `compact_output_contract`
-
-Use when you want concise prose instead of a schema.
-
-```xml
-
-Keep the final answer compact and structured.
-Do not include long scene-setting or repeated recap.
-
-```
-
-## Follow-through and Completion
-
-### `default_follow_through_policy`
-
-Use when Codex should act without asking routine questions.
-
-```xml
-
-Default to the most reasonable low-risk interpretation and keep going.
-Only stop to ask questions when a missing detail changes correctness, safety, or an irreversible action.
-
-```
-
-### `completeness_contract`
-
-Use for debugging, implementation, or any multi-step task that should not stop early.
-
-```xml
-
-Resolve the task fully before stopping.
-Do not stop at the first plausible answer.
-Check whether there are follow-on fixes, edge cases, or cleanup needed for a correct result.
-
-```
-
-### `verification_loop`
-
-Use when correctness matters.
-
-```xml
-
-Before finalizing, verify the result against the task requirements and the changed files or tool outputs.
-If a check fails, revise the answer instead of reporting the first draft.
-
-```
-
-## Grounding and Missing Context
-
-### `missing_context_gating`
-
-Use when Codex might otherwise guess.
-
-```xml
-
-Do not guess missing repository facts.
-If required context is absent, retrieve it with tools or state exactly what remains unknown.
-
-```
-
-### `grounding_rules`
-
-Use for review, research, or root-cause analysis.
-
-```xml
-
-Ground every claim in the provided context or your tool outputs.
-Do not present inferences as facts.
-If a point is a hypothesis, label it clearly.
-
-```
-
-### `citation_rules`
-
-Use when external research or quotes matter.
-
-```xml
-
-Back important claims with citations or explicit references to the source material you inspected.
-Prefer primary sources.
-
-```
-
-## Safety and Scope
-
-### `action_safety`
-
-Use for write-capable or potentially broad tasks.
-
-```xml
-
-Keep changes tightly scoped to the stated task.
-Avoid unrelated refactors, renames, or cleanup unless they are required for correctness.
-Call out any risky or irreversible action before taking it.
-
-```
-
-### `tool_persistence_rules`
-
-Use for long-running tool-heavy tasks.
-
-```xml
-
-Keep using tools until you have enough evidence to finish the task confidently.
-Do not abandon the workflow after a partial read when another targeted check would change the answer.
-
-```
-
-## Task-Specific Blocks
-
-### `research_mode`
-
-Use for exploration, comparisons, or recommendations.
-
-```xml
-
-Separate observed facts, reasoned inferences, and open questions.
-Prefer breadth first, then go deeper only where the evidence changes the recommendation.
-
-```
-
-### `dig_deeper_nudge`
-
-Use for review and adversarial inspection.
-
-```xml
-
-After you find the first plausible issue, check for second-order failures, empty-state behavior, retries, stale state, and rollback paths before you finalize.
-
-```
-
-### `progress_updates`
-
-Use when the run may take a while.
-
-```xml
-
-If you provide progress updates, keep them brief and outcome-based.
-Mention only major phase changes or blockers.
-
-```
diff --git a/scripts/prepare-generated-dir.mjs b/scripts/prepare-generated-dir.mjs
new file mode 100644
index 000000000..7d78dd0bd
--- /dev/null
+++ b/scripts/prepare-generated-dir.mjs
@@ -0,0 +1,3 @@
+import fs from "node:fs";
+
+fs.mkdirSync(new URL("../plugins/codex/.generated/app-server-types", import.meta.url), { recursive: true });
diff --git a/tests/broker-lifecycle.test.mjs b/tests/broker-lifecycle.test.mjs
new file mode 100644
index 000000000..cc477a10c
--- /dev/null
+++ b/tests/broker-lifecycle.test.mjs
@@ -0,0 +1,182 @@
+import fs from "node:fs";
+import net from "node:net";
+import path from "node:path";
+import test from "node:test";
+import assert from "node:assert/strict";
+import { spawn } from "node:child_process";
+import { fileURLToPath, pathToFileURL } from "node:url";
+
+import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs";
+import { initGitRepo, makeTempDir } from "./helpers.mjs";
+import { withBrokerLock } from "../plugins/codex/scripts/lib/broker-lock.mjs";
+import { createBrokerEndpoint, parseBrokerEndpoint } from "../plugins/codex/scripts/lib/broker-endpoint.mjs";
+import {
+ loadBrokerSession,
+ loadReusableBrokerSession,
+ sendBrokerShutdown
+} from "../plugins/codex/scripts/lib/broker-lifecycle.mjs";
+import { resolveStateDir } from "../plugins/codex/scripts/lib/state.mjs";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const BROKER_LIFECYCLE = path.join(
+ ROOT,
+ "plugins",
+ "codex",
+ "scripts",
+ "lib",
+ "broker-lifecycle.mjs"
+);
+const BROKER_LIFECYCLE_URL = pathToFileURL(BROKER_LIFECYCLE).href;
+
+function startEnsureProcess(cwd, env) {
+ const source = [
+ `import { ensureBrokerSession } from ${JSON.stringify(BROKER_LIFECYCLE_URL)};`,
+ "const session = await ensureBrokerSession(process.cwd());",
+ "console.log(JSON.stringify(session));"
+ ].join("\n");
+ const child = spawn(process.execPath, ["--input-type=module", "-e", source], {
+ cwd,
+ env,
+ stdio: ["ignore", "pipe", "pipe"]
+ });
+ let stdout = "";
+ let stderr = "";
+ child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
+ child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
+ return new Promise((resolve) => {
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
+ });
+}
+
+async function startProbeBroker(endpoint, { busy, shutdownResponseChunks = ['{"id":1,"result":{}}\n'] }) {
+ const requests = [];
+ const target = parseBrokerEndpoint(endpoint);
+ if (target.kind === "unix") {
+ fs.rmSync(target.path, { force: true });
+ }
+ const server = net.createServer((socket) => {
+ socket.setEncoding("utf8");
+ let buffer = "";
+ socket.on("data", (chunk) => {
+ buffer += chunk;
+ let newlineIndex = buffer.indexOf("\n");
+ while (newlineIndex !== -1) {
+ const line = buffer.slice(0, newlineIndex);
+ buffer = buffer.slice(newlineIndex + 1);
+ newlineIndex = buffer.indexOf("\n");
+ if (!line.trim()) continue;
+ const message = JSON.parse(line);
+ requests.push(message.method);
+ if (message.method === "initialize") {
+ socket.write('{"id":1,"result":{"userAgent":"probe"}}\n');
+ } else if (message.method === "thread/list") {
+ socket.write(
+ busy
+ ? '{"id":2,"error":{"code":-32001,"message":"Shared Codex broker is busy."}}\n'
+ : '{"id":2,"result":{"data":[],"nextCursor":null}}\n'
+ );
+ } else if (message.method === "broker/shutdown") {
+ for (const chunk of shutdownResponseChunks) socket.write(chunk);
+ }
+ }
+ });
+ });
+ await new Promise((resolve) => server.listen(target.path, resolve));
+ return {
+ requests,
+ close: async () => {
+ await new Promise((resolve) => server.close(resolve));
+ if (target.kind === "unix") fs.rmSync(target.path, { force: true });
+ }
+ };
+}
+
+test("concurrent startup creates and records only one shared broker", async () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+ const env = buildEnv(binDir);
+
+ const [first, second] = await Promise.all([
+ startEnsureProcess(repo, env),
+ startEnsureProcess(repo, env)
+ ]);
+
+ assert.equal(first.code, 0, first.stderr);
+ assert.equal(second.code, 0, second.stderr);
+ assert.equal(JSON.parse(first.stdout).endpoint, JSON.parse(second.stdout).endpoint);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).appServerStarts, 1);
+
+ const broker = loadBrokerSession(repo);
+ await sendBrokerShutdown(broker.endpoint);
+});
+
+test("broker lock recovers immediately when its owner process is gone", async () => {
+ const repo = makeTempDir();
+ const stateDir = resolveStateDir(repo);
+ fs.mkdirSync(stateDir, { recursive: true });
+ fs.writeFileSync(path.join(stateDir, "broker.lock"), "2147483647:0:abandoned", "utf8");
+
+ const result = await withBrokerLock(repo, { lockTimeoutMs: 100 }, async () => "acquired");
+
+ assert.equal(result, "acquired");
+ assert.equal(fs.existsSync(path.join(stateDir, "broker.lock")), false);
+});
+
+test("stale reachable brokers are preserved when the broker reports an active turn", async () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const sessionDir = makeTempDir();
+ const endpoint = createBrokerEndpoint(sessionDir);
+ const probeBroker = await startProbeBroker(endpoint, { busy: true });
+ installFakeCodex(binDir, "review-ok", "codex-cli 0.144.0");
+ initGitRepo(repo);
+
+ const stateDir = resolveStateDir(repo);
+ fs.mkdirSync(stateDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(stateDir, "broker.json"),
+ `${JSON.stringify(
+ {
+ endpoint,
+ pidFile: path.join(sessionDir, "broker.pid"),
+ logFile: path.join(sessionDir, "broker.log"),
+ sessionDir,
+ pid: 999999,
+ runtime: { pluginVersion: "1.0.6", codexVersion: "codex-cli 0.143.0" }
+ },
+ null,
+ 2
+ )}\n`
+ );
+
+ let killed = false;
+ const options = {
+ env: buildEnv(binDir),
+ killProcess: () => {
+ killed = true;
+ }
+ };
+ const result = await loadReusableBrokerSession(repo, options);
+
+ assert.equal(result, null);
+ assert.equal(killed, false);
+ assert.equal(options.deferBrokerReplacement, true);
+ assert.equal(probeBroker.requests.includes("broker/shutdown"), false);
+ assert.equal(fs.existsSync(path.join(stateDir, "broker.json")), true);
+ await probeBroker.close();
+});
+
+test("broker shutdown accepts a response split across socket chunks", async () => {
+ const sessionDir = makeTempDir();
+ const endpoint = createBrokerEndpoint(sessionDir);
+ const probeBroker = await startProbeBroker(endpoint, {
+ busy: false,
+ shutdownResponseChunks: ['{"id":1,"result":', '{}', '}\n']
+ });
+
+ assert.equal(await sendBrokerShutdown(endpoint), true);
+ await probeBroker.close();
+});
diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs
index c34b06059..0ae3a83fd 100644
--- a/tests/commands.test.mjs
+++ b/tests/commands.test.mjs
@@ -22,6 +22,9 @@ test("review command uses AskUserQuestion and background Bash while staying revi
assert.match(source, /```typescript/);
assert.match(source, /review "\$ARGUMENTS"/);
assert.match(source, /\[--scope auto\|working-tree\|branch\]/);
+ assert.match(source, /--model /);
+ assert.match(source, /--effort /);
+ assert.match(source, /are not focus text/i);
assert.match(source, /run_in_background:\s*true/);
assert.match(source, /command:\s*`node "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/codex-companion\.mjs" review "\$ARGUMENTS"`/);
assert.match(source, /description:\s*"Codex review"/);
@@ -49,7 +52,10 @@ test("adversarial review command uses AskUserQuestion and background Bash while
assert.match(source, /```bash/);
assert.match(source, /```typescript/);
assert.match(source, /adversarial-review "\$ARGUMENTS"/);
- assert.match(source, /\[--scope auto\|working-tree\|branch\] \[focus \.\.\.\]/);
+ assert.match(source, /\[--scope auto\|working-tree\|branch\].*\[focus \.\.\.\]/);
+ assert.match(source, /--model /);
+ assert.match(source, /--effort /);
+ assert.match(source, /must not become part of the focus text/i);
assert.match(source, /run_in_background:\s*true/);
assert.match(source, /command:\s*`node "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/codex-companion\.mjs" adversarial-review "\$ARGUMENTS"`/);
assert.match(source, /description:\s*"Codex adversarial review"/);
@@ -75,6 +81,7 @@ test("continue is not exposed as a user-facing command", () => {
assert.deepEqual(commandFiles, [
"adversarial-review.md",
"cancel.md",
+ "orchestrate.md",
"rescue.md",
"result.md",
"review.md",
@@ -104,7 +111,7 @@ test("rescue command absorbs continue semantics", () => {
assert.match(rescue, /--background\|--wait/);
assert.match(rescue, /--resume\|--fresh/);
assert.match(rescue, /--model /);
- assert.match(rescue, /--effort /);
+ assert.match(rescue, /--effort /);
assert.match(rescue, /task-resume-candidate --json/);
assert.match(rescue, /AskUserQuestion/);
assert.match(rescue, /Continue current Codex thread/);
@@ -114,7 +121,7 @@ test("rescue command absorbs continue semantics", () => {
assert.match(rescue, /Do not forward them to `task`/i);
assert.match(rescue, /`--model` and `--effort` are runtime-selection flags/i);
assert.match(rescue, /Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort/i);
- assert.match(rescue, /If they ask for `spark`, map it to `gpt-5\.3-codex-spark`/i);
+ assert.match(rescue, /If they ask for `spark`, map it to `gpt-5\.6-luna`/i);
assert.match(rescue, /If the request includes `--resume`, do not ask whether to continue/i);
assert.match(rescue, /If the request includes `--fresh`, do not ask whether to continue/i);
assert.match(rescue, /If the user chooses continue, add `--resume`/i);
@@ -134,29 +141,27 @@ test("rescue command absorbs continue semantics", () => {
assert.match(agent, /Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
assert.match(agent, /Leave `--effort` unset unless the user explicitly requests a specific reasoning effort/i);
assert.match(agent, /Leave model unset by default/i);
- assert.match(agent, /If the user asks for `spark`, map that to `--model gpt-5\.3-codex-spark`/i);
- assert.match(agent, /If the user asks for a concrete model name such as `gpt-5\.4-mini`, pass it through with `--model`/i);
+ assert.match(agent, /If the user asks for `spark`, map that to `--model gpt-5\.6-luna`/i);
assert.match(agent, /Return the stdout of the `codex-companion` command exactly as-is/i);
assert.match(agent, /If the Bash call fails or Codex cannot be invoked, return nothing/i);
- assert.match(agent, /gpt-5-4-prompting/);
+ assert.match(agent, /codex-prompting/);
assert.match(agent, /only to tighten the user's request into a better Codex prompt/i);
assert.match(agent, /Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work/i);
assert.match(runtimeSkill, /only job is to invoke `task` once and return that stdout unchanged/i);
assert.match(runtimeSkill, /Do not call `setup`, `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
- assert.match(runtimeSkill, /use the `gpt-5-4-prompting` skill to rewrite the user's request into a tighter Codex prompt/i);
+ assert.match(runtimeSkill, /use the `codex-prompting` skill to rewrite the user's request into a tighter Codex prompt/i);
assert.match(runtimeSkill, /That prompt drafting is the only Claude-side work allowed/i);
assert.match(runtimeSkill, /Leave `--effort` unset unless the user explicitly requests a specific effort/i);
assert.match(runtimeSkill, /Leave model unset by default/i);
- assert.match(runtimeSkill, /Map `spark` to `--model gpt-5\.3-codex-spark`/i);
+ assert.match(runtimeSkill, /Map `spark` to `--model gpt-5\.6-luna`/i);
assert.match(runtimeSkill, /If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only/i);
assert.match(runtimeSkill, /Strip it before calling `task`/i);
- assert.match(runtimeSkill, /`--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`/i);
+ assert.match(runtimeSkill, /`--effort`: accepted transport values are `none`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`/i);
assert.match(runtimeSkill, /Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(runtimeSkill, /If the Bash call fails or Codex cannot be invoked, return nothing/i);
assert.match(readme, /`codex:codex-rescue` subagent/i);
assert.match(readme, /if you do not pass `--model` or `--effort`, Codex chooses its own defaults/i);
- assert.match(readme, /--model gpt-5\.4-mini --effort medium/i);
- assert.match(readme, /`spark`, the plugin maps that to `gpt-5\.3-codex-spark`/i);
+ assert.match(readme, /`spark`, the plugin maps that to `gpt-5\.6-luna`/i);
assert.match(readme, /continue a previous Codex task/i);
assert.match(readme, /### `\/codex:setup`/);
assert.match(readme, /### `\/codex:review`/);
@@ -174,32 +179,35 @@ test("transfer, result, and cancel commands are exposed as deterministic runtime
const transfer = read("commands/transfer.md");
const result = read("commands/result.md");
const cancel = read("commands/cancel.md");
+ const dispatcher = read("scripts/orchestration/dispatch.mjs");
const resultHandling = read("skills/codex-result-handling/SKILL.md");
assert.match(transfer, /disable-model-invocation:\s*true/);
assert.match(transfer, /codex-companion\.mjs" transfer "\$ARGUMENTS"/);
assert.match(transfer, /codex resume /);
assert.match(result, /disable-model-invocation:\s*true/);
- assert.match(result, /codex-companion\.mjs" result "\$ARGUMENTS"/);
+ assert.match(result, /orchestration\/dispatch\.mjs" result "\$ARGUMENTS"/);
assert.match(cancel, /disable-model-invocation:\s*true/);
- assert.match(cancel, /codex-companion\.mjs" cancel "\$ARGUMENTS"/);
+ assert.match(cancel, /orchestration\/dispatch\.mjs" cancel "\$ARGUMENTS"/);
+ assert.match(dispatcher, /codex-companion\.mjs/);
+ assert.match(dispatcher, /orchestration.*cli\.mjs/);
assert.match(resultHandling, /do not turn a failed or incomplete Codex run into a Claude-side implementation attempt/i);
assert.match(resultHandling, /if Codex was never successfully invoked, do not generate a substitute answer at all/i);
});
test("internal docs use task terminology for rescue runs", () => {
const runtimeSkill = read("skills/codex-cli-runtime/SKILL.md");
- const promptingSkill = read("skills/gpt-5-4-prompting/SKILL.md");
- const promptRecipes = read("skills/gpt-5-4-prompting/references/codex-prompt-recipes.md");
+ const promptingSkill = read("skills/codex-prompting/SKILL.md");
+ const promptRecipes = read("skills/codex-prompting/references/codex-prompt-recipes.md");
assert.match(runtimeSkill, /codex-companion\.mjs" task ""/);
assert.match(runtimeSkill, /Use `task` for every rescue request/i);
assert.match(runtimeSkill, /task --resume-last/i);
- assert.match(promptingSkill, /Use `task` when the task is diagnosis/i);
- assert.match(promptRecipes, /Codex task prompts/i);
- assert.match(promptRecipes, /Use these as starting templates for Codex task prompts/i);
+ assert.match(promptingSkill, /concrete job and expected end state/i);
+ assert.match(promptRecipes, /# Codex Prompt Recipes/);
+ assert.match(promptRecipes, /Use these as starting points/i);
assert.match(promptRecipes, /## Diagnosis/);
- assert.match(promptRecipes, /## Narrow Fix/);
+ assert.match(promptRecipes, /## Narrow fix/);
});
test("hooks keep session-end cleanup and stop gating enabled", () => {
@@ -214,10 +222,11 @@ test("setup command can offer Codex install and still points users to codex logi
const setup = read("commands/setup.md");
const readme = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");
- assert.match(setup, /argument-hint:\s*'\[--enable-review-gate\|--disable-review-gate\]'/);
+ assert.match(setup, /argument-hint:\s*'\[--enable-review-gate\|--disable-review-gate\] \[--enable-orchestration\|--disable-orchestration\]'/);
assert.match(setup, /AskUserQuestion/);
assert.match(setup, /npm install -g @openai\/codex/);
- assert.match(setup, /codex-companion\.mjs" setup --json \$ARGUMENTS/);
+ assert.match(setup, /orchestration\/setup-dispatch\.mjs" --json \$ARGUMENTS/);
+ assert.match(read("scripts/orchestration/setup-dispatch.mjs"), /codex-companion\.mjs.*setup/);
assert.match(readme, /!codex login/);
assert.match(readme, /offer to install Codex for you/i);
assert.match(readme, /\/codex:setup --enable-review-gate/);
diff --git a/tests/fake-codex-fixture.mjs b/tests/fake-codex-fixture.mjs
index f83c96a0d..0e3bfc649 100644
--- a/tests/fake-codex-fixture.mjs
+++ b/tests/fake-codex-fixture.mjs
@@ -4,7 +4,7 @@ import process from "node:process";
import { writeExecutable } from "./helpers.mjs";
-export function installFakeCodex(binDir, behavior = "review-ok") {
+export function installFakeCodex(binDir, behavior = "review-ok", version = "codex-cli test") {
const statePath = path.join(binDir, "fake-codex-state.json");
const scriptPath = path.join(binDir, "codex");
const source = `#!/usr/bin/env node
@@ -14,7 +14,8 @@ const path = require("node:path");
const readline = require("node:readline");
const STATE_PATH = ${JSON.stringify(statePath)};
- const BEHAVIOR = ${JSON.stringify(behavior)};
+ const BEHAVIOR = ${JSON.stringify(behavior)};
+ const VERSION = ${JSON.stringify(version)};
const interruptibleTurns = new Map();
function loadState() {
@@ -25,7 +26,9 @@ const readline = require("node:readline");
}
function saveState(state) {
- fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
+ const tempPath = STATE_PATH + "." + process.pid + "." + crypto.randomUUID() + ".tmp";
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
+ fs.renameSync(tempPath, STATE_PATH);
}
function requiresExperimental(field, message, state) {
@@ -71,7 +74,7 @@ function buildAccountReadResult() {
case "auth-run-fails":
return { account: null, requiresOpenaiAuth: true };
case "provider-no-auth":
- case "env-key-provider":
+ case "env-key-provider":
return { account: null, requiresOpenaiAuth: false };
case "api-key-account-only":
return { account: { type: "apiKey" }, requiresOpenaiAuth: true };
@@ -88,8 +91,33 @@ function buildConfigReadResult() {
case "provider-no-auth":
return {
config: { model_provider: "ollama" },
- origins: {}
- };
+ origins: {}
+ };
+ case "custom-provider":
+ return {
+ config: { model_provider: "custom" },
+ origins: {}
+ };
+ case "inherited-sol-max":
+ return {
+ config: { model_provider: "openai", model: "gpt-5.6-sol", model_reasoning_effort: "max" },
+ origins: {}
+ };
+ case "inherited-luna-ultra":
+ return {
+ config: { model_provider: "openai", model: "gpt-5.6-luna", model_reasoning_effort: "ultra" },
+ origins: {}
+ };
+ case "inherited-default-luna-ultra":
+ return {
+ config: { model_provider: "openai", model_reasoning_effort: "ultra" },
+ origins: {}
+ };
+ case "config-luna":
+ return {
+ config: { model_provider: "openai", model: "gpt-5.6-luna", model_reasoning_effort: "high" },
+ origins: {}
+ };
case "env-key-provider":
return {
config: {
@@ -249,7 +277,7 @@ function taskPayload(prompt, resume) {
const args = process.argv.slice(2);
if (args[0] === "--version") {
- console.log("codex-cli test");
+ console.log(VERSION);
process.exit(0);
}
if (args[0] === "app-server" && args[1] === "--help") {
@@ -312,8 +340,27 @@ rl.on("line", (line) => {
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
throw new Error("thread/start.persistFullHistory requires experimentalApi capability");
}
- const thread = nextThread(state, message.params.cwd, message.params.ephemeral);
- send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
+ const thread = nextThread(state, message.params.cwd, message.params.ephemeral);
+ const inheritedSelection = BEHAVIOR === "inherited-sol-max"
+ ? { model: "gpt-5.6-sol", effort: "max" }
+ : BEHAVIOR === "inherited-luna-ultra"
+ ? { model: "gpt-5.6-luna", effort: "ultra" }
+ : BEHAVIOR === "inherited-default-luna-ultra"
+ ? { model: "gpt-5.6-luna", effort: "ultra" }
+ : null;
+ const selectedModel = message.params.model || inheritedSelection?.model || "gpt-5.6-terra";
+ const selectedEffort = message.params.config?.model_reasoning_effort || inheritedSelection?.effort || null;
+ const modelProvider = BEHAVIOR === "custom-provider" ? "custom" : "openai";
+ thread.model = selectedModel;
+ thread.reasoningEffort = selectedEffort;
+ state.lastThreadStart = {
+ model: selectedModel,
+ effort: selectedEffort,
+ config: message.params.config ?? null,
+ sandbox: message.params.sandbox ?? null
+ };
+ saveState(state);
+ send({ id: message.id, result: { thread: buildThread(thread), model: selectedModel, modelProvider, serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: selectedEffort } });
send({ method: "thread/started", params: { thread: { id: thread.id } } });
break;
}
@@ -347,10 +394,44 @@ rl.on("line", (line) => {
const thread = ensureThread(state, message.params.threadId);
thread.updatedAt = now();
saveState(state);
- send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
- break;
+ const selectedModel = message.params.model || thread.model || "gpt-5.6-terra";
+ const selectedEffort = BEHAVIOR === "inherited-sol-max" ? "max" : thread.reasoningEffort || null;
+ state.lastThreadResume = {
+ model: selectedModel,
+ effort: selectedEffort,
+ sandbox: message.params.sandbox ?? null
+ };
+ saveState(state);
+ send({ id: message.id, result: { thread: buildThread(thread), model: selectedModel, modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: selectedEffort } });
+ break;
}
+ case "model/list": {
+ if (BEHAVIOR === "model-list-unsupported") {
+ send({ id: message.id, error: { code: -32601, message: "Unsupported method: model/list" } });
+ break;
+ }
+ const model = (name, efforts) => ({
+ id: name,
+ model: name,
+ isDefault: BEHAVIOR === "inherited-default-luna-ultra" && name === "gpt-5.6-luna",
+ hidden: false,
+ supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: reasoningEffort }))
+ });
+ send({
+ id: message.id,
+ result: {
+ data: [
+ model("gpt-5.6-sol", ["low", "medium", "high", "xhigh", "max", "ultra"]),
+ model("gpt-5.6-terra", ["low", "medium", "high", "xhigh", "max", "ultra"]),
+ model("gpt-5.6-luna", ["low", "medium", "high", "xhigh", "max"])
+ ],
+ nextCursor: null
+ }
+ });
+ break;
+ }
+
case "externalAgentConfig/import": {
if (BEHAVIOR === "external-import-unsupported") {
send({ id: message.id, error: { code: -32601, message: "Unsupported method: externalAgentConfig/import" } });
@@ -437,13 +518,25 @@ rl.on("line", (line) => {
}
case "turn/start": {
+ if (BEHAVIOR === "reject-gpt-5.6" && String(message.params.model || "").startsWith("gpt-5.6-")) {
+ send({
+ id: message.id,
+ error: {
+ code: -32000,
+ message: "The '" + message.params.model + "' model requires a newer version of Codex."
+ }
+ });
+ break;
+ }
const thread = ensureThread(state, message.params.threadId);
const prompt = (message.params.input || [])
.filter((item) => item.type === "text")
.map((item) => item.text)
.join("\\n");
- const turnId = nextTurnId(state);
- thread.updatedAt = now();
+ const turnId = nextTurnId(state);
+ thread.updatedAt = now();
+ thread.model = message.params.model ?? thread.model ?? null;
+ thread.reasoningEffort = message.params.effort ?? thread.reasoningEffort ?? null;
state.lastTurnStart = {
threadId: message.params.threadId,
turnId,
diff --git a/tests/helpers.mjs b/tests/helpers.mjs
index d6981197a..172e9b749 100644
--- a/tests/helpers.mjs
+++ b/tests/helpers.mjs
@@ -12,13 +12,37 @@ export function writeExecutable(filePath, source) {
fs.writeFileSync(filePath, source, { encoding: "utf8", mode: 0o755 });
}
+function ensureWindowsNodeShim(env) {
+ if (process.platform !== "win32") return;
+ const searchPath = env?.PATH ?? process.env.PATH ?? "";
+ for (const directory of searchPath.split(path.delimiter).filter(Boolean)) {
+ const extensionlessNode = path.join(directory, "node");
+ const nodeExe = path.join(directory, "node.exe");
+ const nodeCmd = path.join(directory, "node.cmd");
+ if (
+ fs.existsSync(extensionlessNode)
+ && !fs.existsSync(nodeExe)
+ && !fs.existsSync(nodeCmd)
+ ) {
+ fs.writeFileSync(
+ nodeCmd,
+ `@echo off\r\n"${process.execPath}" %*\r\n`,
+ "utf8"
+ );
+ return;
+ }
+ }
+}
+
export function run(command, args, options = {}) {
- return spawnSync(command, args, {
+ ensureWindowsNodeShim(options.env);
+ const executable = command === "node" ? process.execPath : command;
+ return spawnSync(executable, args, {
cwd: options.cwd,
env: options.env,
encoding: "utf8",
input: options.input,
- shell: options.shell ?? (process.platform === "win32" && !path.isAbsolute(command)),
+ shell: options.shell ?? (process.platform === "win32" && !path.isAbsolute(executable)),
windowsHide: true
});
}
diff --git a/tests/model-catalog.test.mjs b/tests/model-catalog.test.mjs
new file mode 100644
index 000000000..af6046d1f
--- /dev/null
+++ b/tests/model-catalog.test.mjs
@@ -0,0 +1,95 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { validateReasoningSelection } from "../plugins/codex/scripts/lib/model-catalog.mjs";
+
+function clientWith(models) {
+ return {
+ async request(method) {
+ assert.equal(method, "model/list");
+ return { data: models, nextCursor: null };
+ }
+ };
+}
+
+function model(name, efforts, isDefault = false) {
+ return {
+ id: name,
+ model: name,
+ isDefault,
+ supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort }))
+ };
+}
+
+test("catalog accepts Ultra for Sol and Terra", async () => {
+ const client = clientWith([
+ model("gpt-5.6-sol", ["high", "max", "ultra"]),
+ model("gpt-5.6-terra", ["high", "max", "ultra"])
+ ]);
+
+ await validateReasoningSelection(client, {
+ model: "gpt-5.6-sol",
+ effort: "ultra",
+ modelProvider: "openai"
+ });
+ await validateReasoningSelection(client, {
+ model: "gpt-5.6-terra",
+ effort: "ultra",
+ modelProvider: "openai"
+ });
+});
+
+test("catalog rejects Luna with Ultra and lists supported efforts", async () => {
+ const client = clientWith([model("gpt-5.6-luna", ["low", "medium", "high", "xhigh", "max"])]);
+
+ await assert.rejects(
+ validateReasoningSelection(client, {
+ model: "gpt-5.6-luna",
+ effort: "ultra",
+ modelProvider: "openai"
+ }),
+ /Reasoning effort "ultra" is not supported by model "gpt-5\.6-luna".*low, medium, high, xhigh, max/i
+ );
+});
+
+test("catalog validates effort against the default model when no model is selected", async () => {
+ const client = clientWith([
+ model("gpt-5.6-luna", ["low", "medium", "high", "xhigh", "max"], true)
+ ]);
+
+ await assert.rejects(
+ validateReasoningSelection(client, { effort: "ultra", modelProvider: "openai" }),
+ /Reasoning effort "ultra" is not supported by model "gpt-5\.6-luna"/i
+ );
+});
+
+test("catalog fallback allows older CLIs without model/list", async () => {
+ const client = {
+ async request() {
+ const error = new Error("Unsupported method: model/list");
+ error.rpcCode = -32601;
+ throw error;
+ }
+ };
+
+ await validateReasoningSelection(client, {
+ model: "gpt-5.6-sol",
+ effort: "ultra",
+ modelProvider: "openai"
+ });
+});
+
+test("catalog does not block custom providers or unknown models", async () => {
+ const client = clientWith([model("gpt-5.6-luna", ["high"])]);
+
+ await validateReasoningSelection(client, {
+ model: "gpt-5.6-luna",
+ effort: "ultra",
+ modelProvider: "custom"
+ });
+ await validateReasoningSelection(client, {
+ model: "custom-model",
+ effort: "ultra",
+ modelProvider: "openai"
+ });
+});
diff --git a/tests/model-policy.test.mjs b/tests/model-policy.test.mjs
new file mode 100644
index 000000000..aee0f4fc7
--- /dev/null
+++ b/tests/model-policy.test.mjs
@@ -0,0 +1,41 @@
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex");
+const SCRIPT = path.join(PLUGIN_ROOT, "scripts", "codex-companion.mjs");
+
+test("spark resolves to Luna", () => {
+ const source = fs.readFileSync(SCRIPT, "utf8");
+ const retiredSparkModel = new RegExp(["gpt", "5\\.3", "codex", "spark"].join("-"));
+ assert.match(source, /\["spark", "gpt-5\.6-luna"\]/);
+ assert.doesNotMatch(source, retiredSparkModel);
+});
+
+test("minimal effort is rejected before Codex starts", () => {
+ const result = spawnSync(
+ process.execPath,
+ [SCRIPT, "task", "--effort", "minimal", "no-op"],
+ { cwd: ROOT, encoding: "utf8" }
+ );
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /Unsupported reasoning effort "minimal"/);
+});
+
+test("command hints omit minimal effort", () => {
+ const files = [
+ "commands/review.md",
+ "commands/adversarial-review.md",
+ "commands/rescue.md",
+ "skills/codex-cli-runtime/SKILL.md"
+ ];
+ for (const relativePath of files) {
+ const source = fs.readFileSync(path.join(PLUGIN_ROOT, relativePath), "utf8");
+ assert.doesNotMatch(source, /minimal/);
+ }
+});
diff --git a/tests/orchestration-contracts.test.mjs b/tests/orchestration-contracts.test.mjs
new file mode 100644
index 000000000..4862fd782
--- /dev/null
+++ b/tests/orchestration-contracts.test.mjs
@@ -0,0 +1,15 @@
+
+import test from "node:test";
+import assert from "node:assert/strict";
+import { DEFAULT_ORCHESTRATION_CONFIG } from "../plugins/codex/scripts/orchestration/config.mjs";
+import { normalizeOrchestrationPlan } from "../plugins/codex/scripts/orchestration/plan-contract.mjs";
+import { validatePackageResult } from "../plugins/codex/scripts/orchestration/result-contract.mjs";
+
+function plan() { return { version: 1, objective: "Compare independent hypotheses", complexityScore: 5, requestedBy: { explicit: true, sessionId: null }, packages: [
+ { id: "pkg-a", title: "A", role: { class: "explorer", label: "a" }, objective: "Inspect A", dependencies: [], optional: false, access: "read-only", workspace: { mode: "shared" }, model: { name: "gpt-5.6-luna", effort: "high" }, nativeSubagents: { policy: "allowed", maxChildren: 1 }, acceptanceCriteria: ["Evidence"], expectedOutputs: ["claims"] },
+ { id: "pkg-b", title: "B", role: { class: "verifier", label: "b" }, objective: "Verify A", dependencies: ["pkg-a"], access: "read-only", workspace: { mode: "shared" }, model: { name: "gpt-5.6-terra", effort: "high" }, nativeSubagents: { policy: "forbidden", maxChildren: 0 }, acceptanceCriteria: ["Compare"], expectedOutputs: ["evidence"] }
+] }; }
+test("normalizes a valid read-only plan", () => { const value = normalizeOrchestrationPlan(plan(), { config: DEFAULT_ORCHESTRATION_CONFIG }); assert.equal(value.packages[1].optional, false); assert.equal(value.budget.workerParallelism, 3); });
+test("rejects non-boolean optional", () => { const value = plan(); value.packages[0].optional = "false"; assert.throws(() => normalizeOrchestrationPlan(value, { config: DEFAULT_ORCHESTRATION_CONFIG }), /optional must be a boolean/); });
+test("rejects write packages and cycles", () => { const value = plan(); value.packages[0].access = "write"; assert.throws(() => normalizeOrchestrationPlan(value, { config: DEFAULT_ORCHESTRATION_CONFIG }), /read-only/); const cycle = plan(); cycle.packages[0].dependencies = ["pkg-b"]; assert.throws(() => normalizeOrchestrationPlan(cycle, { config: DEFAULT_ORCHESTRATION_CONFIG }), /cycle/); });
+test("validates canonical package results", () => { const result = validatePackageResult({ packageId: "pkg-a", status: "completed", summary: "done", claims: [], evidence: [], changedFiles: [], verification: { passed: true, commands: [] }, residualRisks: [], confidence: 0.8, followUpRequests: [] }, "pkg-a"); assert.equal(result.status, "completed"); });
diff --git a/tests/orchestration-runtime.test.mjs b/tests/orchestration-runtime.test.mjs
new file mode 100644
index 000000000..960e48139
--- /dev/null
+++ b/tests/orchestration-runtime.test.mjs
@@ -0,0 +1,273 @@
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { makeTempDir, writeExecutable } from "./helpers.mjs";
+import { ensureControllerServer } from "../plugins/codex/scripts/orchestration/controller-lifecycle.mjs";
+import { OrchestrationControllerClient } from "../plugins/codex/scripts/orchestration/controller-client.mjs";
+
+const CLI = fileURLToPath(new URL("../plugins/codex/scripts/orchestration/cli.mjs", import.meta.url));
+
+function installFake(binDir, eventFile) {
+ const script = path.join(binDir, "codex");
+ writeExecutable(
+ script,
+ `#!/usr/bin/env node
+const fs = require("node:fs");
+const readline = require("node:readline");
+const events = ${JSON.stringify(eventFile)};
+let nextThread = 1;
+let nextTurn = 1;
+const timers = new Map();
+function send(value) { process.stdout.write(JSON.stringify(value) + "\\n"); }
+function event(value) { fs.appendFileSync(events, JSON.stringify({ time: Date.now(), pid: process.pid, ...value }) + "\\n"); }
+const args = process.argv.slice(2);
+if (args[0] === "--version") { console.log("codex-cli fake"); process.exit(0); }
+if (args[0] === "app-server" && args[1] === "--help") { console.log("help"); process.exit(0); }
+if (args[0] !== "app-server") process.exit(1);
+const rl = readline.createInterface({ input: process.stdin });
+rl.on("line", (line) => {
+ if (!line.trim()) return;
+ const message = JSON.parse(line);
+ switch (message.method) {
+ case "initialize":
+ send({ id: message.id, result: { userAgent: "fake" } });
+ break;
+ case "initialized":
+ break;
+ case "config/read":
+ send({ id: message.id, result: { config: { model_provider: "openai" }, origins: {} } });
+ break;
+ case "model/list":
+ send({
+ id: message.id,
+ result: {
+ data: ["sol", "terra", "luna"].map((name) => ({
+ id: "gpt-5.6-" + name,
+ model: "gpt-5.6-" + name,
+ isDefault: name === "terra",
+ supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"].map((reasoningEffort) => ({ reasoningEffort, description: reasoningEffort }))
+ })),
+ nextCursor: null
+ }
+ });
+ break;
+ case "thread/start": {
+ const threadId = "thr_" + nextThread++;
+ send({
+ id: message.id,
+ result: {
+ thread: { id: threadId },
+ model: message.params.model || "gpt-5.6-terra",
+ modelProvider: "openai",
+ reasoningEffort: message.params.config?.model_reasoning_effort || null
+ }
+ });
+ send({ method: "thread/started", params: { thread: { id: threadId } } });
+ break;
+ }
+ case "turn/start": {
+ const threadId = message.params.threadId;
+ const turnId = "turn_" + nextTurn++;
+ const prompt = (message.params.input || []).map((item) => item.text || "").join("\\n");
+ const packageId = (prompt.match(/([^<]+)/) || [])[1] || "unknown";
+ const delayMs = Number((prompt.match(/delay-(\\d+)/) || [])[1] || 20);
+ event({ type: "turn-started", packageId, threadId, turnId });
+ send({ id: message.id, result: { turn: { id: turnId, status: "inProgress", items: [] } } });
+ send({ method: "turn/started", params: { threadId, turn: { id: turnId, status: "inProgress", items: [] } } });
+ const timer = setTimeout(() => {
+ const payload = JSON.stringify({
+ packageId,
+ status: "completed",
+ summary: "Completed " + packageId,
+ claims: ["claim-" + packageId],
+ evidence: [{ type: "observation", description: "evidence-" + packageId, path: null, lineStart: null, lineEnd: null, command: null, exitCode: null }],
+ changedFiles: [],
+ verification: { passed: true, commands: [] },
+ residualRisks: [],
+ confidence: 0.9,
+ followUpRequests: []
+ });
+ send({ method: "item/completed", params: { threadId, turnId, item: { type: "agentMessage", id: "msg_" + turnId, text: payload, phase: "final_answer" } } });
+ send({ method: "turn/completed", params: { threadId, turn: { id: turnId, status: "completed", items: [] } } });
+ event({ type: "turn-completed", packageId, threadId, turnId });
+ }, delayMs);
+ timers.set(turnId, { timer, threadId });
+ break;
+ }
+ case "turn/interrupt":
+ for (const [turnId, current] of timers) {
+ if (current.threadId === message.params.threadId) {
+ clearTimeout(current.timer);
+ event({ type: "turn-interrupted", turnId, threadId: current.threadId });
+ send({ method: "turn/completed", params: { threadId: current.threadId, turn: { id: turnId, status: "interrupted", items: [] } } });
+ timers.delete(turnId);
+ }
+ }
+ send({ id: message.id, result: {} });
+ break;
+ default:
+ send({ id: message.id, error: { code: -32601, message: "unknown " + message.method } });
+ }
+});`
+ );
+
+ if (process.platform === "win32") {
+ fs.writeFileSync(
+ path.join(binDir, "codex.cmd"),
+ `@echo off\r\n"${process.execPath}" "%~dp0codex" %*\r\n`,
+ "utf8"
+ );
+ }
+}
+
+function run(args, env) {
+ return spawnSync(process.execPath, [CLI, ...args], {
+ encoding: "utf8",
+ env,
+ cwd: env.WORKSPACE
+ });
+}
+
+function buildPlan(packages) {
+ return {
+ version: 1,
+ objective: "runtime",
+ complexityScore: 5,
+ requestedBy: { explicit: true, sessionId: null },
+ packages
+ };
+}
+
+function buildPackage(id, dependencies = [], objective = `delay-250 ${id}`) {
+ return {
+ id,
+ title: id,
+ role: { class: "explorer", label: id },
+ objective,
+ dependencies,
+ optional: false,
+ access: "read-only",
+ workspace: { mode: "shared" },
+ model: { name: "gpt-5.6-luna", effort: "high" },
+ nativeSubagents: { policy: "forbidden", maxChildren: 0 },
+ acceptanceCriteria: ["evidence"],
+ expectedOutputs: ["claims"]
+ };
+}
+
+async function waitFor(orchestrationId, env, predicate, timeoutMs = 30000) {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ const response = run(["status", orchestrationId, "--cwd", env.WORKSPACE, "--json"], env);
+ if (response.status === 0) {
+ const value = JSON.parse(response.stdout);
+ if (predicate(value)) return value;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+ throw new Error(`Timed out waiting for ${orchestrationId}.`);
+}
+
+async function shutdown(workspace, env) {
+ const session = await ensureControllerServer(workspace, { env });
+ await new OrchestrationControllerClient(session.endpoint).shutdown(true).catch(() => {});
+}
+
+test("runs independent Codex Roots concurrently and isolates results", async () => {
+ const workspace = makeTempDir();
+ const binDir = makeTempDir();
+ const pluginDataDir = makeTempDir();
+ const eventFile = path.join(pluginDataDir, "events.jsonl");
+ installFake(binDir, eventFile);
+ const env = {
+ ...process.env,
+ PATH: `${binDir}${path.delimiter}${process.env.PATH}`,
+ CLAUDE_PLUGIN_DATA: pluginDataDir,
+ WORKSPACE: workspace
+ };
+ const planFile = path.join(workspace, "plan.json");
+ fs.writeFileSync(
+ planFile,
+ JSON.stringify(
+ buildPlan([
+ buildPackage("pkg-a", [], "delay-3000 pkg-a"),
+ buildPackage("pkg-b", [], "delay-3000 pkg-b"),
+ buildPackage("pkg-c", ["pkg-a", "pkg-b"], "delay-20 pkg-c")
+ ])
+ )
+ );
+
+ const launch = run(["start", "--cwd", workspace, "--plan-file", planFile, "--json"], env);
+ assert.equal(launch.status, 0, launch.stderr);
+ const orchestrationId = JSON.parse(launch.stdout).orchestrationId;
+ const state = await waitFor(
+ orchestrationId,
+ env,
+ (value) => ["completed", "degraded", "failed"].includes(value.status)
+ );
+ assert.equal(state.status, "completed", JSON.stringify(state, null, 2));
+
+ const resultResponse = run(["result", orchestrationId, "--cwd", workspace, "--json"], env);
+ assert.equal(resultResponse.status, 0, resultResponse.stderr);
+ const result = JSON.parse(resultResponse.stdout);
+ assert.equal(result.packages[0].result.claims[0], "claim-pkg-a");
+ assert.equal(result.packages[1].result.claims[0], "claim-pkg-b");
+
+ const events = fs.readFileSync(eventFile, "utf8").trim().split(/\n/).map(JSON.parse);
+ const starts = events.filter(
+ (entry) => entry.type === "turn-started" && ["pkg-a", "pkg-b"].includes(entry.packageId)
+ );
+ const completions = events.filter(
+ (entry) => entry.type === "turn-completed" && ["pkg-a", "pkg-b"].includes(entry.packageId)
+ );
+ assert.equal(starts.length, 2);
+ assert.equal(new Set(starts.map((entry) => entry.pid)).size, 2);
+ assert.equal(
+ Math.max(...starts.map((entry) => entry.time))
+ < Math.min(...completions.map((entry) => entry.time)),
+ true,
+ JSON.stringify({ starts, completions }, null, 2)
+ );
+ await shutdown(workspace, env);
+});
+
+test("cancels an active orchestration with a soft turn interrupt", async () => {
+ const workspace = makeTempDir();
+ const binDir = makeTempDir();
+ const pluginDataDir = makeTempDir();
+ const eventFile = path.join(pluginDataDir, "events.jsonl");
+ installFake(binDir, eventFile);
+ const env = {
+ ...process.env,
+ PATH: `${binDir}${path.delimiter}${process.env.PATH}`,
+ CLAUDE_PLUGIN_DATA: pluginDataDir,
+ WORKSPACE: workspace
+ };
+ const planFile = path.join(workspace, "plan.json");
+ fs.writeFileSync(
+ planFile,
+ JSON.stringify(buildPlan([buildPackage("pkg-long", [], "delay-5000 pkg-long")]))
+ );
+
+ const launch = run(["start", "--cwd", workspace, "--plan-file", planFile, "--json"], env);
+ assert.equal(launch.status, 0, launch.stderr);
+ const orchestrationId = JSON.parse(launch.stdout).orchestrationId;
+ await waitFor(
+ orchestrationId,
+ env,
+ (value) => value.packages?.["pkg-long"]?.status === "running"
+ );
+
+ const cancel = run(["cancel", orchestrationId, "--cwd", workspace, "--json"], env);
+ assert.equal(cancel.status, 0, cancel.stderr);
+ const state = await waitFor(orchestrationId, env, (value) => value.status === "cancelled");
+ assert.equal(state.status, "cancelled");
+ const events = fs.readFileSync(eventFile, "utf8").trim().split(/\n/).map(JSON.parse);
+ assert.equal(events.some((entry) => entry.type === "turn-interrupted"), true);
+ await shutdown(workspace, env);
+});
diff --git a/tests/orchestration-scheduler.test.mjs b/tests/orchestration-scheduler.test.mjs
new file mode 100644
index 000000000..e44fdc231
--- /dev/null
+++ b/tests/orchestration-scheduler.test.mjs
@@ -0,0 +1,7 @@
+
+import test from "node:test";
+import assert from "node:assert/strict";
+import { createSchedulerState, deriveOrchestrationStatus, getReadyPackageIds, markPackageCompleted, markPackageFailed, markPackageReady, markPackageRunning, propagateBlockedPackages } from "../plugins/codex/scripts/orchestration/scheduler.mjs";
+const plan = { packages: [ { id: "a", dependencies: [], optional: false }, { id: "b", dependencies: [], optional: true }, { id: "c", dependencies: ["a"], optional: false } ] };
+test("schedules dependency-ready packages", () => { let state = createSchedulerState(plan); assert.deepEqual(getReadyPackageIds(state), ["a", "b"]); state = markPackageReady(state, "a"); state = markPackageRunning(state, "a", 1); state = markPackageCompleted(state, "a"); assert.deepEqual(getReadyPackageIds(state), ["b", "c"]); });
+test("blocks descendants of failed dependencies", () => { let state = createSchedulerState(plan); state = markPackageReady(state, "a"); state = markPackageRunning(state, "a", 1); state = markPackageFailed(state, "a", "x"); state = propagateBlockedPackages(state); assert.equal(state.packages.c.status, "blocked"); assert.equal(deriveOrchestrationStatus(state), "running"); });
diff --git a/tests/orchestration-skill.test.mjs b/tests/orchestration-skill.test.mjs
new file mode 100644
index 000000000..28a90cb2c
--- /dev/null
+++ b/tests/orchestration-skill.test.mjs
@@ -0,0 +1,6 @@
+
+import fs from "node:fs";
+import test from "node:test";
+import assert from "node:assert/strict";
+const read = (path) => fs.readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
+test("ships the explicit orchestration command and automatic-entry skill", () => { const command = read("plugins/codex/commands/orchestrate.md"); const skill = read("plugins/codex/skills/codex-orchestration/SKILL.md"); assert.match(command, /3–6 line plan/); assert.match(command, /cli\.mjs/); assert.doesNotMatch(command, /codex-rescue/); assert.match(skill, /Complexity Score/); assert.match(skill, /Sol > Terra > Luna/); assert.match(skill, /strictly read-only/); assert.match(skill, /autoEnabled/); });
diff --git a/tests/orchestration-state.test.mjs b/tests/orchestration-state.test.mjs
new file mode 100644
index 000000000..98426ac5b
--- /dev/null
+++ b/tests/orchestration-state.test.mjs
@@ -0,0 +1,8 @@
+
+import fs from "node:fs";
+import test from "node:test";
+import assert from "node:assert/strict";
+import { makeTempDir } from "./helpers.mjs";
+import { createOrchestrationState, listOrchestrations, loadOrchestrationState, resolveOrchestrationReference, updateOrchestrationState } from "../plugins/codex/scripts/orchestration/state-store.mjs";
+const plan = { objective: "state", packages: [{ id: "pkg-a", title: "A" }], budget: { timeoutMinutes: 15 } };
+test("persists orchestration state atomically", async () => { const workspace = makeTempDir(); const pluginDataDir = makeTempDir(); const state = await createOrchestrationState(workspace, plan, { claudeSessionId: "s" }, { pluginDataDir }); await updateOrchestrationState(workspace, state.id, (value) => { value.status = "running"; return value; }, { pluginDataDir }); assert.equal(loadOrchestrationState(workspace, state.id, { pluginDataDir }).status, "running"); assert.equal(listOrchestrations(workspace, { pluginDataDir }).length, 1); assert.equal(resolveOrchestrationReference(workspace, "pkg-a", { pluginDataDir }).packageId, "pkg-a"); });
diff --git a/tests/process.test.mjs b/tests/process.test.mjs
index 80e0715b0..cc362371a 100644
--- a/tests/process.test.mjs
+++ b/tests/process.test.mjs
@@ -53,3 +53,30 @@ test("terminateProcessTree treats missing Windows processes as already stopped",
assert.equal(outcome.result.status, 128);
assert.match(outcome.result.stdout, /not found/i);
});
+
+test("terminateProcessTree falls back to direct kill after a partial taskkill failure", () => {
+ let killed = null;
+ const outcome = terminateProcessTree(1234, {
+ platform: "win32",
+ runCommandImpl(command, args) {
+ return {
+ command,
+ args,
+ status: 255,
+ signal: null,
+ stdout: "",
+ stderr: "ERROR: The operation attempted is not supported.",
+ error: null
+ };
+ },
+ killImpl(pid, signal) {
+ killed = { pid, signal };
+ }
+ });
+
+ assert.deepEqual(killed, { pid: 1234, signal: "SIGTERM" });
+ assert.equal(outcome.attempted, true);
+ assert.equal(outcome.delivered, true);
+ assert.equal(outcome.method, "kill");
+ assert.equal(outcome.result.status, 255);
+});
diff --git a/tests/prompting-skill.test.mjs b/tests/prompting-skill.test.mjs
new file mode 100644
index 000000000..1242703d1
--- /dev/null
+++ b/tests/prompting-skill.test.mjs
@@ -0,0 +1,81 @@
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+import assert from "node:assert/strict";
+import { execFileSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex");
+const read = (relativePath) => fs.readFileSync(path.join(PLUGIN_ROOT, relativePath), "utf8");
+
+test("rescue uses version-neutral Codex prompting guidance", () => {
+ const agent = read("agents/codex-rescue.md");
+ const runtime = read("skills/codex-cli-runtime/SKILL.md");
+ const skill = read("skills/codex-prompting/SKILL.md");
+
+ assert.match(agent, /^\s*- codex-prompting\s*$/m);
+ assert.match(agent, /Sol > Terra > Luna/);
+ assert.match(agent, /reasoning effort is a separate/i);
+ assert.match(agent, /spark.*gpt-5\.6-luna/i);
+ assert.match(runtime, /codex-prompting/i);
+ assert.match(runtime, /spark.*gpt-5\.6-luna/i);
+ assert.match(runtime, /current Codex model catalog/i);
+ assert.match(skill, /Favor lean, outcome-first prompts/i);
+ assert.match(skill, /Define autonomy and approval boundaries/i);
+ assert.match(skill, /Sol > Terra > Luna/);
+ assert.match(skill, /Do not treat a higher effort on a lower tier as reversing/i);
+});
+
+test("generation-pinned prompting aliases are absent", () => {
+ const legacyName = ["gpt", "5", "4", "prompting"].join("-");
+ assert.equal(fs.existsSync(path.join(PLUGIN_ROOT, "skills", legacyName)), false);
+});
+
+test("prompting references are generation-neutral", () => {
+ const files = [
+ "skills/codex-prompting/SKILL.md",
+ "skills/codex-prompting/references/prompt-blocks.md",
+ "skills/codex-prompting/references/codex-prompt-recipes.md",
+ "skills/codex-prompting/references/codex-prompt-antipatterns.md"
+ ];
+ const dottedLegacy = new RegExp(["GPT", "5\\.4"].join("-"), "i");
+ const dashedLegacy = new RegExp(["gpt", "5", "4"].join("-"), "i");
+
+ for (const file of files) {
+ const source = read(file);
+ assert.doesNotMatch(source, dottedLegacy);
+ assert.doesNotMatch(source, dashedLegacy);
+ }
+});
+
+test("repository context contains no generation-4 model identifiers", () => {
+ const dottedLegacy = new RegExp(["gpt", "5\\.4"].join("-"), "i");
+ const dashedLegacy = new RegExp(["gpt", "5", "4"].join("-"), "i");
+ const files = execFileSync("git", ["ls-files", "-z"], {
+ cwd: ROOT,
+ encoding: "utf8"
+ })
+ .split("\0")
+ .filter(Boolean)
+ .filter((relativePath) => fs.existsSync(path.join(ROOT, relativePath)))
+ .filter((relativePath) =>
+ /(?:^|\/)(?:[^/]+\.(?:md|mjs|js|json|ts|yml|yaml|toml|txt)|README|LICENSE|NOTICE|\.gitignore)$/.test(relativePath)
+ );
+
+ for (const relativePath of files) {
+ const source = fs.readFileSync(path.join(ROOT, relativePath), "utf8");
+ assert.doesNotMatch(source, dottedLegacy, relativePath);
+ assert.doesNotMatch(source, dashedLegacy, relativePath);
+ }
+});
+
+test("README presents GPT-5.6 as the only documented model family", () => {
+ const readme = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");
+ assert.match(readme, /gpt-5\.6-sol/);
+ assert.match(readme, /gpt-5\.6-terra/);
+ assert.match(readme, /gpt-5\.6-luna/);
+ assert.match(readme, /spark.*gpt-5\.6-luna/i);
+ assert.match(readme, /Sol > Terra > Luna/);
+ assert.match(readme, /reasoning effort is a separate/i);
+});
diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs
index 8f276835b..09a1b0df7 100644
--- a/tests/runtime.test.mjs
+++ b/tests/runtime.test.mjs
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs";
import { initGitRepo, makeTempDir, run } from "./helpers.mjs";
-import { loadBrokerSession, saveBrokerSession } from "../plugins/codex/scripts/lib/broker-lifecycle.mjs";
+import { loadBrokerSession } from "../plugins/codex/scripts/lib/broker-lifecycle.mjs";
import { resolveStateDir } from "../plugins/codex/scripts/lib/state.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -716,6 +716,46 @@ test("write task output focuses on the Codex result without generic follow-up hi
assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n");
});
+test("task --write starts Codex with unrestricted sandbox access", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "--write", "capture the page"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastThreadStart.sandbox, "danger-full-access");
+});
+
+test("resuming task --write upgrades the thread to unrestricted sandbox access", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+
+ const firstRun = run("node", [SCRIPT, "task", "initial task"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.equal(firstRun.status, 0, firstRun.stderr);
+
+ const result = run("node", [SCRIPT, "task", "--write", "--resume-last", "capture the page"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastThreadResume.sandbox, "danger-full-access");
+});
+
test("task --resume acts like --resume-last without leaking the flag into the prompt", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
@@ -732,17 +772,75 @@ test("task --resume acts like --resume-last without leaking the flag into the pr
});
assert.equal(firstRun.status, 0, firstRun.stderr);
- const result = run("node", [SCRIPT, "task", "--resume", "follow up"], {
- cwd: repo,
- env: buildEnv(binDir)
- });
+ const result = run(
+ "node",
+ [SCRIPT, "task", "--resume", "--model", "gpt-5.6-terra", "--effort", "max", "follow up"],
+ {
+ cwd: repo,
+ env: buildEnv(binDir)
+ }
+ );
assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
assert.equal(fakeState.lastTurnStart.threadId, "thr_1");
+ assert.equal(fakeState.lastThreadResume.model, "gpt-5.6-terra");
+ assert.equal(fakeState.lastTurnStart.model, "gpt-5.6-terra");
+ assert.equal(fakeState.lastTurnStart.effort, "max");
assert.equal(fakeState.lastTurnStart.prompt, "follow up");
});
+test("resume validates the persisted thread provider even when current config uses a custom provider", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "custom-provider");
+ initGitRepo(repo);
+
+ const first = run("node", [SCRIPT, "task", "initial task"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.equal(first.status, 0, first.stderr);
+ const initialTurnId = JSON.parse(fs.readFileSync(statePath, "utf8")).lastTurnStart.turnId;
+
+ const resumed = run(
+ "node",
+ [SCRIPT, "task", "--resume", "--model", "gpt-5.6-luna", "--effort", "ultra", "follow up"],
+ { cwd: repo, env: buildEnv(binDir) }
+ );
+
+ assert.notEqual(resumed.status, 0);
+ assert.match(resumed.stderr, /not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).lastTurnStart.turnId, initialTurnId);
+});
+
+test("resume validates effort against the persisted thread model instead of current config", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "config-luna");
+ initGitRepo(repo);
+
+ const first = run(
+ "node",
+ [SCRIPT, "task", "--model", "gpt-5.6-sol", "--effort", "high", "initial task"],
+ { cwd: repo, env: buildEnv(binDir) }
+ );
+ assert.equal(first.status, 0, first.stderr);
+
+ const resumed = run("node", [SCRIPT, "task", "--resume", "--effort", "ultra", "follow up"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(resumed.status, 0, resumed.stderr);
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastThreadResume.model, "gpt-5.6-sol");
+ assert.equal(state.lastTurnStart.model, null);
+ assert.equal(state.lastTurnStart.effort, "ultra");
+});
+
test("task --fresh is treated as routing control and does not leak into the prompt", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
@@ -780,10 +878,187 @@ test("task forwards model selection and reasoning effort to app-server turn/star
assert.equal(result.status, 0, result.stderr);
const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8"));
- assert.equal(fakeState.lastTurnStart.model, "gpt-5.3-codex-spark");
+ assert.equal(fakeState.lastTurnStart.model, "gpt-5.6-luna");
assert.equal(fakeState.lastTurnStart.effort, "low");
});
+test("task supports max and ultra while rejecting unsupported model combinations locally", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+
+ const max = run("node", [SCRIPT, "task", "--model", "gpt-5.6-luna", "--effort", "max", "check"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.equal(max.status, 0, max.stderr);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).lastTurnStart.effort, "max");
+
+ const ultra = run("node", [SCRIPT, "task", "--model", "gpt-5.6-sol", "--effort", "ultra", "check"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.equal(ultra.status, 0, ultra.stderr);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).lastTurnStart.effort, "ultra");
+
+ const invalid = run("node", [SCRIPT, "task", "--model", "gpt-5.6-luna", "--effort", "ultra", "check"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.notEqual(invalid.status, 0);
+ assert.match(invalid.stderr, /not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).lastThreadStart.model, "gpt-5.6-sol");
+});
+
+test("task validates model and effort inherited from Codex config", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "inherited-sol-max");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "check inherited selection"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastThreadStart.model, "gpt-5.6-sol");
+ assert.equal(state.lastThreadStart.effort, "max");
+ assert.equal(state.lastTurnStart.model, null);
+ assert.equal(state.lastTurnStart.effort, null);
+});
+
+test("task rejects an unsupported model and effort inherited from Codex config", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "inherited-luna-ultra");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "check inherited selection"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /Reasoning effort "ultra" is not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).threads.length, 0);
+});
+
+test("task prevalidates a partial explicit selection against Codex config", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "inherited-luna-ultra");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "--effort", "ultra", "check selection"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).threads.length, 0);
+});
+
+test("task prevalidates effort against the catalog default before creating a persistent thread", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "inherited-default-luna-ultra");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "--effort", "ultra", "check default selection"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).threads.length, 0);
+});
+
+test("task falls back cleanly when an older Codex CLI does not expose model/list", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ installFakeCodex(binDir, "model-list-unsupported");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "--model", "gpt-5.6-sol", "--effort", "max", "check"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+});
+
+test("task does not apply the OpenAI effort matrix to a custom provider", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ installFakeCodex(binDir, "custom-provider");
+ initGitRepo(repo);
+
+ const result = run("node", [SCRIPT, "task", "--model", "gpt-5.6-luna", "--effort", "ultra", "check"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.equal(result.status, 0, result.stderr);
+});
+
+test("review rejects an unsupported explicit selection before creating a thread", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+ fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
+
+ const result = run("node", [SCRIPT, "review", "--model", "gpt-5.6-luna", "--effort", "ultra"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /not supported by model "gpt-5\.6-luna"/i);
+ assert.equal(JSON.parse(fs.readFileSync(statePath, "utf8")).threads.length, 0);
+});
+
+test("review and adversarial-review consume model and effort flags instead of leaking them into focus text", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const statePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir);
+ initGitRepo(repo);
+ fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
+
+ const review = run("node", [SCRIPT, "review", "--model", "gpt-5.6-sol", "--effort", "max"], {
+ cwd: repo,
+ env: buildEnv(binDir)
+ });
+ assert.equal(review.status, 0, review.stderr);
+ let state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastThreadStart.model, "gpt-5.6-sol");
+ assert.equal(state.lastThreadStart.effort, "max");
+
+ const adversarial = run(
+ "node",
+ [SCRIPT, "adversarial-review", "--model", "gpt-5.6-terra", "--effort", "xhigh", "challenge retries"],
+ { cwd: repo, env: buildEnv(binDir) }
+ );
+ assert.equal(adversarial.status, 0, adversarial.stderr);
+ state = JSON.parse(fs.readFileSync(statePath, "utf8"));
+ assert.equal(state.lastTurnStart.model, "gpt-5.6-terra");
+ assert.equal(state.lastTurnStart.effort, "xhigh");
+ assert.doesNotMatch(state.lastTurnStart.prompt, /--model|--effort/);
+ assert.match(state.lastTurnStart.prompt, /challenge retries/);
+});
+
test("task logs reasoning summaries and assistant messages to the job log", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
@@ -929,10 +1204,21 @@ test("task --background enqueues a detached worker and exposes per-job status",
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });
- const launched = run("node", [SCRIPT, "task", "--background", "--json", "investigate the failing test"], {
- cwd: repo,
- env: buildEnv(binDir)
- });
+ const launched = run(
+ "node",
+ [
+ SCRIPT,
+ "task",
+ "--background",
+ "--json",
+ "--model",
+ "gpt-5.6-luna",
+ "--effort",
+ "max",
+ "investigate the failing test"
+ ],
+ { cwd: repo, env: buildEnv(binDir) }
+ );
assert.equal(launched.status, 0, launched.stderr);
const launchPayload = JSON.parse(launched.stdout);
@@ -967,6 +1253,9 @@ test("task --background enqueues a detached worker and exposes per-job status",
assert.equal(resultPayload.job.id, launchPayload.jobId);
assert.equal(resultPayload.job.status, "completed");
assert.match(resultPayload.storedJob.rendered, /Handled the requested task/);
+ const fakeState = JSON.parse(fs.readFileSync(path.join(binDir, "fake-codex-state.json"), "utf8"));
+ assert.equal(fakeState.lastTurnStart.model, "gpt-5.6-luna");
+ assert.equal(fakeState.lastTurnStart.effort, "max");
});
test("review rejects focus text because it is native-review only", () => {
@@ -1768,6 +2057,8 @@ test("cancel sends turn interrupt to the shared app-server before killing a brok
return null;
}, { timeoutMs: 15000 });
+ installFakeCodex(binDir, "interruptible-slow-task", "codex-cli 0.144.0");
+
const cancelResult = run("node", [SCRIPT, "cancel", jobId, "--json"], {
cwd: repo,
env
@@ -1789,6 +2080,7 @@ test("cancel sends turn interrupt to the shared app-server before killing a brok
threadId: runningJob.threadId,
turnId: runningJob.turnId
});
+ assert.equal(fakeState.appServerStarts, 1);
const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], {
cwd: repo,
@@ -2161,6 +2453,59 @@ test("commands lazily start and reuse one shared app-server after first use", as
assert.equal(cleanup.status, 0, cleanup.stderr);
});
+test("shared broker invalidates stale CLI, plugin, and legacy runtime state", () => {
+ const repo = makeTempDir();
+ const binDir = makeTempDir();
+ const fakeStatePath = path.join(binDir, "fake-codex-state.json");
+ installFakeCodex(binDir, "reject-gpt-5.6", "codex-cli 0.143.0");
+ initGitRepo(repo);
+ fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
+ const env = buildEnv(binDir);
+
+ const first = run("node", [SCRIPT, "task", "first"], { cwd: repo, env });
+ assert.equal(first.status, 0, first.stderr);
+ assert.ok(loadBrokerSession(repo), "expected the first task to create a shared broker");
+
+ installFakeCodex(binDir, "review-ok", "codex-cli 0.144.0");
+ const second = run(
+ "node",
+ [SCRIPT, "task", "--model", "gpt-5.6-sol", "--effort", "high", "second"],
+ { cwd: repo, env }
+ );
+ assert.equal(second.status, 0, second.stderr);
+
+ let state = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
+ assert.equal(state.appServerStarts, 2);
+ assert.equal(loadBrokerSession(repo).runtime.codexVersion, "codex-cli 0.144.0");
+
+ let broker = loadBrokerSession(repo);
+ fs.writeFileSync(
+ path.join(resolveStateDir(repo), "broker.json"),
+ `${JSON.stringify({ ...broker, runtime: { ...broker.runtime, pluginVersion: "1.0.6" } }, null, 2)}\n`
+ );
+ const pluginUpgrade = run("node", [SCRIPT, "task", "after plugin upgrade"], { cwd: repo, env });
+ assert.equal(pluginUpgrade.status, 0, pluginUpgrade.stderr);
+ state = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
+ assert.equal(state.appServerStarts, 3);
+
+ broker = loadBrokerSession(repo);
+ const { runtime: _runtime, ...legacyBroker } = broker;
+ fs.writeFileSync(
+ path.join(resolveStateDir(repo), "broker.json"),
+ `${JSON.stringify(legacyBroker, null, 2)}\n`
+ );
+ const legacyUpgrade = run("node", [SCRIPT, "task", "after legacy upgrade"], { cwd: repo, env });
+ assert.equal(legacyUpgrade.status, 0, legacyUpgrade.stderr);
+ state = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
+ assert.equal(state.appServerStarts, 4);
+
+ run("node", [SESSION_HOOK, "SessionEnd"], {
+ cwd: repo,
+ env,
+ input: JSON.stringify({ hook_event_name: "SessionEnd", cwd: repo })
+ });
+});
+
test("setup reuses an existing shared app-server without starting another one", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
@@ -2238,22 +2583,39 @@ test("status reports shared session runtime when a lazy broker is active", () =>
test("setup and status honor --cwd when reading shared session runtime", () => {
const targetWorkspace = makeTempDir();
const invocationWorkspace = makeTempDir();
+ const binDir = makeTempDir();
+ installFakeCodex(binDir);
+ initGitRepo(targetWorkspace);
- saveBrokerSession(targetWorkspace, {
- endpoint: "unix:/tmp/fake-broker.sock"
+ const task = run("node", [SCRIPT, "task", "start shared runtime"], {
+ cwd: targetWorkspace,
+ env: buildEnv(binDir)
});
+ assert.equal(task.status, 0, task.stderr);
+ const broker = loadBrokerSession(targetWorkspace);
+ if (!broker) {
+ return;
+ }
const status = run("node", [SCRIPT, "status", "--cwd", targetWorkspace], {
- cwd: invocationWorkspace
+ cwd: invocationWorkspace,
+ env: buildEnv(binDir)
});
assert.equal(status.status, 0, status.stderr);
assert.match(status.stdout, /Session runtime: shared session/);
const setup = run("node", [SCRIPT, "setup", "--cwd", targetWorkspace, "--json"], {
- cwd: invocationWorkspace
+ cwd: invocationWorkspace,
+ env: buildEnv(binDir)
});
assert.equal(setup.status, 0, setup.stderr);
const payload = JSON.parse(setup.stdout);
assert.equal(payload.sessionRuntime.mode, "shared");
- assert.equal(payload.sessionRuntime.endpoint, "unix:/tmp/fake-broker.sock");
+ assert.equal(payload.sessionRuntime.endpoint, broker.endpoint);
+
+ run("node", [SESSION_HOOK, "SessionEnd"], {
+ cwd: targetWorkspace,
+ env: buildEnv(binDir),
+ input: JSON.stringify({ hook_event_name: "SessionEnd", cwd: targetWorkspace })
+ });
});
diff --git a/tsconfig.app-server.json b/tsconfig.app-server.json
index 3f8c11f4a..4ad49a1fa 100644
--- a/tsconfig.app-server.json
+++ b/tsconfig.app-server.json
@@ -10,7 +10,9 @@
"noImplicitAny": false,
"useUnknownInCatchVariables": false,
"skipLibCheck": true,
- "types": ["node"]
+ "types": [
+ "node"
+ ]
},
"include": [
"plugins/codex/scripts/lib/app-server.mjs",
@@ -18,6 +20,7 @@
"plugins/codex/scripts/lib/fs.mjs",
"plugins/codex/scripts/lib/process.mjs",
"plugins/codex/scripts/lib/app-server-protocol.d.ts",
+ "plugins/codex/scripts/orchestration/**/*.mjs",
"plugins/codex/.generated/app-server-types/**/*.ts"
]
}
]