@kbediako/codex-orchestrator 0.1.21 → 0.1.22

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
@@ -167,6 +167,25 @@ function applyRlmEnvOverrides(flags, goal) {
167
167
  if (goal) {
168
168
  process.env.RLM_GOAL = goal;
169
169
  }
170
+ const collabRaw = flags['collab'];
171
+ if (collabRaw !== undefined) {
172
+ const normalized = typeof collabRaw === 'string' ? collabRaw.trim().toLowerCase() : collabRaw === true ? 'true' : '';
173
+ const enabled = collabRaw === true || normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'auto';
174
+ const disabled = normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off';
175
+ if (enabled) {
176
+ process.env.RLM_SYMBOLIC_COLLAB = '1';
177
+ // Collab is only used in the symbolic loop; make the flag do what users expect.
178
+ if (!process.env.RLM_MODE) {
179
+ process.env.RLM_MODE = 'symbolic';
180
+ }
181
+ }
182
+ else if (disabled) {
183
+ process.env.RLM_SYMBOLIC_COLLAB = '0';
184
+ }
185
+ else if (typeof collabRaw === 'string') {
186
+ throw new Error('Invalid --collab value. Use --collab (or: --collab auto|true|false).');
187
+ }
188
+ }
170
189
  const validator = readStringFlag(flags, 'validator');
171
190
  if (validator) {
172
191
  process.env.RLM_VALIDATOR = validator;
@@ -879,6 +898,7 @@ Commands:
879
898
  --cloud Shortcut for --execution-mode cloud.
880
899
  --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
881
900
  --goal "<goal>" When pipeline is rlm, set the RLM goal.
901
+ --collab [auto|true|false] When pipeline is rlm, enable collab subagents (implies symbolic mode).
882
902
  --validator <cmd|none> When pipeline is rlm, set the validator command.
883
903
  --max-iterations <n> When pipeline is rlm, override max iterations.
884
904
  --max-minutes <n> When pipeline is rlm, override max minutes.
@@ -888,6 +908,7 @@ Commands:
888
908
 
889
909
  rlm "<goal>" Run RLM loop until validator passes.
890
910
  --task <id> Override task identifier.
911
+ --collab [auto|true|false] Enable collab subagents (implies symbolic mode).
891
912
  --validator <cmd|none> Set validator command or disable validation.
892
913
  --max-iterations <n> Override max iterations (0 = unlimited with validator).
893
914
  --max-minutes <n> Optional time-based guardrail in minutes.
@@ -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({
@@ -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,56 @@ 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 mcp add delegation -- codex-orchestrator delegate-server --repo ${cwd}`,
119
+ "Enable for a run with: codex -c 'mcp_servers.delegation.enabled=true' ...",
120
+ 'See: codex-orchestrator init codex'
121
+ ]
122
+ }
73
123
  };
74
124
  }
75
125
  export function formatDoctorSummary(result) {
@@ -118,5 +168,164 @@ export function formatDoctorSummary(result) {
118
168
  for (const line of result.devtools.enablement) {
119
169
  lines.push(` - ${line}`);
120
170
  }
171
+ lines.push(`Codex CLI: ${result.codex_cli.active.command}`);
172
+ lines.push(` - managed: ${result.codex_cli.managed.status} (${result.codex_cli.managed.config.path})`);
173
+ if (result.codex_cli.managed.status === 'invalid' && result.codex_cli.managed.config.error) {
174
+ lines.push(` error: ${result.codex_cli.managed.config.error}`);
175
+ }
176
+ if (result.codex_cli.managed.status === 'ok') {
177
+ lines.push(` - binary: ${result.codex_cli.managed.binary.status} (${result.codex_cli.managed.binary.path})`);
178
+ if (result.codex_cli.managed.install?.version) {
179
+ lines.push(` - version: ${result.codex_cli.managed.install.version}`);
180
+ }
181
+ }
182
+ lines.push(`Collab: ${result.collab.status}`);
183
+ if (result.collab.enabled !== null) {
184
+ lines.push(` - enabled: ${result.collab.enabled}`);
185
+ }
186
+ for (const line of result.collab.enablement) {
187
+ lines.push(` - ${line}`);
188
+ }
189
+ lines.push(`Cloud: ${result.cloud.status}`);
190
+ lines.push(` - CODEX_CLOUD_ENV_ID: ${result.cloud.env_id_configured ? 'set' : 'missing'}`);
191
+ lines.push(` - CODEX_CLOUD_BRANCH: ${result.cloud.branch ?? '<unset>'}`);
192
+ for (const line of result.cloud.enablement) {
193
+ lines.push(` - ${line}`);
194
+ }
195
+ lines.push(`Delegation: ${result.delegation.status}`);
196
+ const delegationConfigLabel = result.delegation.config.status === 'ok'
197
+ ? `ok (${result.delegation.config.path})`
198
+ : `missing (${result.delegation.config.path})`;
199
+ lines.push(` - config.toml: ${delegationConfigLabel}`);
200
+ if (result.delegation.config.detail) {
201
+ lines.push(` detail: ${result.delegation.config.detail}`);
202
+ }
203
+ for (const line of result.delegation.enablement) {
204
+ lines.push(` - ${line}`);
205
+ }
121
206
  return lines;
122
207
  }
208
+ function readCodexFeatureFlags(codexBin) {
209
+ const result = spawnSync(codexBin, ['features', 'list'], {
210
+ encoding: 'utf8',
211
+ stdio: ['ignore', 'pipe', 'pipe'],
212
+ timeout: 5000
213
+ });
214
+ if (result.error || result.status !== 0) {
215
+ return null;
216
+ }
217
+ const stdout = String(result.stdout ?? '');
218
+ const flags = {};
219
+ for (const line of stdout.split(/\r?\n/u)) {
220
+ const trimmed = line.trim();
221
+ if (!trimmed) {
222
+ continue;
223
+ }
224
+ const tokens = trimmed.split(/\s+/u);
225
+ if (tokens.length < 2) {
226
+ continue;
227
+ }
228
+ const name = tokens[0] ?? '';
229
+ const enabledToken = tokens[tokens.length - 1] ?? '';
230
+ if (!name) {
231
+ continue;
232
+ }
233
+ if (enabledToken === 'true') {
234
+ flags[name] = true;
235
+ }
236
+ else if (enabledToken === 'false') {
237
+ flags[name] = false;
238
+ }
239
+ }
240
+ return flags;
241
+ }
242
+ function canRunCommand(command, args) {
243
+ const result = spawnSync(command, args, {
244
+ encoding: 'utf8',
245
+ stdio: ['ignore', 'ignore', 'ignore'],
246
+ timeout: 5000
247
+ });
248
+ if (result.error) {
249
+ return false;
250
+ }
251
+ return result.status === 0;
252
+ }
253
+ function inspectDelegationConfig(env = process.env) {
254
+ const codexHome = resolveCodexHome(env);
255
+ const configPath = join(codexHome, 'config.toml');
256
+ if (!existsSync(configPath)) {
257
+ return { status: 'missing', path: configPath, detail: 'config.toml not found' };
258
+ }
259
+ try {
260
+ const raw = readFileSync(configPath, 'utf8');
261
+ const hasEntry = hasMcpServerEntry(raw, 'delegation');
262
+ if (hasEntry) {
263
+ return { status: 'ok', path: configPath };
264
+ }
265
+ return { status: 'missing', path: configPath, detail: 'mcp_servers.delegation entry not found' };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ status: 'missing',
270
+ path: configPath,
271
+ detail: error instanceof Error ? error.message : String(error)
272
+ };
273
+ }
274
+ }
275
+ function hasMcpServerEntry(raw, serverName) {
276
+ const lines = raw.split('\n');
277
+ let currentTable = null;
278
+ for (const line of lines) {
279
+ const trimmed = stripTomlComment(line).trim();
280
+ if (!trimmed) {
281
+ continue;
282
+ }
283
+ const tableMatch = trimmed.match(/^\[(.+)\]$/u);
284
+ if (tableMatch) {
285
+ currentTable = tableMatch[1]?.trim() ?? null;
286
+ if (currentTable === `mcp_servers.${serverName}` ||
287
+ currentTable === `mcp_servers."${serverName}"` ||
288
+ currentTable === `mcp_servers.'${serverName}'`) {
289
+ return true;
290
+ }
291
+ continue;
292
+ }
293
+ if (trimmed.startsWith('mcp_servers.')) {
294
+ if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
295
+ return true;
296
+ }
297
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
298
+ return true;
299
+ }
300
+ if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
301
+ return true;
302
+ }
303
+ if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
304
+ return true;
305
+ }
306
+ if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
307
+ return true;
308
+ }
309
+ if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
310
+ return true;
311
+ }
312
+ }
313
+ if (currentTable === 'mcp_servers') {
314
+ const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
315
+ if (entryPattern.test(trimmed)) {
316
+ return true;
317
+ }
318
+ }
319
+ }
320
+ return false;
321
+ }
322
+ function stripTomlComment(line) {
323
+ const index = line.indexOf('#');
324
+ if (index === -1) {
325
+ return line;
326
+ }
327
+ return line.slice(0, index);
328
+ }
329
+ function escapeRegExp(value) {
330
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
331
+ }
@@ -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,7 +101,7 @@ 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]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands.
105
105
  - `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
106
106
  - `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
107
  - `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
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.22",
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.