@kbediako/codex-orchestrator 0.1.32 → 0.1.34

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.
Files changed (41) hide show
  1. package/README.md +96 -12
  2. package/codex.orchestrator.json +448 -0
  3. package/dist/bin/codex-orchestrator.js +703 -136
  4. package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
  5. package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +22 -0
  6. package/dist/orchestrator/src/cli/config/userConfig.js +20 -9
  7. package/dist/orchestrator/src/cli/delegationSetup.js +111 -14
  8. package/dist/orchestrator/src/cli/doctor.js +264 -8
  9. package/dist/orchestrator/src/cli/doctorIssueLog.js +350 -0
  10. package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
  11. package/dist/orchestrator/src/cli/init.js +24 -1
  12. package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
  13. package/dist/orchestrator/src/cli/orchestrator.js +180 -5
  14. package/dist/orchestrator/src/cli/rlmRunner.js +289 -35
  15. package/dist/orchestrator/src/cli/run/manifest.js +31 -6
  16. package/dist/orchestrator/src/cli/services/commandRunner.js +10 -2
  17. package/dist/orchestrator/src/cli/services/pipelineResolver.js +70 -18
  18. package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
  19. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
  20. package/dist/orchestrator/src/cli/skills.js +3 -8
  21. package/dist/orchestrator/src/cli/utils/advancedAutopilot.js +114 -0
  22. package/dist/orchestrator/src/cli/utils/codexCli.js +21 -0
  23. package/dist/orchestrator/src/cli/utils/commandPreview.js +10 -0
  24. package/dist/orchestrator/src/cli/utils/delegationGuardRunner.js +85 -8
  25. package/dist/orchestrator/src/cli/utils/devtools.js +2 -1
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
  27. package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +46 -6
  28. package/dist/orchestrator/src/control-plane/request-builder.js +9 -8
  29. package/dist/scripts/lib/pr-watch-merge.js +367 -3
  30. package/docs/README.md +17 -11
  31. package/package.json +2 -1
  32. package/schemas/manifest.json +27 -0
  33. package/skills/collab-deliberation/SKILL.md +6 -0
  34. package/skills/collab-evals/SKILL.md +4 -0
  35. package/skills/collab-subagents-first/SKILL.md +29 -7
  36. package/skills/delegation-usage/DELEGATION_GUIDE.md +31 -5
  37. package/skills/delegation-usage/SKILL.md +29 -4
  38. package/skills/elegance-review/SKILL.md +14 -3
  39. package/skills/standalone-review/SKILL.md +8 -2
  40. package/templates/README.md +1 -1
  41. package/templates/codex/AGENTS.md +12 -1
@@ -86,6 +86,7 @@ export function formatCodexCliSetupSummary(result) {
86
86
  lines.push(`- Installed SHA256: ${result.install.sha256}`);
87
87
  }
88
88
  lines.push(`- Command: ${result.plan.commandLine}`);
89
+ lines.push('- Selection: stock `codex` stays default. Set CODEX_CLI_USE_MANAGED=1 to use this managed binary.');
89
90
  if (result.status === 'planned') {
90
91
  lines.push('Run with --yes to apply this setup.');
91
92
  }
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path';
2
+ import process from 'node:process';
3
+ export const REPO_CONFIG_REQUIRED_ENV_KEY = 'CODEX_ORCHESTRATOR_REPO_CONFIG_REQUIRED';
4
+ const REPO_CONFIG_REQUIRED_TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
5
+ export function isRepoConfigRequired(env = process.env) {
6
+ const raw = env[REPO_CONFIG_REQUIRED_ENV_KEY];
7
+ if (typeof raw !== 'string') {
8
+ return false;
9
+ }
10
+ const normalized = raw.trim().toLowerCase();
11
+ if (!normalized) {
12
+ return false;
13
+ }
14
+ return REPO_CONFIG_REQUIRED_TRUE_VALUES.has(normalized);
15
+ }
16
+ export function formatRepoConfigRequiredError(repoRoot) {
17
+ return [
18
+ `Repo-local codex.orchestrator.json is required when ${REPO_CONFIG_REQUIRED_ENV_KEY}=1.`,
19
+ `Expected: ${join(repoRoot, 'codex.orchestrator.json')}.`,
20
+ 'Run `codex-orchestrator init codex` to scaffold repo-local config.'
21
+ ].join(' ');
22
+ }
@@ -2,17 +2,21 @@ import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { logger } from '../../logger.js';
4
4
  import { findPackageRoot } from '../utils/packageInfo.js';
