@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.
Files changed (55) hide show
  1. package/README.md +9 -7
  2. package/dist/bin/codex-orchestrator.js +214 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  5. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  6. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  7. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  8. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  9. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  10. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  11. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  12. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  13. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  14. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  15. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  16. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  17. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  18. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  19. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  20. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  21. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  22. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  23. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  24. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  25. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  26. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  27. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  28. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  29. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  30. package/dist/packages/orchestrator/src/index.js +1 -0
  31. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  32. package/dist/packages/shared/streams/stdio.js +2 -112
  33. package/dist/packages/shared/utils/strings.js +17 -0
  34. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  35. package/dist/scripts/design/pipeline/context.js +5 -5
  36. package/dist/scripts/design/pipeline/extract.js +9 -6
  37. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  38. package/dist/scripts/design/pipeline/permit.js +59 -0
  39. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  40. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  41. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  42. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  43. package/dist/scripts/lib/cli-args.js +53 -0
  44. package/dist/scripts/lib/docs-helpers.js +111 -0
  45. package/dist/scripts/lib/npm-pack.js +20 -0
  46. package/dist/scripts/lib/run-manifests.js +160 -0
  47. package/package.json +5 -2
  48. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  49. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  50. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  51. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  52. package/dist/orchestrator/src/control-plane/index.js +0 -3
  53. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  54. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  55. 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
- 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 {
@@ -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 requestedPipelineId = options.pipelineId ??
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/index.js';
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
- const specGuardPath = join(process.cwd(), 'scripts', 'spec-guard.mjs');
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
- const cleaned = value.trim().replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+/g, '-');
3
- const normalized = cleaned.replace(/^-|-$/g, '');
4
- if (!normalized) {
5
- return fallback;
6
- }
7
- return normalized.slice(0, 80);
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, readdir } from 'node:fs/promises';
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 './writeAtomicFile.js';
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
- this.outDir = options.outDir ?? join(process.cwd(), 'out');
25
- this.runsDir = options.runsDir ?? join(process.cwd(), '.runs');
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 './writeAtomicFile.js';
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
- import { WINDOWS_FORBIDDEN_CHARACTERS } from './identifierGuards.js';
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) {