@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.
@@ -7,7 +7,7 @@ import { promisify } from 'node:util';
7
7
  import { createInterface } from 'node:readline/promises';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { logger } from '../logger.js';
10
- import { resolveCodexCommand } from './utils/devtools.js';
10
+ import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from './runtime/index.js';
11
11
  import { detectValidator } from './rlm/validator.js';
12
12
  import { buildRlmPrompt } from './rlm/prompt.js';
13
13
  import { runRlmLoop } from './rlm/runner.js';
@@ -57,6 +57,8 @@ const COLLAB_FEATURE_CANONICAL = 'multi_agent';
57
57
  const COLLAB_FEATURE_LEGACY = 'collab';
58
58
  const COLLAB_ROLE_TAG_PATTERN = /^\s*\[(?:agent_type|role)\s*:\s*([a-z0-9._-]+)\]/i;
59
59
  const COLLAB_ROLE_TOKEN_PATTERN = /^[a-z0-9._-]+$/;
60
+ let runtimeCodexContextPromise = null;
61
+ let runtimeCodexContextLogged = false;
60
62
  function parseArgs(argv) {
61
63
  const parsed = {};
62
64
  for (let i = 0; i < argv.length; i += 1) {
@@ -332,8 +334,9 @@ async function runCodexAgent(input, env, repoRoot, nonInteractive, subagentsEnab
332
334
  return { output };
333
335
  }
334
336
  async function runCodexExec(args, env, repoRoot, nonInteractive, subagentsEnabled, mirrorOutput) {
335
- const { command, args: resolvedArgs } = resolveCodexCommand(args, env);
336
- const childEnv = { ...process.env, ...env };
337
+ const runtimeContext = await resolveRlmRuntimeCodexContext(env, repoRoot);
338
+ const { command, args: resolvedArgs } = resolveRuntimeCodexCommand(args, runtimeContext);
339
+ const childEnv = { ...process.env, ...env, ...runtimeContext.env };
337
340
  if (nonInteractive) {
338
341
  childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
339
342
  childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
@@ -370,6 +373,27 @@ async function runCodexExec(args, env, repoRoot, nonInteractive, subagentsEnable
370
373
  });
371
374
  return { stdout, stderr };
372
375
  }
376
+ async function resolveRlmRuntimeCodexContext(env, repoRoot) {
377
+ if (!runtimeCodexContextPromise) {
378
+ const requestedMode = parseRuntimeMode(env.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE ?? env.CODEX_ORCHESTRATOR_RUNTIME_MODE ?? null);
379
+ const runId = typeof env.CODEX_ORCHESTRATOR_RUN_ID === 'string' && env.CODEX_ORCHESTRATOR_RUN_ID.trim().length > 0
380
+ ? env.CODEX_ORCHESTRATOR_RUN_ID.trim()
381
+ : `rlm-${Date.now()}`;
382
+ runtimeCodexContextPromise = createRuntimeCodexCommandContext({
383
+ requestedMode,
384
+ executionMode: 'mcp',
385
+ repoRoot,
386
+ env: { ...process.env, ...env },
387
+ runId
388
+ });
389
+ }
390
+ const runtimeContext = await runtimeCodexContextPromise;
391
+ if (!runtimeCodexContextLogged) {
392
+ logger.info(`[rlm-runtime] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}`);
393
+ runtimeCodexContextLogged = true;
394
+ }
395
+ return runtimeContext;
396
+ }
373
397
  async function runCodexCompletion(prompt, env, repoRoot, nonInteractive, subagentsEnabled, mirrorOutput) {
374
398
  const { stdout, stderr } = await runCodexExec(['exec', prompt], env, repoRoot, nonInteractive, subagentsEnabled, mirrorOutput);
375
399
  return [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
@@ -13,6 +13,19 @@ const HEARTBEAT_INTERVAL_SECONDS = 5;
13
13
  const HEARTBEAT_STALE_AFTER_SECONDS = 30;
14
14
  const MAX_ERROR_DETAIL_CHARS = 8 * 1024;
15
15
  const DEFAULT_MIN_EXPERIENCE_REWARD = 0.1;
16
+ function createDefaultRuntimeFallback() {
17
+ return {
18
+ occurred: false,
19
+ code: null,
20
+ reason: null,
21
+ from_mode: null,
22
+ to_mode: null,
23
+ checked_at: isoTimestamp()
24
+ };
25
+ }
26
+ function runtimeProviderForMode(mode) {
27
+ return mode === 'appserver' ? 'AppServerRuntimeProvider' : 'CliRuntimeProvider';
28
+ }
16
29
  export async function bootstrapManifest(runId, options) {
17
30
  const { env, pipeline, parentRunId = null, taskSlug, approvalPolicy = null } = options;
18
31
  const paths = resolveRunPaths(env, runId);
@@ -57,6 +70,10 @@ export async function bootstrapManifest(runId, options) {
57
70
  instructions_sources: [],
58
71
  prompt_packs: [],
59
72
  guardrails_required: pipeline.guardrailsRequired !== false,
73
+ runtime_mode_requested: 'appserver',
74
+ runtime_mode: 'appserver',
75
+ runtime_provider: runtimeProviderForMode('appserver'),
76
+ runtime_fallback: createDefaultRuntimeFallback(),
60
77
  cloud_execution: null,
61
78
  cloud_fallback: null,
62
79
  learning: {
@@ -192,6 +209,8 @@ export function resetForResume(manifest) {
192
209
  manifest.status = 'in_progress';
193
210
  manifest.status_detail = 'resuming';
194
211
  manifest.guardrail_status = undefined;
212
+ manifest.runtime_provider = runtimeProviderForMode(manifest.runtime_mode);
213
+ manifest.runtime_fallback = createDefaultRuntimeFallback();
195
214
  manifest.cloud_execution = null;
196
215
  manifest.cloud_fallback = null;
197
216
  }
@@ -0,0 +1,39 @@
1
+ import { resolveCodexCommand } from '../utils/devtools.js';
2
+ import { resolveRuntimeMode } from './mode.js';
3
+ import { resolveRuntimeSelection } from './provider.js';
4
+ export async function createRuntimeCodexCommandContext(options) {
5
+ const runtimeEnv = options.env ?? process.env;
6
+ const modeResolution = resolveRuntimeMode({
7
+ flag: options.requestedMode,
8
+ env: runtimeEnv,
9
+ configDefault: options.configDefault
10
+ });
11
+ const runtime = await resolveRuntimeSelection({
12
+ requestedMode: modeResolution.mode,
13
+ source: modeResolution.source,
14
+ executionMode: options.executionMode ?? 'mcp',
15
+ repoRoot: options.repoRoot,
16
+ env: runtimeEnv,
17
+ runId: options.runId,
18
+ allowFallback: options.allowFallback
19
+ });
20
+ return {
21
+ runtime,
22
+ env: {
23
+ ...runtimeEnv,
24
+ ...runtime.env_overrides
25
+ }
26
+ };
27
+ }
28
+ export function resolveRuntimeCodexCommand(args, context) {
29
+ return resolveCodexCommand(args, context.env);
30
+ }
31
+ export function formatRuntimeSelectionSummary(selection) {
32
+ const base = `runtime requested=${selection.requested_mode} selected=${selection.selected_mode} provider=${selection.provider}`;
33
+ if (!selection.fallback.occurred) {
34
+ return base;
35
+ }
36
+ const code = selection.fallback.code ?? 'unknown';
37
+ const reason = selection.fallback.reason ?? 'fallback occurred';
38
+ return `${base} fallback=${code} (${reason})`;
39
+ }
@@ -0,0 +1,3 @@
1
+ export { parseRuntimeMode, resolveRuntimeMode, DEFAULT_RUNTIME_MODE, DEFAULT_RUNTIME_MODE_ENV_KEY } from './mode.js';
2
+ export { resolveRuntimeSelection } from './provider.js';
3
+ export { createRuntimeCodexCommandContext, resolveRuntimeCodexCommand, formatRuntimeSelectionSummary } from './codexCommand.js';
@@ -0,0 +1,53 @@
1
+ const RUNTIME_MODES = new Set(['cli', 'appserver']);
2
+ const DEFAULT_RUNTIME_MODE = 'appserver';
3
+ const DEFAULT_RUNTIME_MODE_ENV_KEY = 'CODEX_ORCHESTRATOR_RUNTIME_MODE';
4
+ function normalizeRuntimeMode(value) {
5
+ const normalized = value.trim().toLowerCase();
6
+ return RUNTIME_MODES.has(normalized) ? normalized : null;
7
+ }
8
+ function parseRuntimeModeFromSource(value, sourceLabel) {
9
+ const parsed = normalizeRuntimeMode(value);
10
+ if (!parsed) {
11
+ throw new Error(`Invalid runtime mode "${value}" from ${sourceLabel}. Expected one of: cli, appserver.`);
12
+ }
13
+ return parsed;
14
+ }
15
+ export function parseRuntimeMode(value) {
16
+ if (typeof value !== 'string' || value.trim().length === 0) {
17
+ return null;
18
+ }
19
+ return normalizeRuntimeMode(value);
20
+ }
21
+ export function resolveRuntimeMode(options = {}) {
22
+ if (typeof options.flag === 'string' && options.flag.trim().length > 0) {
23
+ return {
24
+ mode: parseRuntimeModeFromSource(options.flag, 'CLI flag'),
25
+ source: 'flag'
26
+ };
27
+ }
28
+ const envKey = options.envKey ?? DEFAULT_RUNTIME_MODE_ENV_KEY;
29
+ const envValue = options.env?.[envKey] ?? process.env[envKey];
30
+ if (typeof envValue === 'string' && envValue.trim().length > 0) {
31
+ return {
32
+ mode: parseRuntimeModeFromSource(envValue, `env ${envKey}`),
33
+ source: 'env'
34
+ };
35
+ }
36
+ if (typeof options.configDefault === 'string' && options.configDefault.trim().length > 0) {
37
+ return {
38
+ mode: parseRuntimeModeFromSource(options.configDefault, 'config default'),
39
+ source: 'config'
40
+ };
41
+ }
42
+ if (options.preferManifest && typeof options.manifestMode === 'string' && options.manifestMode.trim().length > 0) {
43
+ return {
44
+ mode: parseRuntimeModeFromSource(options.manifestMode, 'manifest'),
45
+ source: 'manifest'
46
+ };
47
+ }
48
+ return {
49
+ mode: DEFAULT_RUNTIME_MODE,
50
+ source: 'default'
51
+ };
52
+ }
53
+ export { DEFAULT_RUNTIME_MODE, DEFAULT_RUNTIME_MODE_ENV_KEY };
@@ -0,0 +1,205 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { resolveCodexCliBin } from '../utils/codexCli.js';
4
+ import { isoTimestamp } from '../utils/time.js';
5
+ const execFileAsync = promisify(execFile);
6
+ const APP_SERVER_HELP_TIMEOUT_MS = 8000;
7
+ const LOGIN_STATUS_TIMEOUT_MS = 8000;
8
+ function envFlagEnabled(value) {
9
+ if (!value) {
10
+ return false;
11
+ }
12
+ const normalized = value.trim().toLowerCase();
13
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
14
+ }
15
+ function allowRuntimeFallback(env, override) {
16
+ if (typeof override === 'boolean') {
17
+ return override;
18
+ }
19
+ const raw = env.CODEX_ORCHESTRATOR_RUNTIME_FALLBACK;
20
+ if (!raw) {
21
+ return true;
22
+ }
23
+ const normalized = raw.trim().toLowerCase();
24
+ return !['0', 'false', 'off', 'deny', 'disabled', 'never', 'strict'].includes(normalized);
25
+ }
26
+ function createNoFallback(now) {
27
+ return {
28
+ occurred: false,
29
+ code: null,
30
+ reason: null,
31
+ from_mode: null,
32
+ to_mode: null,
33
+ checked_at: now()
34
+ };
35
+ }
36
+ function createFallback(params) {
37
+ return {
38
+ occurred: true,
39
+ code: params.code,
40
+ reason: params.reason,
41
+ from_mode: params.fromMode,
42
+ to_mode: params.toMode,
43
+ checked_at: params.now()
44
+ };
45
+ }
46
+ function summarizePreflightFailures(issues) {
47
+ const codes = issues.map((issue) => issue.code).join(', ');
48
+ const messages = issues.map((issue) => issue.message).join(' ');
49
+ return `Appserver preflight failed (${codes}). ${messages}`.trim();
50
+ }
51
+ function resolveRequestedMode(options) {
52
+ if (options.executionMode === 'cloud' &&
53
+ options.requestedMode === 'appserver' &&
54
+ (options.source === 'default' || options.source === 'manifest')) {
55
+ return 'cli';
56
+ }
57
+ return options.requestedMode;
58
+ }
59
+ async function runCodexProbe(command, args, cwd, env, timeoutMs) {
60
+ try {
61
+ const { stdout, stderr } = await execFileAsync(command, args, {
62
+ cwd,
63
+ env,
64
+ timeout: timeoutMs,
65
+ maxBuffer: 256 * 1024
66
+ });
67
+ return {
68
+ ok: true,
69
+ stdout: stdout.toString(),
70
+ stderr: stderr.toString(),
71
+ code: 'ok'
72
+ };
73
+ }
74
+ catch (error) {
75
+ const typed = error;
76
+ const stdout = typeof typed.stdout === 'string' ? typed.stdout : typed.stdout?.toString() ?? '';
77
+ const stderr = typeof typed.stderr === 'string' ? typed.stderr : typed.stderr?.toString() ?? '';
78
+ const timeout = typed.signal === 'SIGTERM' || typed.killed === true;
79
+ return {
80
+ ok: false,
81
+ stdout,
82
+ stderr,
83
+ code: timeout ? 'timeout' : typed.code ?? 'failed'
84
+ };
85
+ }
86
+ }
87
+ async function runAppserverPreflight(params) {
88
+ const codexBin = resolveCodexCliBin(params.env);
89
+ const issues = [];
90
+ if (envFlagEnabled(params.env.CODEX_ORCHESTRATOR_APPSERVER_FORCE_PRECHECK_FAIL)) {
91
+ issues.push({
92
+ code: 'forced-preflight-failure',
93
+ message: 'Forced appserver preflight failure via CODEX_ORCHESTRATOR_APPSERVER_FORCE_PRECHECK_FAIL.'
94
+ });
95
+ return {
96
+ ok: false,
97
+ issues
98
+ };
99
+ }
100
+ const appServerHelp = await runCodexProbe(codexBin, ['app-server', '--help'], params.repoRoot, params.env, APP_SERVER_HELP_TIMEOUT_MS);
101
+ if (!appServerHelp.ok) {
102
+ issues.push({
103
+ code: 'appserver-command-unavailable',
104
+ message: appServerHelp.code === 'timeout'
105
+ ? 'Timed out probing `codex app-server --help`.'
106
+ : 'Failed probing `codex app-server --help`.'
107
+ });
108
+ }
109
+ if (!envFlagEnabled(params.env.CODEX_ORCHESTRATOR_APPSERVER_SKIP_LOGIN_CHECK)) {
110
+ const loginStatus = await runCodexProbe(codexBin, ['login', 'status'], params.repoRoot, params.env, LOGIN_STATUS_TIMEOUT_MS);
111
+ if (!loginStatus.ok) {
112
+ issues.push({
113
+ code: 'login-status-failed',
114
+ message: loginStatus.code === 'timeout'
115
+ ? 'Timed out probing `codex login status`.'
116
+ : 'Failed probing `codex login status`.'
117
+ });
118
+ }
119
+ else {
120
+ const text = `${loginStatus.stdout}\n${loginStatus.stderr}`.toLowerCase();
121
+ const loggedIn = text.includes('logged in');
122
+ const loggedOut = text.includes('not logged') || text.includes('logged out');
123
+ if (!loggedIn || loggedOut) {
124
+ issues.push({
125
+ code: 'login-required',
126
+ message: '`codex login status` did not report an active ChatGPT login.'
127
+ });
128
+ }
129
+ }
130
+ }
131
+ return {
132
+ ok: issues.length === 0,
133
+ issues
134
+ };
135
+ }
136
+ export async function resolveRuntimeSelection(options) {
137
+ const now = options.now ?? isoTimestamp;
138
+ const requestedMode = resolveRequestedMode(options);
139
+ if (options.executionMode === 'cloud' && requestedMode === 'appserver') {
140
+ throw new Error('Unsupported mode combination: executionMode=cloud does not support runtimeMode=appserver. ' +
141
+ 'Use --runtime-mode cli or remove the runtime override for cloud execution.');
142
+ }
143
+ if (requestedMode === 'cli') {
144
+ return {
145
+ requested_mode: 'cli',
146
+ selected_mode: 'cli',
147
+ source: options.source,
148
+ provider: 'CliRuntimeProvider',
149
+ env_overrides: {
150
+ CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE: 'cli',
151
+ CODEX_ORCHESTRATOR_RUNTIME_MODE: 'cli',
152
+ CODEX_RUNTIME_MODE: 'cli',
153
+ CODEX_ORCHESTRATOR_APPSERVER_SESSION_ID: ''
154
+ },
155
+ runtime_session_id: null,
156
+ fallback: createNoFallback(now)
157
+ };
158
+ }
159
+ const preflight = await runAppserverPreflight({
160
+ repoRoot: options.repoRoot,
161
+ env: options.env
162
+ });
163
+ if (preflight.ok) {
164
+ const runtimeSessionId = `appserver-${options.runId}`;
165
+ return {
166
+ requested_mode: 'appserver',
167
+ selected_mode: 'appserver',
168
+ source: options.source,
169
+ provider: 'AppServerRuntimeProvider',
170
+ env_overrides: {
171
+ CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE: 'appserver',
172
+ CODEX_ORCHESTRATOR_RUNTIME_MODE: 'appserver',
173
+ CODEX_ORCHESTRATOR_APPSERVER_SESSION_ID: runtimeSessionId,
174
+ CODEX_RUNTIME_MODE: 'appserver'
175
+ },
176
+ runtime_session_id: runtimeSessionId,
177
+ fallback: createNoFallback(now)
178
+ };
179
+ }
180
+ const reason = summarizePreflightFailures(preflight.issues);
181
+ const fallbackAllowed = allowRuntimeFallback(options.env, options.allowFallback);
182
+ if (!fallbackAllowed) {
183
+ throw new Error(`${reason} Runtime fallback is disabled by CODEX_ORCHESTRATOR_RUNTIME_FALLBACK.`);
184
+ }
185
+ return {
186
+ requested_mode: 'appserver',
187
+ selected_mode: 'cli',
188
+ source: options.source,
189
+ provider: 'CliRuntimeProvider',
190
+ env_overrides: {
191
+ CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE: 'cli',
192
+ CODEX_ORCHESTRATOR_RUNTIME_MODE: 'cli',
193
+ CODEX_RUNTIME_MODE: 'cli',
194
+ CODEX_ORCHESTRATOR_APPSERVER_SESSION_ID: ''
195
+ },
196
+ runtime_session_id: null,
197
+ fallback: createFallback({
198
+ code: preflight.issues[0]?.code ?? 'appserver-preflight-failed',
199
+ reason,
200
+ fromMode: 'appserver',
201
+ toMode: 'cli',
202
+ now
203
+ })
204
+ };
205
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -154,10 +154,13 @@ export async function runCommandStage(context, hooks = {}) {
154
154
  const unsubscribe = runner.on(handleEvent);
155
155
  try {
156
156
  const sessionConfig = stage.session ?? {};
157
- const sessionId = sessionConfig.id;
158
- const wantsPersist = Boolean(sessionConfig.persist || sessionConfig.reuse);
159
- const persistSession = Boolean(sessionId && wantsPersist);
160
- const reuseSession = Boolean(sessionId && (sessionConfig.reuse ?? persistSession));
157
+ const stageSessionId = runtimeSessionIdOrNull(sessionConfig.id);
158
+ const inheritedRuntimeSessionId = runtimeSessionIdOrNull(context.runtimeSessionId);
159
+ const effectiveSessionId = stageSessionId ?? inheritedRuntimeSessionId;
160
+ const usesInheritedRuntimeSession = !stageSessionId && Boolean(inheritedRuntimeSessionId);
161
+ const wantsPersist = Boolean(sessionConfig.persist || sessionConfig.reuse || usesInheritedRuntimeSession);
162
+ const persistSession = Boolean(effectiveSessionId && wantsPersist);
163
+ const reuseSession = Boolean(effectiveSessionId && (sessionConfig.reuse ?? persistSession));
161
164
  const baseEnv = {
162
165
  ...process.env,
163
166
  ...(envOverrides ?? {}),
@@ -172,6 +175,10 @@ export async function runCommandStage(context, hooks = {}) {
172
175
  CODEX_ORCHESTRATOR_ROOT: env.repoRoot,
173
176
  CODEX_ORCHESTRATOR_PACKAGE_ROOT: PACKAGE_ROOT
174
177
  };
178
+ baseEnv.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE =
179
+ context.runtimeMode ?? (manifest.runtime_mode === 'appserver' ? 'appserver' : 'cli');
180
+ // Keep both keys during migration because downstream tools still read either name.
181
+ baseEnv.CODEX_ORCHESTRATOR_RUNTIME_MODE = baseEnv.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE;
175
182
  const execEnv = { ...baseEnv, ...stage.env };
176
183
  const invocationId = `cli-command:${manifest.run_id}:${stage.id}:${Date.now()}`;
177
184
  let result;
@@ -181,7 +188,7 @@ export async function runCommandStage(context, hooks = {}) {
181
188
  command: stage.command,
182
189
  cwd: stage.cwd ?? env.repoRoot,
183
190
  env: execEnv,
184
- sessionId: sessionId ?? undefined,
191
+ sessionId: effectiveSessionId ?? undefined,
185
192
  persistSession,
186
193
  reuseSession,
187
194
  invocationId,
@@ -275,6 +282,13 @@ export async function runCommandStage(context, hooks = {}) {
275
282
  privacyLog.end();
276
283
  }
277
284
  }
285
+ function runtimeSessionIdOrNull(value) {
286
+ if (typeof value !== 'string') {
287
+ return null;
288
+ }
289
+ const trimmed = value.trim();
290
+ return trimmed.length > 0 ? trimmed : null;
291
+ }
278
292
  function recordHandle(manifest, descriptor, context) {
279
293
  const handles = Array.isArray(manifest.handles) ? [...manifest.handles] : [];
280
294
  const entry = {
@@ -35,6 +35,7 @@ export async function prepareRun(options) {
35
35
  const resolvedPipeline = options.pipeline
36
36
  ? {
37
37
  pipeline: options.pipeline,
38
+ userConfig: null,
38
39
  source: options.pipelineSource ?? null,
39
40
  configNotice: options.configNotice ?? null,
40
41
  envOverrides: options.envOverrides ?? {}
@@ -54,6 +55,7 @@ export async function prepareRun(options) {
54
55
  env,
55
56
  pipeline: resolvedPipeline.pipeline,
56
57
  pipelineSource: resolvedPipeline.source ?? null,
58
+ runtimeModeDefault: options.runtimeModeDefault ?? resolvedPipeline.userConfig?.runtimeMode ?? null,
57
59
  configNotice: resolvedPipeline.configNotice ?? null,
58
60
  envOverrides: resolvedPipeline.envOverrides ?? {},
59
61
  planner,
@@ -27,6 +27,18 @@ export function applyPrivacyToRunSummary(runSummary, manifest) {
27
27
  allowedFrames: manifest.privacy.totals.allowed_frames
28
28
  };
29
29
  }
30
+ export function applyRuntimeToRunSummary(runSummary, manifest) {
31
+ const fallback = manifest.runtime_fallback ?? null;
32
+ runSummary.runtime = {
33
+ modeRequested: manifest.runtime_mode_requested,
34
+ modeUsed: manifest.runtime_mode,
35
+ provider: manifest.runtime_provider,
36
+ fallbackOccurred: fallback?.occurred ?? false,
37
+ fallbackCode: fallback?.code ?? null,
38
+ fallbackReason: fallback?.reason ?? null,
39
+ checkedAt: fallback?.checked_at ?? null
40
+ };
41
+ }
30
42
  export function applyCloudExecutionToRunSummary(runSummary, manifest) {
31
43
  if (!manifest.cloud_execution) {
32
44
  return;
@@ -16,7 +16,7 @@ import path from 'node:path';
16
16
  import process from 'node:process';
17
17
  import { createInterface } from 'node:readline';
18
18
  import { promisify } from 'node:util';
19
- import { resolveCodexCommand } from '../orchestrator/src/cli/utils/devtools.js';
19
+ import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from '../orchestrator/src/cli/runtime/index.js';
20
20
  import { runDoctor } from '../orchestrator/src/cli/doctor.js';
21
21
  import { formatDoctorIssueLogSummary, writeDoctorIssueLog } from '../orchestrator/src/cli/doctorIssueLog.js';
22
22
  import { parseArgs as parseCliArgs, hasFlag } from './lib/cli-args.js';
@@ -180,6 +180,16 @@ function parseBooleanOptionValue(raw, label) {
180
180
  }
181
181
  throw new Error(`Invalid ${label} value "${raw}". Expected true|false.`);
182
182
  }
183
+ function parseRuntimeModeOption(raw, label) {
184
+ if (typeof raw !== 'string') {
185
+ throw new Error(`${label} requires a value. Expected one of: cli, appserver.`);
186
+ }
187
+ const parsed = parseRuntimeMode(raw);
188
+ if (!parsed) {
189
+ throw new Error(`Invalid ${label} value "${raw}". Expected one of: cli, appserver.`);
190
+ }
191
+ return parsed;
192
+ }
183
193
  function inferTaskFromManifestPath(manifestPath) {
184
194
  const segments = path.normalize(manifestPath).split(path.sep).filter((segment) => segment.length > 0);
185
195
  const fileName = segments.at(-1);
@@ -218,6 +228,9 @@ function parseArgs(argv) {
218
228
  else if (entry.key === 'task' && typeof entry.value === 'string') {
219
229
  options.task = entry.value;
220
230
  }
231
+ else if (entry.key === 'runtime-mode') {
232
+ options.runtimeMode = parseRuntimeModeOption(entry.value, '--runtime-mode');
233
+ }
221
234
  else if (entry.key === 'base' && typeof entry.value === 'string') {
222
235
  options.base = entry.value;
223
236
  }
@@ -413,7 +426,13 @@ async function main() {
413
426
  console.log('Set FORCE_CODEX_REVIEW=1 to invoke `codex review` in this environment.');
414
427
  return;
415
428
  }
416
- await ensureReviewCommandAvailable();
429
+ const runtimeContext = await resolveReviewRuntimeContext({
430
+ options,
431
+ manifestPath,
432
+ env: reviewEnv
433
+ });
434
+ console.log(`[run-review] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}.`);
435
+ await ensureReviewCommandAvailable(runtimeContext);
417
436
  const disableDelegationMcp = options.disableDelegationMcp ??
418
437
  (options.enableDelegationMcp === undefined ? false : !options.enableDelegationMcp);
419
438
  if (disableDelegationMcp) {
@@ -426,7 +445,7 @@ async function main() {
426
445
  includeScopeFlags: true,
427
446
  disableDelegationMcp
428
447
  });
429
- const resolvedScoped = resolveReviewCommand(scopedReviewArgs);
448
+ const resolvedScoped = resolveReviewCommand(scopedReviewArgs, runtimeContext);
430
449
  console.log(`Review prompt saved to: ${path.relative(repoRoot, artifactPaths.promptPath)}`);
431
450
  console.log(`Review output log: ${path.relative(repoRoot, artifactPaths.outputLogPath)}`);
432
451
  console.log(`Launching Codex review (evidence: ${relativeManifest})`);
@@ -454,7 +473,7 @@ async function main() {
454
473
  const runReview = async (resolved) => runCodexReview({
455
474
  command: resolved.command,
456
475
  args: resolved.args,
457
- env: reviewEnv,
476
+ env: runtimeContext.env,
458
477
  stdio: nonInteractive ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'pipe', 'pipe'],
459
478
  blockHeavyCommands: enforceBoundedMode,
460
479
  timeoutMs,
@@ -497,7 +516,7 @@ async function main() {
497
516
  includeScopeFlags: false,
498
517
  disableDelegationMcp
499
518
  });
500
- const resolvedUnscoped = resolveReviewCommand(unscopedArgs);
519
+ const resolvedUnscoped = resolveReviewCommand(unscopedArgs, runtimeContext);
501
520
  try {
502
521
  await runReview(resolvedUnscoped);
503
522
  const telemetrySummary = await writeTelemetry('succeeded');
@@ -551,8 +570,8 @@ main().catch((error) => {
551
570
  console.error('[run-review] failed:', error.message ?? error);
552
571
  process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 1;
553
572
  });
554
- async function ensureReviewCommandAvailable() {
555
- const resolved = resolveCodexCommand(['--help'], process.env);
573
+ async function ensureReviewCommandAvailable(context) {
574
+ const resolved = resolveRuntimeCodexCommand(['--help'], context);
556
575
  const hasReview = await new Promise((resolve, reject) => {
557
576
  const detached = process.platform !== 'win32';
558
577
  const child = spawn(resolved.command, resolved.args, { stdio: ['ignore', 'pipe', 'pipe'], detached });
@@ -607,6 +626,32 @@ async function ensureReviewCommandAvailable() {
607
626
  throw new Error('codex CLI is missing the `review` subcommand (or is not installed).');
608
627
  }
609
628
  }
629
+ async function resolveReviewRuntimeContext(params) {
630
+ const runId = await resolveReviewRunId(params.manifestPath);
631
+ const requestedMode = params.options.runtimeMode ??
632
+ parseRuntimeMode(params.env.CODEX_ORCHESTRATOR_RUNTIME_MODE_ACTIVE ??
633
+ params.env.CODEX_ORCHESTRATOR_RUNTIME_MODE ??
634
+ null);
635
+ return await createRuntimeCodexCommandContext({
636
+ requestedMode,
637
+ executionMode: 'mcp',
638
+ repoRoot,
639
+ env: params.env,
640
+ runId: runId ?? `review-${Date.now()}`
641
+ });
642
+ }
643
+ async function resolveReviewRunId(manifestPath) {
644
+ try {
645
+ const raw = await readFile(manifestPath, 'utf8');
646
+ const parsed = JSON.parse(raw);
647
+ return typeof parsed.run_id === 'string' && parsed.run_id.trim().length > 0
648
+ ? parsed.run_id.trim()
649
+ : null;
650
+ }
651
+ catch {
652
+ return null;
653
+ }
654
+ }
610
655
  function resolveScopeFlag(options) {
611
656
  if (options.commit) {
612
657
  return { mode: 'commit', args: ['--commit', options.commit] };
@@ -644,8 +689,8 @@ function buildReviewArgs(options, prompt, opts) {
644
689
  args.push(prompt);
645
690
  return args;
646
691
  }
647
- function resolveReviewCommand(reviewArgs) {
648
- return resolveCodexCommand(reviewArgs, process.env);
692
+ function resolveReviewCommand(reviewArgs, context) {
693
+ return resolveRuntimeCodexCommand(reviewArgs, context);
649
694
  }
650
695
  async function buildScopeNotes(options) {
651
696
  const lines = [];
@@ -1954,6 +1999,7 @@ Standalone review wrapper for Codex review with manifest-backed context.
1954
1999
  Options:
1955
2000
  --manifest <path> Explicit run manifest path.
1956
2001
  --task <task-id> Task id used to resolve latest manifest.
2002
+ --runtime-mode <cli|appserver> Runtime mode for the underlying Codex review call.
1957
2003
  --runs-dir <path> Override .runs root for manifest discovery.
1958
2004
  --uncommitted Review uncommitted diff scope.
1959
2005
  --base <ref> Review diff from base ref.
package/docs/README.md CHANGED
@@ -216,7 +216,7 @@ Note: the commands below assume a source checkout; `scripts/` helpers are not in
216
216
  | `node scripts/delegation-guard.mjs` | Enforces subagent delegation evidence before review (repo-only). |
217
217
  | `node scripts/spec-guard.mjs --dry-run` | Validates spec freshness; required before review (repo-only). |
218
218
  | `node scripts/diff-budget.mjs` | Guards against oversized diffs before review (repo-only; defaults: 25 files / 800 lines; supports explicit overrides). |
219
- | `npm run pack:smoke` | Downstream simulation gate for npm consumers (tarball install in temp mock repo, `review` wrapper artifacts, delegate-server JSONL, and `skills install --only long-poll-wait`). Core-lane runs it automatically when downstream-facing paths change. |
219
+ | `npm run pack:smoke` | Downstream simulation gate for npm consumers (tarball install in temp mock repo, `review` wrapper artifacts, delegate-server JSONL, and `skills install --only long-poll-wait`). Core lane runs it automatically when downstream-facing paths change, and `.github/workflows/pack-smoke-backstop.yml` runs a weekly `main` backstop. |
220
220
  | `codex-orchestrator review` | Runs the standalone review wrapper with task-scoped manifest evidence; delegation MCP is enabled by default (explicit disable available via `CODEX_REVIEW_DISABLE_DELEGATION_MCP=1` / `--disable-delegation-mcp`), runtime guards are opt-in via `CODEX_REVIEW_*` env vars, and patience-first checkpoints log by default (`CODEX_REVIEW_MONITOR_INTERVAL_SECONDS` tunes/disables). Large uncommitted scopes get an automatic prompt advisory (`CODEX_REVIEW_LARGE_SCOPE_FILE_THRESHOLD` / `CODEX_REVIEW_LARGE_SCOPE_LINE_THRESHOLD`). Optional auto failure issue logging via `CODEX_REVIEW_AUTO_ISSUE_LOG=1` or `--auto-issue-log`. |
221
221
  | `npm run review` | Runs `codex review` with task-scoped manifest evidence; delegation MCP is enabled by default (explicit disable available via `CODEX_REVIEW_DISABLE_DELEGATION_MCP=1` / `--disable-delegation-mcp`), runtime guards are opt-in via `CODEX_REVIEW_*` env vars, and patience-first checkpoints log by default (`CODEX_REVIEW_MONITOR_INTERVAL_SECONDS` tunes/disables). Large uncommitted scopes get an automatic prompt advisory (`CODEX_REVIEW_LARGE_SCOPE_FILE_THRESHOLD` / `CODEX_REVIEW_LARGE_SCOPE_LINE_THRESHOLD`). Optional auto failure issue logging via `CODEX_REVIEW_AUTO_ISSUE_LOG=1` or `--auto-issue-log`. |
222
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,6 +51,7 @@
51
51
  "docs:freshness": "node scripts/docs-freshness.mjs --check",
52
52
  "docs:sync": "node --loader ts-node/esm scripts/docs-hygiene.ts --sync",
53
53
  "ci:cloud-canary": "node scripts/cloud-canary-ci.mjs",
54
+ "canary:runtime": "node scripts/runtime-mode-canary.mjs",
54
55
  "prelint": "node scripts/build-patterns-if-needed.mjs",
55
56
  "lint": "eslint orchestrator/src orchestrator/tests packages/orchestrator/src packages/orchestrator/tests packages/shared adapters evaluation/harness evaluation/tests --ext .ts,.tsx",
56
57
  "pack:audit": "node scripts/pack-audit.mjs",