@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.
- package/README.md +96 -12
- package/codex.orchestrator.json +448 -0
- package/dist/bin/codex-orchestrator.js +703 -136
- package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +22 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +20 -9
- package/dist/orchestrator/src/cli/delegationSetup.js +111 -14
- package/dist/orchestrator/src/cli/doctor.js +264 -8
- package/dist/orchestrator/src/cli/doctorIssueLog.js +350 -0
- package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
- package/dist/orchestrator/src/cli/init.js +24 -1
- package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
- package/dist/orchestrator/src/cli/orchestrator.js +180 -5
- package/dist/orchestrator/src/cli/rlmRunner.js +289 -35
- package/dist/orchestrator/src/cli/run/manifest.js +31 -6
- package/dist/orchestrator/src/cli/services/commandRunner.js +10 -2
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +70 -18
- package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/skills.js +3 -8
- package/dist/orchestrator/src/cli/utils/advancedAutopilot.js +114 -0
- package/dist/orchestrator/src/cli/utils/codexCli.js +21 -0
- package/dist/orchestrator/src/cli/utils/commandPreview.js +10 -0
- package/dist/orchestrator/src/cli/utils/delegationGuardRunner.js +85 -8
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -1
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
- package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +46 -6
- package/dist/orchestrator/src/control-plane/request-builder.js +9 -8
- package/dist/scripts/lib/pr-watch-merge.js +367 -3
- package/docs/README.md +17 -11
- package/package.json +2 -1
- package/schemas/manifest.json +27 -0
- package/skills/collab-deliberation/SKILL.md +6 -0
- package/skills/collab-evals/SKILL.md +4 -0
- package/skills/collab-subagents-first/SKILL.md +29 -7
- package/skills/delegation-usage/DELEGATION_GUIDE.md +31 -5
- package/skills/delegation-usage/SKILL.md +29 -4
- package/skills/elegance-review/SKILL.md +14 -3
- package/skills/standalone-review/SKILL.md +8 -2
- package/templates/README.md +1 -1
- 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
|
-
|
|
9
|
+
if (!options.quiet) {
|
|
10
|
+
logger.info(`[codex-config] Loaded user config from ${repoConfigPath}`);
|
|
11
|
+
}
|
|
10
12
|
return normalizeUserConfig(repoConfig, 'repo');
|
|
11
13
|
}
|
|
12
|
-
|
|
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
|
-
|
|
28
|
+
if (!options.quiet) {
|
|
29
|
+
logger.info(`[codex-config] Loaded user config from ${packageConfigPath}`);
|
|
30
|
+
}
|
|
25
31
|
return normalizeUserConfig(packageConfig, 'package');
|
|
26
32
|
}
|
|
27
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
83
|
-
removeExisting:
|
|
92
|
+
configured: false,
|
|
93
|
+
removeExisting: true,
|
|
84
94
|
envVars,
|
|
85
|
-
reason:
|
|
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
|
-
|
|
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, [
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 --
|
|
101
|
-
'Or set:
|
|
102
|
-
'If
|
|
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
|
-
'
|
|
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',
|