@kbediako/codex-orchestrator 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,7 +89,7 @@ Delegation guard profile:
89
89
  ## Delegation + RLM flow
90
90
 
91
91
  RLM (Recursive Language Model) is the long-horizon loop used by the `rlm` pipeline (`codex-orchestrator rlm "<goal>"` or `codex-orchestrator start rlm --goal "<goal>"`). Delegated runs only enter RLM when the child is launched with the `rlm` pipeline (or the rlm runner directly). In auto mode it resolves to symbolic when delegated, when `RLM_CONTEXT_PATH` is set, or when the context exceeds `RLM_SYMBOLIC_MIN_BYTES`; otherwise it stays iterative. The runner writes state to `.runs/<task-id>/cli/<run-id>/rlm/state.json` and stops when the validator passes or budgets are exhausted.
92
- Symbolic subcalls can optionally use collab tools when `RLM_SYMBOLIC_COLLAB=1` (requires `collab=true` in `codex features list`). Collab tool calls parsed from `codex exec --json --enable collab` are stored in `manifest.collab_tool_calls` (bounded by `CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS`, set to `0` to disable). `codex-orchestrator codex setup` remains available when you want a managed/pinned CLI path.
92
+ Symbolic subcalls can optionally use collab tools. Fast path: `codex-orchestrator rlm --collab "<goal>"` (sets `RLM_SYMBOLIC_COLLAB=1` and implies symbolic mode). Collab requires `collab=true` in `codex features list`. Collab tool calls parsed from `codex exec --json --enable collab` are stored in `manifest.collab_tool_calls` (bounded by `CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS`, set to `0` to disable). `codex-orchestrator codex setup` remains available when you want a managed/pinned CLI path.
93
93
 
94
94
  ### Delegation flow
95
95
  ```mermaid
@@ -152,11 +152,16 @@ Bundled skills (may vary by release):
152
152
 
153
153
  ## DevTools readiness
154
154
 
155
- Check DevTools readiness (skill + MCP config):
155
+ Check readiness (deps + capability wiring):
156
156
  ```bash
157
157
  codex-orchestrator doctor --format json
158
158
  ```
159
159
 
160
+ Usage snapshot (scans local `.runs/`):
161
+ ```bash
162
+ codex-orchestrator doctor --usage
163
+ ```
164
+
160
165
  Print DevTools MCP setup guidance:
161
166
  ```bash
162
167
  codex-orchestrator devtools setup
@@ -171,6 +176,7 @@ codex-orchestrator devtools setup
171
176
  - `codex-orchestrator init codex --codex-cli --yes --codex-source <path>` — optionally provision a CO-managed Codex CLI binary (build-from-source default; set `CODEX_CLI_SOURCE` to avoid passing `--codex-source` every time).
172
177
  - `codex-orchestrator init codex --codex-cli --yes --codex-download-url <url> --codex-download-sha256 <sha>` — opt-in to a prebuilt Codex CLI download.
173
178
  - `codex-orchestrator codex setup` — plan/apply a CO-managed Codex CLI install (optional managed/pinned path; use `--download-url` + `--download-sha256` for prebuilts).
179
+ - `codex-orchestrator delegation setup --yes` — configure delegation MCP server wiring.
174
180
  - `codex-orchestrator self-check --format json` — JSON health payload.
175
181
  - `codex-orchestrator mcp serve` — Codex MCP stdio server.
176
182
 
@@ -14,8 +14,10 @@ import { evaluateInteractiveGate } from '../orchestrator/src/cli/utils/interacti
14
14
  import { buildSelfCheckResult } from '../orchestrator/src/cli/selfCheck.js';
15
15
  import { initCodexTemplates, formatInitSummary } from '../orchestrator/src/cli/init.js';
16
16
  import { runDoctor, formatDoctorSummary } from '../orchestrator/src/cli/doctor.js';
17
+ import { formatDoctorUsageSummary, runDoctorUsage } from '../orchestrator/src/cli/doctorUsage.js';
17
18
  import { formatDevtoolsSetupSummary, runDevtoolsSetup } from '../orchestrator/src/cli/devtoolsSetup.js';
18
19
  import { formatCodexCliSetupSummary, runCodexCliSetup } from '../orchestrator/src/cli/codexCliSetup.js';
20
+ import { formatDelegationSetupSummary, runDelegationSetup } from '../orchestrator/src/cli/delegationSetup.js';
19
21
  import { formatSkillsInstallSummary, installSkills } from '../orchestrator/src/cli/skills.js';
20
22
  import { loadPackageInfo } from '../orchestrator/src/cli/utils/packageInfo.js';
21
23
  import { slugify } from '../orchestrator/src/cli/utils/strings.js';
