@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.
Files changed (32) hide show
  1. package/README.md +77 -9
  2. package/dist/bin/codex-orchestrator.js +339 -59
  3. package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
  4. package/dist/orchestrator/src/cli/doctor.js +186 -7
  5. package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
  6. package/dist/orchestrator/src/cli/init.js +1 -1
  7. package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
  8. package/dist/orchestrator/src/cli/orchestrator.js +161 -2
  9. package/dist/orchestrator/src/cli/rlmRunner.js +289 -35
  10. package/dist/orchestrator/src/cli/run/manifest.js +31 -6
  11. package/dist/orchestrator/src/cli/services/commandRunner.js +10 -2
  12. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
  13. package/dist/orchestrator/src/cli/skills.js +3 -8
  14. package/dist/orchestrator/src/cli/utils/advancedAutopilot.js +114 -0
  15. package/dist/orchestrator/src/cli/utils/codexCli.js +21 -0
  16. package/dist/orchestrator/src/cli/utils/delegationGuardRunner.js +85 -8
  17. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
  18. package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +25 -6
  19. package/dist/orchestrator/src/control-plane/request-builder.js +9 -8
  20. package/dist/scripts/lib/pr-watch-merge.js +367 -3
  21. package/docs/README.md +6 -5
  22. package/package.json +1 -1
  23. package/schemas/manifest.json +27 -0
  24. package/skills/collab-deliberation/SKILL.md +6 -0
  25. package/skills/collab-evals/SKILL.md +4 -0
  26. package/skills/collab-subagents-first/SKILL.md +29 -7
  27. package/skills/delegation-usage/DELEGATION_GUIDE.md +31 -5
  28. package/skills/delegation-usage/SKILL.md +29 -4
  29. package/skills/elegance-review/SKILL.md +14 -3
  30. package/skills/standalone-review/SKILL.md +8 -2
  31. package/templates/README.md +1 -1
  32. 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) {