@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 +1 -1
- package/dist/bin/codex-orchestrator.js +56 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +17 -1
- package/dist/orchestrator/src/cli/frontendTestingRunner.js +24 -6
- package/dist/orchestrator/src/cli/orchestrator.js +119 -16
- package/dist/orchestrator/src/cli/rlmRunner.js +27 -3
- package/dist/orchestrator/src/cli/run/manifest.js +19 -0
- package/dist/orchestrator/src/cli/runtime/codexCommand.js +39 -0
- package/dist/orchestrator/src/cli/runtime/index.js +3 -0
- package/dist/orchestrator/src/cli/runtime/mode.js +53 -0
- package/dist/orchestrator/src/cli/runtime/provider.js +205 -0
- package/dist/orchestrator/src/cli/runtime/types.js +1 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +19 -5
- package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +12 -0
- package/dist/scripts/run-review.js +55 -9
- package/docs/README.md +1 -1
- package/package.json +2 -1
- package/schemas/manifest.json +20 -0
|
@@ -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 {
|
|
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
|
|
336
|
-
const
|
|
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
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
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:
|
|
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 {
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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",
|