@kbediako/codex-orchestrator 0.1.32 → 0.1.33
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 +77 -9
- package/dist/bin/codex-orchestrator.js +339 -59
- package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
- package/dist/orchestrator/src/cli/doctor.js +186 -7
- package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
- package/dist/orchestrator/src/cli/init.js +1 -1
- package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
- package/dist/orchestrator/src/cli/orchestrator.js +161 -2
- 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/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/delegationGuardRunner.js +85 -8
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
- package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +25 -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 +6 -5
- package/package.json +1 -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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
4
|
+
const DEFAULT_MCP_COMMAND_TIMEOUT_MS = 30_000;
|
|
5
|
+
export async function runMcpEnable(options = {}) {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
const codexBin = resolveCodexCliBin(env);
|
|
8
|
+
const commandRunner = options.commandRunner ?? defaultMcpCommandRunner;
|
|
9
|
+
const listResult = await commandRunner({
|
|
10
|
+
command: codexBin,
|
|
11
|
+
args: ['mcp', 'list', '--json'],
|
|
12
|
+
env
|
|
13
|
+
});
|
|
14
|
+
if (listResult.exitCode !== 0) {
|
|
15
|
+
throw new Error(`codex mcp list failed: ${compactError(listResult.stderr, listResult.stdout)}`);
|
|
16
|
+
}
|
|
17
|
+
let servers;
|
|
18
|
+
try {
|
|
19
|
+
servers = parseMcpServerList(listResult.stdout);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
23
|
+
throw new Error(`codex mcp list --json returned invalid output: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
const requestedNames = dedupeNames(options.serverNames ?? []);
|
|
26
|
+
const targetNames = requestedNames.length > 0
|
|
27
|
+
? requestedNames
|
|
28
|
+
: servers
|
|
29
|
+
.filter((server) => !server.enabled)
|
|
30
|
+
.map((server) => server.name);
|
|
31
|
+
const actions = [];
|
|
32
|
+
for (const targetName of targetNames) {
|
|
33
|
+
const server = servers.find((item) => item.name === targetName);
|
|
34
|
+
if (!server) {
|
|
35
|
+
actions.push({
|
|
36
|
+
name: targetName,
|
|
37
|
+
status: 'missing',
|
|
38
|
+
reason: 'MCP server not found in codex mcp list output.'
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (server.enabled) {
|
|
43
|
+
actions.push({ name: targetName, status: 'already_enabled' });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const enablePlan = buildEnablePlan(server);
|
|
47
|
+
if (!enablePlan.ok) {
|
|
48
|
+
actions.push({
|
|
49
|
+
name: targetName,
|
|
50
|
+
status: 'unsupported',
|
|
51
|
+
reason: enablePlan.reason
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!options.apply) {
|
|
56
|
+
const displayArgs = redactArgsForDisplay(enablePlan.args);
|
|
57
|
+
actions.push({
|
|
58
|
+
name: targetName,
|
|
59
|
+
status: 'planned',
|
|
60
|
+
command_line: `${shellEscape(codexBin)} ${displayArgs.map(shellEscape).join(' ')}`
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const applyResult = await commandRunner({
|
|
65
|
+
command: codexBin,
|
|
66
|
+
args: enablePlan.args,
|
|
67
|
+
env
|
|
68
|
+
});
|
|
69
|
+
if (applyResult.exitCode !== 0) {
|
|
70
|
+
actions.push({
|
|
71
|
+
name: targetName,
|
|
72
|
+
status: 'failed',
|
|
73
|
+
reason: compactError(applyResult.stderr, applyResult.stdout)
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
actions.push({ name: targetName, status: 'enabled' });
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
status: options.apply ? 'applied' : 'planned',
|
|
81
|
+
codex_bin: codexBin,
|
|
82
|
+
targets: targetNames,
|
|
83
|
+
actions
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function formatMcpEnableSummary(result) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push(`MCP enable: ${result.status}`);
|
|
89
|
+
lines.push(`- Codex bin: ${result.codex_bin}`);
|
|
90
|
+
lines.push(`- Targets: ${result.targets.length > 0 ? result.targets.join(', ') : '<none>'}`);
|
|
91
|
+
const byStatus = summarizeByStatus(result.actions);
|
|
92
|
+
lines.push(`- Results: enabled=${byStatus.enabled}, planned=${byStatus.planned}, already_enabled=${byStatus.already_enabled}, missing=${byStatus.missing}, unsupported=${byStatus.unsupported}, failed=${byStatus.failed}`);
|
|
93
|
+
for (const action of result.actions) {
|
|
94
|
+
if (action.status === 'planned' && action.command_line) {
|
|
95
|
+
lines.push(` - ${action.name}: planned -> ${action.command_line}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (action.reason) {
|
|
99
|
+
lines.push(` - ${action.name}: ${action.status} (${action.reason})`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
lines.push(` - ${action.name}: ${action.status}`);
|
|
103
|
+
}
|
|
104
|
+
if (result.status === 'planned' && byStatus.planned > 0) {
|
|
105
|
+
lines.push('Run with --yes to apply.');
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
function parseMcpServerList(raw) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new Error('invalid JSON payload.');
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(parsed)) {
|
|
118
|
+
throw new Error('expected top-level JSON array.');
|
|
119
|
+
}
|
|
120
|
+
const servers = [];
|
|
121
|
+
for (const item of parsed) {
|
|
122
|
+
if (!item || typeof item !== 'object') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const record = item;
|
|
126
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
127
|
+
if (!name) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeof record.enabled !== 'boolean') {
|
|
131
|
+
throw new Error(`expected boolean "enabled" for server "${name}".`);
|
|
132
|
+
}
|
|
133
|
+
servers.push({
|
|
134
|
+
name,
|
|
135
|
+
enabled: record.enabled,
|
|
136
|
+
startupTimeoutSec: typeof record.startup_timeout_sec === 'number' && Number.isFinite(record.startup_timeout_sec)
|
|
137
|
+
? record.startup_timeout_sec
|
|
138
|
+
: null,
|
|
139
|
+
toolTimeoutSec: typeof record.tool_timeout_sec === 'number' && Number.isFinite(record.tool_timeout_sec)
|
|
140
|
+
? record.tool_timeout_sec
|
|
141
|
+
: null,
|
|
142
|
+
transport: record.transport ?? undefined
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return servers;
|
|
146
|
+
}
|
|
147
|
+
function buildEnablePlan(server) {
|
|
148
|
+
if (server.startupTimeoutSec !== null || server.toolTimeoutSec !== null) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: 'Server defines startup/tool timeout settings; codex mcp add cannot preserve those fields. Enable this server manually.'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const transport = server.transport;
|
|
155
|
+
if (!transport || typeof transport !== 'object') {
|
|
156
|
+
return { ok: false, reason: 'Server transport details are missing.' };
|
|
157
|
+
}
|
|
158
|
+
const type = typeof transport.type === 'string' ? transport.type.trim() : '';
|
|
159
|
+
if (type === 'stdio') {
|
|
160
|
+
const command = typeof transport.command === 'string' ? transport.command.trim() : '';
|
|
161
|
+
if (!command) {
|
|
162
|
+
return { ok: false, reason: 'stdio transport is missing command.' };
|
|
163
|
+
}
|
|
164
|
+
const hasUnsupportedStdioFields = Array.isArray(transport.env_vars) && transport.env_vars.length > 0
|
|
165
|
+
|| hasRecordEntries(transport.env_vars)
|
|
166
|
+
|| (typeof transport.cwd === 'string' && transport.cwd.trim().length > 0);
|
|
167
|
+
if (hasUnsupportedStdioFields) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
reason: 'stdio env_vars/cwd settings are configured; codex mcp add cannot preserve these fields. Enable this server manually.'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const args = ['mcp', 'add', server.name];
|
|
174
|
+
const envObject = normalizeStringRecord(transport.env);
|
|
175
|
+
for (const [key, value] of Object.entries(envObject)) {
|
|
176
|
+
args.push('--env', `${key}=${value}`);
|
|
177
|
+
}
|
|
178
|
+
args.push('--', command, ...normalizeStringArray(transport.args));
|
|
179
|
+
return { ok: true, args };
|
|
180
|
+
}
|
|
181
|
+
if (type === 'streamable_http') {
|
|
182
|
+
const url = typeof transport.url === 'string' ? transport.url.trim() : '';
|
|
183
|
+
if (!url) {
|
|
184
|
+
return { ok: false, reason: 'streamable_http transport is missing url.' };
|
|
185
|
+
}
|
|
186
|
+
const args = ['mcp', 'add', server.name, '--url', url];
|
|
187
|
+
const bearerTokenEnvVar = typeof transport.bearer_token_env_var === 'string' ? transport.bearer_token_env_var.trim() : '';
|
|
188
|
+
if (bearerTokenEnvVar) {
|
|
189
|
+
args.push('--bearer-token-env-var', bearerTokenEnvVar);
|
|
190
|
+
}
|
|
191
|
+
const hasUnsupportedHeaders = hasRecordEntries(transport.http_headers)
|
|
192
|
+
|| hasRecordEntries(transport.env_http_headers)
|
|
193
|
+
|| (Array.isArray(transport.http_headers) && transport.http_headers.length > 0)
|
|
194
|
+
|| (Array.isArray(transport.env_http_headers) && transport.env_http_headers.length > 0);
|
|
195
|
+
if (hasUnsupportedHeaders) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
reason: 'streamable_http headers/env_http_headers are configured; codex mcp add does not expose equivalent flags. Enable this server manually.'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, args };
|
|
202
|
+
}
|
|
203
|
+
return { ok: false, reason: `Unsupported transport type "${type || 'unknown'}".` };
|
|
204
|
+
}
|
|
205
|
+
function normalizeStringArray(value) {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return value
|
|
210
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
211
|
+
.filter((item) => item.length > 0);
|
|
212
|
+
}
|
|
213
|
+
function normalizeStringRecord(value) {
|
|
214
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
const record = {};
|
|
218
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
219
|
+
if (typeof entry !== 'string') {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const normalizedKey = key.trim();
|
|
223
|
+
if (!normalizedKey) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
record[normalizedKey] = entry;
|
|
227
|
+
}
|
|
228
|
+
return record;
|
|
229
|
+
}
|
|
230
|
+
function hasRecordEntries(value) {
|
|
231
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return Object.keys(value).length > 0;
|
|
235
|
+
}
|
|
236
|
+
function dedupeNames(items) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const normalized = [];
|
|
239
|
+
for (const item of items) {
|
|
240
|
+
const value = item.trim();
|
|
241
|
+
if (!value || seen.has(value)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
seen.add(value);
|
|
245
|
+
normalized.push(value);
|
|
246
|
+
}
|
|
247
|
+
return normalized;
|
|
248
|
+
}
|
|
249
|
+
function summarizeByStatus(actions) {
|
|
250
|
+
return actions.reduce((acc, action) => {
|
|
251
|
+
acc[action.status] += 1;
|
|
252
|
+
return acc;
|
|
253
|
+
}, {
|
|
254
|
+
planned: 0,
|
|
255
|
+
enabled: 0,
|
|
256
|
+
already_enabled: 0,
|
|
257
|
+
missing: 0,
|
|
258
|
+
unsupported: 0,
|
|
259
|
+
failed: 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function defaultMcpCommandRunner(request) {
|
|
263
|
+
return await new Promise((resolve) => {
|
|
264
|
+
const child = spawn(request.command, request.args, {
|
|
265
|
+
env: request.env,
|
|
266
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
267
|
+
});
|
|
268
|
+
const timeoutMs = Math.max(1, request.timeoutMs ?? DEFAULT_MCP_COMMAND_TIMEOUT_MS);
|
|
269
|
+
let stdout = '';
|
|
270
|
+
let stderr = '';
|
|
271
|
+
let settled = false;
|
|
272
|
+
const finalize = (result) => {
|
|
273
|
+
if (settled) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
settled = true;
|
|
277
|
+
clearTimeout(timeoutHandle);
|
|
278
|
+
resolve(result);
|
|
279
|
+
};
|
|
280
|
+
const timeoutHandle = setTimeout(() => {
|
|
281
|
+
child.kill('SIGTERM');
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
if (!settled) {
|
|
284
|
+
child.kill('SIGKILL');
|
|
285
|
+
}
|
|
286
|
+
}, 2_000).unref();
|
|
287
|
+
finalize({
|
|
288
|
+
exitCode: 124,
|
|
289
|
+
stdout,
|
|
290
|
+
stderr: `${stderr}\ncommand timed out after ${timeoutMs}ms`.trim()
|
|
291
|
+
});
|
|
292
|
+
}, timeoutMs);
|
|
293
|
+
timeoutHandle.unref();
|
|
294
|
+
child.stdout?.on('data', (chunk) => {
|
|
295
|
+
stdout += chunk.toString();
|
|
296
|
+
});
|
|
297
|
+
child.stderr?.on('data', (chunk) => {
|
|
298
|
+
stderr += chunk.toString();
|
|
299
|
+
});
|
|
300
|
+
child.once('error', (error) => {
|
|
301
|
+
finalize({
|
|
302
|
+
exitCode: 1,
|
|
303
|
+
stdout,
|
|
304
|
+
stderr: `${stderr}\n${error.message}`.trim()
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
child.once('close', (code) => {
|
|
308
|
+
finalize({
|
|
309
|
+
exitCode: typeof code === 'number' ? code : 1,
|
|
310
|
+
stdout,
|
|
311
|
+
stderr
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function compactError(...values) {
|
|
317
|
+
const text = values
|
|
318
|
+
.map((value) => value.trim())
|
|
319
|
+
.filter((value) => value.length > 0)
|
|
320
|
+
.join(' | ');
|
|
321
|
+
return text.length > 0 ? text : 'no stderr/stdout captured';
|
|
322
|
+
}
|
|
323
|
+
function shellEscape(value) {
|
|
324
|
+
if (/^[A-Za-z0-9_./:-]+$/u.test(value)) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
return `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
328
|
+
}
|
|
329
|
+
function redactArgsForDisplay(args) {
|
|
330
|
+
const redacted = [...args];
|
|
331
|
+
for (let index = 0; index < redacted.length - 1; index += 1) {
|
|
332
|
+
if (redacted[index] !== '--env') {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const envPair = redacted[index + 1];
|
|
336
|
+
if (!envPair) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const delimiter = envPair.indexOf('=');
|
|
340
|
+
if (delimiter <= 0) {
|
|
341
|
+
redacted[index + 1] = '<redacted>';
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const key = envPair.slice(0, delimiter);
|
|
345
|
+
redacted[index + 1] = `${key}=<redacted>`;
|
|
346
|
+
}
|
|
347
|
+
for (let index = 0; index < redacted.length; index += 1) {
|
|
348
|
+
const token = redacted[index] ?? '';
|
|
349
|
+
const longWithEquals = token.match(/^--([^=\s]+)=(.+)$/u);
|
|
350
|
+
if (longWithEquals
|
|
351
|
+
&& (looksSensitiveFlag(longWithEquals[1] ?? '') || looksSensitiveValue(longWithEquals[2] ?? ''))) {
|
|
352
|
+
redacted[index] = `--${longWithEquals[1]}=<redacted>`;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const longFlag = token.match(/^--([A-Za-z0-9_.-]+)$/u);
|
|
356
|
+
if (!longFlag) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const next = redacted[index + 1];
|
|
360
|
+
if (!next || next.startsWith('-')) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (!looksSensitiveFlag(longFlag[1] ?? '') && !looksSensitiveValue(next)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
redacted[index + 1] = '<redacted>';
|
|
367
|
+
}
|
|
368
|
+
// Command payload after "--" can contain arbitrary user args. Keep only the
|
|
369
|
+
// command token + first argument for operator context, redact the rest.
|
|
370
|
+
const separatorIndex = redacted.indexOf('--');
|
|
371
|
+
if (separatorIndex >= 0) {
|
|
372
|
+
const commandIndex = separatorIndex + 1;
|
|
373
|
+
if (commandIndex < redacted.length && looksSensitiveValue(redacted[commandIndex] ?? '')) {
|
|
374
|
+
redacted[commandIndex] = '<redacted>';
|
|
375
|
+
}
|
|
376
|
+
for (let index = separatorIndex + 2; index < redacted.length; index += 1) {
|
|
377
|
+
redacted[index] = '<redacted>';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return redacted;
|
|
381
|
+
}
|
|
382
|
+
function looksSensitiveFlag(flagName) {
|
|
383
|
+
const normalized = flagName.toLowerCase();
|
|
384
|
+
return /(api[-_]?key|token|secret|password|passwd|bearer|auth|cookie|credential)/u.test(normalized);
|
|
385
|
+
}
|
|
386
|
+
function looksSensitiveValue(value) {
|
|
387
|
+
const normalized = value.toLowerCase();
|
|
388
|
+
if (/(api[-_]?key|token|secret|password|passwd|bearer|auth|cookie|credential)/u.test(normalized)) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^@\s]+@/iu.test(value);
|
|
392
|
+
}
|
|
@@ -21,7 +21,7 @@ import { PipelineResolver } from './services/pipelineResolver.js';
|
|
|
21
21
|
import { ControlPlaneService } from './services/controlPlaneService.js';
|
|
22
22
|
import { ControlWatcher } from './control/controlWatcher.js';
|
|
23
23
|
import { SchedulerService } from './services/schedulerService.js';
|
|
24
|
-
import { applyHandlesToRunSummary, applyPrivacyToRunSummary, applyCloudExecutionToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
|
|
24
|
+
import { applyHandlesToRunSummary, applyPrivacyToRunSummary, applyCloudExecutionToRunSummary, applyCloudFallbackToRunSummary, applyUsageKpiToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
|
|
25
25
|
import { prepareRun, resolvePipelineForResume, overrideTaskEnvironment } from './services/runPreparation.js';
|
|
26
26
|
import { loadPackageConfig, loadUserConfig } from './config/userConfig.js';
|
|
27
27
|
import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
|
|
@@ -33,11 +33,16 @@ import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
|
33
33
|
import { CodexCloudTaskExecutor } from '../cloud/CodexCloudTaskExecutor.js';
|
|
34
34
|
import { persistPipelineExperience } from './services/pipelineExperience.js';
|
|
35
35
|
import { runCloudPreflight } from './utils/cloudPreflight.js';
|
|
36
|
+
import { writeJsonAtomic } from './utils/fs.js';
|
|
37
|
+
import { buildAutoScoutEvidence, resolveAdvancedAutopilotDecision } from './utils/advancedAutopilot.js';
|
|
36
38
|
const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
|
|
37
39
|
const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
|
|
38
40
|
const DEFAULT_CLOUD_POLL_INTERVAL_SECONDS = 10;
|
|
39
41
|
const DEFAULT_CLOUD_TIMEOUT_SECONDS = 1800;
|
|
40
42
|
const DEFAULT_CLOUD_ATTEMPTS = 1;
|
|
43
|
+
const DEFAULT_CLOUD_STATUS_RETRY_LIMIT = 12;
|
|
44
|
+
const DEFAULT_CLOUD_STATUS_RETRY_BACKOFF_MS = 1500;
|
|
45
|
+
const DEFAULT_AUTO_SCOUT_TIMEOUT_MS = 4000;
|
|
41
46
|
const MAX_CLOUD_PROMPT_EXPERIENCES = 3;
|
|
42
47
|
const MAX_CLOUD_PROMPT_EXPERIENCE_CHARS = 320;
|
|
43
48
|
function collectDelegationEnvOverrides(env = process.env) {
|
|
@@ -75,6 +80,18 @@ function readCloudNumber(raw, fallback) {
|
|
|
75
80
|
}
|
|
76
81
|
return parsed;
|
|
77
82
|
}
|
|
83
|
+
function allowCloudFallback(envOverrides) {
|
|
84
|
+
const raw = readCloudString(envOverrides?.CODEX_ORCHESTRATOR_CLOUD_FALLBACK) ??
|
|
85
|
+
readCloudString(process.env.CODEX_ORCHESTRATOR_CLOUD_FALLBACK);
|
|
86
|
+
if (!raw) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
const normalized = raw.toLowerCase();
|
|
90
|
+
return !['0', 'false', 'off', 'deny', 'disabled', 'never', 'strict'].includes(normalized);
|
|
91
|
+
}
|
|
92
|
+
function normalizeCloudFallbackIssues(issues) {
|
|
93
|
+
return issues.map((issue) => ({ code: issue.code, message: issue.message }));
|
|
94
|
+
}
|
|
78
95
|
function readCloudFeatureList(raw) {
|
|
79
96
|
if (!raw) {
|
|
80
97
|
return [];
|
|
@@ -568,8 +585,29 @@ export class CodexOrchestrator {
|
|
|
568
585
|
env: mergedEnv
|
|
569
586
|
});
|
|
570
587
|
if (!preflight.ok) {
|
|
588
|
+
if (!allowCloudFallback(options.envOverrides)) {
|
|
589
|
+
const detail = `Cloud preflight failed and cloud fallback is disabled. ` +
|
|
590
|
+
preflight.issues.map((issue) => issue.message).join(' ');
|
|
591
|
+
finalizeStatus(options.manifest, 'failed', 'cloud-preflight-failed');
|
|
592
|
+
appendSummary(options.manifest, detail);
|
|
593
|
+
logger.error(detail);
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
notes: [detail],
|
|
597
|
+
manifest: options.manifest,
|
|
598
|
+
manifestPath: options.paths.manifestPath,
|
|
599
|
+
logPath: options.paths.logPath
|
|
600
|
+
};
|
|
601
|
+
}
|
|
571
602
|
const detail = `Cloud preflight failed; falling back to mcp. ` +
|
|
572
603
|
preflight.issues.map((issue) => issue.message).join(' ');
|
|
604
|
+
options.manifest.cloud_fallback = {
|
|
605
|
+
mode_requested: 'cloud',
|
|
606
|
+
mode_used: 'mcp',
|
|
607
|
+
reason: detail,
|
|
608
|
+
issues: normalizeCloudFallbackIssues(preflight.issues),
|
|
609
|
+
checked_at: isoTimestamp()
|
|
610
|
+
};
|
|
573
611
|
appendSummary(options.manifest, detail);
|
|
574
612
|
logger.warn(detail);
|
|
575
613
|
const fallback = await this.executePipeline({ ...options, mode: 'mcp', executionModeOverride: 'mcp' });
|
|
@@ -582,6 +620,17 @@ export class CodexOrchestrator {
|
|
|
582
620
|
const notes = [];
|
|
583
621
|
let success = true;
|
|
584
622
|
manifest.guardrail_status = undefined;
|
|
623
|
+
const advancedDecision = resolveAdvancedAutopilotDecision({
|
|
624
|
+
pipelineId: pipeline.id,
|
|
625
|
+
targetMetadata: (options.target.metadata ?? null),
|
|
626
|
+
taskMetadata: (options.task.metadata ?? null),
|
|
627
|
+
env: { ...process.env, ...(envOverrides ?? {}) }
|
|
628
|
+
});
|
|
629
|
+
if (advancedDecision.enabled || advancedDecision.source !== 'default') {
|
|
630
|
+
const advancedSummary = `Advanced mode (${advancedDecision.mode}) ${advancedDecision.enabled ? 'enabled' : 'disabled'}: ${advancedDecision.reason}.`;
|
|
631
|
+
appendSummary(manifest, advancedSummary);
|
|
632
|
+
notes.push(advancedSummary);
|
|
633
|
+
}
|
|
585
634
|
const persister = options.persister ??
|
|
586
635
|
new ManifestPersister({
|
|
587
636
|
manifest,
|
|
@@ -604,6 +653,25 @@ export class CodexOrchestrator {
|
|
|
604
653
|
updateHeartbeat(manifest);
|
|
605
654
|
await schedulePersist({ manifest: true, heartbeat: true, force: true });
|
|
606
655
|
runEvents?.runStarted(snapshotStages(manifest, pipeline), manifest.status);
|
|
656
|
+
if (advancedDecision.autoScout) {
|
|
657
|
+
const scoutOutcome = await this.runAutoScout({
|
|
658
|
+
env,
|
|
659
|
+
paths,
|
|
660
|
+
manifest,
|
|
661
|
+
mode: options.mode,
|
|
662
|
+
pipeline,
|
|
663
|
+
target: options.target,
|
|
664
|
+
task: options.task,
|
|
665
|
+
envOverrides,
|
|
666
|
+
advancedDecision
|
|
667
|
+
});
|
|
668
|
+
const scoutMessage = scoutOutcome.status === 'recorded'
|
|
669
|
+
? `Auto scout: evidence recorded at ${scoutOutcome.path}.`
|
|
670
|
+
: `Auto scout: ${scoutOutcome.message} (non-blocking).`;
|
|
671
|
+
appendSummary(manifest, scoutMessage);
|
|
672
|
+
notes.push(scoutMessage);
|
|
673
|
+
await schedulePersist({ manifest: true, force: true });
|
|
674
|
+
}
|
|
607
675
|
const heartbeatInterval = setInterval(() => {
|
|
608
676
|
void pushHeartbeat(false).catch((error) => {
|
|
609
677
|
logger.warn(`Heartbeat update failed for run ${manifest.run_id}: ${error?.message ?? String(error)}`);
|
|
@@ -812,6 +880,37 @@ export class CodexOrchestrator {
|
|
|
812
880
|
updateHeartbeat(manifest);
|
|
813
881
|
await schedulePersist({ manifest: true, heartbeat: true, force: true });
|
|
814
882
|
runEvents?.runStarted(snapshotStages(manifest, pipeline), manifest.status);
|
|
883
|
+
const advancedDecision = resolveAdvancedAutopilotDecision({
|
|
884
|
+
pipelineId: pipeline.id,
|
|
885
|
+
targetMetadata: (target.metadata ?? null),
|
|
886
|
+
taskMetadata: (task.metadata ?? null),
|
|
887
|
+
env: { ...process.env, ...(envOverrides ?? {}) }
|
|
888
|
+
});
|
|
889
|
+
if (advancedDecision.enabled || advancedDecision.source !== 'default') {
|
|
890
|
+
const advancedSummary = `Advanced mode (${advancedDecision.mode}) ${advancedDecision.enabled ? 'enabled' : 'disabled'}: ${advancedDecision.reason}.`;
|
|
891
|
+
appendSummary(manifest, advancedSummary);
|
|
892
|
+
notes.push(advancedSummary);
|
|
893
|
+
await schedulePersist({ manifest: true, force: true });
|
|
894
|
+
}
|
|
895
|
+
if (advancedDecision.autoScout) {
|
|
896
|
+
const scoutOutcome = await this.runAutoScout({
|
|
897
|
+
env,
|
|
898
|
+
paths,
|
|
899
|
+
manifest,
|
|
900
|
+
mode: options.mode,
|
|
901
|
+
pipeline,
|
|
902
|
+
target,
|
|
903
|
+
task,
|
|
904
|
+
envOverrides,
|
|
905
|
+
advancedDecision
|
|
906
|
+
});
|
|
907
|
+
const scoutMessage = scoutOutcome.status === 'recorded'
|
|
908
|
+
? `Auto scout: evidence recorded at ${scoutOutcome.path}.`
|
|
909
|
+
: `Auto scout: ${scoutOutcome.message} (non-blocking).`;
|
|
910
|
+
appendSummary(manifest, scoutMessage);
|
|
911
|
+
notes.push(scoutMessage);
|
|
912
|
+
await schedulePersist({ manifest: true, force: true });
|
|
913
|
+
}
|
|
815
914
|
const heartbeatInterval = setInterval(() => {
|
|
816
915
|
void pushHeartbeat(false).catch((error) => {
|
|
817
916
|
logger.warn(`Heartbeat update failed for run ${manifest.run_id}: ${error?.message ?? String(error)}`);
|
|
@@ -900,6 +999,8 @@ export class CodexOrchestrator {
|
|
|
900
999
|
const pollIntervalSeconds = readCloudNumber(envOverrides?.CODEX_CLOUD_POLL_INTERVAL_SECONDS ?? process.env.CODEX_CLOUD_POLL_INTERVAL_SECONDS, DEFAULT_CLOUD_POLL_INTERVAL_SECONDS);
|
|
901
1000
|
const timeoutSeconds = readCloudNumber(envOverrides?.CODEX_CLOUD_TIMEOUT_SECONDS ?? process.env.CODEX_CLOUD_TIMEOUT_SECONDS, DEFAULT_CLOUD_TIMEOUT_SECONDS);
|
|
902
1001
|
const attempts = readCloudNumber(envOverrides?.CODEX_CLOUD_EXEC_ATTEMPTS ?? process.env.CODEX_CLOUD_EXEC_ATTEMPTS, DEFAULT_CLOUD_ATTEMPTS);
|
|
1002
|
+
const statusRetryLimit = readCloudNumber(envOverrides?.CODEX_CLOUD_STATUS_RETRY_LIMIT ?? process.env.CODEX_CLOUD_STATUS_RETRY_LIMIT, DEFAULT_CLOUD_STATUS_RETRY_LIMIT);
|
|
1003
|
+
const statusRetryBackoffMs = readCloudNumber(envOverrides?.CODEX_CLOUD_STATUS_RETRY_BACKOFF_MS ?? process.env.CODEX_CLOUD_STATUS_RETRY_BACKOFF_MS, DEFAULT_CLOUD_STATUS_RETRY_BACKOFF_MS);
|
|
903
1004
|
const branch = readCloudString(envOverrides?.CODEX_CLOUD_BRANCH) ??
|
|
904
1005
|
readCloudString(process.env.CODEX_CLOUD_BRANCH);
|
|
905
1006
|
const enableFeatures = readCloudFeatureList(readCloudString(envOverrides?.CODEX_CLOUD_ENABLE_FEATURES) ??
|
|
@@ -922,6 +1023,8 @@ export class CodexOrchestrator {
|
|
|
922
1023
|
pollIntervalSeconds,
|
|
923
1024
|
timeoutSeconds,
|
|
924
1025
|
attempts,
|
|
1026
|
+
statusRetryLimit,
|
|
1027
|
+
statusRetryBackoffMs,
|
|
925
1028
|
branch,
|
|
926
1029
|
enableFeatures,
|
|
927
1030
|
disableFeatures,
|
|
@@ -1011,6 +1114,59 @@ export class CodexOrchestrator {
|
|
|
1011
1114
|
lines.push(...buildCloudExperiencePromptLines({ manifest, pipeline, target, stage }));
|
|
1012
1115
|
return lines.join('\n');
|
|
1013
1116
|
}
|
|
1117
|
+
async runAutoScout(params) {
|
|
1118
|
+
const mergedEnv = { ...process.env, ...(params.envOverrides ?? {}) };
|
|
1119
|
+
const timeoutMs = readCloudNumber(mergedEnv.CODEX_ORCHESTRATOR_AUTO_SCOUT_TIMEOUT_MS, DEFAULT_AUTO_SCOUT_TIMEOUT_MS);
|
|
1120
|
+
const work = async () => {
|
|
1121
|
+
const cloudEnvironmentId = resolveCloudEnvironmentId(params.task, params.target, params.envOverrides);
|
|
1122
|
+
const cloudBranch = readCloudString(params.envOverrides?.CODEX_CLOUD_BRANCH) ??
|
|
1123
|
+
readCloudString(process.env.CODEX_CLOUD_BRANCH);
|
|
1124
|
+
const cloudRequested = params.mode === 'cloud' || params.manifest.cloud_fallback?.mode_requested === 'cloud';
|
|
1125
|
+
const evidence = buildAutoScoutEvidence({
|
|
1126
|
+
taskId: params.manifest.task_id,
|
|
1127
|
+
pipelineId: params.pipeline.id,
|
|
1128
|
+
targetId: params.target.id,
|
|
1129
|
+
targetDescription: params.target.description,
|
|
1130
|
+
executionMode: params.mode,
|
|
1131
|
+
cloudRequested,
|
|
1132
|
+
advanced: params.advancedDecision,
|
|
1133
|
+
cloudEnvironmentId,
|
|
1134
|
+
cloudBranch,
|
|
1135
|
+
env: mergedEnv,
|
|
1136
|
+
generatedAt: isoTimestamp()
|
|
1137
|
+
});
|
|
1138
|
+
const evidencePath = join(params.paths.runDir, 'auto-scout.json');
|
|
1139
|
+
await writeJsonAtomic(evidencePath, evidence);
|
|
1140
|
+
return { status: 'recorded', path: relativeToRepo(params.env, evidencePath) };
|
|
1141
|
+
};
|
|
1142
|
+
try {
|
|
1143
|
+
let timeoutHandle = null;
|
|
1144
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1145
|
+
timeoutHandle = setTimeout(() => {
|
|
1146
|
+
resolve({
|
|
1147
|
+
status: 'timeout',
|
|
1148
|
+
message: `timed out after ${Math.round(timeoutMs / 1000)}s`
|
|
1149
|
+
});
|
|
1150
|
+
}, timeoutMs);
|
|
1151
|
+
timeoutHandle.unref?.();
|
|
1152
|
+
});
|
|
1153
|
+
const workPromise = work().catch((error) => ({
|
|
1154
|
+
status: 'error',
|
|
1155
|
+
message: error?.message ?? String(error)
|
|
1156
|
+
}));
|
|
1157
|
+
const result = await Promise.race([workPromise, timeoutPromise]);
|
|
1158
|
+
if (timeoutHandle) {
|
|
1159
|
+
clearTimeout(timeoutHandle);
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
catch (error) {
|
|
1164
|
+
return {
|
|
1165
|
+
status: 'error',
|
|
1166
|
+
message: error?.message ?? String(error)
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1014
1170
|
async performRunLifecycle(context) {
|
|
1015
1171
|
const { env, pipeline, manifest, paths, planner, taskContext, runId, persister, envOverrides, executionModeOverride } = context;
|
|
1016
1172
|
let latestPipelineResult = null;
|
|
@@ -1085,6 +1241,8 @@ export class CodexOrchestrator {
|
|
|
1085
1241
|
applyHandlesToRunSummary(runSummary, manifest);
|
|
1086
1242
|
applyPrivacyToRunSummary(runSummary, manifest);
|
|
1087
1243
|
applyCloudExecutionToRunSummary(runSummary, manifest);
|
|
1244
|
+
applyCloudFallbackToRunSummary(runSummary, manifest);
|
|
1245
|
+
applyUsageKpiToRunSummary(runSummary, manifest);
|
|
1088
1246
|
this.controlPlane.applyControlPlaneToRunSummary(runSummary, controlPlaneResult);
|
|
1089
1247
|
await persistRunSummary(env, paths, manifest, runSummary, persister);
|
|
1090
1248
|
context.runEvents?.runCompleted({
|
|
@@ -1137,7 +1295,8 @@ export class CodexOrchestrator {
|
|
|
1137
1295
|
activity,
|
|
1138
1296
|
commands: manifest.commands,
|
|
1139
1297
|
child_runs: manifest.child_runs,
|
|
1140
|
-
cloud_execution: manifest.cloud_execution ?? null
|
|
1298
|
+
cloud_execution: manifest.cloud_execution ?? null,
|
|
1299
|
+
cloud_fallback: manifest.cloud_fallback ?? null
|
|
1141
1300
|
};
|
|
1142
1301
|
}
|
|
1143
1302
|
renderStatus(manifest, activity) {
|