@kbediako/codex-orchestrator 0.1.2 → 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.
- package/README.md +9 -7
- package/dist/bin/codex-orchestrator.js +214 -121
- package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
- package/dist/orchestrator/src/cli/exec/context.js +5 -2
- package/dist/orchestrator/src/cli/exec/learning.js +5 -3
- package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
- package/dist/orchestrator/src/cli/exec/summary.js +1 -1
- package/dist/orchestrator/src/cli/orchestrator.js +16 -7
- package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
- package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
- package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
- package/dist/orchestrator/src/cli/rlm/types.js +1 -0
- package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
- package/dist/orchestrator/src/cli/run/environment.js +4 -11
- package/dist/orchestrator/src/cli/run/manifest.js +7 -1
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
- package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
- package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
- package/dist/orchestrator/src/cli/utils/strings.js +8 -6
- package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
- package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
- package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
- package/dist/packages/orchestrator/src/index.js +1 -0
- package/dist/packages/shared/design-artifacts/writer.js +4 -14
- package/dist/packages/shared/streams/stdio.js +2 -112
- package/dist/packages/shared/utils/strings.js +17 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
- package/dist/scripts/design/pipeline/context.js +5 -5
- package/dist/scripts/design/pipeline/extract.js +9 -6
- package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
- package/dist/scripts/design/pipeline/permit.js +59 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
- package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
- package/dist/scripts/design/pipeline/visual-regression.js +2 -11
- package/dist/scripts/lib/cli-args.js +53 -0
- package/dist/scripts/lib/docs-helpers.js +111 -0
- package/dist/scripts/lib/npm-pack.js +20 -0
- package/dist/scripts/lib/run-manifests.js +160 -0
- package/package.json +5 -2
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
- package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
- package/dist/orchestrator/src/control-plane/index.js +0 -3
- package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
- package/dist/orchestrator/src/scheduler/index.js +0 -1
|
@@ -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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import { loadUserConfig } from '../config/userConfig.js';
|
|
2
|
+
import { loadPackageConfig, loadUserConfig } from '../config/userConfig.js';
|
|
3
3
|
import { resolvePipeline } from '../pipelines/index.js';
|
|
4
4
|
import { loadDesignConfig, shouldActivateDesignPipeline, designPipelineId } from '../../../../packages/shared/config/index.js';
|
|
5
5
|
import { logger } from '../../logger.js';
|
|
6
|
+
const DEVTOOLS_PIPELINE_ALIASES = new Map([
|
|
7
|
+
['implementation-gate-devtools', 'implementation-gate'],
|
|
8
|
+
['frontend-testing-devtools', 'frontend-testing']
|
|
9
|
+
]);
|
|
6
10
|
export class PipelineResolver {
|
|
7
11
|
async loadDesignConfig(rootDir) {
|
|
8
12
|
const designConfig = await loadDesignConfig({ rootDir });
|
|
@@ -19,9 +23,15 @@ export class PipelineResolver {
|
|
|
19
23
|
logger.info(`PipelineResolver.resolve loaded design config from ${designConfig.path}`);
|
|
20
24
|
const userConfig = await loadUserConfig(env);
|
|
21
25
|
logger.info(`PipelineResolver.resolve loaded user config`);
|
|
22
|
-
const
|
|
26
|
+
const pipelineCandidate = options.pipelineId ??
|
|
23
27
|
(shouldActivateDesignPipeline(designConfig) ? designPipelineId(designConfig) : undefined);
|
|
28
|
+
const resolvedAlias = this.resolvePipelineAlias(pipelineCandidate);
|
|
29
|
+
const requestedPipelineId = resolvedAlias.pipelineId;
|
|
24
30
|
const envOverrides = this.resolveDesignEnvOverrides(designConfig, requestedPipelineId);
|
|
31
|
+
if (resolvedAlias.devtoolsRequested) {
|
|
32
|
+
envOverrides.CODEX_REVIEW_DEVTOOLS = '1';
|
|
33
|
+
logger.warn(`[pipeline] ${resolvedAlias.aliasId} is deprecated; use ${requestedPipelineId} with CODEX_REVIEW_DEVTOOLS=1.`);
|
|
34
|
+
}
|
|
25
35
|
try {
|
|
26
36
|
const { pipeline, source } = resolvePipeline(env, {
|
|
27
37
|
pipelineId: requestedPipelineId,
|
|
@@ -31,6 +41,17 @@ export class PipelineResolver {
|
|
|
31
41
|
return { pipeline, userConfig, designConfig, source, envOverrides };
|
|
32
42
|
}
|
|
33
43
|
catch (error) {
|
|
44
|
+
if (requestedPipelineId === 'rlm' && userConfig?.source === 'repo') {
|
|
45
|
+
const packageConfig = await loadPackageConfig(env);
|
|
46
|
+
if (packageConfig) {
|
|
47
|
+
const { pipeline, source } = resolvePipeline(env, {
|
|
48
|
+
pipelineId: requestedPipelineId,
|
|
49
|
+
config: packageConfig
|
|
50
|
+
});
|
|
51
|
+
logger.info(`PipelineResolver.resolve selected package pipeline ${pipeline.id}`);
|
|
52
|
+
return { pipeline, userConfig: packageConfig, designConfig, source, envOverrides };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
logger.error(`PipelineResolver.resolve failed for ${requestedPipelineId ?? '<default>'}: ${error.message}`);
|
|
35
56
|
throw error;
|
|
36
57
|
}
|
|
@@ -44,4 +65,14 @@ export class PipelineResolver {
|
|
|
44
65
|
}
|
|
45
66
|
return envOverrides;
|
|
46
67
|
}
|
|
68
|
+
resolvePipelineAlias(pipelineId) {
|
|
69
|
+
if (!pipelineId) {
|
|
70
|
+
return { pipelineId, devtoolsRequested: false };
|
|
71
|
+
}
|
|
72
|
+
const target = DEVTOOLS_PIPELINE_ALIASES.get(pipelineId);
|
|
73
|
+
if (!target) {
|
|
74
|
+
return { pipelineId, devtoolsRequested: false };
|
|
75
|
+
}
|
|
76
|
+
return { pipelineId: target, devtoolsRequested: true, aliasId: pipelineId };
|
|
77
|
+
}
|
|
47
78
|
}
|
|
@@ -62,11 +62,17 @@ export async function prepareRun(options) {
|
|
|
62
62
|
planPreview
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
export function resolvePipelineForResume(env, manifest, config) {
|
|
65
|
+
export function resolvePipelineForResume(env, manifest, config, fallbackConfig = null) {
|
|
66
66
|
const existing = findPipeline(config ?? null, manifest.pipeline_id);
|
|
67
67
|
if (existing) {
|
|
68
68
|
return existing;
|
|
69
69
|
}
|
|
70
|
+
if (manifest.pipeline_id === 'rlm' && fallbackConfig) {
|
|
71
|
+
const fallback = findPipeline(fallbackConfig, manifest.pipeline_id);
|
|
72
|
+
if (fallback) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
70
76
|
const { pipeline } = resolvePipeline(env, { pipelineId: manifest.pipeline_id, config });
|
|
71
77
|
return pipeline;
|
|
72
78
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sanitizeTaskId } from '../run/environment.js';
|
|
2
2
|
import { persistManifest } from '../run/manifestPersister.js';
|
|
3
|
-
import { buildSchedulerRunSummary, createSchedulerPlan, finalizeSchedulerPlan, serializeSchedulerPlan } from '../../scheduler/
|
|
3
|
+
import { buildSchedulerRunSummary, createSchedulerPlan, finalizeSchedulerPlan, serializeSchedulerPlan } from '../../scheduler/plan.js';
|
|
4
4
|
import { isoTimestamp } from '../utils/time.js';
|
|
5
5
|
export class SchedulerService {
|
|
6
6
|
now;
|
|
@@ -3,7 +3,9 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { logger } from '../../logger.js';
|
|
6
|
-
|
|
6
|
+
import { resolveEnvironmentPaths } from '../../../../scripts/lib/run-manifests.js';
|
|
7
|
+
const { repoRoot } = resolveEnvironmentPaths();
|
|
8
|
+
const specGuardPath = join(repoRoot, 'scripts', 'spec-guard.mjs');
|
|
7
9
|
if (!existsSync(specGuardPath)) {
|
|
8
10
|
logger.warn(`[spec-guard] skipped: ${specGuardPath} not found`);
|
|
9
11
|
process.exit(0);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { slugify as sharedSlugify } from '../../../../packages/shared/utils/strings.js';
|
|
1
2
|
export function slugify(value, fallback = 'command') {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
return sharedSlugify(value, {
|
|
4
|
+
fallback,
|
|
5
|
+
maxLength: 80,
|
|
6
|
+
lowercase: false,
|
|
7
|
+
pattern: /[^a-zA-Z0-9]+/g,
|
|
8
|
+
collapseDashes: true
|
|
9
|
+
});
|
|
8
10
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { readFile, mkdir, rm
|
|
2
|
+
import { readFile, mkdir, rm } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { listDirectories, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
|
|
4
5
|
import { acquireLockWithRetry } from './lockFile.js';
|
|
5
6
|
import { sanitizeTaskId } from './sanitizeTaskId.js';
|
|
6
|
-
import { writeAtomicFile } from '
|
|
7
|
+
import { writeAtomicFile } from '../utils/atomicWrite.js';
|
|
7
8
|
export class ExperienceStoreLockError extends Error {
|
|
8
9
|
taskId;
|
|
9
10
|
constructor(message, taskId) {
|
|
@@ -21,8 +22,9 @@ export class ExperienceStore {
|
|
|
21
22
|
lockRetry;
|
|
22
23
|
now;
|
|
23
24
|
constructor(options = {}) {
|
|
24
|
-
|
|
25
|
-
this.
|
|
25
|
+
const envPaths = resolveEnvironmentPaths();
|
|
26
|
+
this.outDir = options.outDir ?? envPaths.outRoot;
|
|
27
|
+
this.runsDir = options.runsDir ?? envPaths.runsRoot;
|
|
26
28
|
this.maxWords = Math.max(1, options.maxSummaryWords ?? DEFAULT_MAX_WORDS);
|
|
27
29
|
const defaults = {
|
|
28
30
|
maxAttempts: 5,
|
|
@@ -183,18 +185,6 @@ export class ExperienceStore {
|
|
|
183
185
|
return `exp-${Date.now().toString(36)}-${suffix}`;
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
|
-
async function listDirectories(path) {
|
|
187
|
-
try {
|
|
188
|
-
const entries = await readdir(path, { withFileTypes: true });
|
|
189
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
if (error.code === 'ENOENT') {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
throw error;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
188
|
function truncateSummary(value, maxWords) {
|
|
199
189
|
const tokens = value.trim().split(/\s+/u).filter(Boolean);
|
|
200
190
|
if (tokens.length <= maxWords) {
|
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { acquireLockWithRetry } from './lockFile.js';
|
|
4
4
|
import { sanitizeTaskId } from './sanitizeTaskId.js';
|
|
5
|
-
import { writeAtomicFile } from '
|
|
5
|
+
import { writeAtomicFile } from '../utils/atomicWrite.js';
|
|
6
6
|
export class TaskStateStoreLockError extends Error {
|
|
7
7
|
taskId;
|
|
8
8
|
constructor(message, taskId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const WINDOWS_FORBIDDEN_CHARACTERS = new Set(['<', '>', ':', '"', '|', '?', '*']);
|
|
2
2
|
export function sanitizeIdentifier(kind, value) {
|
|
3
3
|
const label = kind === 'task' ? 'task' : 'run';
|
|
4
4
|
if (!value) {
|