@kbediako/codex-orchestrator 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +11 -8
  2. package/dist/bin/codex-orchestrator.js +245 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/devtoolsSetup.js +66 -0
  5. package/dist/orchestrator/src/cli/doctor.js +46 -21
  6. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  7. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  8. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  9. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  10. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  11. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  12. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  13. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  14. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  15. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  16. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  17. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  18. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  19. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  20. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  21. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  22. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  23. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  24. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  25. package/dist/orchestrator/src/cli/utils/devtools.js +178 -0
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  27. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  28. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  29. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  30. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  31. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  32. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  33. package/dist/packages/orchestrator/src/index.js +1 -0
  34. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  35. package/dist/packages/shared/streams/stdio.js +2 -112
  36. package/dist/packages/shared/utils/strings.js +17 -0
  37. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  38. package/dist/scripts/design/pipeline/context.js +5 -5
  39. package/dist/scripts/design/pipeline/extract.js +9 -6
  40. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  41. package/dist/scripts/design/pipeline/permit.js +59 -0
  42. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  43. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  44. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  45. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  46. package/dist/scripts/lib/cli-args.js +53 -0
  47. package/dist/scripts/lib/docs-helpers.js +111 -0
  48. package/dist/scripts/lib/npm-pack.js +20 -0
  49. package/dist/scripts/lib/run-manifests.js +160 -0
  50. package/package.json +17 -6
  51. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  52. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  53. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  54. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  55. package/dist/orchestrator/src/control-plane/index.js +0 -3
  56. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  57. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  58. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { buildDevtoolsSetupPlan, resolveDevtoolsReadiness } from './utils/devtools.js';
4
+ export async function runDevtoolsSetup(options = {}) {
5
+ const env = options.env ?? process.env;
6
+ const plan = buildDevtoolsSetupPlan(env);
7
+ const readiness = resolveDevtoolsReadiness(env);
8
+ if (!options.apply) {
9
+ return { status: 'planned', plan, readiness };
10
+ }
11
+ if (readiness.config.status === 'ok') {
12
+ return {
13
+ status: 'skipped',
14
+ reason: 'DevTools MCP is already configured.',
15
+ plan,
16
+ readiness
17
+ };
18
+ }
19
+ if (readiness.config.status === 'invalid') {
20
+ throw new Error(`Cannot apply DevTools setup because config.toml is invalid: ${readiness.config.path}`);
21
+ }
22
+ await applyDevtoolsSetup(plan, env);
23
+ return { status: 'applied', plan, readiness };
24
+ }
25
+ export function formatDevtoolsSetupSummary(result) {
26
+ const lines = [];
27
+ lines.push(`DevTools setup: ${result.status}`);
28
+ if (result.reason) {
29
+ lines.push(`Note: ${result.reason}`);
30
+ }
31
+ lines.push(`- Codex home: ${result.plan.codexHome}`);
32
+ lines.push(`- Skill: ${result.readiness.skill.status} (${result.readiness.skill.path})`);
33
+ const configLabel = result.readiness.config.status === 'invalid'
34
+ ? `invalid (${result.readiness.config.path})`
35
+ : `${result.readiness.config.status} (${result.readiness.config.path})`;
36
+ lines.push(`- Config: ${configLabel}`);
37
+ if (result.readiness.config.detail) {
38
+ lines.push(` detail: ${result.readiness.config.detail}`);
39
+ }
40
+ if (result.readiness.config.error) {
41
+ lines.push(` error: ${result.readiness.config.error}`);
42
+ }
43
+ lines.push(`- Command: ${result.plan.commandLine}`);
44
+ lines.push('- Config snippet:');
45
+ for (const line of result.plan.configSnippet.split('\n')) {
46
+ lines.push(` ${line}`);
47
+ }
48
+ if (result.status === 'planned') {
49
+ lines.push('Run with --yes to apply this setup.');
50
+ }
51
+ return lines;
52
+ }
53
+ async function applyDevtoolsSetup(plan, env) {
54
+ await new Promise((resolve, reject) => {
55
+ const child = spawn(plan.command, plan.args, { stdio: 'inherit', env });
56
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
57
+ child.once('exit', (code) => {
58
+ if (code === 0) {
59
+ resolve();
60
+ }
61
+ else {
62
+ reject(new Error(`codex mcp add exited with code ${code ?? 'unknown'}`));
63
+ }
64
+ });
65
+ });
66
+ }
@@ -1,7 +1,5 @@
1
- import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
1
  import process from 'node:process';
