@kbediako/codex-orchestrator 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -307,7 +307,7 @@ codex-orchestrator devtools setup
307
307
  - `codex-orchestrator mcp enable --servers <csv> --yes` — enable specific disabled MCP servers from existing Codex config entries.
308
308
  - `codex-orchestrator self-check --format json` — JSON health payload.
309
309
  - `codex-orchestrator mcp serve` — Codex MCP stdio server.
310
- - `npm run pack:smoke` — maintainer smoke gate for packaged downstream behavior (tarball install + review/skill checks).
310
+ - `npm run pack:smoke` — maintainer smoke gate for packaged downstream behavior (tarball install + review/skill checks). Core lane runs it on downstream-facing diffs; `.github/workflows/pack-smoke-backstop.yml` runs a weekly `main` backstop.
311
311
 
312
312
  ## What ships in the npm release
313
313
 
@@ -393,6 +393,23 @@ function resolveExecutionModeFlag(flags) {
393
393
  }
394
394
  return normalized;
395
395
  }
396
+ function resolveRuntimeModeFlag(flags) {
397
+ if (flags['runtime-mode'] === true) {
398
+ throw new Error('--runtime-mode requires a value. Expected one of: cli, appserver.');
399
+ }
400
+ const rawMode = readStringFlag(flags, 'runtime-mode');
401
+ if (flags['runtime-mode'] !== undefined && !rawMode) {
402
+ throw new Error('--runtime-mode requires a non-empty value. Expected one of: cli, appserver.');
403
+ }
404
+ if (!rawMode) {
405
+ return undefined;
406
+ }
407
+ const normalized = rawMode.toLowerCase();
408
+ if (normalized !== 'cli' && normalized !== 'appserver') {
409
+ throw new Error('Invalid --runtime-mode value. Expected one of: cli, appserver.');
410
+ }
411
+ return normalized;
412
+ }
396
413
  function normalizeRlmMultiAgentValue(raw) {
397
414
  if (raw === true) {
398
415
  return 'enabled';
@@ -580,6 +597,7 @@ async function handleStart(orchestrator, rawArgs) {
580
597
  const pipelineId = positionals[0];
581
598
  const format = flags['format'] === 'json' ? 'json' : 'text';
582
599
  const executionMode = resolveExecutionModeFlag(flags);
600
+ const runtimeMode = resolveRuntimeModeFlag(flags);
583
601
  applyRepoConfigRequiredPolicy(flags);
584
602
  const autoIssueLogEnabled = resolveAutoIssueLogEnabled(flags);
585
603
  if (pipelineId === 'rlm') {
@@ -607,6 +625,7 @@ async function handleStart(orchestrator, rawArgs) {
607
625
  approvalPolicy: typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined,
608
626
  targetStageId: resolveTargetStageId(flags),
609
627
  executionMode,
628
+ runtimeMode,
610
629
  runEvents
611
630
  });
612
631
  const issueLogCapture = result.manifest.status !== 'succeeded'
@@ -618,6 +637,9 @@ async function handleStart(orchestrator, rawArgs) {
618
637
  })
619
638
  : { issueLog: null, issueLogError: null };
620
639
  emitRunOutput(result, format, 'Run started', issueLogCapture);
640
+ if (result.manifest.status === 'failed' || result.manifest.status === 'cancelled') {
641
+ process.exitCode = 1;
642
+ }
621
643
  if (result.manifest.status === 'succeeded' && result.manifest.pipeline_id !== 'rlm') {
622
644
  await maybeEmitRunAdoptionHint({
623
645
  format,
@@ -640,6 +662,7 @@ async function handleFrontendTest(orchestrator, rawArgs) {
640
662
  const { positionals, flags } = parseArgs(rawArgs);
641
663
  const format = flags['format'] === 'json' ? 'json' : 'text';
642
664
  const devtools = Boolean(flags['devtools']);
665
+ const runtimeMode = resolveRuntimeModeFlag(flags);
643
666
  applyRepoConfigRequiredPolicy(flags);
644
667
  if (positionals.length > 0) {
645
668
  console.error(`[frontend-test] ignoring extra arguments: ${positionals.join(' ')}`);
@@ -656,9 +679,13 @@ async function handleFrontendTest(orchestrator, rawArgs) {
656
679
  parentRunId: typeof flags['parent-run'] === 'string' ? flags['parent-run'] : undefined,
657
680
  approvalPolicy: typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined,
658
681
  targetStageId: resolveTargetStageId(flags),
682
+ runtimeMode,
659
683
  runEvents
660
684
  });
661
685
  emitRunOutput(result, format, 'Run started');
686
+ if (result.manifest.status === 'failed' || result.manifest.status === 'cancelled') {
687
+ process.exitCode = 1;
688
+ }
662
689
  });
663
690
  }
664
691
  finally {
@@ -683,6 +710,7 @@ async function handleFlow(orchestrator, rawArgs) {
683
710
  }
684
711
  const format = flags['format'] === 'json' ? 'json' : 'text';
685
712
  const executionMode = resolveExecutionModeFlag(flags);
713
+ const runtimeMode = resolveRuntimeModeFlag(flags);
686
714
  applyRepoConfigRequiredPolicy(flags);
687
715
  const autoIssueLogEnabled = resolveAutoIssueLogEnabled(flags);
688
716
  const taskId = typeof flags['task'] === 'string' ? flags['task'] : undefined;
@@ -699,6 +727,7 @@ async function handleFlow(orchestrator, rawArgs) {
699
727
  approvalPolicy,
700
728
  targetStageId: docsReviewTargetStageId,
701
729
  executionMode,
730
+ runtimeMode,
702
731
  runEvents
703
732
  });
704
733
  const docsPayload = toRunOutputPayload(docsReviewResult);
@@ -744,6 +773,7 @@ async function handleFlow(orchestrator, rawArgs) {
744
773
  approvalPolicy,
745
774
  targetStageId: implementationGateTargetStageId,
746
775
  executionMode,
776
+ runtimeMode,
747
777
  runEvents
748
778
  });
749
779
  const implementationPayload = toRunOutputPayload(implementationGateResult);
@@ -886,6 +916,7 @@ async function handleRlm(orchestrator, rawArgs) {
886
916
  printRlmHelp();
887
917
  return;
888
918
  }
919
+ const runtimeMode = resolveRuntimeModeFlag(flags);
889
920
  applyRepoConfigRequiredPolicy(flags);
890
921
  const goalFromArgs = positionals.length > 0 ? positionals.join(' ') : undefined;
891
922
  const goal = goalFromArgs ?? readStringFlag(flags, 'goal') ?? process.env.RLM_GOAL?.trim();
@@ -921,6 +952,7 @@ async function handleRlm(orchestrator, rawArgs) {
921
952
  taskId,
922
953
  parentRunId: typeof flags['parent-run'] === 'string' ? flags['parent-run'] : undefined,
923
954
  approvalPolicy: typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined,
955
+ runtimeMode,
924
956
  runEvents
925
957
  });
926
958
  emitRunOutput(startResult, 'text', 'Run started');
@@ -949,6 +981,7 @@ async function handleResume(orchestrator, rawArgs) {
949
981
  printResumeHelp();
950
982
  return;
951
983
  }
984
+ const runtimeMode = resolveRuntimeModeFlag(flags);
952
985
  applyRepoConfigRequiredPolicy(flags);
953
986
  const runId = (flags['run'] ?? positionals[0]);
954
987
  if (!runId) {
@@ -962,6 +995,7 @@ async function handleResume(orchestrator, rawArgs) {
962
995
  actor: typeof flags['actor'] === 'string' ? flags['actor'] : undefined,
963
996
  reason: typeof flags['reason'] === 'string' ? flags['reason'] : undefined,
964
997
  targetStageId: resolveTargetStageId(flags),
998
+ runtimeMode,
965
999
  runEvents
966
1000
  });
967
1001
  emitRunOutput(result, format, 'Run resumed');
@@ -1034,6 +1068,13 @@ function emitRunOutput(result, format, label, issueLogCapture = { issueLog: null
1034
1068
  console.log(`Status: ${payload.status}`);
1035
1069
  console.log(`Manifest: ${payload.manifest}`);
1036
1070
  console.log(`Log: ${payload.log_path}`);
1071
+ if (payload.runtime_mode) {
1072
+ console.log(`Runtime: ${payload.runtime_mode}${payload.runtime_mode_requested ? ` (requested ${payload.runtime_mode_requested})` : ''}` +
1073
+ (payload.runtime_provider ? ` via ${payload.runtime_provider}` : ''));
1074
+ if (payload.runtime_fallback?.occurred) {
1075
+ console.log(`Runtime fallback: ${payload.runtime_fallback.code ?? 'runtime-fallback'} (${payload.runtime_fallback.reason ?? 'n/a'})`);
1076
+ }
1077
+ }
1037
1078
  if (payload.cloud_fallback_reason) {
1038
1079
  console.log(`Cloud fallback: ${payload.cloud_fallback_reason}`);
1039
1080
  }
@@ -1060,6 +1101,10 @@ function toRunOutputPayload(result, issueLogCapture = { issueLog: null, issueLog
1060
1101
  manifest: `${result.manifest.artifact_root}/manifest.json`,
1061
1102
  log_path: result.manifest.log_path,
1062
1103
  summary: result.manifest.summary ?? null,
1104
+ runtime_mode_requested: result.manifest.runtime_mode_requested ?? null,
1105
+ runtime_mode: result.manifest.runtime_mode ?? null,
1106
+ runtime_provider: result.manifest.runtime_provider ?? null,
1107
+ runtime_fallback: result.manifest.runtime_fallback ?? null,
1063
1108
  cloud_fallback_reason: result.manifest.cloud_fallback?.reason ?? null,
1064
1109
  issue_log: issueLogCapture.issueLog,
1065
1110
  issue_log_error: issueLogCapture.issueLogError
@@ -1998,6 +2043,7 @@ Commands:
1998
2043
  --format json Emit machine-readable output.
1999
2044
  --execution-mode <mcp|cloud> Force execution mode for this run and child subpipelines.
2000
2045
  --cloud Shortcut for --execution-mode cloud.
2046
+ --runtime-mode <cli|appserver> Force runtime mode for this run and child subpipelines.
2001
2047
  --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
2002
2048
  --auto-issue-log [true|false] On failure, auto-write doctor issue bundle/log entry.
2003
2049
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
@@ -2013,6 +2059,7 @@ Commands:
2013
2059
 
2014
2060
  rlm "<goal>" Run RLM loop until validator passes.
2015
2061
  --task <id> Override task identifier.
2062
+ --runtime-mode <cli|appserver> Force runtime mode for this run.
2016
2063
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
2017
2064
  --multi-agent [auto|true|false] Preferred alias for multi-agent collab subagents (implies symbolic mode).
2018
2065
  --collab [auto|true|false] Legacy alias for --multi-agent.
@@ -2028,6 +2075,7 @@ Commands:
2028
2075
  frontend-test Run frontend testing pipeline.
2029
2076
  --devtools Enable Chrome DevTools MCP for this run.
2030
2077
  --task <id> Override task identifier (defaults to MCP_RUNNER_TASK_ID).
2078
+ --runtime-mode <cli|appserver> Force runtime mode for this run.
2031
2079
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
2032
2080
  --parent-run <id> Link run to parent run id.
2033
2081
  --approval-policy <p> Record approval policy metadata.
@@ -2043,6 +2091,7 @@ Commands:
2043
2091
  --format json Emit machine-readable output summary for both runs.
2044
2092
  --execution-mode <mcp|cloud> Force execution mode for both runs.
2045
2093
  --cloud Shortcut for --execution-mode cloud.
2094
+ --runtime-mode <cli|appserver> Force runtime mode for both runs.
2046
2095
  --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
2047
2096
  --auto-issue-log [true|false] On failure, auto-write doctor issue bundle/log entry.
2048
2097
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
@@ -2058,6 +2107,7 @@ Commands:
2058
2107
  --base <branch> Review against base branch.
2059
2108
  --commit <sha> Review specific commit.
2060
2109
  --non-interactive Force non-interactive review behavior.
2110
+ --runtime-mode <cli|appserver> Force runtime mode for the underlying codex review call.
2061
2111
  --auto-issue-log [true|false] Auto-capture issue bundle on review failure.
2062
2112
  --disable-delegation-mcp [true|false] Disable delegation MCP for this review.
2063
2113
 
@@ -2080,6 +2130,7 @@ Commands:
2080
2130
  --actor <name> Record who resumed the run.
2081
2131
  --reason <text> Record why the run was resumed.
2082
2132
  --target <stage-id> Override stage selection before resuming (alias: --target-stage).
2133
+ --runtime-mode <cli|appserver> Force runtime mode before resuming.
2083
2134
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
2084
2135
  --format json Emit machine-readable output.
2085
2136
  --interactive | --ui Enable read-only HUD when running in a TTY.
@@ -2226,6 +2277,7 @@ Options:
2226
2277
  --actor <name> Record who resumed the run.
2227
2278
  --reason <text> Record why the run was resumed.
2228
2279
  --target <stage-id> Override stage selection before resuming.
2280
+ --runtime-mode <cli|appserver> Force runtime mode before resuming.
2229
2281
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
2230
2282
  --format json Emit machine-readable output.
2231
2283
  --interactive | --ui Enable read-only HUD when running in a TTY.
@@ -2281,6 +2333,7 @@ function printRlmHelp() {
2281
2333
  Options:
2282
2334
  --goal "<goal>" Alternate way to set the goal (positional is preferred).
2283
2335
  --task <id> Override task identifier (defaults to MCP_RUNNER_TASK_ID).
2336
+ --runtime-mode <cli|appserver> Force runtime mode for this run.
2284
2337
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
2285
2338
  --multi-agent [auto|true|false] Preferred alias for multi-agent collab subagents (implies symbolic mode).
2286
2339
  --collab [auto|true|false] Legacy alias for --multi-agent.
@@ -2315,6 +2368,7 @@ Options:
2315
2368
  --format json Emit machine-readable output for both runs.
2316
2369
  --execution-mode <mcp|cloud> Force execution mode for both runs.
2317
2370
  --cloud Shortcut for --execution-mode cloud.
2371
+ --runtime-mode <cli|appserver> Force runtime mode for both runs.
2318
2372
  --target <stage-id> Focus plan/build metadata (applies where the stage exists).
2319
2373
  --auto-issue-log [true|false] On failure, auto-write doctor issue bundle/log entry.
2320
2374
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
@@ -2344,6 +2398,7 @@ Common options:
2344
2398
  --commit <sha> Review a specific commit.
2345
2399
  --title "<text>" Optional review title in the prompt.
2346
2400
  --non-interactive Force non-interactive behavior.
2401
+ --runtime-mode <cli|appserver> Force runtime mode for the underlying codex review call.
2347
2402
  --auto-issue-log [true|false] Auto-capture issue bundle on review failure.
2348
2403
  --disable-delegation-mcp [true|false] Disable delegation MCP for this review.
2349
2404
  --enable-delegation-mcp [true|false] Legacy delegation MCP toggle (disable via false).
@@ -2373,6 +2428,7 @@ Options:
2373
2428
  --format json Emit machine-readable output.
2374
2429
  --execution-mode <mcp|cloud> Force execution mode for this run.
2375
2430
  --cloud Shortcut for --execution-mode cloud.
2431
+ --runtime-mode <cli|appserver> Force runtime mode for this run.
2376
2432
  --target <stage-id> Focus plan/build metadata on a specific stage.
2377
2433
  --auto-issue-log [true|false] On failure, auto-write doctor issue bundle/log entry.
2378
2434
  --repo-config-required [true|false] Require repo-local codex.orchestrator.json (no package fallback).
@@ -55,11 +55,17 @@ function normalizeUserConfig(config, source) {
55
55
  if (!config) {
56
56
  return null;
57
57
  }
58
+ const runtimeMode = normalizeRuntimeMode(config.runtimeMode);
58
59
  const stageSets = normalizeStageSets(config.stageSets);
59
60
  const pipelines = Array.isArray(config.pipelines)
60
61
  ? config.pipelines.map((pipeline) => expandPipelineStages(pipeline, stageSets))
61
62
  : config.pipelines;
62
- return { pipelines, defaultPipeline: config.defaultPipeline, source };
63
+ return {
64
+ pipelines,
65
+ defaultPipeline: config.defaultPipeline,
66
+ runtimeMode,
67
+ source
68
+ };
63
69
  }
64
70
  async function readConfig(configPath) {
65
71
  try {
@@ -111,3 +117,13 @@ function expandPipelineStages(pipeline, stageSets) {
111
117
  function isStageSetRef(stage) {
112
118
  return stage.kind === 'stage-set';
113
119
  }
120
+ function normalizeRuntimeMode(value) {
121
+ if (typeof value !== 'string') {
122
+ return undefined;
123
+ }
124
+ const normalized = value.trim().toLowerCase();
125
+ if (normalized === 'cli' || normalized === 'appserver') {
126
+ return normalized;
127
+ }
128
+ throw new Error(`Invalid codex.orchestrator.json runtimeMode "${value}". Expected one of: cli, appserver.`);
129
+ }
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { logger } from '../logger.js';
7
- import { resolveCodexCommand } from './utils/devtools.js';
7
+ import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from './runtime/index.js';
8
8
  const DEFAULT_PROMPT = [
9
9
  'You are running frontend testing for the current project.',
10
10
  '',
@@ -37,9 +37,9 @@ export async function loadFrontendTestingPrompt(env = process.env) {
37
37
  }
38
38
  return DEFAULT_PROMPT;
39
39
  }
40
- export function resolveFrontendTestingCommand(prompt, env = process.env) {
40
+ export function resolveFrontendTestingCommand(prompt, context) {
41
41
  const args = ['exec', prompt];
42
- return resolveCodexCommand(args, env);
42
+ return resolveRuntimeCodexCommand(args, context);
43
43
  }
44
44
  function envFlagEnabled(value) {
45
45
  if (!value) {
@@ -59,16 +59,21 @@ function shouldForceNonInteractive(env) {
59
59
  }
60
60
  export async function runFrontendTesting(env = process.env) {
61
61
  const prompt = await loadFrontendTestingPrompt(env);
62
- const { command, args } = resolveFrontendTestingCommand(prompt, env);
62
+ const repoRoot = typeof env.CODEX_ORCHESTRATOR_ROOT === 'string' && env.CODEX_ORCHESTRATOR_ROOT.trim().length > 0
63
+ ? env.CODEX_ORCHESTRATOR_ROOT.trim()
64
+ : process.cwd();
65
+ const runtimeContext = await resolveFrontendTestingRuntimeContext(env, repoRoot);
66
+ logger.info(`[frontend-testing-runtime] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}`);
67
+ const { command, args } = resolveFrontendTestingCommand(prompt, runtimeContext);
63
68
  const nonInteractive = shouldForceNonInteractive(env);
64
- const childEnv = { ...process.env, ...env };
69
+ const childEnv = { ...process.env, ...env, ...runtimeContext.env };
65
70
  if (nonInteractive) {
66
71
  childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
67
72
  childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
68
73
  childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
69
74
  }
70
75
  const stdio = nonInteractive ? ['ignore', 'inherit', 'inherit'] : 'inherit';
71
- const child = spawn(command, args, { stdio, env: childEnv });
76
+ const child = spawn(command, args, { stdio, env: childEnv, cwd: repoRoot });
72
77
  await new Promise((resolvePromise, reject) => {
73
78
  child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
74
79
  child.once('exit', (code) => {
@@ -81,6 +86,19 @@ export async function runFrontendTesting(env = process.env) {
81
86
  });
82
87
  });
83
88
  }
89
+ async function resolveFrontendTestingRuntimeContext(env, repoRoot) {
90
+ const requestedMode = parseRuntimeMode(env.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE ?? env.CODEX_ORCHESTRATOR_RUNTIME_MODE ?? null);
91
+ const runId = typeof env.CODEX_ORCHESTRATOR_RUN_ID === 'string' && env.CODEX_ORCHESTRATOR_RUN_ID.trim().length > 0
92
+ ? env.CODEX_ORCHESTRATOR_RUN_ID.trim()
93
+ : `frontend-testing-${Date.now()}`;
94
+ return await createRuntimeCodexCommandContext({
95
+ requestedMode,
96
+ executionMode: 'mcp',
97
+ repoRoot,
98
+ env: { ...process.env, ...env },
99
+ runId
100
+ });
101
+ }
84
102
  async function main() {
85
103
  await runFrontendTesting();
86
104
  }
@@ -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, applyCloudFallbackToRunSummary, applyUsageKpiToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
24
+ import { applyRuntimeToRunSummary, 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 { formatRepoConfigRequiredError, isRepoConfigRequired } from './config/repoConfigPolicy.js';
@@ -35,6 +35,7 @@ import { CodexCloudTaskExecutor } from '../cloud/CodexCloudTaskExecutor.js';
35
35
  import { persistPipelineExperience } from './services/pipelineExperience.js';
36
36
  import { runCloudPreflight } from './utils/cloudPreflight.js';
37
37
  import { writeJsonAtomic } from './utils/fs.js';
38
+ import { resolveRuntimeMode, resolveRuntimeSelection } from './runtime/index.js';
38
39
  import { buildAutoScoutEvidence, resolveAdvancedAutopilotDecision } from './utils/advancedAutopilot.js';
39
40
  const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
40
41
  const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
@@ -233,6 +234,11 @@ export class CodexOrchestrator {
233
234
  planTargetFallback: null
234
235
  });
235
236
  const runId = generateRunId();
237
+ const runtimeModeResolution = resolveRuntimeMode({
238
+ flag: options.runtimeMode,
239
+ env: { ...process.env, ...(preparation.envOverrides ?? {}) },
240
+ configDefault: preparation.runtimeModeDefault
241
+ });
236
242
  const { manifest, paths } = await bootstrapManifest(runId, {
237
243
  env: preparation.env,
238
244
  pipeline: preparation.pipeline,
@@ -241,6 +247,7 @@ export class CodexOrchestrator {
241
247
  approvalPolicy: options.approvalPolicy ?? null,
242
248
  planTargetId: preparation.planPreview?.targetId ?? preparation.plannerTargetId ?? null
243
249
  });
250
+ this.applyRequestedRuntimeMode(manifest, runtimeModeResolution.mode);
244
251
  if (preparation.configNotice) {
245
252
  appendSummary(manifest, preparation.configNotice);
246
253
  }
@@ -305,6 +312,8 @@ export class CodexOrchestrator {
305
312
  onEventEntry,
306
313
  persister,
307
314
  envOverrides: preparation.envOverrides,
315
+ runtimeModeRequested: runtimeModeResolution.mode,
316
+ runtimeModeSource: runtimeModeResolution.source,
308
317
  executionModeOverride: options.executionMode
309
318
  });
310
319
  }
@@ -362,6 +371,7 @@ export class CodexOrchestrator {
362
371
  const preparation = await prepareRun({
363
372
  baseEnv: actualEnv,
364
373
  pipeline,
374
+ runtimeModeDefault: userConfig?.runtimeMode ?? null,
365
375
  resolver,
366
376
  taskIdOverride: manifest.task_id,
367
377
  targetStageId: options.targetStageId,
@@ -371,6 +381,14 @@ export class CodexOrchestrator {
371
381
  if (preparation.configNotice && !(manifest.summary ?? '').includes(preparation.configNotice)) {
372
382
  appendSummary(manifest, preparation.configNotice);
373
383
  }
384
+ const runtimeModeResolution = resolveRuntimeMode({
385
+ flag: options.runtimeMode,
386
+ env: { ...process.env, ...(preparation.envOverrides ?? {}) },
387
+ configDefault: preparation.runtimeModeDefault,
388
+ manifestMode: manifest.runtime_mode_requested ?? manifest.runtime_mode ?? null,
389
+ preferManifest: true
390
+ });
391
+ this.applyRequestedRuntimeMode(manifest, runtimeModeResolution.mode);
374
392
  manifest.plan_target_id = preparation.planPreview?.targetId ?? preparation.plannerTargetId ?? null;
375
393
  const persister = new ManifestPersister({
376
394
  manifest,
@@ -433,7 +451,9 @@ export class CodexOrchestrator {
433
451
  eventStream: stream,
434
452
  onEventEntry,
435
453
  persister,
436
- envOverrides: preparation.envOverrides
454
+ envOverrides: preparation.envOverrides,
455
+ runtimeModeRequested: runtimeModeResolution.mode,
456
+ runtimeModeSource: runtimeModeResolution.source
437
457
  });
438
458
  }
439
459
  finally {
@@ -582,21 +602,52 @@ export class CodexOrchestrator {
582
602
  return Boolean(task.metadata?.execution?.parallel);
583
603
  }
584
604
  async executePipeline(options) {
605
+ const baseEnvOverrides = { ...(options.envOverrides ?? {}) };
606
+ const mergedEnv = { ...process.env, ...baseEnvOverrides };
607
+ let runtimeSelection;
608
+ try {
609
+ runtimeSelection = await resolveRuntimeSelection({
610
+ requestedMode: options.runtimeModeRequested,
611
+ source: options.runtimeModeSource,
612
+ executionMode: options.mode,
613
+ repoRoot: options.env.repoRoot,
614
+ env: mergedEnv,
615
+ runId: options.manifest.run_id
616
+ });
617
+ }
618
+ catch (error) {
619
+ const detail = `Runtime selection failed: ${error?.message ?? String(error)}`;
620
+ finalizeStatus(options.manifest, 'failed', 'runtime-selection-failed');
621
+ appendSummary(options.manifest, detail);
622
+ logger.error(detail);
623
+ return {
624
+ success: false,
625
+ notes: [detail],
626
+ manifest: options.manifest,
627
+ manifestPath: options.paths.manifestPath,
628
+ logPath: options.paths.logPath
629
+ };
630
+ }
631
+ this.applyRuntimeSelection(options.manifest, runtimeSelection);
632
+ const effectiveEnvOverrides = {
633
+ ...baseEnvOverrides,
634
+ ...runtimeSelection.env_overrides
635
+ };
636
+ const effectiveMergedEnv = { ...process.env, ...effectiveEnvOverrides };
585
637
  if (options.mode === 'cloud') {
586
- const environmentId = resolveCloudEnvironmentId(options.task, options.target, options.envOverrides);
587
- const branch = readCloudString(options.envOverrides?.CODEX_CLOUD_BRANCH) ??
638
+ const environmentId = resolveCloudEnvironmentId(options.task, options.target, effectiveEnvOverrides);
639
+ const branch = readCloudString(effectiveEnvOverrides.CODEX_CLOUD_BRANCH) ??
588
640
  readCloudString(process.env.CODEX_CLOUD_BRANCH);
589
- const mergedEnv = { ...process.env, ...(options.envOverrides ?? {}) };
590
- const codexBin = resolveCodexCliBin(mergedEnv);
641
+ const codexBin = resolveCodexCliBin(effectiveMergedEnv);
591
642
  const preflight = await runCloudPreflight({
592
643
  repoRoot: options.env.repoRoot,
593
644
  codexBin,
594
645
  environmentId,
595
646
  branch,
596
- env: mergedEnv
647
+ env: effectiveMergedEnv
597
648
  });
598
649
  if (!preflight.ok) {
599
- if (!allowCloudFallback(options.envOverrides)) {
650
+ if (!allowCloudFallback(effectiveEnvOverrides)) {
600
651
  const detail = `Cloud preflight failed and cloud fallback is disabled. ` +
601
652
  preflight.issues.map((issue) => issue.message).join(' ');
602
653
  finalizeStatus(options.manifest, 'failed', 'cloud-preflight-failed');
@@ -621,21 +672,35 @@ export class CodexOrchestrator {
621
672
  };
622
673
  appendSummary(options.manifest, detail);
623
674
  logger.warn(detail);
624
- const fallback = await this.executePipeline({ ...options, mode: 'mcp', executionModeOverride: 'mcp' });
675
+ const fallback = await this.executePipeline({
676
+ ...options,
677
+ mode: 'mcp',
678
+ executionModeOverride: 'mcp',
679
+ runtimeModeRequested: runtimeSelection.selected_mode,
680
+ runtimeModeSource: runtimeSelection.source,
681
+ envOverrides: effectiveEnvOverrides
682
+ });
625
683
  fallback.notes.unshift(detail);
626
684
  return fallback;
627
685
  }
628
- return await this.executeCloudPipeline(options);
686
+ return await this.executeCloudPipeline({ ...options, envOverrides: effectiveEnvOverrides });
629
687
  }
630
- const { env, pipeline, manifest, paths, runEvents, envOverrides } = options;
688
+ const { env, pipeline, manifest, paths, runEvents } = options;
631
689
  const notes = [];
632
690
  let success = true;
633
691
  manifest.guardrail_status = undefined;
692
+ if (runtimeSelection.fallback.occurred) {
693
+ const fallbackCode = runtimeSelection.fallback.code ?? 'runtime-fallback';
694
+ const fallbackReason = runtimeSelection.fallback.reason ?? 'runtime fallback occurred';
695
+ const fallbackSummary = `Runtime fallback (${fallbackCode}): ${fallbackReason}`;
696
+ appendSummary(manifest, fallbackSummary);
697
+ notes.push(fallbackSummary);
698
+ }
634
699
  const advancedDecision = resolveAdvancedAutopilotDecision({
635
700
  pipelineId: pipeline.id,
636
701
  targetMetadata: (options.target.metadata ?? null),
637
702
  taskMetadata: (options.task.metadata ?? null),
638
- env: { ...process.env, ...(envOverrides ?? {}) }
703
+ env: effectiveMergedEnv
639
704
  });
640
705
  if (advancedDecision.enabled || advancedDecision.source !== 'default') {
641
706
  const advancedSummary = `Advanced mode (${advancedDecision.mode}) ${advancedDecision.enabled ? 'enabled' : 'disabled'}: ${advancedDecision.reason}.`;
@@ -673,7 +738,7 @@ export class CodexOrchestrator {
673
738
  pipeline,
674
739
  target: options.target,
675
740
  task: options.task,
676
- envOverrides,
741
+ envOverrides: effectiveEnvOverrides,
677
742
  advancedDecision
678
743
  });
679
744
  const scoutMessage = scoutOutcome.status === 'recorded'
@@ -719,7 +784,9 @@ export class CodexOrchestrator {
719
784
  index: entry.index,
720
785
  events: runEvents,
721
786
  persister,
722
- envOverrides
787
+ envOverrides: effectiveEnvOverrides,
788
+ runtimeMode: runtimeSelection.selected_mode,
789
+ runtimeSessionId: runtimeSelection.runtime_session_id
723
790
  });
724
791
  notes.push(`${stage.title}: ${result.summary}`);
725
792
  const updatedEntry = manifest.commands[i];
@@ -769,7 +836,8 @@ export class CodexOrchestrator {
769
836
  pipelineId: stage.pipeline,
770
837
  parentRunId: manifest.run_id,
771
838
  format: 'json',
772
- executionMode: options.executionModeOverride
839
+ executionMode: options.executionModeOverride,
840
+ runtimeMode: options.runtimeModeRequested
773
841
  });
774
842
  entry.completed_at = isoTimestamp();
775
843
  entry.sub_run_id = child.manifest.run_id;
@@ -1184,7 +1252,7 @@ export class CodexOrchestrator {
1184
1252
  }
1185
1253
  }
1186
1254
  async performRunLifecycle(context) {
1187
- const { env, pipeline, manifest, paths, planner, taskContext, runId, persister, envOverrides, executionModeOverride } = context;
1255
+ const { env, pipeline, manifest, paths, planner, taskContext, runId, persister, envOverrides, runtimeModeRequested, runtimeModeSource, executionModeOverride } = context;
1188
1256
  let latestPipelineResult = null;
1189
1257
  const executingByKey = new Map();
1190
1258
  const executePipeline = async (input) => {
@@ -1199,6 +1267,8 @@ export class CodexOrchestrator {
1199
1267
  manifest,
1200
1268
  paths,
1201
1269
  mode: input.mode,
1270
+ runtimeModeRequested,
1271
+ runtimeModeSource,
1202
1272
  executionModeOverride,
1203
1273
  target: input.target,
1204
1274
  task: taskContext,
@@ -1254,6 +1324,7 @@ export class CodexOrchestrator {
1254
1324
  persister
1255
1325
  });
1256
1326
  this.scheduler.applySchedulerToRunSummary(runSummary, schedulerPlan);
1327
+ applyRuntimeToRunSummary(runSummary, manifest);
1257
1328
  applyHandlesToRunSummary(runSummary, manifest);
1258
1329
  applyPrivacyToRunSummary(runSummary, manifest);
1259
1330
  applyCloudExecutionToRunSummary(runSummary, manifest);
@@ -1283,6 +1354,25 @@ export class CodexOrchestrator {
1283
1354
  });
1284
1355
  });
1285
1356
  }
1357
+ applyRequestedRuntimeMode(manifest, mode) {
1358
+ manifest.runtime_mode_requested = mode;
1359
+ manifest.runtime_mode = mode;
1360
+ manifest.runtime_provider = mode === 'appserver' ? 'AppServerRuntimeProvider' : 'CliRuntimeProvider';
1361
+ manifest.runtime_fallback = {
1362
+ occurred: false,
1363
+ code: null,
1364
+ reason: null,
1365
+ from_mode: null,
1366
+ to_mode: null,
1367
+ checked_at: isoTimestamp()
1368
+ };
1369
+ }
1370
+ applyRuntimeSelection(manifest, selection) {
1371
+ manifest.runtime_mode_requested = selection.requested_mode;
1372
+ manifest.runtime_mode = selection.selected_mode;
1373
+ manifest.runtime_provider = selection.provider;
1374
+ manifest.runtime_fallback = selection.fallback;
1375
+ }
1286
1376
  async validateResumeToken(paths, manifest, provided) {
1287
1377
  let stored = manifest.resume_token;
1288
1378
  if (!stored) {
@@ -1311,6 +1401,10 @@ export class CodexOrchestrator {
1311
1401
  activity,
1312
1402
  commands: manifest.commands,
1313
1403
  child_runs: manifest.child_runs,
1404
+ runtime_mode_requested: manifest.runtime_mode_requested,
1405
+ runtime_mode: manifest.runtime_mode,
1406
+ runtime_provider: manifest.runtime_provider,
1407
+ runtime_fallback: manifest.runtime_fallback ?? null,
1314
1408
  cloud_execution: manifest.cloud_execution ?? null,
1315
1409
  cloud_fallback: manifest.cloud_fallback ?? null
1316
1410
  };
@@ -1321,6 +1415,15 @@ export class CodexOrchestrator {
1321
1415
  logger.info(`Started: ${manifest.started_at}`);
1322
1416
  logger.info(`Completed: ${manifest.completed_at ?? 'in-progress'}`);
1323
1417
  logger.info(`Manifest: ${manifest.artifact_root}/manifest.json`);
1418
+ if (manifest.runtime_mode || manifest.runtime_mode_requested || manifest.runtime_provider) {
1419
+ const selectedMode = manifest.runtime_mode ?? 'unknown';
1420
+ logger.info(`Runtime: ${selectedMode}${manifest.runtime_mode_requested ? ` (requested ${manifest.runtime_mode_requested})` : ''}` +
1421
+ (manifest.runtime_provider ? ` via ${manifest.runtime_provider}` : ''));
1422
+ }
1423
+ if (manifest.runtime_fallback?.occurred) {
1424
+ const fallbackCode = manifest.runtime_fallback.code ?? 'runtime-fallback';
1425
+ logger.info(`Runtime fallback: ${fallbackCode} — ${manifest.runtime_fallback.reason ?? 'n/a'}`);
1426
+ }
1324
1427
  if (activity.observed_at) {
1325
1428
  const staleSuffix = activity.stale === null ? '' : activity.stale ? ' [stale]' : ' [active]';
1326
1429
  const sourceLabel = activity.observed_source ? ` via ${activity.observed_source}` : '';