@@ -85,6 +87,9 @@ async function main() {
85
87
  case 'delegation-server':
86
88
  await handleDelegationServer(args);
87
89
  break;
90
+ case 'delegation':
91
+ await handleDelegation(args);
92
+ break;
88
93
  case 'version':
89
94
  printVersion();
90
95
  break;
@@ -167,6 +172,25 @@ function applyRlmEnvOverrides(flags, goal) {
167
172
  if (goal) {
168
173
  process.env.RLM_GOAL = goal;
169
174
  }
175
+ const collabRaw = flags['collab'];
176
+ if (collabRaw !== undefined) {
177
+ const normalized = typeof collabRaw === 'string' ? collabRaw.trim().toLowerCase() : collabRaw === true ? 'true' : '';
178
+ const enabled = collabRaw === true || normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'auto';
179
+ const disabled = normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off';
180
+ if (enabled) {
181
+ process.env.RLM_SYMBOLIC_COLLAB = '1';
182
+ // Collab is only used in the symbolic loop; make the flag do what users expect.
183
+ if (!process.env.RLM_MODE) {
184
+ process.env.RLM_MODE = 'symbolic';
185
+ }
186
+ }
187
+ else if (disabled) {
188
+ process.env.RLM_SYMBOLIC_COLLAB = '0';
189
+ }
190
+ else if (typeof collabRaw === 'string') {
191
+ throw new Error('Invalid --collab value. Use --collab (or: --collab auto|true|false).');
192
+ }
193
+ }
170
194
  const validator = readStringFlag(flags, 'validator');
171
195
  if (validator) {
172
196
  process.env.RLM_VALIDATOR = validator;
@@ -313,6 +337,16 @@ async function handleRlm(orchestrator, rawArgs) {
313
337
  process.env.MCP_RUNNER_TASK_ID = taskId;
314
338
  applyRlmEnvOverrides(flags, goal);
315
339
  console.log(`Task: ${taskId}`);
340
+ const collabUserChoice = flags['collab'] !== undefined || process.env.RLM_SYMBOLIC_COLLAB !== undefined;
341
+ if (!collabUserChoice) {
342
+ const doctor = runDoctor();
343
+ if (doctor.collab.status === 'ok') {
344
+ console.log('Tip: collab is enabled. Try: codex-orchestrator rlm --collab auto \"<goal>\"');
345
+ }
346
+ else if (doctor.collab.status === 'disabled') {
347
+ console.log('Tip: collab is available but disabled. Enable with: codex features enable collab');
348
+ }
349
+ }
316
350
  let startResult = null;
317
351
  await withRunUi(flags, 'text', async (runEvents) => {
318
352
  startResult = await orchestrator.start({
@@ -525,15 +559,38 @@ async function handleInit(rawArgs) {
525
559
  async function handleDoctor(rawArgs) {
526
560
  const { flags } = parseArgs(rawArgs);
527
561
  const format = flags['format'] === 'json' ? 'json' : 'text';
528
- const result = runDoctor();
562
+ const includeUsage = Boolean(flags['usage']);
563
+ const windowDaysRaw = readStringFlag(flags, 'window-days');
564
+ let windowDays = undefined;
565
+ if (windowDaysRaw) {
566
+ if (!/^\d+$/u.test(windowDaysRaw)) {
567
+ throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
568
+ }
569
+ const parsed = Number(windowDaysRaw);
570
+ if (!Number.isInteger(parsed) || parsed <= 0) {
571
+ throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
572
+ }
573
+ windowDays = parsed;
574
+ }
575
+ const taskFilter = readStringFlag(flags, 'task') ?? null;
576
+ const doctorResult = runDoctor();
577
+ const usageResult = includeUsage ? await runDoctorUsage({ windowDays, taskFilter }) : null;
529
578
  if (format === 'json') {
530
- console.log(JSON.stringify(result, null, 2));
579
+ if (usageResult) {
580
+ console.log(JSON.stringify({ ...doctorResult, usage: usageResult }, null, 2));
581
+ return;
582
+ }
583
+ console.log(JSON.stringify(doctorResult, null, 2));
531
584
  return;
532
585
  }
533
- const summary = formatDoctorSummary(result);
534
- for (const line of summary) {
586
+ for (const line of formatDoctorSummary(doctorResult)) {
535
587
  console.log(line);
536
588
  }
589
+ if (usageResult) {
590
+ for (const line of formatDoctorUsageSummary(usageResult)) {
591
+ console.log(line);
592
+ }
593
+ }
537
594
  }
538
595
  async function handleDevtools(rawArgs) {
539
596
  const { positionals, flags } = parseArgs(rawArgs);
@@ -559,6 +616,30 @@ async function handleDevtools(rawArgs) {
559
616
  console.log(line);
560
617
  }
561
618
  }
619
+ async function handleDelegation(rawArgs) {
620
+ const { positionals, flags } = parseArgs(rawArgs);
621
+ const subcommand = positionals.shift();
622
+ if (!subcommand) {
623
+ throw new Error('delegation requires a subcommand (setup).');
624
+ }
625
+ if (subcommand !== 'setup') {
626
+ throw new Error(`Unknown delegation subcommand: ${subcommand}`);
627
+ }
628
+ const format = flags['format'] === 'json' ? 'json' : 'text';
629
+ const apply = Boolean(flags['yes']);
630
+ if (format === 'json' && apply) {
631
+ throw new Error('delegation setup does not support --format json with --yes.');
632
+ }
633
+ const repoRoot = readStringFlag(flags, 'repo') ?? process.cwd();
634
+ const result = await runDelegationSetup({ apply, repoRoot });
635
+ if (format === 'json') {
636
+ console.log(JSON.stringify(result, null, 2));
637
+ return;
638
+ }
639
+ for (const line of formatDelegationSetupSummary(result)) {
640
+ console.log(line);
641
+ }
642
+ }
562
643
  async function handleCodex(rawArgs) {
563
644
  const { positionals, flags } = parseArgs(rawArgs);
564
645
  const subcommand = positionals.shift();
@@ -879,6 +960,7 @@ Commands:
879
960
  --cloud Shortcut for --execution-mode cloud.
880
961
  --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
881
962
  --goal "<goal>" When pipeline is rlm, set the RLM goal.
963
+ --collab [auto|true|false] When pipeline is rlm, enable collab subagents (implies symbolic mode).
882
964
  --validator <cmd|none> When pipeline is rlm, set the validator command.
883
965
  --max-iterations <n> When pipeline is rlm, override max iterations.
884
966
  --max-minutes <n> When pipeline is rlm, override max minutes.
@@ -888,6 +970,7 @@ Commands:
888
970
 
889
971
  rlm "<goal>" Run RLM loop until validator passes.
890
972
  --task <id> Override task identifier.
973
+ --collab [auto|true|false] Enable collab subagents (implies symbolic mode).
891
974
  --validator <cmd|none> Set validator command or disable validation.
892
975
  --max-iterations <n> Override max iterations (0 = unlimited with validator).
893
976
  --max-minutes <n> Optional time-based guardrail in minutes.
@@ -940,7 +1023,11 @@ Commands:
940
1023
  --codex-download-sha256 <sha> Expected SHA256 for the prebuilt download.
941
1024
  --codex-force Overwrite existing CO-managed codex binary.
942
1025
  --yes Apply codex CLI setup (otherwise plan only).
943
- doctor [--format json]
1026
+ doctor [--format json] [--usage] [--window-days <n>] [--task <id>]
1027
+ --usage Include a local usage snapshot (scans .runs/).
1028
+ --window-days <n> Window for --usage (default 30).
1029
+ --task <id> Limit --usage scan to a specific task directory.
1030
+ --format json Emit machine-readable output.
944
1031
  codex setup
945
1032
  --source <path> Build from local Codex repo (or git URL).
946
1033
  --ref <ref> Git ref (branch/tag/sha) when building from repo.
@@ -952,6 +1039,10 @@ Commands:
952
1039
  devtools setup Print DevTools MCP setup instructions.
953
1040
  --yes Apply setup by running "codex mcp add ...".
954
1041
  --format json Emit machine-readable output (dry-run only).
1042
+ delegation setup Configure delegation MCP server wiring.
1043
+ --repo <path> Repo root for delegation server (default cwd).
1044
+ --yes Apply setup by running "codex mcp add ...".
1045
+ --format json Emit machine-readable output (dry-run only).
955
1046
  skills install Install bundled skills into $CODEX_HOME/skills.
956
1047
  --force Overwrite existing skill files.
957
1048
  --only <skills> Install only selected skills (comma-separated).
@@ -8,6 +8,22 @@ export class CommandReviewer {
8
8
  const result = this.requireResult();
9
9
  if (input.mode === 'cloud') {
10
10
  const cloudExecution = result.manifest.cloud_execution;
11
+ if (!cloudExecution) {
12
+ const summaryLines = [
13
+ result.success
14
+ ? 'Cloud mode requested but preflight failed; fell back to MCP mode successfully.'
15
+ : 'Cloud mode requested but preflight failed; fell back to MCP mode and the run failed.',
16
+ `Manifest: ${result.manifestPath}`,
17
+ `Runner log: ${result.logPath}`
18
+ ];
19
+ return {
20
+ summary: summaryLines.join('\n'),
21
+ decision: {
22
+ approved: result.success,
23
+ feedback: result.notes.join('\n') || result.manifest.summary || undefined
24
+ }
25
+ };
26
+ }
11
27
  const status = cloudExecution?.status ?? 'unknown';
12
28
  const cloudTask = cloudExecution?.task_id ?? '<unknown>';
13
29
  const approved = status === 'ready' && result.success;
@@ -9,6 +9,31 @@ export class CommandTester {
9
9
  const result = this.requireResult();
10
10
  if (input.mode === 'cloud') {
11
11
  const cloudExecution = result.manifest.cloud_execution;
12
+ if (!cloudExecution) {
13
+ // Cloud mode can fall back to MCP when preflight fails; treat that as a normal guardrail run.
14
+ const guardrailStatus = ensureGuardrailStatus(result.manifest);
15
+ const reports = [
16
+ {
17
+ name: 'cloud-preflight',
18
+ status: 'passed',
19
+ details: result.manifest.summary?.trim() ||
20
+ 'Cloud execution was skipped due to preflight failure; fell back to MCP mode.'
21
+ },
22
+ {
23
+ name: 'guardrails',
24
+ status: guardrailStatus.present ? 'passed' : 'failed',
25
+ details: guardrailStatus.present
26
+ ? guardrailStatus.summary
27
+ : guardrailStatus.recommendation ?? guardrailStatus.summary
28
+ }
29
+ ];
30
+ return {
31
+ subtaskId: input.build.subtaskId,
32
+ success: guardrailStatus.present && result.success,
33
+ reports,
34
+ runId: input.runId
35
+ };
36
+ }
12
37
  const status = cloudExecution?.status ?? 'unknown';
13
38
  const passed = status === 'ready' && result.success;
14
39
  const diagnosis = diagnoseCloudFailure({
@@ -0,0 +1,239 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { resolveCodexCliBin } from './utils/codexCli.js';
6
+ import { resolveCodexHome } from './utils/codexPaths.js';
7
+ export async function runDelegationSetup(options = {}) {
8
+ const env = options.env ?? process.env;
9
+ const repoRoot = options.repoRoot ?? process.cwd();
10
+ const codexBin = resolveCodexCliBin(env);
11
+ const codexHome = resolveCodexHome(env);
12
+ const configPath = join(codexHome, 'config.toml');
13
+ const plan = {
14
+ codexBin,
15
+ codexHome,
16
+ repoRoot,
17
+ commandLine: `"${codexBin}" mcp add delegation -- codex-orchestrator delegate-server`
18
+ };
19
+ const probe = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env });
20
+ const readiness = { configured: probe.configured, configPath };
21
+ if (!options.apply) {
22
+ return { status: 'planned', plan, readiness };
23
+ }
24
+ if (probe.configured) {
25
+ return { status: 'skipped', reason: probe.reason ?? 'Delegation MCP is already configured.', plan, readiness };
26
+ }
27
+ await applyDelegationSetup({ codexBin, removeExisting: probe.removeExisting, envVars: probe.envVars }, env);
28
+ const configuredAfter = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env }).configured;
29
+ return {
30
+ status: 'applied',
31
+ reason: probe.reason,
32
+ plan,
33
+ readiness: { ...readiness, configured: configuredAfter }
34
+ };
35
+ }
36
+ export function formatDelegationSetupSummary(result) {
37
+ const lines = [];
38
+ lines.push(`Delegation setup: ${result.status}`);
39
+ if (result.reason) {
40
+ lines.push(`Note: ${result.reason}`);
41
+ }
42
+ lines.push(`- Codex home: ${result.plan.codexHome}`);
43
+ lines.push(`- Config: ${result.readiness.configured ? 'ok' : 'missing'} (${result.readiness.configPath})`);
44
+ lines.push(`- Command: ${result.plan.commandLine}`);
45
+ if (result.status === 'planned') {
46
+ lines.push('Run with --yes to apply this setup.');
47
+ }
48
+ return lines;
49
+ }
50
+ function inspectDelegationReadiness(options) {
51
+ const requestedRepo = resolve(options.repoRoot);
52
+ const existing = readDelegationMcpServer(options.codexBin, options.env);
53
+ if (existing) {
54
+ const envVars = existing.envVars;
55
+ const isDelegationServer = existing.args.includes('delegate-server') || existing.args.includes('delegation-server');
56
+ if (!isDelegationServer) {
57
+ return {
58
+ configured: false,
59
+ removeExisting: true,
60
+ envVars,
61
+ reason: 'Existing delegation MCP entry does not point to codex-orchestrator delegate-server; reconfiguring.'
62
+ };
63
+ }
64
+ if (existing.pinnedRepo) {
65
+ const pinnedRepo = resolve(existing.pinnedRepo);
66
+ if (pinnedRepo !== requestedRepo) {
67
+ return {
68
+ configured: false,
69
+ removeExisting: true,
70
+ envVars,
71
+ reason: `Existing delegation MCP entry is pinned to ${existing.pinnedRepo}; reconfiguring.`
72
+ };
73
+ }
74
+ return {
75
+ configured: true,
76
+ removeExisting: false,
77
+ envVars,
78
+ reason: `Delegation MCP is already configured (pinned to ${existing.pinnedRepo}).`
79
+ };
80
+ }
81
+ return {
82
+ configured: true,
83
+ removeExisting: false,
84
+ envVars,
85
+ reason: 'Delegation MCP is already configured.'
86
+ };
87
+ }
88
+ // Fall back to directly scanning config.toml when the Codex CLI probe is unavailable.
89
+ const configured = isDelegationConfiguredFallback(options.configPath);
90
+ return { configured, removeExisting: false, envVars: {} };
91
+ }
92
+ function applyDelegationSetup(plan, env) {
93
+ const envFlags = [];
94
+ for (const [key, value] of Object.entries(plan.envVars ?? {})) {
95
+ envFlags.push('--env', `${key}=${value}`);
96
+ }
97
+ return new Promise((resolve, reject) => {
98
+ const runAdd = () => {
99
+ const child = spawn(plan.codexBin, ['mcp', 'add', 'delegation', ...envFlags, '--', 'codex-orchestrator', 'delegate-server'], { stdio: 'inherit', env });
100
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
101
+ child.once('exit', (code) => {
102
+ if (code === 0) {
103
+ resolve();
104
+ }
105
+ else {
106
+ reject(new Error(`codex mcp add exited with code ${code ?? 'unknown'}`));
107
+ }
108
+ });
109
+ };
110
+ if (!plan.removeExisting) {
111
+ runAdd();
112
+ return;
113
+ }
114
+ const child = spawn(plan.codexBin, ['mcp', 'remove', 'delegation'], { stdio: 'inherit', env });
115
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
116
+ child.once('exit', (code) => {
117
+ if (code === 0) {
118
+ runAdd();
119
+ }
120
+ else {
121
+ reject(new Error(`codex mcp remove exited with code ${code ?? 'unknown'}`));
122
+ }
123
+ });
124
+ });
125
+ }
126
+ function readDelegationMcpServer(codexBin, env) {
127
+ const result = spawnSync(codexBin, ['mcp', 'get', 'delegation', '--json'], {
128
+ encoding: 'utf8',
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ timeout: 5000,
131
+ env
132
+ });
133
+ if (result.error || result.status !== 0) {
134
+ return null;
135
+ }
136
+ const stdout = String(result.stdout ?? '').trim();
137
+ if (!stdout) {
138
+ return null;
139
+ }
140
+ try {
141
+ const parsed = JSON.parse(stdout);
142
+ const transport = parsed.transport;
143
+ const args = Array.isArray(transport?.args)
144
+ ? transport.args.filter((value) => typeof value === 'string')
145
+ : [];
146
+ const envVars = {};
147
+ const envRecord = transport?.env;
148
+ if (envRecord && typeof envRecord === 'object' && !Array.isArray(envRecord)) {
149
+ for (const [key, value] of Object.entries(envRecord)) {
150
+ if (typeof value === 'string') {
151
+ envVars[key] = value;
152
+ }
153
+ }
154
+ }
155
+ const pinnedRepo = readPinnedRepo(args);
156
+ return { args, pinnedRepo, envVars };
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ function readPinnedRepo(args) {
163
+ const index = args.indexOf('--repo');
164
+ if (index === -1) {
165
+ return null;
166
+ }
167
+ const candidate = args[index + 1];
168
+ return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate.trim() : null;
169
+ }
170
+ function isDelegationConfiguredFallback(configPath) {
171
+ if (!existsSync(configPath)) {
172
+ return false;
173
+ }
174
+ try {
175
+ // Keep parsing loose; we only need to know whether a delegation entry exists.
176
+ const raw = readFileSync(configPath, 'utf8');
177
+ return hasMcpServerEntry(raw, 'delegation');
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
183
+ function hasMcpServerEntry(raw, serverName) {
184
+ const lines = raw.split('\n');
185
+ let currentTable = null;
186
+ for (const line of lines) {
187
+ const trimmed = stripTomlComment(line).trim();
188
+ if (!trimmed) {
189
+ continue;
190
+ }
191
+ const tableMatch = trimmed.match(/^\[(.+)\]$/u);
192
+ if (tableMatch) {
193
+ currentTable = tableMatch[1]?.trim() ?? null;
194
+ if (currentTable === `mcp_servers.${serverName}` ||
195
+ currentTable === `mcp_servers."${serverName}"` ||
196
+ currentTable === `mcp_servers.'${serverName}'`) {
197
+ return true;
198
+ }
199
+ continue;
200
+ }
201
+ if (trimmed.startsWith('mcp_servers.')) {
202
+ if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
203
+ return true;
204
+ }
205
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
206
+ return true;
207
+ }
208
+ if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
209
+ return true;
210
+ }
211
+ if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
212
+ return true;
213
+ }
214
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
215
+ return true;
216
+ }
217
+ if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
218
+ return true;
219
+ }
220
+ }
221
+ if (currentTable === 'mcp_servers') {
222
+ const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
223
+ if (entryPattern.test(trimmed)) {
224
+ return true;
225
+ }
226
+ }
227
+ }
228
+ return false;
229
+ }
230
+ function stripTomlComment(line) {
231
+ const index = line.indexOf('#');
232
+ if (index === -1) {
233
+ return line;
234
+ }
235
+ return line.slice(0, index);
236
+ }
237
+ function escapeRegExp(value) {
238
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
239
+ }
@@ -1,5 +1,10 @@
1
1
  import process from 'node:process';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
2
5
  import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
6
+ import { resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
7
+ import { resolveCodexHome } from './utils/codexPaths.js';
3
8
  import { resolveOptionalDependency } from './utils/optionalDeps.js';
4
9
  const OPTIONAL_DEPENDENCIES = [
5
10
  {
@@ -65,11 +70,57 @@ export function runDoctor(cwd = process.cwd()) {
65
70
  if (readiness.config.status !== 'ok') {
66
71
  missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
67
72
  }
73
+ const codexBin = resolveCodexCliBin(process.env);
74
+ const managedCodex = resolveCodexCliReadiness(process.env);
75
+ const features = readCodexFeatureFlags(codexBin);
76
+ const collabEnabled = features?.collab ?? null;
77
+ const collabStatus = features === null ? 'unavailable' : collabEnabled ? 'ok' : 'disabled';
78
+ const cloudCmdAvailable = canRunCommand(codexBin, ['cloud', '--help']);
79
+ const cloudEnvIdConfigured = typeof process.env.CODEX_CLOUD_ENV_ID === 'string' && process.env.CODEX_CLOUD_ENV_ID.trim().length > 0;
80
+ const cloudBranch = typeof process.env.CODEX_CLOUD_BRANCH === 'string' && process.env.CODEX_CLOUD_BRANCH.trim().length > 0
81
+ ? process.env.CODEX_CLOUD_BRANCH.trim().replace(/^refs\/heads\//u, '')
82
+ : null;
83
+ const cloudStatus = !cloudCmdAvailable ? 'unavailable' : cloudEnvIdConfigured ? 'ok' : 'not_configured';
84
+ const delegationConfig = inspectDelegationConfig();
85
+ const delegationStatus = delegationConfig.status === 'ok' ? 'ok' : 'missing-config';
68
86
  return {
69
87
  status: missing.length === 0 ? 'ok' : 'warning',
70
88
  missing,
71
89
  dependencies,
72
- devtools
90
+ devtools,
91
+ codex_cli: {
92
+ active: { command: codexBin },
93
+ managed: managedCodex
94
+ },
95
+ collab: {
96
+ status: collabStatus,
97
+ enabled: collabEnabled,
98
+ enablement: [
99
+ 'Enable collab for symbolic RLM runs with: codex-orchestrator rlm --collab "<goal>"',
100
+ 'Or set: RLM_SYMBOLIC_COLLAB=1 (implies symbolic mode when using --collab).',
101
+ 'If collab is disabled in codex features: codex features enable collab'
102
+ ]
103
+ },
104
+ cloud: {
105
+ status: cloudStatus,
106
+ env_id_configured: cloudEnvIdConfigured,
107
+ branch: cloudBranch,
108
+ enablement: [
109
+ 'Set CODEX_CLOUD_ENV_ID to a valid Codex Cloud environment id.',
110
+ 'Optional: set CODEX_CLOUD_BRANCH (must exist on origin).',
111
+ 'Then run a pipeline stage in cloud mode with: codex-orchestrator start <pipeline> --cloud --target <stage-id>'
112
+ ]
113
+ },
114
+ delegation: {
115
+ status: delegationStatus,
116
+ config: delegationConfig,
117
+ enablement: [
118
+ 'Run: codex-orchestrator delegation setup --yes',
119
+ 'Or manually: codex mcp add delegation -- codex-orchestrator delegate-server',
120
+ "Enable for a run with: codex -c 'mcp_servers.delegation.enabled=true' ...",
121
+ 'See: codex-orchestrator init codex'
122
+ ]
123
+ }
73
124
  };
74
125
  }
75
126
  export function formatDoctorSummary(result) {
@@ -118,5 +169,164 @@ export function formatDoctorSummary(result) {
118
169
  for (const line of result.devtools.enablement) {
119
170
  lines.push(` - ${line}`);
120
171
  }
172
+ lines.push(`Codex CLI: ${result.codex_cli.active.command}`);
173
+ lines.push(` - managed: ${result.codex_cli.managed.status} (${result.codex_cli.managed.config.path})`);
174
+ if (result.codex_cli.managed.status === 'invalid' && result.codex_cli.managed.config.error) {
175
+ lines.push(` error: ${result.codex_cli.managed.config.error}`);
176
+ }
177
+ if (result.codex_cli.managed.status === 'ok') {
178
+ lines.push(` - binary: ${result.codex_cli.managed.binary.status} (${result.codex_cli.managed.binary.path})`);
179
+ if (result.codex_cli.managed.install?.version) {
180
+ lines.push(` - version: ${result.codex_cli.managed.install.version}`);
181
+ }
182
+ }
183
+ lines.push(`Collab: ${result.collab.status}`);
184
+ if (result.collab.enabled !== null) {
185
+ lines.push(` - enabled: ${result.collab.enabled}`);
186
+ }
187
+ for (const line of result.collab.enablement) {
188
+ lines.push(` - ${line}`);
189
+ }
190
+ lines.push(`Cloud: ${result.cloud.status}`);
191
+ lines.push(` - CODEX_CLOUD_ENV_ID: ${result.cloud.env_id_configured ? 'set' : 'missing'}`);
192
+ lines.push(` - CODEX_CLOUD_BRANCH: ${result.cloud.branch ?? '<unset>'}`);
193
+ for (const line of result.cloud.enablement) {
194
+ lines.push(` - ${line}`);
195
+ }
196
+ lines.push(`Delegation: ${result.delegation.status}`);
197
+ const delegationConfigLabel = result.delegation.config.status === 'ok'
198
+ ? `ok (${result.delegation.config.path})`
199
+ : `missing (${result.delegation.config.path})`;
200
+ lines.push(` - config.toml: ${delegationConfigLabel}`);
201
+ if (result.delegation.config.detail) {
202
+ lines.push(` detail: ${result.delegation.config.detail}`);
203
+ }
204
+ for (const line of result.delegation.enablement) {
205
+ lines.push(` - ${line}`);
206
+ }
121
207
  return lines;
122
208
  }
209
+ function readCodexFeatureFlags(codexBin) {
210
+ const result = spawnSync(codexBin, ['features', 'list'], {
211
+ encoding: 'utf8',
212
+ stdio: ['ignore', 'pipe', 'pipe'],
213
+ timeout: 5000
214
+ });
215
+ if (result.error || result.status !== 0) {
216
+ return null;
217
+ }
218
+ const stdout = String(result.stdout ?? '');
219
+ const flags = {};
220
+ for (const line of stdout.split(/\r?\n/u)) {
221
+ const trimmed = line.trim();
222
+ if (!trimmed) {
223
+ continue;
224
+ }
225
+ const tokens = trimmed.split(/\s+/u);
226
+ if (tokens.length < 2) {
227
+ continue;
228
+ }
229
+ const name = tokens[0] ?? '';
230
+ const enabledToken = tokens[tokens.length - 1] ?? '';
231
+ if (!name) {
232
+ continue;
233
+ }
234
+ if (enabledToken === 'true') {
235
+ flags[name] = true;
236
+ }
237
+ else if (enabledToken === 'false') {
238
+ flags[name] = false;
239
+ }
240
+ }
241
+ return flags;
242
+ }
243
+ function canRunCommand(command, args) {
244
+ const result = spawnSync(command, args, {
245
+ encoding: 'utf8',
246
+ stdio: ['ignore', 'ignore', 'ignore'],
247
+ timeout: 5000
248
+ });
249
+ if (result.error) {
250
+ return false;
251
+ }
252
+ return result.status === 0;
253
+ }
254
+ function inspectDelegationConfig(env = process.env) {
255
+ const codexHome = resolveCodexHome(env);
256
+ const configPath = join(codexHome, 'config.toml');
257
+ if (!existsSync(configPath)) {
258
+ return { status: 'missing', path: configPath, detail: 'config.toml not found' };
259
+ }
260
+ try {
261
+ const raw = readFileSync(configPath, 'utf8');
262
+ const hasEntry = hasMcpServerEntry(raw, 'delegation');
263
+ if (hasEntry) {
264
+ return { status: 'ok', path: configPath };
265
+ }
266
+ return { status: 'missing', path: configPath, detail: 'mcp_servers.delegation entry not found' };
267
+ }
268
+ catch (error) {
269
+ return {
270
+ status: 'missing',
271
+ path: configPath,
272
+ detail: error instanceof Error ? error.message : String(error)
273
+ };
274
+ }
275
+ }
276
+ function hasMcpServerEntry(raw, serverName) {
277
+ const lines = raw.split('\n');
278
+ let currentTable = null;
279
+ for (const line of lines) {
280
+ const trimmed = stripTomlComment(line).trim();
281
+ if (!trimmed) {
282
+ continue;
283
+ }
284
+ const tableMatch = trimmed.match(/^\[(.+)\]$/u);
285
+ if (tableMatch) {
286
+ currentTable = tableMatch[1]?.trim() ?? null;
287
+ if (currentTable === `mcp_servers.${serverName}` ||
288
+ currentTable === `mcp_servers."${serverName}"` ||
289
+ currentTable === `mcp_servers.'${serverName}'`) {
290
+ return true;
291
+ }
292
+ continue;
293
+ }
294
+ if (trimmed.startsWith('mcp_servers.')) {
295
+ if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
296
+ return true;
297
+ }
298
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
299
+ return true;
300
+ }
301
+ if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
302
+ return true;
303
+ }
304
+ if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
305
+ return true;
306
+ }
307
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
308
+ return true;
309
+ }
310
+ if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
311
+ return true;
312
+ }
313
+ }
314
+ if (currentTable === 'mcp_servers') {
315
+ const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
316
+ if (entryPattern.test(trimmed)) {
317
+ return true;
318
+ }
319
+ }
320
+ }
321
+ return false;
322
+ }
323
+ function stripTomlComment(line) {
324
+ const index = line.indexOf('#');
325
+ if (index === -1) {
326
+ return line;
327
+ }
328
+ return line.slice(0, index);
329
+ }
330
+ function escapeRegExp(value) {
331
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
332
+ }
@@ -0,0 +1,210 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import process from 'node:process';
5
+ import { collectManifests, findSubagentManifests, parseRunIdTimestamp, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
6
+ import { normalizeTaskKey as normalizeTaskKeyAny } from '../../../scripts/lib/docs-helpers.js';
7
+ const normalizeTaskKey = normalizeTaskKeyAny;
8
+ export async function runDoctorUsage(options = {}) {
9
+ const windowDays = clampInt(options.windowDays ?? 30, 1, 3650);
10
+ const cutoffMs = Date.now() - windowDays * 24 * 60 * 60 * 1000;
11
+ const cutoffIso = new Date(cutoffMs).toISOString();
12
+ const env = resolveEnvironmentPaths();
13
+ const manifestPaths = await collectManifests(env.runsRoot, options.taskFilter ?? undefined);
14
+ const seenRunIds = new Set();
15
+ const pipelines = new Map();
16
+ const cloudByStatus = {};
17
+ const statusCounts = { total: 0, succeeded: 0, failed: 0, cancelled: 0, other: 0 };
18
+ let cloudRuns = 0;
19
+ let rlmRuns = 0;
20
+ let collabRunsWithToolCalls = 0;
21
+ let collabTotalToolCalls = 0;
22
+ const collabCaptureDisabled = String(process.env.CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS ?? '').trim() === '0';
23
+ const activeIndexTasks = new Set();
24
+ const taskKeys = readTaskIndexKeys(env.repoRoot);
25
+ for (const manifestPath of manifestPaths) {
26
+ const runIdFromPath = extractRunIdFromManifestPath(manifestPath);
27
+ if (!runIdFromPath) {
28
+ continue;
29
+ }
30
+ if (seenRunIds.has(runIdFromPath)) {
31
+ continue;
32
+ }
33
+ const timestamp = parseRunIdTimestamp(runIdFromPath);
34
+ if (timestamp && timestamp.getTime() < cutoffMs) {
35
+ continue;
36
+ }
37
+ let manifest;
38
+ try {
39
+ const raw = await readFile(manifestPath, 'utf8');
40
+ manifest = JSON.parse(raw);
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ const runId = typeof manifest.run_id === 'string' && manifest.run_id ? manifest.run_id : runIdFromPath;
46
+ if (seenRunIds.has(runId)) {
47
+ continue;
48
+ }
49
+ seenRunIds.add(runId);
50
+ const startedAtMs = Date.parse(manifest.started_at ?? '') || timestamp?.getTime() || 0;
51
+ if (!startedAtMs || startedAtMs < cutoffMs) {
52
+ continue;
53
+ }
54
+ statusCounts.total += 1;
55
+ if (manifest.status === 'succeeded') {
56
+ statusCounts.succeeded += 1;
57
+ }
58
+ else if (manifest.status === 'failed') {
59
+ statusCounts.failed += 1;
60
+ }
61
+ else if (manifest.status === 'cancelled') {
62
+ statusCounts.cancelled += 1;
63
+ }
64
+ else {
65
+ statusCounts.other += 1;
66
+ }
67
+ const pipelineId = typeof manifest.pipeline_id === 'string' && manifest.pipeline_id ? manifest.pipeline_id : 'unknown';
68
+ pipelines.set(pipelineId, (pipelines.get(pipelineId) ?? 0) + 1);
69
+ if (pipelineId === 'rlm') {
70
+ rlmRuns += 1;
71
+ }
72
+ if (manifest.cloud_execution) {
73
+ cloudRuns += 1;
74
+ const status = (manifest.cloud_execution.status ?? 'unknown').trim() || 'unknown';
75
+ cloudByStatus[status] = (cloudByStatus[status] ?? 0) + 1;
76
+ }
77
+ if (Array.isArray(manifest.collab_tool_calls) && manifest.collab_tool_calls.length > 0) {
78
+ collabRunsWithToolCalls += 1;
79
+ collabTotalToolCalls += manifest.collab_tool_calls.length;
80
+ }
81
+ if (taskKeys.has(manifest.task_id)) {
82
+ activeIndexTasks.add(manifest.task_id);
83
+ }
84
+ }
85
+ const pipelineTop = [...pipelines.entries()]
86
+ .sort((a, b) => b[1] - a[1])
87
+ .slice(0, 10)
88
+ .map(([id, runs]) => ({ id, runs }));
89
+ const delegationErrors = [];
90
+ let activeWithSubagents = 0;
91
+ let totalSubagentManifests = 0;
92
+ const activeTasks = [...activeIndexTasks];
93
+ const subagentResults = await Promise.all(activeTasks.map(async (taskId) => {
94
+ const result = await findSubagentManifests(env.runsRoot, taskId);
95
+ if (result.error) {
96
+ delegationErrors.push(result.error);
97
+ }
98
+ return { taskId, found: result.found };
99
+ }));
100
+ for (const item of subagentResults) {
101
+ totalSubagentManifests += item.found.length;
102
+ if (item.found.length > 0) {
103
+ activeWithSubagents += 1;
104
+ }
105
+ }
106
+ return {
107
+ window_days: windowDays,
108
+ cutoff_iso: cutoffIso,
109
+ runs: statusCounts,
110
+ cloud: {
111
+ runs: cloudRuns,
112
+ by_status: cloudByStatus
113
+ },
114
+ rlm: {
115
+ runs: rlmRuns
116
+ },
117
+ collab: {
118
+ runs_with_tool_calls: collabRunsWithToolCalls,
119
+ total_tool_calls: collabTotalToolCalls,
120
+ capture_disabled: collabCaptureDisabled
121
+ },
122
+ delegation: {
123
+ active_top_level_tasks: activeTasks.length,
124
+ active_with_subagents: activeWithSubagents,
125
+ total_subagent_manifests: totalSubagentManifests,
126
+ errors: delegationErrors
127
+ },
128
+ pipelines: {
129
+ total: pipelines.size,
130
+ top: pipelineTop
131
+ }
132
+ };
133
+ }
134
+ export function formatDoctorUsageSummary(result) {
135
+ const lines = [];
136
+ lines.push(`Usage (last ${result.window_days}d, cutoff ${result.cutoff_iso})`);
137
+ lines.push(` - runs: ${result.runs.total} (ok=${result.runs.succeeded}, failed=${result.runs.failed}, cancelled=${result.runs.cancelled}, other=${result.runs.other})`);
138
+ lines.push(` - cloud: ${result.cloud.runs} (${formatPercent(result.cloud.runs, result.runs.total)})${formatCloudStatuses(result.cloud.by_status)}`);
139
+ lines.push(` - rlm: ${result.rlm.runs} (${formatPercent(result.rlm.runs, result.runs.total)})`);
140
+ const collabSuffix = result.collab.capture_disabled ? ' (capture disabled)' : '';
141
+ lines.push(` - collab: ${result.collab.runs_with_tool_calls} (${formatPercent(result.collab.runs_with_tool_calls, result.runs.total)})${collabSuffix}`);
142
+ if (result.delegation.active_top_level_tasks > 0) {
143
+ lines.push(` - delegation: ${result.delegation.active_with_subagents}/${result.delegation.active_top_level_tasks} top-level tasks have subagent manifests`);
144
+ }
145
+ else {
146
+ lines.push(' - delegation: no top-level tasks detected in tasks/index.json for this window');
147
+ }
148
+ if (result.pipelines.top.length > 0) {
149
+ lines.push('Top pipelines:');
150
+ for (const entry of result.pipelines.top) {
151
+ lines.push(` - ${entry.id}: ${entry.runs}`);
152
+ }
153
+ }
154
+ if (result.delegation.errors.length > 0) {
155
+ lines.push('Delegation scan warnings:');
156
+ for (const warning of result.delegation.errors.slice(0, 3)) {
157
+ lines.push(` - ${warning}`);
158
+ }
159
+ }
160
+ return lines;
161
+ }
162
+ function extractRunIdFromManifestPath(manifestPath) {
163
+ if (!manifestPath) {
164
+ return null;
165
+ }
166
+ // .../<run-id>/manifest.json
167
+ const dir = manifestPath.endsWith('manifest.json') ? basename(dirname(manifestPath)) : null;
168
+ return dir && dir !== '..' ? dir : null;
169
+ }
170
+ function clampInt(value, min, max) {
171
+ const rounded = Math.floor(value);
172
+ if (!Number.isFinite(rounded)) {
173
+ return min;
174
+ }
175
+ return Math.max(min, Math.min(max, rounded));
176
+ }
177
+ function formatPercent(numerator, denominator) {
178
+ if (!denominator) {
179
+ return '0%';
180
+ }
181
+ const pct = (numerator / denominator) * 100;
182
+ return `${Math.round(pct * 10) / 10}%`;
183
+ }
184
+ function formatCloudStatuses(byStatus) {
185
+ const entries = Object.entries(byStatus);
186
+ if (entries.length === 0) {
187
+ return '';
188
+ }
189
+ entries.sort((a, b) => b[1] - a[1]);
190
+ const top = entries
191
+ .slice(0, 3)
192
+ .map(([status, count]) => `${status}=${count}`)
193
+ .join(', ');
194
+ return ` [${top}]`;
195
+ }
196
+ function readTaskIndexKeys(repoRoot) {
197
+ const indexPath = join(repoRoot, 'tasks', 'index.json');
198
+ try {
199
+ const raw = readFileSync(indexPath, 'utf8');
200
+ const parsed = JSON.parse(raw);
201
+ const items = Array.isArray(parsed?.items) ? parsed.items : [];
202
+ const keys = items
203
+ .map((item) => normalizeTaskKey(item))
204
+ .filter((key) => typeof key === 'string' && key.length > 0);
205
+ return new Set(keys);
206
+ }
207
+ catch {
208
+ return new Set();
209
+ }
210
+ }
@@ -61,7 +61,7 @@ export function formatInitSummary(result, cwd) {
61
61
  lines.push('No files written.');
62
62
  }
63
63
  lines.push('Next steps (recommended):');
64
- lines.push(` - codex mcp add delegation -- codex-orchestrator delegate-server --repo ${cwd}`);
64
+ lines.push(' - codex-orchestrator delegation setup --yes');
65
65
  lines.push(' - codex-orchestrator codex setup # optional: managed/pinned Codex CLI (stock CLI works by default)');
66
66
  return lines;
67
67
  }
@@ -31,6 +31,7 @@ import { CLI_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from '../utils/
31
31
  import { resolveCodexCliBin } from './utils/codexCli.js';
32
32
  import { CodexCloudTaskExecutor } from '../cloud/CodexCloudTaskExecutor.js';
33
33
  import { persistPipelineExperience } from './services/pipelineExperience.js';
34
+ import { runCloudPreflight } from './utils/cloudPreflight.js';
34
35
  const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
35
36
  const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
36
37
  const DEFAULT_CLOUD_POLL_INTERVAL_SECONDS = 10;
@@ -552,6 +553,27 @@ export class CodexOrchestrator {
552
553
  }
553
554
  async executePipeline(options) {
554
555
  if (options.mode === 'cloud') {
556
+ const environmentId = resolveCloudEnvironmentId(options.task, options.target, options.envOverrides);
557
+ const branch = readCloudString(options.envOverrides?.CODEX_CLOUD_BRANCH) ??
558
+ readCloudString(process.env.CODEX_CLOUD_BRANCH);
559
+ const mergedEnv = { ...process.env, ...(options.envOverrides ?? {}) };
560
+ const codexBin = resolveCodexCliBin(mergedEnv);
561
+ const preflight = await runCloudPreflight({
562
+ repoRoot: options.env.repoRoot,
563
+ codexBin,
564
+ environmentId,
565
+ branch,
566
+ env: mergedEnv
567
+ });
568
+ if (!preflight.ok) {
569
+ const detail = `Cloud preflight failed; falling back to mcp. ` +
570
+ preflight.issues.map((issue) => issue.message).join(' ');
571
+ appendSummary(options.manifest, detail);
572
+ logger.warn(detail);
573
+ const fallback = await this.executePipeline({ ...options, mode: 'mcp', executionModeOverride: 'mcp' });
574
+ fallback.notes.unshift(detail);
575
+ return fallback;
576
+ }
555
577
  return await this.executeCloudPipeline(options);
556
578
  }
557
579
  const { env, pipeline, manifest, paths, runEvents, envOverrides } = options;
@@ -883,6 +905,12 @@ export class CodexOrchestrator {
883
905
  const disableFeatures = readCloudFeatureList(readCloudString(envOverrides?.CODEX_CLOUD_DISABLE_FEATURES) ??
884
906
  readCloudString(process.env.CODEX_CLOUD_DISABLE_FEATURES));
885
907
  const codexBin = resolveCodexCliBin({ ...process.env, ...(envOverrides ?? {}) });
908
+ const cloudEnvOverrides = {
909
+ ...(envOverrides ?? {}),
910
+ CODEX_NON_INTERACTIVE: envOverrides?.CODEX_NON_INTERACTIVE ?? process.env.CODEX_NON_INTERACTIVE ?? '1',
911
+ CODEX_NO_INTERACTIVE: envOverrides?.CODEX_NO_INTERACTIVE ?? process.env.CODEX_NO_INTERACTIVE ?? '1',
912
+ CODEX_INTERACTIVE: envOverrides?.CODEX_INTERACTIVE ?? process.env.CODEX_INTERACTIVE ?? '0'
913
+ };
886
914
  const cloudResult = await executor.execute({
887
915
  codexBin,
888
916
  prompt,
@@ -895,7 +923,7 @@ export class CodexOrchestrator {
895
923
  branch,
896
924
  enableFeatures,
897
925
  disableFeatures,
898
- env: envOverrides
926
+ env: cloudEnvOverrides
899
927
  });
900
928
  success = cloudResult.success;
901
929
  notes.push(...cloudResult.notes);
@@ -53,6 +53,7 @@ export async function persistPipelineExperience(params) {
53
53
  maxSummaryWords: instructions.experienceMaxWords
54
54
  });
55
55
  await store.recordBatch([record], relativeToRepo(env, paths.manifestPath));
56
+ logger.info(`[experience] Recorded pipeline experience (domain=${domain}, words=${tokenCount}) for ${pipeline.id} run ${manifest.run_id}.`);
56
57
  }
57
58
  catch (error) {
58
59
  logger.warn(`Failed to persist pipeline experience for run ${manifest.run_id}: ${error?.message ?? String(error)}`);
@@ -0,0 +1,101 @@
1
+ import { spawn } from 'node:child_process';
2
+ function runCommand(command, args, options) {
3
+ const timeoutMs = options.timeoutMs ?? 10_000;
4
+ return new Promise((resolve) => {
5
+ const child = spawn(command, args, {
6
+ cwd: options.cwd,
7
+ env: options.env,
8
+ stdio: ['ignore', 'pipe', 'pipe']
9
+ });
10
+ let stdout = '';
11
+ let stderr = '';
12
+ let settled = false;
13
+ const timer = setTimeout(() => {
14
+ if (settled) {
15
+ return;
16
+ }
17
+ settled = true;
18
+ child.kill('SIGTERM');
19
+ setTimeout(() => child.kill('SIGKILL'), 4000).unref();
20
+ resolve({
21
+ exitCode: 124,
22
+ stdout,
23
+ stderr: `${stderr}\nTimed out after ${Math.round(timeoutMs / 1000)}s.`.trim()
24
+ });
25
+ }, timeoutMs);
26
+ timer.unref();
27
+ child.stdout?.on('data', (chunk) => {
28
+ stdout += chunk.toString();
29
+ });
30
+ child.stderr?.on('data', (chunk) => {
31
+ stderr += chunk.toString();
32
+ });
33
+ child.once('error', (error) => {
34
+ if (settled) {
35
+ return;
36
+ }
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ resolve({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}`.trim() });
40
+ });
41
+ child.once('close', (code) => {
42
+ if (settled) {
43
+ return;
44
+ }
45
+ settled = true;
46
+ clearTimeout(timer);
47
+ resolve({ exitCode: typeof code === 'number' ? code : 1, stdout, stderr });
48
+ });
49
+ });
50
+ }
51
+ function normalizeBranch(raw) {
52
+ const trimmed = String(raw ?? '').trim();
53
+ if (!trimmed) {
54
+ return null;
55
+ }
56
+ return trimmed.replace(/^refs\/heads\//u, '');
57
+ }
58
+ export async function runCloudPreflight(params) {
59
+ const issues = [];
60
+ const branch = normalizeBranch(params.branch);
61
+ if (!params.environmentId) {
62
+ issues.push({
63
+ code: 'missing_environment',
64
+ message: 'Missing CODEX_CLOUD_ENV_ID (or target metadata.cloudEnvId).'
65
+ });
66
+ }
67
+ const codexCheck = await runCommand(params.codexBin, ['--version'], {
68
+ cwd: params.repoRoot,
69
+ env: params.env
70
+ });
71
+ if (codexCheck.exitCode !== 0) {
72
+ issues.push({
73
+ code: 'codex_unavailable',
74
+ message: `Codex CLI is unavailable (${params.codexBin} --version failed).`
75
+ });
76
+ }
77
+ if (branch) {
78
+ const gitCheck = await runCommand('git', ['--version'], { cwd: params.repoRoot, env: params.env });
79
+ if (gitCheck.exitCode !== 0) {
80
+ issues.push({ code: 'git_unavailable', message: 'git is unavailable (required for cloud preflight).' });
81
+ }
82
+ else {
83
+ const branchCheck = await runCommand('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch], { cwd: params.repoRoot, env: params.env });
84
+ if (branchCheck.exitCode !== 0) {
85
+ issues.push({
86
+ code: 'branch_missing',
87
+ message: `Cloud branch '${branch}' was not found on origin. Push it first or set CODEX_CLOUD_BRANCH to an existing remote branch.`
88
+ });
89
+ }
90
+ }
91
+ }
92
+ return {
93
+ ok: issues.length === 0,
94
+ issues,
95
+ details: {
96
+ codexBin: params.codexBin,
97
+ environmentId: params.environmentId,
98
+ branch
99
+ }
100
+ };
101
+ }
package/docs/README.md CHANGED
@@ -25,7 +25,7 @@ Codex Orchestrator is the coordination layer that glues together Codex-driven ag
25
25
 
26
26
  ## How It Works
27
27
  - **Planner → Builder → Tester → Reviewer:** The core `TaskManager` (see `orchestrator/src/manager.ts`) wires together agent interfaces that decide *what* to run (planner), execute the selected pipeline stage (builder), verify results (tester), and give a final decision (reviewer).
28
- - **Execution modes:** Each plan item can flag `requires_cloud` and task metadata can set `execution.parallel`; the mode policy picks `mcp` (local MCP runtime) or `cloud` execution accordingly.
28
+ - **Execution modes:** Each plan item can flag `requires_cloud` and task metadata can set `execution.parallel`; the mode policy picks `mcp` (local MCP runtime) or `cloud` execution accordingly. Cloud runs perform a quick preflight (env id, codex availability, optional remote branch) and fall back to `mcp` with a recorded summary when preflight fails.
29
29
  - **Event-driven persistence:** Milestones emit typed events on `EventBus`. `PersistenceCoordinator` captures run summaries in the task state store and writes manifests so nothing is lost if the process crashes.
30
30
  - **CLI lifecycle:** `CodexOrchestrator` (in `orchestrator/src/cli/orchestrator.ts`) resolves instruction sources (`AGENTS.md`, `docs/AGENTS.md`, `.agent/AGENTS.md`), loads the chosen pipeline, executes each command stage via `runCommandStage`, and keeps heartbeats plus command status current inside the manifest (approval evidence will surface once prompt wiring lands).
31
31
  - **Control-plane & scheduler integrations:** Optional validation (`control-plane/`) and scheduling (`scheduler/`) modules enrich manifests with drift checks, plan assignments, and remote run metadata.
@@ -101,8 +101,9 @@ Use `npx @kbediako/codex-orchestrator resume --run <run-id>` to continue interru
101
101
  ## Companion Package Commands
102
102
  - `codex-orchestrator mcp serve [--repo <path>] [--dry-run] [-- <extra args>]`: launch the MCP stdio server (delegates to `codex mcp-server`; stdout guard keeps protocol-only output, logs to stderr).
103
103
  - `codex-orchestrator init codex [--cwd <path>] [--force]`: copy starter templates into a repo (includes `mcp-client.json` and `AGENTS.md`; no overwrite unless `--force`).
104
- - `codex-orchestrator doctor [--format json]`: check optional tooling dependencies and print install commands.
104
+ - `codex-orchestrator doctor [--format json] [--usage]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands. `--usage` appends a local usage snapshot (scans `.runs/`).
105
105
  - `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
106
+ - `codex-orchestrator delegation setup [--yes]`: configure delegation MCP wiring (`--yes` applies `codex mcp add ...`).
106
107
  - `codex-orchestrator skills install [--force] [--only <skills>] [--codex-home <path>]`: install bundled skills into `$CODEX_HOME/skills` (global skills remain the primary reference when installed).
107
108
  - `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
108
109
  - `codex-orchestrator --version`: print the package version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -137,6 +137,7 @@ Do not treat wrapper handoff-only output as a completed review.
137
137
 
138
138
  - Symptoms: missing collab/delegate tool-call evidence, framing/parsing errors, or unstable collab behavior after CLI upgrades.
139
139
  - Check versions first: `codex --version` and `codex-orchestrator --version`.
140
+ - Confirm feature readiness: `codex-orchestrator doctor` (checks collab/cloud/delegation readiness and prints enablement commands).
140
141
  - CO repo refresh path (safe default): `scripts/codex-cli-refresh.sh --repo <codex-repo> --no-push`.
141
142
  - Rebuild managed CLI only: `codex-orchestrator codex setup --source <codex-repo> --yes --force`.
142
143
  - If local codex is materially behind upstream, sync before diagnosing collab behavior differences.
@@ -63,7 +63,7 @@ Fix by re-registering the server with a TOML-quoted override:
63
63
  codex mcp remove delegation
64
64
  codex mcp add delegation \
65
65
  --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' \
66
- -- codex-orchestrator delegate-server --repo /path/to/repo
66
+ -- codex-orchestrator delegate-server
67
67
  ```
68
68
 
69
69
  ## Server mode vs child mode (don’t mix them up)
@@ -98,7 +98,7 @@ If you want deeper recursion or longer wall-clock time for delegated runs, set R
98
98
  ```bash
99
99
  codex mcp add delegation \
100
100
  --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' \
101
- -- codex-orchestrator delegate-server --repo /path/to/repo
101
+ -- codex-orchestrator delegate-server
102
102
  ```
103
103
 
104
104
  For the `rlm` pipeline specifically, use:
@@ -35,16 +35,16 @@ codex exec \
35
35
  ```
36
36
 
37
37
  Optional (only if you need it):
38
- - Add `--repo /path/to/repo` to the MCP args when registering the server or when you need repo-scoped config.
38
+ - Add `--repo /path/to/repo` only when you want to pin the server to a repo even if Codex is launched outside that repo (default uses cwd).
39
39
  - Add `-c 'features.skills=false'` for a minimal, deterministic background run.
40
40
  - Add `-c 'delegate.mode=question_only'` when the child only needs `delegate.question.*` (and optional `delegate.status`).
41
41
  - Add `-c 'delegate.mode=full'` when the child needs `delegate.spawn/pause/cancel` (nested delegation / run control).
42
42
  - If the task needs external docs or APIs, enable only the relevant MCP server for that environment.
43
43
  - If `delegate.spawn` is missing, re-register the MCP server with full mode (server config controls tool surface):
44
44
  - `codex mcp remove delegation`
45
- - `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server --repo /path/to/repo`
45
+ - `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server`
46
46
  - To raise RLM budgets for delegated runs, re-register with an override (TOML-quoted):
47
- - `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server --repo /path/to/repo`
47
+ - `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server`
48
48
 
49
49
  For deeper background patterns and troubleshooting, see `DELEGATION_GUIDE.md`.
50
50
  For runner + delegation coordination (short `--task` flow), see `docs/delegation-runner-workflow.md`.
@@ -64,8 +64,10 @@ For runner + delegation coordination (short `--task` flow), see `docs/delegation
64
64
  ### 0) One-time setup (register the MCP server)
65
65
 
66
66
  - Register the delegation server once:
67
+ - Preferred: `codex-orchestrator delegation setup --yes`
68
+ - This wraps `codex mcp add delegation ...` and keeps wiring discoverable via `codex-orchestrator doctor`.
67
69
  - `codex mcp add delegation -- codex-orchestrator delegate-server`
68
- - Optional (recommended for repo-scoped config): append `--repo /path/to/repo` to the args.
70
+ - Optional: append `--repo /path/to/repo` to pin the server to one repo (not recommended if you work across repos).
69
71
  - `delegate-server` is the canonical name; `delegation-server` is supported as an alias.
70
72
  - Per-run `-c 'mcp_servers.delegation.enabled=true'` only works **after** registration.
71
73
  - If `delegate.*` tools are missing mid-task, start a new run with: