@kbediako/codex-orchestrator 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +11 -8
  2. package/dist/bin/codex-orchestrator.js +245 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/devtoolsSetup.js +66 -0
  5. package/dist/orchestrator/src/cli/doctor.js +46 -21
  6. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  7. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  8. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  9. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  10. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  11. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  12. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  13. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  14. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  15. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  16. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  17. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  18. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  19. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  20. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  21. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  22. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  23. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  24. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  25. package/dist/orchestrator/src/cli/utils/devtools.js +178 -0
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  27. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  28. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  29. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  30. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  31. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  32. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  33. package/dist/packages/orchestrator/src/index.js +1 -0
  34. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  35. package/dist/packages/shared/streams/stdio.js +2 -112
  36. package/dist/packages/shared/utils/strings.js +17 -0
  37. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  38. package/dist/scripts/design/pipeline/context.js +5 -5
  39. package/dist/scripts/design/pipeline/extract.js +9 -6
  40. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  41. package/dist/scripts/design/pipeline/permit.js +59 -0
  42. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  43. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  44. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  45. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  46. package/dist/scripts/lib/cli-args.js +53 -0
  47. package/dist/scripts/lib/docs-helpers.js +111 -0
  48. package/dist/scripts/lib/npm-pack.js +20 -0
  49. package/dist/scripts/lib/run-manifests.js +160 -0
  50. package/package.json +17 -6
  51. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  52. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  53. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  54. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  55. package/dist/orchestrator/src/control-plane/index.js +0 -3
  56. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  57. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  58. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -0,0 +1,159 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const NODE_PACKAGE_MANAGER_MAP = {
4
+ pnpm: 'pnpm test',
5
+ yarn: 'yarn test',
6
+ npm: 'npm test',
7
+ bun: 'bun test'
8
+ };
9
+ const NODE_LOCKFILES = [
10
+ { file: 'pnpm-lock.yaml', command: 'pnpm test' },
11
+ { file: 'yarn.lock', command: 'yarn test' },
12
+ { file: 'package-lock.json', command: 'npm test' },
13
+ { file: 'bun.lockb', command: 'bun test' }
14
+ ];
15
+ async function fileExists(path) {
16
+ try {
17
+ await access(path);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function ecosystemReason(ecosystem, reason) {
25
+ return `${ecosystem}: ${reason}`;
26
+ }
27
+ async function detectNodeCandidates(repoRoot) {
28
+ const candidates = [];
29
+ const packageJsonPath = join(repoRoot, 'package.json');
30
+ if (await fileExists(packageJsonPath)) {
31
+ try {
32
+ const raw = await readFile(packageJsonPath, 'utf8');
33
+ const parsed = JSON.parse(raw);
34
+ const packageManager = parsed.packageManager?.trim();
35
+ if (packageManager) {
36
+ const tool = packageManager.split('@')[0]?.trim();
37
+ const command = tool ? NODE_PACKAGE_MANAGER_MAP[tool] : undefined;
38
+ if (command) {
39
+ candidates.push({
40
+ command,
41
+ reason: ecosystemReason('node', `packageManager=${packageManager}`),
42
+ ecosystem: 'node'
43
+ });
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // Ignore invalid package.json for auto-detection.
49
+ }
50
+ }
51
+ for (const lockfile of NODE_LOCKFILES) {
52
+ const lockPath = join(repoRoot, lockfile.file);
53
+ if (await fileExists(lockPath)) {
54
+ candidates.push({
55
+ command: lockfile.command,
56
+ reason: ecosystemReason('node', lockfile.file),
57
+ ecosystem: 'node'
58
+ });
59
+ }
60
+ }
61
+ return candidates;
62
+ }
63
+ async function detectPythonCandidates(repoRoot) {
64
+ const candidates = [];
65
+ const pyprojectPath = join(repoRoot, 'pyproject.toml');
66
+ if (await fileExists(pyprojectPath)) {
67
+ candidates.push({
68
+ command: 'python -m pytest',
69
+ reason: ecosystemReason('python', 'pyproject.toml'),
70
+ ecosystem: 'python'
71
+ });
72
+ }
73
+ const pytestIni = join(repoRoot, 'pytest.ini');
74
+ if (await fileExists(pytestIni)) {
75
+ candidates.push({
76
+ command: 'pytest',
77
+ reason: ecosystemReason('python', 'pytest.ini'),
78
+ ecosystem: 'python'
79
+ });
80
+ }
81
+ const requirements = join(repoRoot, 'requirements.txt');
82
+ if (await fileExists(requirements)) {
83
+ candidates.push({
84
+ command: 'pytest',
85
+ reason: ecosystemReason('python', 'requirements.txt'),
86
+ ecosystem: 'python'
87
+ });
88
+ }
89
+ return candidates;
90
+ }
91
+ async function detectGoCandidates(repoRoot) {
92
+ const goModPath = join(repoRoot, 'go.mod');
93
+ if (await fileExists(goModPath)) {
94
+ return [
95
+ {
96
+ command: 'go test ./...',
97
+ reason: ecosystemReason('go', 'go.mod'),
98
+ ecosystem: 'go'
99
+ }
100
+ ];
101
+ }
102
+ return [];
103
+ }
104
+ async function detectRustCandidates(repoRoot) {
105
+ const cargoPath = join(repoRoot, 'Cargo.toml');
106
+ if (await fileExists(cargoPath)) {
107
+ return [
108
+ {
109
+ command: 'cargo test',
110
+ reason: ecosystemReason('rust', 'Cargo.toml'),
111
+ ecosystem: 'rust'
112
+ }
113
+ ];
114
+ }
115
+ return [];
116
+ }
117
+ export async function detectValidatorCandidates(repoRoot) {
118
+ const [node, python, go, rust] = await Promise.all([
119
+ detectNodeCandidates(repoRoot),
120
+ detectPythonCandidates(repoRoot),
121
+ detectGoCandidates(repoRoot),
122
+ detectRustCandidates(repoRoot)
123
+ ]);
124
+ return [...node, ...python, ...go, ...rust];
125
+ }
126
+ export async function detectValidator(repoRoot) {
127
+ const rawCandidates = await detectValidatorCandidates(repoRoot);
128
+ if (rawCandidates.length === 0) {
129
+ return { status: 'missing', command: null, candidates: [] };
130
+ }
131
+ const grouped = new Map();
132
+ for (const candidate of rawCandidates) {
133
+ const entry = grouped.get(candidate.command);
134
+ if (entry) {
135
+ entry.reasons.push(candidate.reason);
136
+ }
137
+ else {
138
+ grouped.set(candidate.command, {
139
+ command: candidate.command,
140
+ reasons: [candidate.reason],
141
+ ecosystem: candidate.ecosystem
142
+ });
143
+ }
144
+ }
145
+ const candidates = Array.from(grouped.values()).map((entry) => ({
146
+ command: entry.command,
147
+ reason: entry.reasons.join('; '),
148
+ ecosystem: entry.ecosystem
149
+ }));
150
+ if (candidates.length === 1) {
151
+ return {
152
+ status: 'selected',
153
+ command: candidates[0]?.command ?? null,
154
+ reason: candidates[0]?.reason,
155
+ candidates
156
+ };
157
+ }
158
+ return { status: 'ambiguous', command: null, candidates };
159
+ }
@@ -0,0 +1,417 @@
1
+ import { exec } from 'node:child_process';
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import process from 'node:process';
6
+ import { promisify } from 'node:util';
7
+ import { createInterface } from 'node:readline/promises';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { logger } from '../logger.js';
10
+ import { resolveCodexCommand } from './utils/devtools.js';
11
+ import { detectValidator } from './rlm/validator.js';
12
+ import { buildRlmPrompt } from './rlm/prompt.js';
13
+ import { runRlmLoop } from './rlm/runner.js';
14
+ const execAsync = promisify(exec);
15
+ const DEFAULT_MAX_ITERATIONS = 88;
16
+ function parseArgs(argv) {
17
+ const parsed = {};
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const token = argv[i] ?? '';
20
+ if (!token.startsWith('--')) {
21
+ continue;
22
+ }
23
+ const [flag, inlineValue] = token.slice(2).split('=', 2);
24
+ const nextValue = inlineValue ?? argv[i + 1];
25
+ switch (flag) {
26
+ case 'goal':
27
+ if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
28
+ parsed.goal = nextValue;
29
+ i += 1;
30
+ }
31
+ else if (inlineValue) {
32
+ parsed.goal = inlineValue;
33
+ }
34
+ break;
35
+ case 'validator':
36
+ if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
37
+ parsed.validator = nextValue;
38
+ i += 1;
39
+ }
40
+ else if (inlineValue) {
41
+ parsed.validator = inlineValue;
42
+ }
43
+ break;
44
+ case 'max-iterations':
45
+ if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
46
+ parsed.maxIterations = nextValue;
47
+ i += 1;
48
+ }
49
+ else if (inlineValue) {
50
+ parsed.maxIterations = inlineValue;
51
+ }
52
+ break;
53
+ case 'max-minutes':
54
+ if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
55
+ parsed.maxMinutes = nextValue;
56
+ i += 1;
57
+ }
58
+ else if (inlineValue) {
59
+ parsed.maxMinutes = inlineValue;
60
+ }
61
+ break;
62
+ case 'roles':
63
+ if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
64
+ parsed.roles = nextValue;
65
+ i += 1;
66
+ }
67
+ else if (inlineValue) {
68
+ parsed.roles = inlineValue;
69
+ }
70
+ break;
71
+ default:
72
+ break;
73
+ }
74
+ }
75
+ return parsed;
76
+ }
77
+ function envFlagEnabled(value) {
78
+ if (!value) {
79
+ return false;
80
+ }
81
+ const normalized = value.trim().toLowerCase();
82
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
83
+ }
84
+ function shouldForceNonInteractive(env) {
85
+ const stdinIsTTY = process.stdin?.isTTY === true;
86
+ return (!stdinIsTTY ||
87
+ envFlagEnabled(env.CI) ||
88
+ envFlagEnabled(env.CODEX_REVIEW_NON_INTERACTIVE) ||
89
+ envFlagEnabled(env.CODEX_NON_INTERACTIVE) ||
90
+ envFlagEnabled(env.CODEX_NONINTERACTIVE) ||
91
+ envFlagEnabled(env.CODEX_NO_INTERACTIVE));
92
+ }
93
+ function resolveRepoRoot(env) {
94
+ return env.CODEX_ORCHESTRATOR_ROOT?.trim() || process.cwd();
95
+ }
96
+ function resolveRunIds(env) {
97
+ const taskId = env.CODEX_ORCHESTRATOR_TASK_ID || env.MCP_RUNNER_TASK_ID || 'rlm-adhoc';
98
+ const runId = env.CODEX_ORCHESTRATOR_RUN_ID || 'rlm-adhoc';
99
+ return { taskId, runId };
100
+ }
101
+ function resolveRunsRoot(env, repoRoot) {
102
+ return env.CODEX_ORCHESTRATOR_RUNS_DIR?.trim() || join(repoRoot, '.runs');
103
+ }
104
+ function resolveRunDir(env, repoRoot) {
105
+ const explicit = env.CODEX_ORCHESTRATOR_RUN_DIR?.trim();
106
+ if (explicit) {
107
+ return explicit;
108
+ }
109
+ const { taskId, runId } = resolveRunIds(env);
110
+ const runsRoot = resolveRunsRoot(env, repoRoot);
111
+ return join(runsRoot, taskId, 'cli', runId);
112
+ }
113
+ function parsePositiveInt(value, fallback) {
114
+ if (!value) {
115
+ return fallback;
116
+ }
117
+ const parsed = Number.parseInt(value, 10);
118
+ if (!Number.isFinite(parsed) || parsed < 0) {
119
+ return null;
120
+ }
121
+ return parsed;
122
+ }
123
+ function parseRoles(value, fallback) {
124
+ if (!value) {
125
+ return fallback;
126
+ }
127
+ const normalized = value.trim().toLowerCase();
128
+ if (normalized === 'single' || normalized === 'triad') {
129
+ return normalized;
130
+ }
131
+ return null;
132
+ }
133
+ function normalizeValidator(value) {
134
+ if (!value) {
135
+ return null;
136
+ }
137
+ const trimmed = value.trim();
138
+ if (!trimmed) {
139
+ return null;
140
+ }
141
+ return trimmed;
142
+ }
143
+ async function promptForValidator(candidates) {
144
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
145
+ try {
146
+ console.log('Validator auto-detect found multiple candidates:');
147
+ candidates.forEach((candidate, index) => {
148
+ console.log(` ${index + 1}) ${candidate.command} (${candidate.reason})`);
149
+ });
150
+ console.log(' n) none');
151
+ const answer = (await rl.question('Select validator [1-n or n for none]: ')).trim().toLowerCase();
152
+ if (!answer || answer === 'n' || answer === 'none') {
153
+ return null;
154
+ }
155
+ const selectedIndex = Number.parseInt(answer, 10);
156
+ if (!Number.isFinite(selectedIndex) || selectedIndex < 1 || selectedIndex > candidates.length) {
157
+ throw new Error('Invalid selection');
158
+ }
159
+ return candidates[selectedIndex - 1]?.command ?? null;
160
+ }
161
+ finally {
162
+ rl.close();
163
+ }
164
+ }
165
+ async function promptForValidatorCommand() {
166
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
167
+ try {
168
+ const answer = (await rl.question('Enter validator command (or "none"): ')).trim();
169
+ if (!answer || answer.toLowerCase() === 'none') {
170
+ return null;
171
+ }
172
+ return answer;
173
+ }
174
+ finally {
175
+ rl.close();
176
+ }
177
+ }
178
+ async function writeTerminalState(runDir, state) {
179
+ const rlmDir = join(runDir, 'rlm');
180
+ await mkdir(rlmDir, { recursive: true });
181
+ await writeFile(join(rlmDir, 'state.json'), JSON.stringify(state, null, 2), 'utf8');
182
+ }
183
+ async function runCodexAgent(input, env, repoRoot, nonInteractive, subagentsEnabled) {
184
+ const prompt = buildRlmPrompt(input);
185
+ const { command, args } = resolveCodexCommand(['exec', prompt], env);
186
+ const childEnv = { ...process.env, ...env };
187
+ if (nonInteractive) {
188
+ childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
189
+ childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
190
+ childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
191
+ }
192
+ if (subagentsEnabled) {
193
+ childEnv.CODEX_SUBAGENTS = childEnv.CODEX_SUBAGENTS ?? '1';
194
+ }
195
+ const child = spawn(command, args, { cwd: repoRoot, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] });
196
+ let stdout = '';
197
+ let stderr = '';
198
+ child.stdout?.on('data', (chunk) => {
199
+ stdout += chunk.toString();
200
+ process.stdout.write(chunk);
201
+ });
202
+ child.stderr?.on('data', (chunk) => {
203
+ stderr += chunk.toString();
204
+ process.stderr.write(chunk);
205
+ });
206
+ await new Promise((resolvePromise, reject) => {
207
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
208
+ child.once('exit', (code) => {
209
+ if (code === 0) {
210
+ resolvePromise();
211
+ }
212
+ else {
213
+ reject(new Error(`codex exec exited with code ${code ?? 'unknown'}`));
214
+ }
215
+ });
216
+ });
217
+ const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
218
+ return { output };
219
+ }
220
+ function normalizeExitCode(code) {
221
+ if (typeof code === 'number' && Number.isInteger(code)) {
222
+ return code;
223
+ }
224
+ if (typeof code === 'string') {
225
+ const parsed = Number.parseInt(code, 10);
226
+ if (Number.isInteger(parsed)) {
227
+ return parsed;
228
+ }
229
+ }
230
+ return 1;
231
+ }
232
+ async function runValidatorCommand(command, repoRoot) {
233
+ try {
234
+ const { stdout, stderr } = await execAsync(command, {
235
+ cwd: repoRoot,
236
+ env: { ...process.env },
237
+ maxBuffer: 10 * 1024 * 1024
238
+ });
239
+ return { exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' };
240
+ }
241
+ catch (error) {
242
+ const execError = error;
243
+ const exitCode = normalizeExitCode(execError.code);
244
+ const spawnError = execError.code === 'ENOENT' || exitCode === 127;
245
+ return {
246
+ exitCode,
247
+ stdout: execError.stdout ?? '',
248
+ stderr: execError.stderr ?? execError.message ?? '',
249
+ spawnError
250
+ };
251
+ }
252
+ }
253
+ async function main() {
254
+ const env = process.env;
255
+ const repoRoot = resolveRepoRoot(env);
256
+ const runDir = resolveRunDir(env, repoRoot);
257
+ const parsedArgs = parseArgs(process.argv.slice(2));
258
+ const goal = (parsedArgs.goal ?? env.RLM_GOAL)?.trim();
259
+ const roles = parseRoles(parsedArgs.roles ?? env.RLM_ROLES, 'single');
260
+ const maxIterations = parsePositiveInt(parsedArgs.maxIterations ?? env.RLM_MAX_ITERATIONS, DEFAULT_MAX_ITERATIONS);
261
+ const maxMinutes = parsePositiveInt(parsedArgs.maxMinutes ?? env.RLM_MAX_MINUTES, 0);
262
+ if (!goal) {
263
+ const state = {
264
+ goal: '',
265
+ validator: null,
266
+ roles: roles ?? 'single',
267
+ maxIterations: maxIterations ?? DEFAULT_MAX_ITERATIONS,
268
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
269
+ iterations: [],
270
+ final: { status: 'invalid_config', exitCode: 5 }
271
+ };
272
+ await writeTerminalState(runDir, state);
273
+ console.error('RLM goal is required. Set RLM_GOAL or pass --goal.');
274
+ process.exitCode = 5;
275
+ return;
276
+ }
277
+ if (!roles) {
278
+ const state = {
279
+ goal,
280
+ validator: null,
281
+ roles: 'single',
282
+ maxIterations: maxIterations ?? DEFAULT_MAX_ITERATIONS,
283
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
284
+ iterations: [],
285
+ final: { status: 'invalid_config', exitCode: 5 }
286
+ };
287
+ await writeTerminalState(runDir, state);
288
+ console.error('Invalid RLM roles value. Use "single" or "triad".');
289
+ process.exitCode = 5;
290
+ return;
291
+ }
292
+ if (maxIterations === null) {
293
+ const state = {
294
+ goal,
295
+ validator: null,
296
+ roles,
297
+ maxIterations: DEFAULT_MAX_ITERATIONS,
298
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
299
+ iterations: [],
300
+ final: { status: 'invalid_config', exitCode: 5 }
301
+ };
302
+ await writeTerminalState(runDir, state);
303
+ console.error('Invalid max iterations value.');
304
+ process.exitCode = 5;
305
+ return;
306
+ }
307
+ if (maxMinutes === null) {
308
+ const state = {
309
+ goal,
310
+ validator: null,
311
+ roles,
312
+ maxIterations,
313
+ maxMinutes: null,
314
+ iterations: [],
315
+ final: { status: 'invalid_config', exitCode: 5 }
316
+ };
317
+ await writeTerminalState(runDir, state);
318
+ console.error('Invalid max minutes value.');
319
+ process.exitCode = 5;
320
+ return;
321
+ }
322
+ const validatorOverride = normalizeValidator(parsedArgs.validator ?? env.RLM_VALIDATOR);
323
+ const isInteractive = !shouldForceNonInteractive(env);
324
+ let validatorCommand = null;
325
+ if (validatorOverride && validatorOverride.toLowerCase() !== 'auto') {
326
+ if (validatorOverride.toLowerCase() === 'none') {
327
+ validatorCommand = null;
328
+ }
329
+ else {
330
+ validatorCommand = validatorOverride;
331
+ }
332
+ }
333
+ else {
334
+ const detection = await detectValidator(repoRoot);
335
+ if (detection.status === 'selected' && detection.command) {
336
+ validatorCommand = detection.command;
337
+ console.log(`Validator: ${detection.command} (${detection.reason ?? 'auto-detect'})`);
338
+ }
339
+ else if (detection.status === 'ambiguous') {
340
+ if (isInteractive) {
341
+ validatorCommand = await promptForValidator(detection.candidates);
342
+ }
343
+ else {
344
+ const candidates = detection.candidates
345
+ .map((candidate) => `- ${candidate.command} (${candidate.reason})`)
346
+ .join('\n');
347
+ const state = {
348
+ goal,
349
+ validator: null,
350
+ roles,
351
+ maxIterations,
352
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
353
+ iterations: [],
354
+ final: { status: 'no_validator', exitCode: 2 }
355
+ };
356
+ await writeTerminalState(runDir, state);
357
+ console.error('Validator auto-detect ambiguous. Provide --validator or --validator none.');
358
+ console.error(candidates);
359
+ process.exitCode = 2;
360
+ return;
361
+ }
362
+ }
363
+ else {
364
+ if (isInteractive) {
365
+ validatorCommand = await promptForValidatorCommand();
366
+ }
367
+ else {
368
+ const state = {
369
+ goal,
370
+ validator: null,
371
+ roles,
372
+ maxIterations,
373
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
374
+ iterations: [],
375
+ final: { status: 'no_validator', exitCode: 2 }
376
+ };
377
+ await writeTerminalState(runDir, state);
378
+ console.error('Validator auto-detect failed. Provide --validator or --validator none.');
379
+ process.exitCode = 2;
380
+ return;
381
+ }
382
+ }
383
+ }
384
+ if (validatorCommand === null) {
385
+ console.log('Validator: none');
386
+ }
387
+ else {
388
+ console.log(`Validator: ${validatorCommand}`);
389
+ }
390
+ const subagentsEnabled = envFlagEnabled(env.CODEX_SUBAGENTS) || envFlagEnabled(env.RLM_SUBAGENTS);
391
+ const nonInteractive = shouldForceNonInteractive(env);
392
+ const result = await runRlmLoop({
393
+ goal,
394
+ validatorCommand,
395
+ maxIterations,
396
+ maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
397
+ roles,
398
+ subagentsEnabled,
399
+ repoRoot,
400
+ runDir: join(runDir, 'rlm'),
401
+ runAgent: (input) => runCodexAgent(input, env, repoRoot, nonInteractive, subagentsEnabled),
402
+ runValidator: (command) => runValidatorCommand(command, repoRoot),
403
+ logger: (line) => logger.info(line)
404
+ });
405
+ const finalStatus = result.state.final?.status ?? 'unknown';
406
+ const iterationCount = result.state.iterations.length;
407
+ console.log(`RLM completed: status=${finalStatus} iterations=${iterationCount} exit=${result.exitCode}`);
408
+ process.exitCode = result.exitCode;
409
+ }
410
+ const entry = process.argv[1] ? resolve(process.argv[1]) : null;
411
+ const self = resolve(fileURLToPath(import.meta.url));
412
+ if (entry && entry === self) {
413
+ main().catch((error) => {
414
+ logger.error(error instanceof Error ? error.message : String(error));
415
+ process.exitCode = 10;
416
+ });
417
+ }
@@ -1,15 +1,5 @@
1
- import { join, resolve } from 'node:path';
2
1
  import { sanitizeTaskId } from '../../persistence/sanitizeTaskId.js';
3
- const DEFAULT_TASK_ID = '0101';
4
- export function resolveEnvironment() {
5
- const repoRoot = resolve(process.env.CODEX_ORCHESTRATOR_ROOT ?? process.cwd());
6
- const runsRoot = resolve(process.env.CODEX_ORCHESTRATOR_RUNS_DIR ?? join(repoRoot, '.runs'));
7
- const outRoot = resolve(process.env.CODEX_ORCHESTRATOR_OUT_DIR ?? join(repoRoot, 'out'));
8
- const rawTaskId = process.env.MCP_RUNNER_TASK_ID ?? DEFAULT_TASK_ID;
9
- const taskId = normalizeTaskId(rawTaskId);
10
- return { repoRoot, runsRoot, outRoot, taskId };
11
- }
12
- function normalizeTaskId(value) {
2
+ export function normalizeTaskId(value) {
13
3
  try {
14
4
  return sanitizeTaskId(value);
15
5
  }
@@ -21,4 +11,7 @@ function normalizeTaskId(value) {
21
11
  throw new Error(`Invalid MCP_RUNNER_TASK_ID: ${message}`);
22
12
  }
23
13
  }
14
+ export function normalizeEnvironmentPaths(paths) {
15
+ return { ...paths, taskId: normalizeTaskId(paths.taskId) };
16
+ }
24
17
  export { sanitizeTaskId };
@@ -239,7 +239,13 @@ function computeGuardrailStatus(manifest) {
239
239
  };
240
240
  }
241
241
  function selectGuardrailCommands(manifest) {
242
- return manifest.commands.filter((entry) => entry.command?.includes('spec-guard'));
242
+ return manifest.commands.filter((entry) => {
243
+ const id = entry.id?.toLowerCase() ?? '';
244
+ const title = entry.title?.toLowerCase() ?? '';
245
+ const command = entry.command?.toLowerCase() ?? '';
246
+ const haystack = `${id} ${title} ${command}`;
247
+ return haystack.includes('spec-guard') || haystack.includes('specguardrunner');
248
+ });
243
249
  }
244
250
  function formatGuardrailSummary(counts) {
245
251
  if (counts.total === 0) {
@@ -129,7 +129,7 @@ export async function runCommandStage(context, hooks = {}) {
129
129
  CODEX_ORCHESTRATOR_RUN_DIR: paths.runDir,
130
130
  CODEX_ORCHESTRATOR_RUNS_DIR: env.runsRoot,
131
131
  CODEX_ORCHESTRATOR_OUT_DIR: env.outRoot,
132
- CODEX_ORCHESTRATOR_REPO_ROOT: env.repoRoot,
132
+ CODEX_ORCHESTRATOR_ROOT: env.repoRoot,
133
133
  CODEX_ORCHESTRATOR_PACKAGE_ROOT: PACKAGE_ROOT
134
134
  };
135
135
  const execEnv = { ...baseEnv, ...stage.env };
@@ -1,5 +1,7 @@
1
1
  import process from 'node:process';
2
- import { buildRunRequestV2, ControlPlaneDriftReporter, ControlPlaneValidationError, ControlPlaneValidator } from '../../control-plane/index.js';
2
+ import { ControlPlaneDriftReporter } from '../../control-plane/drift-reporter.js';
3
+ import { buildRunRequestV2 } from '../../control-plane/request-builder.js';
4
+ import { ControlPlaneValidationError, ControlPlaneValidator } from '../../control-plane/validator.js';
3
5
  import { relativeToRepo } from '../run/runPaths.js';
4
6
  import { appendSummary } from '../run/manifest.js';
5
7
  import { persistManifest } from '../run/manifestPersister.js';
@@ -1,6 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { ExecSessionManager, UnifiedExecRunner, ToolOrchestrator } from '../../../../packages/orchestrator/src/index.js';
3
- import { RemoteExecHandleService } from '../../../../packages/orchestrator/src/exec/handle-service.js';
2
+ import { ExecSessionManager, UnifiedExecRunner, ToolOrchestrator, RemoteExecHandleService } from '../../../../packages/orchestrator/src/index.js';
4
3
  import { PrivacyGuard } from '../../privacy/guard.js';
5
4
  import { resolveEnforcementMode } from '../utils/enforcementMode.js';
6
5
  class CliExecSessionHandle {