2
+ import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
5
3
  import { resolveOptionalDependency } from './utils/optionalDeps.js';
6
4
  const OPTIONAL_DEPENDENCIES = [
7
5
  {
@@ -12,7 +10,6 @@ const OPTIONAL_DEPENDENCIES = [
12
10
  { name: 'pixelmatch', install: 'npm install --save-dev pixelmatch' },
13
11
  { name: 'cheerio', install: 'npm install --save-dev cheerio' }
14
12
  ];
15
- const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
16
13
  export function runDoctor(cwd = process.cwd()) {
17
14
  const dependencies = OPTIONAL_DEPENDENCIES.map((entry) => {
18
15
  const resolved = resolveOptionalDependency(entry.name, cwd);
@@ -26,20 +23,34 @@ export function runDoctor(cwd = process.cwd()) {
26
23
  install: entry.install
27
24
  };
28
25
  });
29
- const codexHome = resolveCodexHome();
30
- const skillPath = join(codexHome, 'skills', DEVTOOLS_SKILL_NAME, 'SKILL.md');
31
- const skillInstalled = existsSync(skillPath);
26
+ const readiness = resolveDevtoolsReadiness();
27
+ const setupPlan = buildDevtoolsSetupPlan();
32
28
  const devtools = {
33
- status: skillInstalled ? 'ok' : 'missing',
29
+ status: readiness.status,
34
30
  skill: {
35
31
  name: DEVTOOLS_SKILL_NAME,
36
- status: skillInstalled ? 'ok' : 'missing',
37
- path: skillPath,
38
- install: skillInstalled
32
+ status: readiness.skill.status,
33
+ path: readiness.skill.path,
34
+ install: readiness.skill.status === 'ok'
39
35
  ? undefined
40
36
  : [
41
- `Copy the ${DEVTOOLS_SKILL_NAME} skill into ${join(codexHome, 'skills', DEVTOOLS_SKILL_NAME)}`,
42
- `Expected file: ${skillPath}`
37
+ `Copy the ${DEVTOOLS_SKILL_NAME} skill into ${setupPlan.codexHome}/skills/${DEVTOOLS_SKILL_NAME}`,
38
+ `Expected file: ${readiness.skill.path}`
39
+ ]
40
+ },
41
+ config: {
42
+ status: readiness.config.status,
43
+ path: readiness.config.path,
44
+ detail: readiness.config.detail,
45
+ error: readiness.config.error,
46
+ install: readiness.config.status === 'ok'
47
+ ? undefined
48
+ : [
49
+ 'Run: codex-orchestrator devtools setup',
50
+ `Run: ${setupPlan.commandLine}`,
51
+ `Config path: ${setupPlan.configPath}`,
52
+ 'Config snippet:',
53
+ ...setupPlan.configSnippet.split('\n')
43
54
  ]
44
55
  },
45
56
  enablement: [
@@ -48,9 +59,12 @@ export function runDoctor(cwd = process.cwd()) {
48
59
  ]
49
60
  };
50
61
  const missing = dependencies.filter((dep) => dep.status === 'missing').map((dep) => dep.name);
51
- if (!skillInstalled) {
62
+ if (readiness.skill.status === 'missing') {
52
63
  missing.push(DEVTOOLS_SKILL_NAME);
53
64
  }
65
+ if (readiness.config.status !== 'ok') {
66
+ missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
67
+ }
54
68
  return {
55
69
  status: missing.length === 0 ? 'ok' : 'warning',
56
70
  missing,
@@ -83,15 +97,26 @@ export function formatDoctorSummary(result) {
83
97
  lines.push(` install: ${instruction}`);
84
98
  }
85
99
  }
100
+ if (result.devtools.config.status === 'ok') {
101
+ lines.push(` - config.toml: ok (${result.devtools.config.path})`);
102
+ }
103
+ else {
104
+ const label = result.devtools.config.status === 'invalid'
105
+ ? `invalid (${result.devtools.config.path})`
106
+ : `missing (${result.devtools.config.path})`;
107
+ lines.push(` - config.toml: ${label}`);
108
+ if (result.devtools.config.detail) {
109
+ lines.push(` detail: ${result.devtools.config.detail}`);
110
+ }
111
+ if (result.devtools.config.error) {
112
+ lines.push(` error: ${result.devtools.config.error}`);
113
+ }
114
+ for (const instruction of result.devtools.config.install ?? []) {
115
+ lines.push(` install: ${instruction}`);
116
+ }
117
+ }
86
118
  for (const line of result.devtools.enablement) {
87
119
  lines.push(` - ${line}`);
88
120
  }
89
121
  return lines;
90
122
  }
91
- function resolveCodexHome() {
92
- const override = process.env.CODEX_HOME?.trim();
93
- if (override) {
94
- return override;
95
- }
96
- return join(homedir(), '.codex');
97
- }
@@ -1,7 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { bootstrapManifest } from '../run/manifest.js';
3
3
  import { generateRunId } from '../utils/runId.js';
4
- import { JsonlWriter } from '../utils/jsonlWriter.js';
5
4
  import { ExperienceStore } from '../../persistence/ExperienceStore.js';
6
5
  import { createTelemetrySink } from '../../../../packages/orchestrator/src/telemetry/otel-exporter.js';
7
6
  import { createNotificationSink } from '../../../../packages/orchestrator/src/notifications/index.js';
@@ -47,7 +46,11 @@ export async function bootstrapExecContext(context, invocation) {
47
46
  targets: invocation.notifyTargets,
48
47
  envTargets: envNotifications
49
48
  });
50
- const jsonlWriter = outputMode === 'jsonl' ? new JsonlWriter(stdout) : null;
49
+ const jsonlWriter = outputMode === 'jsonl'
50
+ ? (event) => {
51
+ stdout.write(`${JSON.stringify(event)}\n`);
52
+ }
53
+ : null;
51
54
  return {
52
55
  env,
53
56
  invocation,
@@ -2,9 +2,6 @@ import process from 'node:process';
2
2
  import { persistManifest } from '../run/manifestPersister.js';
3
3
  import { isoTimestamp } from '../utils/time.js';
4
4
  import { logger } from '../../logger.js';
5
- import { runLearningHarvester } from '../../learning/harvester.js';
6
- import { synthesizeScenario } from '../../learning/runner.js';
7
- import { runScenarioValidation } from '../../learning/validator.js';
8
5
  export async function maybeTriggerLearning(runContext, runStatus) {
9
6
  const enabled = process.env.LEARNING_PIPELINE_ENABLED === '1';
10
7
  if (!enabled) {
@@ -15,6 +12,11 @@ export async function maybeTriggerLearning(runContext, runStatus) {
15
12
  return;
16
13
  }
17
14
  try {
15
+ const [{ runLearningHarvester }, { synthesizeScenario }, { runScenarioValidation }] = await Promise.all([
16
+ import('../../learning/harvester.js'),
17
+ import('../../learning/runner.js'),
18
+ import('../../learning/validator.js')
19
+ ]);
18
20
  const harvester = await runLearningHarvester(runContext.manifest, {
19
21
  repoRoot: runContext.env.repoRoot,
20
22
  runsRoot: runContext.env.runsRoot,
@@ -10,7 +10,7 @@ export async function runExecStage(context) {
10
10
  const serialized = serializeExecEvent(event);
11
11
  context.telemetryTasks.push(Promise.resolve(context.telemetrySink.record(serialized)).then(() => undefined));
12
12
  if (context.outputMode === 'jsonl' && context.jsonlWriter) {
13
- context.jsonlWriter.write(serialized);
13
+ context.jsonlWriter(serialized);
14
14
  }
15
15
  else if (context.outputMode === 'interactive') {
16
16
  streamInteractive(context.stdout, context.stderr, event);
@@ -61,7 +61,7 @@ export function createRunSummaryPayload(params) {
61
61
  }
62
62
  export function renderRunOutput(context, summaryPayload, summaryEvent) {
63
63
  if (context.outputMode === 'jsonl' && context.jsonlWriter) {
64
- context.jsonlWriter.write(summaryEvent);
64
+ context.jsonlWriter(summaryEvent);
65
65
  return;
66
66
  }
67
67
  if (context.outputMode === 'json') {
@@ -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';
@@ -18,14 +21,15 @@ import { ControlPlaneService } from './services/controlPlaneService.js';
18
21
  import { SchedulerService } from './services/schedulerService.js';
19
22
  import { applyHandlesToRunSummary, applyPrivacyToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
20
23
  import { prepareRun, resolvePipelineForResume, overrideTaskEnvironment } from './services/runPreparation.js';
21
- import { loadUserConfig } from './config/userConfig.js';
24
+ import { loadPackageConfig, loadUserConfig } from './config/userConfig.js';
22
25
  import { RunEventPublisher, snapshotStages } from './events/runEvents.js';
23
26
  import { CLI_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from '../utils/executionMode.js';
27
+ const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
24
28
  export class CodexOrchestrator {
25
29
  baseEnv;
26
30
  controlPlane = new ControlPlaneService();
27
31
  scheduler = new SchedulerService();
28
- constructor(baseEnv = resolveEnvironment()) {
32
+ constructor(baseEnv = resolveBaseEnvironment()) {
29
33
  this.baseEnv = baseEnv;
30
34
  }
31
35
  async start(options = {}) {
@@ -77,7 +81,10 @@ export class CodexOrchestrator {
77
81
  const resolver = new PipelineResolver();
78
82
  const designConfig = await resolver.loadDesignConfig(actualEnv.repoRoot);
79
83
  const userConfig = await loadUserConfig(actualEnv);
80
- const pipeline = resolvePipelineForResume(actualEnv, manifest, userConfig);
84
+ const fallbackConfig = manifest.pipeline_id === 'rlm' && userConfig?.source === 'repo'
85
+ ? await loadPackageConfig(actualEnv)
86
+ : null;
87
+ const pipeline = resolvePipelineForResume(actualEnv, manifest, userConfig, fallbackConfig);
81
88
  const envOverrides = resolver.resolveDesignEnvOverrides(designConfig, pipeline.id);
82
89
  await this.validateResumeToken(paths, manifest, options.resumeToken ?? null);
83
90
  recordResumeEvent(manifest, {
@@ -196,11 +203,13 @@ export class CodexOrchestrator {
196
203
  logPath: params.paths.logPath
197
204
  });
198
205
  }
199
- createTaskManager(runId, pipeline, executePipeline, getResult, plannerInstance) {
206
+ createTaskManager(runId, pipeline, executePipeline, getResult, plannerInstance, env) {
200
207
  const planner = plannerInstance ?? new CommandPlanner(pipeline);
201
208
  const builder = new CommandBuilder(executePipeline);
202
209
  const tester = new CommandTester(getResult);
203
210
  const reviewer = new CommandReviewer(getResult);
211
+ const stateStore = new TaskStateStore({ outDir: env.outRoot, runsDir: env.runsRoot });
212
+ const manifestWriter = new RunManifestWriter({ runsDir: env.runsRoot });
204
213
  const options = {
205
214
  planner,
206
215
  builder,
@@ -208,7 +217,7 @@ export class CodexOrchestrator {
208
217
  reviewer,
209
218
  runIdFactory: () => runId,
210
219
  modePolicy: (task, subtask) => this.determineMode(task, subtask),
211
- persistence: { autoStart: true }
220
+ persistence: { autoStart: true, stateStore, manifestWriter }
212
221
  };
213
222
  return new TaskManager(options);
214
223
  }
@@ -445,7 +454,7 @@ export class CodexOrchestrator {
445
454
  return executing;
446
455
  };
447
456
  const getResult = () => pipelineResult;
448
- const manager = this.createTaskManager(runId, pipeline, executePipeline, getResult, planner);
457
+ const manager = this.createTaskManager(runId, pipeline, executePipeline, getResult, planner, env);
449
458
  this.attachPlanTargetTracker(manager, manifest, paths, persister);
450
459
  getPrivacyGuard().reset();
451
460
  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 {};