@kbediako/codex-orchestrator 0.1.2 → 0.1.4

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 (73) hide show
  1. package/README.md +15 -8
  2. package/dist/bin/codex-orchestrator.js +252 -121
  3. package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
  4. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  5. package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
  6. package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
  7. package/dist/orchestrator/src/cli/control/controlState.js +46 -0
  8. package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
  9. package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
  10. package/dist/orchestrator/src/cli/control/questions.js +106 -0
  11. package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
  12. package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
  13. package/dist/orchestrator/src/cli/exec/context.js +9 -3
  14. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  15. package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
  16. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  17. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
  18. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
  19. package/dist/orchestrator/src/cli/orchestrator.js +233 -47
  20. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  21. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  22. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  23. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  24. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  25. package/dist/orchestrator/src/cli/rlmRunner.js +440 -0
  26. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  27. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  28. package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
  29. package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
  30. package/dist/orchestrator/src/cli/services/commandRunner.js +2 -2
  31. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  32. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  33. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  34. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  35. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  36. package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
  37. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  38. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  39. package/dist/orchestrator/src/persistence/ExperienceStore.js +115 -58
  40. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
  41. package/dist/orchestrator/src/persistence/TaskStateStore.js +3 -2
  42. package/dist/orchestrator/src/persistence/lockFile.js +26 -1
  43. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  44. package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
  45. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  46. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  47. package/dist/packages/orchestrator/src/index.js +1 -0
  48. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
  49. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  50. package/dist/packages/shared/streams/stdio.js +2 -112
  51. package/dist/packages/shared/utils/strings.js +17 -0
  52. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  53. package/dist/scripts/design/pipeline/context.js +5 -5
  54. package/dist/scripts/design/pipeline/extract.js +9 -6
  55. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  56. package/dist/scripts/design/pipeline/permit.js +59 -0
  57. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  58. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  59. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  60. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  61. package/dist/scripts/lib/cli-args.js +53 -0
  62. package/dist/scripts/lib/docs-helpers.js +111 -0
  63. package/dist/scripts/lib/npm-pack.js +20 -0
  64. package/dist/scripts/lib/run-manifests.js +160 -0
  65. package/package.json +7 -2
  66. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  67. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  68. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  69. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  70. package/dist/orchestrator/src/control-plane/index.js +0 -3
  71. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  72. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  73. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -5,7 +5,7 @@ import { ensureGuardrailStatus, appendSummary, upsertGuardrailSummary } from '..
5
5
  import { isoTimestamp } from '../utils/time.js';
6
6
  import { persistManifest } from '../run/manifestPersister.js';
7
7
  import { logger } from '../../logger.js';
8
- import { mergePendingMetricsEntries, updateMetricsAggregates, withMetricsLock } from './metricsAggregator.js';
8
+ import { ensureMetricsTrailingNewline, mergePendingMetricsEntries, updateMetricsAggregates, withMetricsLock } from './metricsAggregator.js';
9
9
  import { EnvUtils } from '../../../../packages/shared/config/index.js';
10
10
  const TERMINAL_STATES = new Set(['succeeded', 'failed', 'cancelled']);
11
11
  const METRICS_PENDING_DIRNAME = 'metrics.pending';
@@ -89,6 +89,7 @@ export async function appendMetricsEntry(env, paths, manifest, persister) {
89
89
  };
90
90
  await mkdir(metricsRoot, { recursive: true });
91
91
  const appendEntry = async () => {
92
+ await ensureMetricsTrailingNewline(metricsPath);
92
93
  await appendFile(metricsPath, `${JSON.stringify(entry)}\n`, 'utf8');
93
94
  };
94
95
  const appendPendingEntry = async () => {
@@ -124,11 +125,8 @@ export async function appendMetricsEntry(env, paths, manifest, persister) {
124
125
  await mergePendingMetricsEntries(env);
125
126
  await appendEntry();
126
127
  await finalizeManifest(true);
128
+ await mergePendingMetricsEntries(env);
127
129
  await updateMetricsAggregates(env);
128
- const mergedAfter = await mergePendingMetricsEntries(env);
129
- if (mergedAfter > 0) {
130
- await updateMetricsAggregates(env);
131
- }
132
130
  });
133
131
  if (!acquired) {
134
132
  const pendingPath = await appendPendingEntry();
@@ -2,8 +2,11 @@ import process from 'node:process';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { TaskManager } from '../manager.js';
5
+ import { RunManifestWriter } from '../persistence/RunManifestWriter.js';
6
+ import { TaskStateStore } from '../persistence/TaskStateStore.js';
5
7
  import { CommandPlanner, CommandBuilder, CommandTester, CommandReviewer } from './adapters/index.js';
6
- import { resolveEnvironment } from './run/environment.js';
8
+ import { resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
9
+ import { normalizeEnvironmentPaths } from './run/environment.js';
7
10
  import { bootstrapManifest, loadManifest, updateHeartbeat, finalizeStatus, appendSummary, ensureGuardrailStatus, resetForResume, recordResumeEvent } from './run/manifest.js';
8
11
  import { ManifestPersister, persistManifest } from './run/manifestPersister.js';
9
12
  import { generateRunId } from './utils/runId.js';
@@ -15,17 +18,45 @@ import { logger } from '../logger.js';
15
18
  import { getPrivacyGuard } from './services/execRuntime.js';
16
19
  import { PipelineResolver } from './services/pipelineResolver.js';
17
20
  import { ControlPlaneService } from './services/controlPlaneService.js';
21
+ import { ControlWatcher } from './control/controlWatcher.js';
18
22
  import { SchedulerService } from './services/schedulerService.js';
19
23
  import { applyHandlesToRunSummary, applyPrivacyToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
20
24
  import { prepareRun, resolvePipelineForResume, overrideTaskEnvironment } from './services/runPreparation.js';
21
- import { loadUserConfig } from './config/userConfig.js';
22
- import { RunEventPublisher, snapshotStages } from './events/runEvents.js';
25
+ import { loadPackageConfig, loadUserConfig } from './config/userConfig.js';
26
+ import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
27
+ import { ControlServer } from './control/controlServer.js';
28
+ import { RunEventEmitter, RunEventPublisher, snapshotStages } from './events/runEvents.js';
29
+ import { RunEventStream, attachRunEventAdapter } from './events/runEventStream.js';
23
30
  import { CLI_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from '../utils/executionMode.js';
31
+ const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
32
+ const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
33
+ function collectDelegationEnvOverrides(env = process.env) {
34
+ const layers = [];
35
+ for (const key of CONFIG_OVERRIDE_ENV_KEYS) {
36
+ const raw = env[key];
37
+ if (!raw) {
38
+ continue;
39
+ }
40
+ const values = splitDelegationConfigOverrides(raw);
41
+ for (const value of values) {
42
+ try {
43
+ const layer = parseDelegationConfigOverride(value, 'env');
44
+ if (layer) {
45
+ layers.push(layer);
46
+ }
47
+ }
48
+ catch (error) {
49
+ logger.warn(`Invalid delegation config override (env): ${error?.message ?? String(error)}`);
50
+ }
51
+ }
52
+ }
53
+ return layers;
54
+ }
24
55
  export class CodexOrchestrator {
25
56
  baseEnv;
26
57
  controlPlane = new ControlPlaneService();
27
58
  scheduler = new SchedulerService();
28
- constructor(baseEnv = resolveEnvironment()) {
59
+ constructor(baseEnv = resolveBaseEnvironment()) {
29
60
  this.baseEnv = baseEnv;
30
61
  }
31
62
  async start(options = {}) {
@@ -50,25 +81,90 @@ export class CodexOrchestrator {
50
81
  paths,
51
82
  persistIntervalMs: Math.max(1000, manifest.heartbeat_interval_seconds * 1000)
52
83
  });
53
- const runEvents = this.createRunEventPublisher({
54
- runId,
55
- pipeline: preparation.pipeline,
56
- manifest,
57
- paths,
58
- emitter: options.runEvents
59
- });
60
- return await this.performRunLifecycle({
61
- env: preparation.env,
62
- pipeline: preparation.pipeline,
63
- manifest,
64
- paths,
65
- planner: preparation.planner,
66
- taskContext: preparation.taskContext,
67
- runId,
68
- runEvents,
69
- persister,
70
- envOverrides: preparation.envOverrides
71
- });
84
+ const emitter = options.runEvents ?? new RunEventEmitter();
85
+ let eventStream = null;
86
+ let controlServer = null;
87
+ let detachStream = null;
88
+ let onEventEntry;
89
+ try {
90
+ const stream = await RunEventStream.create({
91
+ paths,
92
+ taskId: manifest.task_id,
93
+ runId,
94
+ pipelineId: preparation.pipeline.id,
95
+ pipelineTitle: preparation.pipeline.title
96
+ });
97
+ eventStream = stream;
98
+ const configFiles = await loadDelegationConfigFiles({ repoRoot: preparation.env.repoRoot });
99
+ const envOverrideLayers = collectDelegationEnvOverrides();
100
+ const layers = [configFiles.global, configFiles.repo, ...envOverrideLayers].filter(Boolean);
101
+ const effectiveConfig = computeEffectiveDelegationConfig({
102
+ repoRoot: preparation.env.repoRoot,
103
+ layers
104
+ });
105
+ controlServer = effectiveConfig.ui.controlEnabled
106
+ ? await ControlServer.start({
107
+ paths,
108
+ config: effectiveConfig,
109
+ eventStream: stream,
110
+ runId
111
+ })
112
+ : null;
113
+ onEventEntry = (entry) => {
114
+ controlServer?.broadcast(entry);
115
+ };
116
+ const onStreamError = (error, payload) => {
117
+ logger.warn(`Failed to append run event ${payload.event}: ${error.message}`);
118
+ };
119
+ detachStream = attachRunEventAdapter(emitter, stream, onEventEntry, onStreamError);
120
+ const runEvents = this.createRunEventPublisher({
121
+ runId,
122
+ pipeline: preparation.pipeline,
123
+ manifest,
124
+ paths,
125
+ emitter
126
+ });
127
+ return await this.performRunLifecycle({
128
+ env: preparation.env,
129
+ pipeline: preparation.pipeline,
130
+ manifest,
131
+ paths,
132
+ planner: preparation.planner,
133
+ taskContext: preparation.taskContext,
134
+ runId,
135
+ runEvents,
136
+ eventStream: stream,
137
+ onEventEntry,
138
+ persister,
139
+ envOverrides: preparation.envOverrides
140
+ });
141
+ }
142
+ finally {
143
+ if (detachStream) {
144
+ try {
145
+ detachStream();
146
+ }
147
+ catch (error) {
148
+ logger.warn(`Failed to detach run event stream: ${error?.message ?? String(error)}`);
149
+ }
150
+ }
151
+ if (controlServer) {
152
+ try {
153
+ await controlServer.close();
154
+ }
155
+ catch (error) {
156
+ logger.warn(`Failed to close control server: ${error?.message ?? String(error)}`);
157
+ }
158
+ }
159
+ if (eventStream) {
160
+ try {
161
+ await eventStream.close();
162
+ }
163
+ catch (error) {
164
+ logger.warn(`Failed to close run event stream: ${error?.message ?? String(error)}`);
165
+ }
166
+ }
167
+ }
72
168
  }
73
169
  async resume(options) {
74
170
  const env = this.baseEnv;
@@ -77,7 +173,10 @@ export class CodexOrchestrator {
77
173
  const resolver = new PipelineResolver();
78
174
  const designConfig = await resolver.loadDesignConfig(actualEnv.repoRoot);
79
175
  const userConfig = await loadUserConfig(actualEnv);
80
- const pipeline = resolvePipelineForResume(actualEnv, manifest, userConfig);
176
+ const fallbackConfig = manifest.pipeline_id === 'rlm' && userConfig?.source === 'repo'
177
+ ? await loadPackageConfig(actualEnv)
178
+ : null;
179
+ const pipeline = resolvePipelineForResume(actualEnv, manifest, userConfig, fallbackConfig);
81
180
  const envOverrides = resolver.resolveDesignEnvOverrides(designConfig, pipeline.id);
82
181
  await this.validateResumeToken(paths, manifest, options.resumeToken ?? null);
83
182
  recordResumeEvent(manifest, {
@@ -103,25 +202,90 @@ export class CodexOrchestrator {
103
202
  persistIntervalMs: Math.max(1000, manifest.heartbeat_interval_seconds * 1000)
104
203
  });
105
204
  await persister.schedule({ manifest: true, heartbeat: true, force: true });
106
- const runEvents = this.createRunEventPublisher({
107
- runId: manifest.run_id,
108
- pipeline,
109
- manifest,
110
- paths,
111
- emitter: options.runEvents
112
- });
113
- return await this.performRunLifecycle({
114
- env: preparation.env,
115
- pipeline,
116
- manifest,
117
- paths,
118
- planner: preparation.planner,
119
- taskContext: preparation.taskContext,
120
- runId: manifest.run_id,
121
- runEvents,
122
- persister,
123
- envOverrides: preparation.envOverrides
124
- });
205
+ const emitter = options.runEvents ?? new RunEventEmitter();
206
+ let eventStream = null;
207
+ let controlServer = null;
208
+ let detachStream = null;
209
+ let onEventEntry;
210
+ try {
211
+ const stream = await RunEventStream.create({
212
+ paths,
213
+ taskId: manifest.task_id,
214
+ runId: manifest.run_id,
215
+ pipelineId: pipeline.id,
216
+ pipelineTitle: pipeline.title
217
+ });
218
+ eventStream = stream;
219
+ const configFiles = await loadDelegationConfigFiles({ repoRoot: preparation.env.repoRoot });
220
+ const envOverrideLayers = collectDelegationEnvOverrides();
221
+ const layers = [configFiles.global, configFiles.repo, ...envOverrideLayers].filter(Boolean);
222
+ const effectiveConfig = computeEffectiveDelegationConfig({
223
+ repoRoot: preparation.env.repoRoot,
224
+ layers
225
+ });
226
+ controlServer = effectiveConfig.ui.controlEnabled
227
+ ? await ControlServer.start({
228
+ paths,
229
+ config: effectiveConfig,
230
+ eventStream: stream,
231
+ runId: manifest.run_id
232
+ })
233
+ : null;
234
+ onEventEntry = (entry) => {
235
+ controlServer?.broadcast(entry);
236
+ };
237
+ const onStreamError = (error, payload) => {
238
+ logger.warn(`Failed to append run event ${payload.event}: ${error.message}`);
239
+ };
240
+ detachStream = attachRunEventAdapter(emitter, stream, onEventEntry, onStreamError);
241
+ const runEvents = this.createRunEventPublisher({
242
+ runId: manifest.run_id,
243
+ pipeline,
244
+ manifest,
245
+ paths,
246
+ emitter
247
+ });
248
+ return await this.performRunLifecycle({
249
+ env: preparation.env,
250
+ pipeline,
251
+ manifest,
252
+ paths,
253
+ planner: preparation.planner,
254
+ taskContext: preparation.taskContext,
255
+ runId: manifest.run_id,
256
+ runEvents,
257
+ eventStream: stream,
258
+ onEventEntry,
259
+ persister,
260
+ envOverrides: preparation.envOverrides
261
+ });
262
+ }
263
+ finally {
264
+ if (detachStream) {
265
+ try {
266
+ detachStream();
267
+ }
268
+ catch (error) {
269
+ logger.warn(`Failed to detach run event stream: ${error?.message ?? String(error)}`);
270
+ }
271
+ }
272
+ if (controlServer) {
273
+ try {
274
+ await controlServer.close();
275
+ }
276
+ catch (error) {
277
+ logger.warn(`Failed to close control server: ${error?.message ?? String(error)}`);
278
+ }
279
+ }
280
+ if (eventStream) {
281
+ try {
282
+ await eventStream.close();
283
+ }
284
+ catch (error) {
285
+ logger.warn(`Failed to close run event stream: ${error?.message ?? String(error)}`);
286
+ }
287
+ }
288
+ }
125
289
  }
126
290
  async status(options) {
127
291
  const env = this.baseEnv;
@@ -196,11 +360,13 @@ export class CodexOrchestrator {
196
360
  logPath: params.paths.logPath
197
361
  });
198
362
  }
199
- createTaskManager(runId, pipeline, executePipeline, getResult, plannerInstance) {
363
+ createTaskManager(runId, pipeline, executePipeline, getResult, plannerInstance, env) {
200
364
  const planner = plannerInstance ?? new CommandPlanner(pipeline);
201
365
  const builder = new CommandBuilder(executePipeline);
202
366
  const tester = new CommandTester(getResult);
203
367
  const reviewer = new CommandReviewer(getResult);
368
+ const stateStore = new TaskStateStore({ outDir: env.outRoot, runsDir: env.runsRoot });
369
+ const manifestWriter = new RunManifestWriter({ runsDir: env.runsRoot });
204
370
  const options = {
205
371
  planner,
206
372
  builder,
@@ -208,7 +374,7 @@ export class CodexOrchestrator {
208
374
  reviewer,
209
375
  runIdFactory: () => runId,
210
376
  modePolicy: (task, subtask) => this.determineMode(task, subtask),
211
- persistence: { autoStart: true }
377
+ persistence: { autoStart: true, stateStore, manifestWriter }
212
378
  };
213
379
  return new TaskManager(options);
214
380
  }
@@ -251,6 +417,13 @@ export class CodexOrchestrator {
251
417
  updateHeartbeat(manifest);
252
418
  return schedulePersist({ manifest: forceManifest, heartbeat: true, force: forceManifest });
253
419
  };
420
+ const controlWatcher = new ControlWatcher({
421
+ paths,
422
+ manifest,
423
+ eventStream: options.eventStream,
424
+ onEntry: options.onEventEntry,
425
+ persist: () => schedulePersist({ manifest: true, force: true })
426
+ });
254
427
  manifest.status = 'in_progress';
255
428
  updateHeartbeat(manifest);
256
429
  await schedulePersist({ manifest: true, heartbeat: true, force: true });
@@ -262,6 +435,13 @@ export class CodexOrchestrator {
262
435
  }, manifest.heartbeat_interval_seconds * 1000);
263
436
  try {
264
437
  for (let i = 0; i < pipeline.stages.length; i += 1) {
438
+ await controlWatcher.sync();
439
+ await controlWatcher.waitForResume();
440
+ if (controlWatcher.isCanceled()) {
441
+ manifest.status_detail = 'run-canceled';
442
+ success = false;
443
+ break;
444
+ }
265
445
  const stage = pipeline.stages[i];
266
446
  const entry = manifest.commands[i];
267
447
  if (!entry) {
@@ -399,7 +579,11 @@ export class CodexOrchestrator {
399
579
  clearInterval(heartbeatInterval);
400
580
  await schedulePersist({ force: true });
401
581
  }
402
- if (success) {
582
+ await controlWatcher.sync();
583
+ if (controlWatcher.isCanceled()) {
584
+ finalizeStatus(manifest, 'cancelled', manifest.status_detail ?? 'run-canceled');
585
+ }
586
+ else if (success) {
403
587
  finalizeStatus(manifest, 'succeeded');
404
588
  }
405
589
  else {
@@ -435,6 +619,8 @@ export class CodexOrchestrator {
435
619
  manifest,
436
620
  paths,
437
621
  runEvents: context.runEvents,
622
+ eventStream: context.eventStream,
623
+ onEventEntry: context.onEventEntry,
438
624
  persister,
439
625
  envOverrides
440
626
  }).then((result) => {
@@ -445,7 +631,7 @@ export class CodexOrchestrator {
445
631
  return executing;
446
632
  };
447
633
  const getResult = () => pipelineResult;
448
- const manager = this.createTaskManager(runId, pipeline, executePipeline, getResult, planner);
634
+ const manager = this.createTaskManager(runId, pipeline, executePipeline, getResult, planner, env);
449
635
  this.attachPlanTargetTracker(manager, manifest, paths, persister);
450
636
  getPrivacyGuard().reset();
451
637
  const controlPlaneResult = await this.controlPlane.guard({
@@ -1,34 +1,23 @@
1
- import { defaultDiagnosticsPipeline } from './defaultDiagnostics.js';
2
- import { designReferencePipeline } from './designReference.js';
3
- import { hiFiDesignToolkitPipeline } from './hiFiDesignToolkit.js';
4
1
  import { findPipeline } from '../config/userConfig.js';
5
- const builtinPipelines = new Map([
6
- [defaultDiagnosticsPipeline.id, defaultDiagnosticsPipeline],
7
- [designReferencePipeline.id, designReferencePipeline],
8
- [hiFiDesignToolkitPipeline.id, hiFiDesignToolkitPipeline]
9
- ]);
10
- function getBuiltinPipeline(id) {
11
- if (!id) {
12
- return null;
13
- }
14
- return builtinPipelines.get(id) ?? null;
2
+ function resolveConfigSource(config) {
3
+ return config?.source === 'package' ? 'default' : 'user';
15
4
  }
16
- export function resolvePipeline(env, options) {
5
+ export function resolvePipeline(_env, options) {
17
6
  const { pipelineId, config } = options;
7
+ const configSource = resolveConfigSource(config);
18
8
  if (pipelineId) {
19
9
  const fromUser = findPipeline(config, pipelineId);
20
10
  if (fromUser) {
21
- return { pipeline: fromUser, source: 'user' };
22
- }
23
- const builtin = getBuiltinPipeline(pipelineId);
24
- if (builtin) {
25
- return { pipeline: builtin, source: 'default' };
11
+ return { pipeline: fromUser, source: configSource };
26
12
  }
27
- throw new Error(`Pipeline '${pipelineId}' not found.`);
13
+ const suffix = config ? '' : ' (missing codex.orchestrator.json)';
14
+ throw new Error(`Pipeline '${pipelineId}' not found${suffix}.`);
28
15
  }
29
- const defaultId = config?.defaultPipeline ?? defaultDiagnosticsPipeline.id;
16
+ const defaultId = config?.defaultPipeline ?? 'diagnostics';
30
17
  const userPipeline = findPipeline(config, defaultId);
31
- const chosen = userPipeline ?? getBuiltinPipeline(defaultId) ?? defaultDiagnosticsPipeline;
32
- const source = userPipeline ? 'user' : 'default';
33
- return { pipeline: chosen, source };
18
+ if (userPipeline) {
19
+ return { pipeline: userPipeline, source: configSource };
20
+ }
21
+ const suffix = config ? '' : ' (missing codex.orchestrator.json)';
22
+ throw new Error(`Pipeline '${defaultId}' not found${suffix}.`);
34
23
  }
@@ -0,0 +1,31 @@
1
+ import { basename } from 'node:path';
2
+ export function buildRlmPrompt(input) {
3
+ const repoName = basename(input.repoRoot) || 'repo';
4
+ const maxIterations = input.maxIterations === 0 ? 'unlimited' : String(input.maxIterations);
5
+ const lines = [
6
+ `You are Codex running an RLM loop in repo "${repoName}".`,
7
+ `Goal: ${input.goal}`,
8
+ `Iteration: ${input.iteration} of ${maxIterations}.`
9
+ ];
10
+ if (input.diffSummary) {
11
+ lines.push('', 'Workspace summary:', input.diffSummary.trim());
12
+ }
13
+ if (input.lastValidatorOutput) {
14
+ lines.push('', 'Last validator output:', input.lastValidatorOutput.trim());
15
+ }
16
+ if (input.validatorCommand) {
17
+ lines.push('', `Validator command (do NOT run it): ${input.validatorCommand}`);
18
+ }
19
+ else {
20
+ lines.push('', 'Validator: none (budgeted run)');
21
+ }
22
+ lines.push('', 'Instructions:', '- Plan and apply minimal changes toward the goal.', '- Use tools as needed (edit files, run commands, inspect diffs).', '- Do not run the validator command; it will be run after you finish.', '- Self-refine before finalizing (ensure changes align with goal).');
23
+ if (input.roles === 'triad') {
24
+ if (input.subagentsEnabled) {
25
+ lines.push('', 'Use subagents if available: Planner, Critic, Reviser.');
26
+ }
27
+ lines.push('', 'Role split (single response with sections):', 'Planner: outline the plan.', 'Critic: identify risks or missing steps.', 'Reviser: execute the plan and summarize changes.');
28
+ }
29
+ lines.push('', 'End your response with:', 'Summary: <one-line summary of changes>', 'Next: <what to try next if validator still fails>');
30
+ return lines.join('\n');
31
+ }
@@ -0,0 +1,177 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { isoTimestamp } from '../utils/time.js';
6
+ const execFileAsync = promisify(execFile);
7
+ const MAX_DIFF_CHARS = 2000;
8
+ const MAX_VALIDATOR_OUTPUT_CHARS = 4000;
9
+ function truncate(value, maxChars) {
10
+ if (value.length <= maxChars) {
11
+ return value;
12
+ }
13
+ return `${value.slice(0, maxChars)}...`;
14
+ }
15
+ function extractSummary(output) {
16
+ const lines = output
17
+ .split('\n')
18
+ .map((line) => line.trim())
19
+ .filter(Boolean);
20
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
21
+ const line = lines[i] ?? '';
22
+ if (line.toLowerCase().startsWith('summary:')) {
23
+ return line.slice('summary:'.length).trim() || null;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ function summarizeValidatorOutput(result) {
29
+ const combined = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
30
+ if (!combined) {
31
+ return null;
32
+ }
33
+ return truncate(combined, MAX_VALIDATOR_OUTPUT_CHARS);
34
+ }
35
+ async function collectGitSummary(repoRoot) {
36
+ try {
37
+ const [statusResult, diffResult] = await Promise.all([
38
+ execFileAsync('git', ['status', '--short'], { cwd: repoRoot }),
39
+ execFileAsync('git', ['diff', '--stat'], { cwd: repoRoot })
40
+ ]);
41
+ const statusText = statusResult.stdout.trim() || 'clean';
42
+ const diffText = diffResult.stdout.trim() || 'no diff';
43
+ return [
44
+ 'status:',
45
+ truncate(statusText, MAX_DIFF_CHARS),
46
+ 'diff:',
47
+ truncate(diffText, MAX_DIFF_CHARS)
48
+ ].join('\n');
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return `git summary unavailable: ${message}`;
53
+ }
54
+ }
55
+ async function writeStateFile(path, state) {
56
+ await writeFile(path, JSON.stringify(state, null, 2), 'utf8');
57
+ }
58
+ async function writeValidatorLog(path, command, result) {
59
+ const lines = [];
60
+ lines.push(`[${isoTimestamp()}] $ ${command}`);
61
+ if (result.spawnError) {
62
+ lines.push('[validator] spawn error');
63
+ }
64
+ if (result.stdout.trim()) {
65
+ lines.push(result.stdout.trimEnd());
66
+ }
67
+ if (result.stderr.trim()) {
68
+ lines.push(result.stderr.trimEnd());
69
+ }
70
+ lines.push(`[exit] ${result.exitCode}`);
71
+ await writeFile(path, `${lines.join('\n')}\n`, 'utf8');
72
+ }
73
+ export async function runRlmLoop(options) {
74
+ const now = options.now ?? isoTimestamp;
75
+ const log = options.logger ?? (() => undefined);
76
+ const state = {
77
+ goal: options.goal,
78
+ validator: options.validatorCommand ?? 'none',
79
+ roles: options.roles,
80
+ maxIterations: options.maxIterations,
81
+ maxMinutes: options.maxMinutes ?? null,
82
+ iterations: []
83
+ };
84
+ const runDir = options.runDir;
85
+ const statePath = join(runDir, 'state.json');
86
+ await mkdir(runDir, { recursive: true });
87
+ const collectSummary = options.collectDiffSummary ?? collectGitSummary;
88
+ const maxIterations = options.maxIterations;
89
+ const maxMinutes = options.maxMinutes ?? null;
90
+ const startTime = Date.now();
91
+ const deadline = maxMinutes && maxMinutes > 0 ? startTime + maxMinutes * 60 * 1000 : null;
92
+ if (maxIterations === 0 && !options.validatorCommand && !deadline) {
93
+ state.final = { status: 'invalid_config', exitCode: 5 };
94
+ await writeStateFile(statePath, state);
95
+ return { state, exitCode: 5, error: 'validator none with unbounded budget' };
96
+ }
97
+ const timeExceeded = () => deadline !== null && Date.now() >= deadline;
98
+ let lastValidatorOutput = null;
99
+ const finalize = async (status) => {
100
+ state.final = status ?? { status: 'error', exitCode: 10 };
101
+ await writeStateFile(statePath, state);
102
+ return { state, exitCode: state.final.exitCode };
103
+ };
104
+ try {
105
+ for (let iteration = 1; maxIterations === 0 || iteration <= maxIterations; iteration += 1) {
106
+ if (timeExceeded()) {
107
+ if (options.validatorCommand) {
108
+ return await finalize({ status: 'max_minutes', exitCode: 3 });
109
+ }
110
+ return await finalize({ status: 'budget_complete', exitCode: 0 });
111
+ }
112
+ const iterationStartedAt = now();
113
+ const preDiffSummary = await collectSummary(options.repoRoot);
114
+ const agentResult = await options.runAgent({
115
+ goal: options.goal,
116
+ iteration,
117
+ maxIterations,
118
+ roles: options.roles,
119
+ subagentsEnabled: options.subagentsEnabled,
120
+ validatorCommand: options.validatorCommand,
121
+ lastValidatorOutput,
122
+ diffSummary: preDiffSummary,
123
+ repoRoot: options.repoRoot
124
+ });
125
+ const postDiffSummary = await collectSummary(options.repoRoot);
126
+ const summary = agentResult.summary ?? extractSummary(agentResult.output) ?? null;
127
+ let validatorExitCode = null;
128
+ let validatorLogPath = null;
129
+ let validatorResult = null;
130
+ if (options.validatorCommand) {
131
+ if (!options.runValidator) {
132
+ throw new Error('Validator runner missing');
133
+ }
134
+ const validatorLogFile = join(runDir, `validator-${iteration}.log`);
135
+ validatorResult = await options.runValidator(options.validatorCommand);
136
+ await writeValidatorLog(validatorLogFile, options.validatorCommand, validatorResult);
137
+ validatorLogPath = relative(options.repoRoot, validatorLogFile);
138
+ validatorExitCode = validatorResult.exitCode;
139
+ lastValidatorOutput = summarizeValidatorOutput(validatorResult);
140
+ }
141
+ state.iterations.push({
142
+ n: iteration,
143
+ startedAt: iterationStartedAt,
144
+ summary,
145
+ validatorExitCode,
146
+ validatorLogPath,
147
+ diffSummary: postDiffSummary
148
+ });
149
+ await writeStateFile(statePath, state);
150
+ if (validatorResult?.spawnError) {
151
+ return await finalize({ status: 'error', exitCode: 4 });
152
+ }
153
+ if (validatorExitCode === 0) {
154
+ return await finalize({ status: 'passed', exitCode: 0 });
155
+ }
156
+ if (maxIterations > 0 && iteration >= maxIterations) {
157
+ if (options.validatorCommand) {
158
+ return await finalize({ status: 'max_iterations', exitCode: 3 });
159
+ }
160
+ return await finalize({ status: 'budget_complete', exitCode: 0 });
161
+ }
162
+ if (timeExceeded()) {
163
+ if (options.validatorCommand) {
164
+ return await finalize({ status: 'max_minutes', exitCode: 3 });
165
+ }
166
+ return await finalize({ status: 'budget_complete', exitCode: 0 });
167
+ }
168
+ log(`RLM iteration ${iteration} complete; continuing.`);
169
+ }
170
+ }
171
+ catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ await finalize({ status: 'error', exitCode: 10 });
174
+ return { state, exitCode: 10, error: message };
175
+ }
176
+ return await finalize({ status: 'error', exitCode: 10 });
177
+ }
@@ -0,0 +1 @@
1
+ export {};