@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.
- package/README.md +15 -8
- package/dist/bin/codex-orchestrator.js +252 -121
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +9 -3
- package/dist/orchestrator/src/cli/exec/learning.js +5 -3
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/exec/summary.js +1 -1
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +233 -47
- 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 +440 -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/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +2 -2
- 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/devtools.js +33 -2
- 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 +115 -58
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +3 -2
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- 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/orchestrator/src/telemetry/otel-exporter.js +21 -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 +7 -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
|
@@ -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 {
|
|
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 {
|
|
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 =
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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(
|
|
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:
|
|
22
|
-
}
|
|
23
|
-
const builtin = getBuiltinPipeline(pipelineId);
|
|
24
|
-
if (builtin) {
|
|
25
|
-
return { pipeline: builtin, source: 'default' };
|
|
11
|
+
return { pipeline: fromUser, source: configSource };
|
|
26
12
|
}
|
|
27
|
-
|
|
13
|
+
const suffix = config ? '' : ' (missing codex.orchestrator.json)';
|
|
14
|
+
throw new Error(`Pipeline '${pipelineId}' not found${suffix}.`);
|
|
28
15
|
}
|
|
29
|
-
const defaultId = config?.defaultPipeline ??
|
|
16
|
+
const defaultId = config?.defaultPipeline ?? 'diagnostics';
|
|
30
17
|
const userPipeline = findPipeline(config, defaultId);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 {};
|