5
- export async function loadRepoConfig(env) {
5
+ export async function loadRepoConfig(env, options = {}) {
6
6
  const repoConfigPath = join(env.repoRoot, 'codex.orchestrator.json');
7
7
  const repoConfig = await readConfig(repoConfigPath);
8
8
  if (repoConfig) {
9
- logger.info(`[codex-config] Loaded user config from ${repoConfigPath}`);
9
+ if (!options.quiet) {
10
+ logger.info(`[codex-config] Loaded user config from ${repoConfigPath}`);
11
+ }
10
12
  return normalizeUserConfig(repoConfig, 'repo');
11
13
  }
12
- logger.warn(`[codex-config] Missing codex.orchestrator.json at ${repoConfigPath}`);
14
+ if (!options.quiet) {
15
+ logger.warn(`[codex-config] Missing codex.orchestrator.json at ${repoConfigPath}`);
16
+ }
13
17
  return null;
14
18
  }
15
- export async function loadPackageConfig(env) {
19
+ export async function loadPackageConfig(env, options = {}) {
16
20
  const repoConfigPath = join(env.repoRoot, 'codex.orchestrator.json');
17
21
  const packageRoot = findPackageRoot();
18
22
  const packageConfigPath = join(packageRoot, 'codex.orchestrator.json');
@@ -21,18 +25,25 @@ export async function loadPackageConfig(env) {
21
25
  }
22
26
  const packageConfig = await readConfig(packageConfigPath);
23
27
  if (packageConfig) {
24
- logger.info(`[codex-config] Loaded user config from ${packageConfigPath}`);
28
+ if (!options.quiet) {
29
+ logger.info(`[codex-config] Loaded user config from ${packageConfigPath}`);
30
+ }
25
31
  return normalizeUserConfig(packageConfig, 'package');
26
32
  }
27
- logger.warn(`[codex-config] Missing codex.orchestrator.json at ${packageConfigPath}`);
33
+ if (!options.quiet) {
34
+ logger.warn(`[codex-config] Missing package config at ${packageConfigPath}`);
35
+ }
28
36
  return null;
29
37
  }
30
- export async function loadUserConfig(env) {
31
- const repoConfig = await loadRepoConfig(env);
38
+ export async function loadUserConfig(env, options = {}) {
39
+ const repoConfig = await loadRepoConfig(env, options);
32
40
  if (repoConfig) {
33
41
  return repoConfig;
34
42
  }
35
- return await loadPackageConfig(env);
43
+ if (options.allowPackageFallback === false) {
44
+ return null;
45
+ }
46
+ return await loadPackageConfig(env, options);
36
47
  }
37
48
  export function findPipeline(config, id) {
38
49
  if (!config?.pipelines) {
@@ -4,9 +4,10 @@ import { existsSync, readFileSync } from 'node:fs';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { resolveCodexCliBin } from './utils/codexCli.js';
6
6
  import { resolveCodexHome } from './utils/codexPaths.js';
7
+ import { buildCommandPreview } from './utils/commandPreview.js';
7
8
  export async function runDelegationSetup(options = {}) {
8
9
  const env = options.env ?? process.env;
9
- const repoRoot = options.repoRoot ?? process.cwd();
10
+ const repoRoot = resolve(options.repoRoot ?? process.cwd());
10
11
  const codexBin = resolveCodexCliBin(env);
11
12
  const codexHome = resolveCodexHome(env);
12
13
  const configPath = join(codexHome, 'config.toml');
@@ -14,7 +15,16 @@ export async function runDelegationSetup(options = {}) {
14
15
  codexBin,
15
16
  codexHome,
16
17
  repoRoot,
17
- commandLine: `"${codexBin}" mcp add delegation -- codex-orchestrator delegate-server`
18
+ commandLine: buildCommandPreview(codexBin, [
19
+ 'mcp',
20
+ 'add',
21
+ 'delegation',
22
+ '--',
23
+ 'codex-orchestrator',
24
+ 'delegate-server',
25
+ '--repo',
26
+ repoRoot
27
+ ])
18
28
  };
19
29
  const probe = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env });
20
30
  const readiness = { configured: probe.configured, configPath };
@@ -24,7 +34,7 @@ export async function runDelegationSetup(options = {}) {
24
34
  if (probe.configured) {
25
35
  return { status: 'skipped', reason: probe.reason ?? 'Delegation MCP is already configured.', plan, readiness };
26
36
  }
27
- await applyDelegationSetup({ codexBin, removeExisting: probe.removeExisting, envVars: probe.envVars }, env);
37
+ await applyDelegationSetup({ codexBin, repoRoot, removeExisting: probe.removeExisting, envVars: probe.envVars }, env);
28
38
  const configuredAfter = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env }).configured;
29
39
  return {
30
40
  status: 'applied',
@@ -79,15 +89,14 @@ function inspectDelegationReadiness(options) {
79
89
  };
80
90
  }
81
91
  return {
82
- configured: true,
83
- removeExisting: false,
92
+ configured: false,
93
+ removeExisting: true,
84
94
  envVars,
85
- reason: 'Delegation MCP is already configured.'
95
+ reason: `Existing delegation MCP entry is not pinned; reconfiguring to ${requestedRepo}.`
86
96
  };
87
97
  }
88
98
  // 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: {} };
99
+ return inspectDelegationReadinessFallback(options.configPath, requestedRepo);
91
100
  }
92
101
  function applyDelegationSetup(plan, env) {
93
102
  const envFlags = [];
@@ -96,7 +105,17 @@ function applyDelegationSetup(plan, env) {
96
105
  }
97
106
  return new Promise((resolve, reject) => {
98
107
  const runAdd = () => {
99
- const child = spawn(plan.codexBin, ['mcp', 'add', 'delegation', ...envFlags, '--', 'codex-orchestrator', 'delegate-server'], { stdio: 'inherit', env });
108
+ const child = spawn(plan.codexBin, [
109
+ 'mcp',
110
+ 'add',
111
+ 'delegation',
112
+ ...envFlags,
113
+ '--',
114
+ 'codex-orchestrator',
115
+ 'delegate-server',
116
+ '--repo',
117
+ plan.repoRoot
118
+ ], { stdio: 'inherit', env });
100
119
  child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
101
120
  child.once('exit', (code) => {
102
121
  if (code === 0) {
@@ -167,18 +186,96 @@ function readPinnedRepo(args) {
167
186
  const candidate = args[index + 1];
168
187
  return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate.trim() : null;
169
188
  }
170
- function isDelegationConfiguredFallback(configPath) {
189
+ function inspectDelegationReadinessFallback(configPath, requestedRepo) {
190
+ const config = readDelegationFallbackConfig(configPath);
191
+ if (!config) {
192
+ return { configured: false, removeExisting: false, envVars: {} };
193
+ }
194
+ const pinnedRepo = readPinnedRepo(config.args);
195
+ if (!pinnedRepo) {
196
+ return {
197
+ configured: false,
198
+ removeExisting: true,
199
+ envVars: config.envVars,
200
+ reason: `Existing delegation MCP entry is not pinned; reconfiguring to ${requestedRepo}.`
201
+ };
202
+ }
203
+ const normalizedPinned = resolve(pinnedRepo);
204
+ if (normalizedPinned !== requestedRepo) {
205
+ return {
206
+ configured: false,
207
+ removeExisting: true,
208
+ envVars: config.envVars,
209
+ reason: `Existing delegation MCP entry is pinned to ${pinnedRepo}; reconfiguring.`
210
+ };
211
+ }
212
+ return {
213
+ configured: true,
214
+ removeExisting: false,
215
+ envVars: config.envVars,
216
+ reason: `Delegation MCP is already configured (pinned to ${pinnedRepo}).`
217
+ };
218
+ }
219
+ function readDelegationFallbackConfig(configPath) {
171
220
  if (!existsSync(configPath)) {
172
- return false;
221
+ return null;
173
222
  }
174
223
  try {
175
- // Keep parsing loose; we only need to know whether a delegation entry exists.
176
224
  const raw = readFileSync(configPath, 'utf8');
177
- return hasMcpServerEntry(raw, 'delegation');
225
+ if (!hasMcpServerEntry(raw, 'delegation')) {
226
+ return null;
227
+ }
228
+ return {
229
+ args: readDelegationArgsFromConfig(raw),
230
+ envVars: readDelegationEnvVarsFromConfig(raw)
231
+ };
178
232
  }
179
233
  catch {
180
- return false;
234
+ return null;
235
+ }
236
+ }
237
+ function readDelegationArgsFromConfig(raw) {
238
+ const sectionMatch = raw.match(/\[mcp_servers(?:\.delegation|\."delegation"|.'delegation')\]([\s\S]*?)(?=\n\[|$)/u);
239
+ if (!sectionMatch) {
240
+ return [];
241
+ }
242
+ const section = sectionMatch[1] ?? '';
243
+ const argsMatch = section.match(/^\s*args\s*=\s*\[([\s\S]*?)\]/mu);
244
+ if (!argsMatch) {
245
+ return [];
246
+ }
247
+ const argsRaw = argsMatch[1] ?? '';
248
+ const args = [];
249
+ const tokenPattern = /"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)'/gu;
250
+ let token = tokenPattern.exec(argsRaw);
251
+ while (token) {
252
+ const quoted = token[1] ?? token[2] ?? '';
253
+ const decoded = quoted.replace(/\\"/gu, '"').replace(/\\'/gu, '\'');
254
+ args.push(decoded);
255
+ token = tokenPattern.exec(argsRaw);
256
+ }
257
+ return args;
258
+ }
259
+ function readDelegationEnvVarsFromConfig(raw) {
260
+ const envVars = {};
261
+ const sectionMatch = raw.match(/\[mcp_servers(?:\.delegation|\."delegation"|.'delegation')\.env\]([\s\S]*?)(?=\n\[|$)/u);
262
+ if (!sectionMatch) {
263
+ return envVars;
264
+ }
265
+ const section = sectionMatch[1] ?? '';
266
+ const linePattern = /^\s*([A-Za-z0-9_.-]+)\s*=\s*("(?:\\"|[^"])*"|'(?:\\'|[^'])*')\s*$/gmu;
267
+ let match = linePattern.exec(section);
268
+ while (match) {
269
+ const key = match[1];
270
+ const rawValue = match[2] ?? '';
271
+ if (key) {
272
+ const unquoted = rawValue.slice(1, -1);
273
+ const decoded = unquoted.replace(/\\"/gu, '"').replace(/\\'/gu, '\'');
274
+ envVars[key] = decoded;
275
+ }
276
+ match = linePattern.exec(section);
181
277
  }
278
+ return envVars;
182
279
  }
183
280
  function hasMcpServerEntry(raw, serverName) {
184
281
  const lines = raw.split('\n');
@@ -1,11 +1,15 @@
1
1
  import process from 'node:process';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
4
+ import { dirname, join, resolve } from 'node:path';
5
5
  import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
6
- import { resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
6
+ import { isManagedCodexCliEnabled, resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
7
7
  import { resolveCodexHome } from './utils/codexPaths.js';
8
8
  import { resolveOptionalDependency } from './utils/optionalDeps.js';
9
+ import { runCloudPreflight } from './utils/cloudPreflight.js';
10
+ import { CommandPlanner } from './adapters/CommandPlanner.js';
11
+ import { PipelineResolver } from './services/pipelineResolver.js';
12
+ import { isRepoConfigRequired } from './config/repoConfigPolicy.js';
9
13
  const OPTIONAL_DEPENDENCIES = [
10
14
  {
11
15
  name: 'playwright',
@@ -72,9 +76,21 @@ export function runDoctor(cwd = process.cwd()) {
72
76
  missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
73
77
  }
74
78
  const codexBin = resolveCodexCliBin(process.env);
79
+ const managedOptIn = isManagedCodexCliEnabled(process.env);
75
80
  const managedCodex = resolveCodexCliReadiness(process.env);
76
81
  const features = readCodexFeatureFlags(codexBin);
77
- const collabEnabled = features?.collab ?? null;
82
+ const collabFeatureKey = features === null
83
+ ? null
84
+ : Object.prototype.hasOwnProperty.call(features, 'multi_agent')
85
+ ? 'multi_agent'
86
+ : Object.prototype.hasOwnProperty.call(features, 'collab')
87
+ ? 'collab'
88
+ : null;
89
+ const collabEnabled = collabFeatureKey === 'multi_agent'
90
+ ? features?.multi_agent ?? null
91
+ : collabFeatureKey === 'collab'
92
+ ? features?.collab ?? null
93
+ : null;
78
94
  const collabStatus = features === null ? 'unavailable' : collabEnabled ? 'ok' : 'disabled';
79
95
  const cloudCmdAvailable = canRunCommand(codexBin, ['cloud', '--help']);
80
96
  const cloudEnvIdConfigured = typeof process.env.CODEX_CLOUD_ENV_ID === 'string' && process.env.CODEX_CLOUD_ENV_ID.trim().length > 0;
@@ -82,6 +98,7 @@ export function runDoctor(cwd = process.cwd()) {
82
98
  ? process.env.CODEX_CLOUD_BRANCH.trim().replace(/^refs\/heads\//u, '')
83
99
  : null;
84
100
  const cloudStatus = !cloudCmdAvailable ? 'unavailable' : cloudEnvIdConfigured ? 'ok' : 'not_configured';
101
+ const cloudFallbackPolicy = resolveCloudFallbackPolicy();
85
102
  const delegationConfig = inspectDelegationConfig();
86
103
  const delegationStatus = delegationConfig.status === 'ok' ? 'ok' : 'missing-config';
87
104
  return {
@@ -90,27 +107,30 @@ export function runDoctor(cwd = process.cwd()) {
90
107
  dependencies,
91
108
  devtools,
92
109
  codex_cli: {
93
- active: { command: codexBin },
110
+ active: { command: codexBin, managed_opt_in: managedOptIn },
94
111
  managed: managedCodex
95
112
  },
96
113
  collab: {
97
114
  status: collabStatus,
98
115
  enabled: collabEnabled,
116
+ feature_key: collabFeatureKey,
99
117
  enablement: [
100
- 'Enable collab for symbolic RLM runs with: codex-orchestrator rlm --collab auto "<goal>"',
101
- 'Or set: RLM_SYMBOLIC_COLLAB=1 (implies symbolic mode when using --collab).',
102
- 'If collab is disabled in codex features: codex features enable collab'
118
+ 'Enable collab for symbolic RLM runs with: codex-orchestrator rlm --multi-agent auto "<goal>" (legacy: --collab auto).',
119
+ 'Or set: RLM_SYMBOLIC_MULTI_AGENT=1 (legacy alias: RLM_SYMBOLIC_COLLAB=1).',
120
+ 'If multi-agent is disabled in codex features: codex features enable multi_agent (legacy alias: collab)'
103
121
  ]
104
122
  },
105
123
  cloud: {
106
124
  status: cloudStatus,
107
125
  env_id_configured: cloudEnvIdConfigured,
108
126
  branch: cloudBranch,
127
+ fallback_policy: cloudFallbackPolicy,
109
128
  enablement: [
110
129
  'Set CODEX_CLOUD_ENV_ID to a valid Codex Cloud environment id.',
111
130
  'Optional: set CODEX_CLOUD_BRANCH (must exist on origin).',
112
131
  'Then run a pipeline stage in cloud mode with: codex-orchestrator start <pipeline> --cloud --target <stage-id>',
113
- 'If cloud preflight fails, CO falls back to mcp and records the reason in manifest.summary (surfaced in start output).'
132
+ 'Cloud fallback is a compatibility safety net; prefer fail-fast lanes with CODEX_ORCHESTRATOR_CLOUD_FALLBACK=deny.',
133
+ 'If cloud preflight fails and fallback is allowed, CO falls back to mcp and records the reason in manifest.summary (surfaced in start output).'
114
134
  ]
115
135
  },
116
136
  delegation: {
@@ -126,6 +146,95 @@ export function runDoctor(cwd = process.cwd()) {
126
146
  }
127
147
  };
128
148
  }
149
+ export async function runDoctorCloudPreflight(options = {}) {
150
+ const env = options.env ?? process.env;
151
+ const cwd = options.cwd ?? process.cwd();
152
+ const configuredRoot = normalizeOptionalString(env.CODEX_ORCHESTRATOR_ROOT);
153
+ const rootHint = configuredRoot ? resolve(cwd, configuredRoot) : cwd;
154
+ const repoRoot = resolveDoctorRepoRoot(rootHint);
155
+ const codexBin = resolveCodexCliBin(env);
156
+ const taskId = normalizeOptionalString(options.taskId)
157
+ ?? normalizeOptionalString(env.MCP_RUNNER_TASK_ID)
158
+ ?? normalizeOptionalString(env.TASK)
159
+ ?? normalizeOptionalString(env.CODEX_ORCHESTRATOR_TASK_ID);
160
+ const explicitEnvironmentId = normalizeOptionalString(options.environmentId);
161
+ const strictRepoConfigRequired = isRepoConfigRequired(env);
162
+ let planMetadataEnvironmentId = null;
163
+ let planMetadataIssue = null;
164
+ if (!explicitEnvironmentId || strictRepoConfigRequired) {
165
+ try {
166
+ planMetadataEnvironmentId = await resolvePlanMetadataCloudEnvironmentId(repoRoot, taskId, env);
167
+ }
168
+ catch (error) {
169
+ if (strictRepoConfigRequired) {
170
+ const detail = error instanceof Error ? error.message : String(error);
171
+ planMetadataIssue = {
172
+ code: 'pipeline_resolution_failed',
173
+ message: `Pipeline resolution failed during doctor cloud preflight: ${detail}`
174
+ };
175
+ }
176
+ }
177
+ }
178
+ const environmentId = explicitEnvironmentId
179
+ ?? planMetadataEnvironmentId
180
+ ?? normalizeOptionalString(env.CODEX_CLOUD_ENV_ID)
181
+ ?? resolveTaskMetadataCloudEnvironmentId(repoRoot, taskId);
182
+ const branch = normalizeOptionalBranch(options.branch) ?? normalizeOptionalBranch(env.CODEX_CLOUD_BRANCH);
183
+ const preflight = await runCloudPreflight({
184
+ repoRoot,
185
+ codexBin,
186
+ environmentId,
187
+ branch,
188
+ env
189
+ });
190
+ const issues = planMetadataIssue ? [planMetadataIssue, ...preflight.issues] : preflight.issues;
191
+ const guidance = buildCloudPreflightGuidance(issues);
192
+ return {
193
+ ok: preflight.ok && planMetadataIssue === null,
194
+ details: {
195
+ codex_bin: preflight.details.codexBin,
196
+ environment_id: preflight.details.environmentId,
197
+ branch: preflight.details.branch
198
+ },
199
+ issues,
200
+ guidance
201
+ };
202
+ }
203
+ function resolveDoctorRepoRoot(cwd) {
204
+ const fallback = resolve(cwd);
205
+ let current = fallback;
206
+ while (current) {
207
+ if (existsSync(join(current, 'tasks', 'index.json'))) {
208
+ return current;
209
+ }
210
+ const parent = dirname(current);
211
+ if (parent === current) {
212
+ break;
213
+ }
214
+ current = parent;
215
+ }
216
+ return fallback;
217
+ }
218
+ export function formatDoctorCloudPreflightSummary(result) {
219
+ const lines = [];
220
+ lines.push(`Cloud preflight: ${result.ok ? 'ok' : 'failed'}`);
221
+ lines.push(` - codex bin: ${result.details.codex_bin}`);
222
+ lines.push(` - environment id: ${result.details.environment_id ?? '<unset>'}`);
223
+ lines.push(` - branch: ${result.details.branch ?? '<unset>'}`);
224
+ if (result.issues.length > 0) {
225
+ lines.push(' - issues:');
226
+ for (const issue of result.issues) {
227
+ lines.push(` - [${issue.code}] ${issue.message}`);
228
+ }
229
+ }
230
+ if (result.guidance.length > 0) {
231
+ lines.push(' - guidance:');
232
+ for (const item of result.guidance) {
233
+ lines.push(` - ${item}`);
234
+ }
235
+ }
236
+ return lines;
237
+ }
129
238
  export function formatDoctorSummary(result) {
130
239
  const lines = [];
131
240
  lines.push(`Status: ${result.status}`);
@@ -173,12 +282,16 @@ export function formatDoctorSummary(result) {
173
282
  lines.push(` - ${line}`);
174
283
  }
175
284
  lines.push(`Codex CLI: ${result.codex_cli.active.command}`);
285
+ lines.push(` - managed opt-in: ${result.codex_cli.active.managed_opt_in ? 'enabled' : 'disabled'} (set CODEX_CLI_USE_MANAGED=1)`);
176
286
  lines.push(` - managed: ${result.codex_cli.managed.status} (${result.codex_cli.managed.config.path})`);
177
287
  if (result.codex_cli.managed.status === 'invalid' && result.codex_cli.managed.config.error) {
178
288
  lines.push(` error: ${result.codex_cli.managed.config.error}`);
179
289
  }
180
290
  if (result.codex_cli.managed.status === 'ok') {
181
291
  lines.push(` - binary: ${result.codex_cli.managed.binary.status} (${result.codex_cli.managed.binary.path})`);
292
+ if (!result.codex_cli.active.managed_opt_in) {
293
+ lines.push(' - note: managed binary is installed but inactive; stock codex is currently selected.');
294
+ }
182
295
  if (result.codex_cli.managed.install?.version) {
183
296
  lines.push(` - version: ${result.codex_cli.managed.install.version}`);
184
297
  }
@@ -187,12 +300,16 @@ export function formatDoctorSummary(result) {
187
300
  if (result.collab.enabled !== null) {
188
301
  lines.push(` - enabled: ${result.collab.enabled}`);
189
302
  }
303
+ if (result.collab.feature_key) {
304
+ lines.push(` - feature key: ${result.collab.feature_key}`);
305
+ }
190
306
  for (const line of result.collab.enablement) {
191
307
  lines.push(` - ${line}`);
192
308
  }
193
309
  lines.push(`Cloud: ${result.cloud.status}`);
194
310
  lines.push(` - CODEX_CLOUD_ENV_ID: ${result.cloud.env_id_configured ? 'set' : 'missing'}`);
195
311
  lines.push(` - CODEX_CLOUD_BRANCH: ${result.cloud.branch ?? '<unset>'}`);
312
+ lines.push(` - fallback policy: ${result.cloud.fallback_policy}`);
196
313
  for (const line of result.cloud.enablement) {
197
314
  lines.push(` - ${line}`);
198
315
  }
@@ -209,6 +326,145 @@ export function formatDoctorSummary(result) {
209
326
  }
210
327
  return lines;
211
328
  }
329
+ function normalizeOptionalString(value) {
330
+ if (typeof value !== 'string') {
331
+ return null;
332
+ }
333
+ const trimmed = value.trim();
334
+ return trimmed.length > 0 ? trimmed : null;
335
+ }
336
+ function normalizeOptionalBranch(value) {
337
+ const normalized = normalizeOptionalString(value);
338
+ return normalized ? normalized.replace(/^refs\/heads\//u, '') : null;
339
+ }
340
+ function resolveCloudFallbackPolicy(env = process.env) {
341
+ const raw = normalizeOptionalString(env.CODEX_ORCHESTRATOR_CLOUD_FALLBACK);
342
+ if (!raw) {
343
+ return 'allow';
344
+ }
345
+ const normalized = raw.toLowerCase();
346
+ if (['0', 'false', 'off', 'deny', 'disabled', 'never', 'strict'].includes(normalized)) {
347
+ return 'deny';
348
+ }
349
+ return 'allow';
350
+ }
351
+ async function resolvePlanMetadataCloudEnvironmentId(repoRoot, taskId, processEnv = process.env) {
352
+ const env = {
353
+ repoRoot,
354
+ runsRoot: join(repoRoot, '.runs'),
355
+ outRoot: join(repoRoot, 'out'),
356
+ taskId: taskId ?? '0101'
357
+ };
358
+ const resolver = new PipelineResolver();
359
+ const resolution = await resolver.resolve(env, { quiet: true, processEnv });
360
+ const planner = new CommandPlanner(resolution.pipeline);
361
+ const context = {
362
+ id: env.taskId,
363
+ title: env.taskId,
364
+ metadata: {}
365
+ };
366
+ const plan = await planner.plan(context);
367
+ const selected = plan.items.find((item) => item.id === plan.targetId)
368
+ ?? plan.items[0]
369
+ ?? null;
370
+ if (!selected || !selected.metadata || typeof selected.metadata !== 'object') {
371
+ return null;
372
+ }
373
+ return resolveCloudEnvironmentIdFromMetadata(selected.metadata);
374
+ }
375
+ function resolveCloudEnvironmentIdFromMetadata(metadata) {
376
+ const stagePlan = metadata.plan && typeof metadata.plan === 'object'
377
+ ? metadata.plan
378
+ : null;
379
+ const candidates = [
380
+ normalizeOptionalString(typeof stagePlan?.cloudEnvId === 'string' ? stagePlan.cloudEnvId : null),
381
+ normalizeOptionalString(typeof stagePlan?.cloud_env_id === 'string' ? stagePlan.cloud_env_id : null),
382
+ normalizeOptionalString(typeof metadata.cloudEnvId === 'string' ? metadata.cloudEnvId : null),
383
+ normalizeOptionalString(typeof metadata.cloud_env_id === 'string' ? metadata.cloud_env_id : null)
384
+ ];
385
+ return candidates.find((value) => Boolean(value)) ?? null;
386
+ }
387
+ function resolveTaskMetadataCloudEnvironmentId(repoRoot, taskId) {
388
+ if (!taskId) {
389
+ return null;
390
+ }
391
+ const tasksPath = join(repoRoot, 'tasks', 'index.json');
392
+ if (!existsSync(tasksPath)) {
393
+ return null;
394
+ }
395
+ try {
396
+ const raw = readFileSync(tasksPath, 'utf8');
397
+ const parsed = JSON.parse(raw);
398
+ const items = Array.isArray(parsed.items) ? parsed.items : [];
399
+ const match = items.find((item) => {
400
+ if (!item || typeof item !== 'object') {
401
+ return false;
402
+ }
403
+ const record = item;
404
+ return matchesTaskIdentifier(record.id, taskId) || matchesTaskIdentifier(record.slug, taskId);
405
+ });
406
+ if (!match || typeof match !== 'object') {
407
+ return null;
408
+ }
409
+ const record = match;
410
+ const metadata = (record.metadata ?? null);
411
+ const cloudMetadata = metadata && typeof metadata.cloud === 'object' && metadata.cloud
412
+ ? metadata.cloud
413
+ : null;
414
+ const candidates = [
415
+ normalizeOptionalString(typeof metadata?.cloudEnvId === 'string' ? metadata.cloudEnvId : null),
416
+ normalizeOptionalString(typeof metadata?.cloud_env_id === 'string' ? metadata.cloud_env_id : null),
417
+ normalizeOptionalString(typeof metadata?.envId === 'string' ? metadata.envId : null),
418
+ normalizeOptionalString(typeof metadata?.environmentId === 'string' ? metadata.environmentId : null),
419
+ normalizeOptionalString(typeof cloudMetadata?.envId === 'string' ? cloudMetadata.envId : null),
420
+ normalizeOptionalString(typeof cloudMetadata?.environmentId === 'string' ? cloudMetadata.environmentId : null),
421
+ normalizeOptionalString(typeof cloudMetadata?.cloudEnvId === 'string' ? cloudMetadata.cloudEnvId : null),
422
+ normalizeOptionalString(typeof cloudMetadata?.cloud_env_id === 'string' ? cloudMetadata.cloud_env_id : null)
423
+ ];
424
+ return candidates.find((value) => Boolean(value)) ?? null;
425
+ }
426
+ catch {
427
+ return null;
428
+ }
429
+ }
430
+ function matchesTaskIdentifier(value, taskId) {
431
+ if (typeof value !== 'string') {
432
+ return false;
433
+ }
434
+ const normalized = normalizeOptionalString(value);
435
+ if (!normalized) {
436
+ return false;
437
+ }
438
+ return normalized === taskId || taskId.startsWith(`${normalized}-`);
439
+ }
440
+ function buildCloudPreflightGuidance(issues) {
441
+ if (issues.length === 0) {
442
+ return ['Cloud preflight passed. You can run cloud mode with `--cloud --target <stage-id>`.'];
443
+ }
444
+ const guidance = [];
445
+ for (const issue of issues) {
446
+ switch (issue.code) {
447
+ case 'missing_environment':
448
+ guidance.push('Set CODEX_CLOUD_ENV_ID or provide target metadata.cloudEnvId.');
449
+ break;
450
+ case 'branch_missing':
451
+ guidance.push('Push the branch to origin or set CODEX_CLOUD_BRANCH to an existing remote branch.');
452
+ break;
453
+ case 'codex_unavailable':
454
+ guidance.push('Install Codex CLI or set CODEX_CLI_BIN to a valid codex binary.');
455
+ break;
456
+ case 'git_unavailable':
457
+ guidance.push('Install git or run with CODEX_CLOUD_BRANCH unset to skip remote branch verification.');
458
+ break;
459
+ case 'pipeline_resolution_failed':
460
+ guidance.push('Fix pipeline/config resolution errors before cloud runs (run `codex-orchestrator init codex`).');
461
+ break;
462
+ default:
463
+ break;
464
+ }
465
+ }
466
+ return [...new Set(guidance)];
467
+ }
212
468
  function readCodexFeatureFlags(codexBin) {
213
469
  const result = spawnSync(codexBin, ['features', 'list'], {
214
470
  encoding: 'utf8',