@kbediako/codex-orchestrator 0.1.0
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/LICENSE +7 -0
- package/README.md +238 -0
- package/dist/bin/codex-orchestrator.js +507 -0
- package/dist/orchestrator/src/agents/builder.js +16 -0
- package/dist/orchestrator/src/agents/index.js +4 -0
- package/dist/orchestrator/src/agents/planner.js +17 -0
- package/dist/orchestrator/src/agents/reviewer.js +13 -0
- package/dist/orchestrator/src/agents/tester.js +13 -0
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
- package/dist/orchestrator/src/cli/adapters/index.js +4 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
- package/dist/orchestrator/src/cli/doctor.js +48 -0
- package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
- package/dist/orchestrator/src/cli/exec/command.js +56 -0
- package/dist/orchestrator/src/cli/exec/context.js +108 -0
- package/dist/orchestrator/src/cli/exec/experience.js +77 -0
- package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
- package/dist/orchestrator/src/cli/exec/learning.js +62 -0
- package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
- package/dist/orchestrator/src/cli/exec/summary.js +109 -0
- package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
- package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
- package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
- package/dist/orchestrator/src/cli/exec/types.js +1 -0
- package/dist/orchestrator/src/cli/init.js +64 -0
- package/dist/orchestrator/src/cli/mcp.js +124 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
- package/dist/orchestrator/src/cli/orchestrator.js +554 -0
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
- package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
- package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
- package/dist/orchestrator/src/cli/run/environment.js +24 -0
- package/dist/orchestrator/src/cli/run/manifest.js +367 -0
- package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
- package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
- package/dist/orchestrator/src/cli/selfCheck.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
- package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
- package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
- package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
- package/dist/orchestrator/src/cli/types.js +1 -0
- package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
- package/dist/orchestrator/src/cli/ui/controller.js +26 -0
- package/dist/orchestrator/src/cli/ui/store.js +240 -0
- package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
- package/dist/orchestrator/src/cli/utils/fs.js +8 -0
- package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
- package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
- package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
- package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
- package/dist/orchestrator/src/cli/utils/runId.js +7 -0
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
- package/dist/orchestrator/src/cli/utils/strings.js +8 -0
- package/dist/orchestrator/src/cli/utils/time.js +6 -0
- package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
- package/dist/orchestrator/src/control-plane/index.js +3 -0
- package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
- package/dist/orchestrator/src/control-plane/types.js +1 -0
- package/dist/orchestrator/src/control-plane/validator.js +50 -0
- package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
- package/dist/orchestrator/src/events/EventBus.js +25 -0
- package/dist/orchestrator/src/learning/crystalizer.js +108 -0
- package/dist/orchestrator/src/learning/harvester.js +146 -0
- package/dist/orchestrator/src/learning/manifest.js +56 -0
- package/dist/orchestrator/src/learning/runner.js +177 -0
- package/dist/orchestrator/src/learning/validator.js +164 -0
- package/dist/orchestrator/src/logger.js +20 -0
- package/dist/orchestrator/src/manager.js +388 -0
- package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
- package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
- package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
- package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
- package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
- package/dist/orchestrator/src/persistence/lockFile.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
- package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
- package/dist/orchestrator/src/privacy/guard.js +111 -0
- package/dist/orchestrator/src/scheduler/index.js +1 -0
- package/dist/orchestrator/src/scheduler/plan.js +171 -0
- package/dist/orchestrator/src/scheduler/types.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
- package/dist/orchestrator/src/types.js +1 -0
- package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
- package/dist/orchestrator/src/utils/errorMessage.js +14 -0
- package/dist/orchestrator/src/utils/executionMode.js +69 -0
- package/dist/packages/control-plane-schemas/src/index.js +1 -0
- package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
- package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
- package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
- package/dist/packages/orchestrator/src/index.js +3 -0
- package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
- package/dist/packages/orchestrator/src/notifications/index.js +74 -0
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
- package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
- package/dist/packages/sdk-node/src/orchestrator.js +195 -0
- package/dist/packages/shared/config/designConfig.js +495 -0
- package/dist/packages/shared/config/env.js +37 -0
- package/dist/packages/shared/config/index.js +2 -0
- package/dist/packages/shared/design-artifacts/writer.js +221 -0
- package/dist/packages/shared/events/serializer.js +84 -0
- package/dist/packages/shared/events/types.js +1 -0
- package/dist/packages/shared/manifest/artifactUtils.js +36 -0
- package/dist/packages/shared/manifest/designArtifacts.js +665 -0
- package/dist/packages/shared/manifest/fileIO.js +29 -0
- package/dist/packages/shared/manifest/toolRuns.js +78 -0
- package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
- package/dist/packages/shared/manifest/types.js +5 -0
- package/dist/packages/shared/manifest/validator.js +73 -0
- package/dist/packages/shared/manifest/writer.js +2 -0
- package/dist/packages/shared/streams/stdio.js +112 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
- package/dist/scripts/design/pipeline/componentize.js +74 -0
- package/dist/scripts/design/pipeline/context.js +34 -0
- package/dist/scripts/design/pipeline/extract.js +249 -0
- package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
- package/dist/scripts/design/pipeline/prepare.js +46 -0
- package/dist/scripts/design/pipeline/reference.js +94 -0
- package/dist/scripts/design/pipeline/state.js +206 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
- package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
- package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
- package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
- package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
- package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
- package/dist/scripts/design/pipeline/visual-regression.js +137 -0
- package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
- package/package.json +97 -0
- package/schemas/manifest.json +1064 -0
- package/templates/README.md +12 -0
- package/templates/codex/mcp-client.json +8 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { EventBus } from './events/EventBus.js';
|
|
2
|
+
import { PersistenceCoordinator } from './persistence/PersistenceCoordinator.js';
|
|
3
|
+
import { TaskStateStore } from './persistence/TaskStateStore.js';
|
|
4
|
+
import { RunManifestWriter } from './persistence/RunManifestWriter.js';
|
|
5
|
+
import { sanitizeRunId } from './persistence/sanitizeRunId.js';
|
|
6
|
+
import { normalizeErrorMessage } from './utils/errorMessage.js';
|
|
7
|
+
import { MANAGER_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from './utils/executionMode.js';
|
|
8
|
+
import { EnvUtils } from '../../packages/shared/config/index.js';
|
|
9
|
+
const defaultModePolicy = (task, subtask) => {
|
|
10
|
+
const requiresCloudFlag = resolveRequiresCloud(subtask);
|
|
11
|
+
if (requiresCloudFlag) {
|
|
12
|
+
return 'cloud';
|
|
13
|
+
}
|
|
14
|
+
if (task.metadata?.execution?.parallel) {
|
|
15
|
+
return 'cloud';
|
|
16
|
+
}
|
|
17
|
+
return 'mcp';
|
|
18
|
+
};
|
|
19
|
+
function resolveRequiresCloud(subtask) {
|
|
20
|
+
const requiresCloudFlag = resolveRequiresCloudPolicy({
|
|
21
|
+
boolFlags: [subtask.requires_cloud, subtask.requiresCloud],
|
|
22
|
+
metadata: {
|
|
23
|
+
mode: typeof subtask.metadata?.mode === 'string' ? subtask.metadata.mode : null,
|
|
24
|
+
executionMode: typeof subtask.metadata?.executionMode === 'string'
|
|
25
|
+
? subtask.metadata.executionMode
|
|
26
|
+
: null
|
|
27
|
+
},
|
|
28
|
+
metadataOrder: ['mode', 'executionMode'],
|
|
29
|
+
parseMode: MANAGER_EXECUTION_MODE_PARSER
|
|
30
|
+
});
|
|
31
|
+
return requiresCloudFlag ?? false;
|
|
32
|
+
}
|
|
33
|
+
const defaultRunIdFactory = (taskId) => {
|
|
34
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
35
|
+
return sanitizeRunId(`${taskId}-${timestamp}`);
|
|
36
|
+
};
|
|
37
|
+
export class TaskManager {
|
|
38
|
+
options;
|
|
39
|
+
eventBus;
|
|
40
|
+
modePolicy;
|
|
41
|
+
runIdFactory;
|
|
42
|
+
_persistenceCoordinator;
|
|
43
|
+
groupExecutionEnabled;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.eventBus = options.eventBus ?? new EventBus();
|
|
47
|
+
this.modePolicy = options.modePolicy ?? defaultModePolicy;
|
|
48
|
+
this.runIdFactory = options.runIdFactory ?? defaultRunIdFactory;
|
|
49
|
+
this.groupExecutionEnabled = EnvUtils.getBoolean('FEATURE_TFGRPO_GROUP');
|
|
50
|
+
if (options.persistence) {
|
|
51
|
+
const stateStore = options.persistence.stateStore ?? new TaskStateStore();
|
|
52
|
+
const manifestWriter = options.persistence.manifestWriter ?? new RunManifestWriter();
|
|
53
|
+
const coordinatorOptions = options.persistence.coordinatorOptions;
|
|
54
|
+
this._persistenceCoordinator = new PersistenceCoordinator(this.eventBus, stateStore, manifestWriter, coordinatorOptions);
|
|
55
|
+
if (options.persistence.autoStart !== false) {
|
|
56
|
+
this._persistenceCoordinator.start();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
get bus() {
|
|
61
|
+
return this.eventBus;
|
|
62
|
+
}
|
|
63
|
+
startPersistence() {
|
|
64
|
+
this._persistenceCoordinator?.start();
|
|
65
|
+
}
|
|
66
|
+
stopPersistence() {
|
|
67
|
+
this._persistenceCoordinator?.stop();
|
|
68
|
+
}
|
|
69
|
+
dispose() {
|
|
70
|
+
this.stopPersistence();
|
|
71
|
+
}
|
|
72
|
+
get persistenceCoordinator() {
|
|
73
|
+
return this._persistenceCoordinator;
|
|
74
|
+
}
|
|
75
|
+
async execute(task) {
|
|
76
|
+
const runId = this.runIdFactory(task.id);
|
|
77
|
+
const { plan, failed, error } = await this.executePlanner(task, runId);
|
|
78
|
+
if (failed) {
|
|
79
|
+
return this.completeFromPlannerFailure(task, plan, runId, error);
|
|
80
|
+
}
|
|
81
|
+
const targets = this.selectExecutableSubtasks(plan);
|
|
82
|
+
const shouldRunGroup = this.groupExecutionEnabled && targets.length > 1;
|
|
83
|
+
const result = shouldRunGroup
|
|
84
|
+
? await this.executeGroup(task, plan, targets, runId)
|
|
85
|
+
: await this.runPipelineStages(task, plan, targets[0], runId);
|
|
86
|
+
this.eventBus.emit({ type: 'run:completed', payload: result.summary });
|
|
87
|
+
return result.summary;
|
|
88
|
+
}
|
|
89
|
+
selectExecutableSubtasks(plan) {
|
|
90
|
+
if (!this.groupExecutionEnabled) {
|
|
91
|
+
return [this.selectSingleSubtask(plan)];
|
|
92
|
+
}
|
|
93
|
+
if (plan.items.length === 0) {
|
|
94
|
+
throw new Error('Planner returned no executable subtasks.');
|
|
95
|
+
}
|
|
96
|
+
const runnable = plan.items.filter((item) => item.runnable !== false);
|
|
97
|
+
if (runnable.length === 0) {
|
|
98
|
+
throw new Error('Planner returned no runnable subtasks after applying selection and target hints.');
|
|
99
|
+
}
|
|
100
|
+
return this.prioritizeGroupTargets(plan, runnable);
|
|
101
|
+
}
|
|
102
|
+
selectSingleSubtask(plan) {
|
|
103
|
+
if (plan.items.length === 0) {
|
|
104
|
+
throw new Error('Planner returned no executable subtasks.');
|
|
105
|
+
}
|
|
106
|
+
const selected = plan.items.find((item) => item.selected === true);
|
|
107
|
+
if (selected) {
|
|
108
|
+
return selected;
|
|
109
|
+
}
|
|
110
|
+
if (plan.targetId) {
|
|
111
|
+
const targeted = plan.items.find((item) => item.id === plan.targetId);
|
|
112
|
+
if (targeted) {
|
|
113
|
+
return targeted;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const runnable = plan.items.filter((item) => item.runnable !== false);
|
|
117
|
+
if (runnable.length === 1) {
|
|
118
|
+
return runnable[0];
|
|
119
|
+
}
|
|
120
|
+
if (runnable.length > 1) {
|
|
121
|
+
const available = runnable.map((item) => item.id).join(', ');
|
|
122
|
+
throw new Error(`Planner returned multiple runnable subtasks without a selection (${available}). ` +
|
|
123
|
+
'Specify a target stage or update the planner configuration.');
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Planner returned no runnable subtasks after applying selection and target hints.');
|
|
126
|
+
}
|
|
127
|
+
prioritizeGroupTargets(plan, runnable) {
|
|
128
|
+
const prioritized = [];
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const preferred = (plan.targetId ? runnable.find((item) => item.id === plan.targetId) : undefined) ??
|
|
131
|
+
plan.items.find((item) => item.selected === true && item.runnable !== false);
|
|
132
|
+
if (preferred) {
|
|
133
|
+
prioritized.push(preferred);
|
|
134
|
+
seen.add(preferred.id);
|
|
135
|
+
}
|
|
136
|
+
for (const item of runnable) {
|
|
137
|
+
if (!seen.has(item.id)) {
|
|
138
|
+
prioritized.push(item);
|
|
139
|
+
seen.add(item.id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return prioritized;
|
|
143
|
+
}
|
|
144
|
+
createRunSummary(task, mode, plan, build, test, review, runId) {
|
|
145
|
+
const timestamp = new Date().toISOString();
|
|
146
|
+
return {
|
|
147
|
+
taskId: task.id,
|
|
148
|
+
runId,
|
|
149
|
+
mode,
|
|
150
|
+
plan,
|
|
151
|
+
build,
|
|
152
|
+
test,
|
|
153
|
+
review,
|
|
154
|
+
timestamp
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async runPipelineStages(task, plan, target, runId) {
|
|
158
|
+
const mode = this.modePolicy(task, target);
|
|
159
|
+
const build = await this.executeBuilder(task, plan, target, mode, runId);
|
|
160
|
+
if (!build.success) {
|
|
161
|
+
const skippedTest = this.createSkippedTestResult(build, runId);
|
|
162
|
+
const skippedReview = this.createSkippedReviewResult('build-failed');
|
|
163
|
+
const summary = this.createRunSummary(task, mode, plan, build, skippedTest, skippedReview, runId);
|
|
164
|
+
return { target, mode, build, test: skippedTest, review: skippedReview, summary };
|
|
165
|
+
}
|
|
166
|
+
const test = await this.executeTester(task, build, mode, runId);
|
|
167
|
+
if (!test.success) {
|
|
168
|
+
const skippedReview = this.createSkippedReviewResult('test-failed');
|
|
169
|
+
const summary = this.createRunSummary(task, mode, plan, build, test, skippedReview, runId);
|
|
170
|
+
return { target, mode, build, test, review: skippedReview, summary };
|
|
171
|
+
}
|
|
172
|
+
const review = await this.executeReviewer(task, plan, build, test, mode, runId);
|
|
173
|
+
const summary = this.createRunSummary(task, mode, plan, build, test, review, runId);
|
|
174
|
+
return { target, mode, build, test, review, summary };
|
|
175
|
+
}
|
|
176
|
+
async executeGroup(task, plan, targets, runId) {
|
|
177
|
+
const entries = [];
|
|
178
|
+
const buildResults = [];
|
|
179
|
+
const testResults = [];
|
|
180
|
+
const reviewResults = [];
|
|
181
|
+
let finalResult = null;
|
|
182
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
183
|
+
const target = targets[index];
|
|
184
|
+
const result = await this.runPipelineStages(task, plan, target, runId);
|
|
185
|
+
buildResults.push(result.build);
|
|
186
|
+
testResults.push(result.test);
|
|
187
|
+
reviewResults.push(result.review);
|
|
188
|
+
entries.push({
|
|
189
|
+
index: index + 1,
|
|
190
|
+
subtaskId: target.id,
|
|
191
|
+
mode: result.mode,
|
|
192
|
+
buildSuccess: result.build.success,
|
|
193
|
+
testSuccess: result.test.success,
|
|
194
|
+
reviewApproved: Boolean(result.review.decision?.approved),
|
|
195
|
+
status: result.build.success && result.test.success ? 'succeeded' : 'failed'
|
|
196
|
+
});
|
|
197
|
+
finalResult = result;
|
|
198
|
+
if (!result.build.success || !result.test.success) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!finalResult) {
|
|
203
|
+
throw new Error('Group execution produced no runnable subtasks.');
|
|
204
|
+
}
|
|
205
|
+
const groupMode = entries.some((entry) => entry.mode === 'cloud') ? 'cloud' : finalResult.mode;
|
|
206
|
+
finalResult.summary.mode = groupMode;
|
|
207
|
+
finalResult.summary.group = {
|
|
208
|
+
enabled: true,
|
|
209
|
+
size: targets.length,
|
|
210
|
+
processed: entries.length,
|
|
211
|
+
entries
|
|
212
|
+
};
|
|
213
|
+
finalResult.summary.builds = buildResults;
|
|
214
|
+
finalResult.summary.tests = testResults;
|
|
215
|
+
finalResult.summary.reviews = reviewResults;
|
|
216
|
+
return finalResult;
|
|
217
|
+
}
|
|
218
|
+
normalizeBuildResult(build, mode, runId) {
|
|
219
|
+
if (typeof build.success !== 'boolean') {
|
|
220
|
+
throw new Error('Builder result missing success flag');
|
|
221
|
+
}
|
|
222
|
+
return { ...build, mode, runId };
|
|
223
|
+
}
|
|
224
|
+
normalizeTestResult(test, runId) {
|
|
225
|
+
return { ...test, runId };
|
|
226
|
+
}
|
|
227
|
+
createSkippedTestResult(build, runId) {
|
|
228
|
+
return {
|
|
229
|
+
subtaskId: build.subtaskId,
|
|
230
|
+
success: false,
|
|
231
|
+
reports: [
|
|
232
|
+
{
|
|
233
|
+
name: 'tests',
|
|
234
|
+
status: 'failed',
|
|
235
|
+
details: 'Skipped because the build stage failed.'
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
runId
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
createSkippedReviewResult(reason) {
|
|
242
|
+
if (reason === 'build-failed') {
|
|
243
|
+
return {
|
|
244
|
+
summary: 'Review skipped: build stage failed.',
|
|
245
|
+
decision: {
|
|
246
|
+
approved: false,
|
|
247
|
+
feedback: 'Build stage failed; review skipped.'
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
summary: 'Review skipped: tests failed.',
|
|
253
|
+
decision: {
|
|
254
|
+
approved: false,
|
|
255
|
+
feedback: 'Tests failed; review skipped.'
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async executePlanner(task, runId) {
|
|
260
|
+
try {
|
|
261
|
+
const plan = await this.options.planner.plan(task);
|
|
262
|
+
this.eventBus.emit({
|
|
263
|
+
type: 'plan:completed',
|
|
264
|
+
payload: { task, plan, runId }
|
|
265
|
+
});
|
|
266
|
+
return { plan, failed: false, error: null };
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const plan = this.createPlannerFailurePlan(error);
|
|
270
|
+
this.eventBus.emit({
|
|
271
|
+
type: 'plan:completed',
|
|
272
|
+
payload: { task, plan, runId }
|
|
273
|
+
});
|
|
274
|
+
return { plan, failed: true, error };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async executeBuilder(task, plan, target, mode, runId) {
|
|
278
|
+
const input = { task, plan, target, mode, runId };
|
|
279
|
+
try {
|
|
280
|
+
const build = this.normalizeBuildResult(await this.options.builder.build(input), mode, runId);
|
|
281
|
+
this.eventBus.emit({
|
|
282
|
+
type: 'build:completed',
|
|
283
|
+
payload: { task, plan, build, runId }
|
|
284
|
+
});
|
|
285
|
+
return build;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
const failure = this.createBuilderErrorResult(target.id, mode, runId, error);
|
|
289
|
+
this.eventBus.emit({
|
|
290
|
+
type: 'build:completed',
|
|
291
|
+
payload: { task, plan, build: failure, runId }
|
|
292
|
+
});
|
|
293
|
+
return failure;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async executeTester(task, build, mode, runId) {
|
|
297
|
+
try {
|
|
298
|
+
const test = this.normalizeTestResult(await this.options.tester.test({ task, build, mode, runId }), runId);
|
|
299
|
+
this.eventBus.emit({
|
|
300
|
+
type: 'test:completed',
|
|
301
|
+
payload: { task, build, test, runId }
|
|
302
|
+
});
|
|
303
|
+
return test;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
const failure = this.createTesterErrorResult(build.subtaskId, runId, error);
|
|
307
|
+
this.eventBus.emit({
|
|
308
|
+
type: 'test:completed',
|
|
309
|
+
payload: { task, build, test: failure, runId }
|
|
310
|
+
});
|
|
311
|
+
return failure;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async executeReviewer(task, plan, build, test, mode, runId) {
|
|
315
|
+
try {
|
|
316
|
+
const review = await this.options.reviewer.review({ task, plan, build, test, mode, runId });
|
|
317
|
+
this.eventBus.emit({
|
|
318
|
+
type: 'review:completed',
|
|
319
|
+
payload: { task, review, runId }
|
|
320
|
+
});
|
|
321
|
+
return review;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const failure = this.createReviewerErrorResult(error);
|
|
325
|
+
this.eventBus.emit({
|
|
326
|
+
type: 'review:completed',
|
|
327
|
+
payload: { task, review: failure, runId }
|
|
328
|
+
});
|
|
329
|
+
return failure;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
completeFromPlannerFailure(task, plan, runId, error) {
|
|
333
|
+
const mode = 'mcp';
|
|
334
|
+
const cause = error ?? new Error('Planner stage failed without an error payload');
|
|
335
|
+
const build = this.createBuilderErrorResult('planner-unavailable', mode, runId, cause);
|
|
336
|
+
const skippedTest = this.createSkippedTestResult(build, runId);
|
|
337
|
+
const skippedReview = this.createSkippedReviewResult('build-failed');
|
|
338
|
+
const summary = this.createRunSummary(task, mode, plan, build, skippedTest, skippedReview, runId);
|
|
339
|
+
this.eventBus.emit({ type: 'run:completed', payload: summary });
|
|
340
|
+
return summary;
|
|
341
|
+
}
|
|
342
|
+
createPlannerFailurePlan(error) {
|
|
343
|
+
return {
|
|
344
|
+
items: [],
|
|
345
|
+
notes: this.formatStageError('planner', error),
|
|
346
|
+
targetId: null
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
createBuilderErrorResult(subtaskId, mode, runId, error) {
|
|
350
|
+
return {
|
|
351
|
+
subtaskId: subtaskId ?? 'unknown',
|
|
352
|
+
artifacts: [],
|
|
353
|
+
mode,
|
|
354
|
+
runId,
|
|
355
|
+
success: false,
|
|
356
|
+
notes: this.formatStageError('builder', error)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
createTesterErrorResult(subtaskId, runId, error) {
|
|
360
|
+
return {
|
|
361
|
+
subtaskId,
|
|
362
|
+
success: false,
|
|
363
|
+
reports: [
|
|
364
|
+
{
|
|
365
|
+
name: 'tests',
|
|
366
|
+
status: 'failed',
|
|
367
|
+
details: this.formatStageError('tester', error)
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
runId
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
createReviewerErrorResult(error) {
|
|
374
|
+
const summary = this.formatStageError('reviewer', error);
|
|
375
|
+
return {
|
|
376
|
+
summary,
|
|
377
|
+
decision: {
|
|
378
|
+
approved: false,
|
|
379
|
+
feedback: summary
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
formatStageError(stage, error) {
|
|
384
|
+
const message = normalizeErrorMessage(error);
|
|
385
|
+
const normalizedStage = stage.charAt(0).toUpperCase() + stage.slice(1);
|
|
386
|
+
return `${normalizedStage} stage error: ${message}`;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { sanitizeTaskId } from './sanitizeTaskId.js';
|
|
4
|
+
import { sanitizeRunId } from './sanitizeRunId.js';
|
|
5
|
+
async function pathExists(path) {
|
|
6
|
+
try {
|
|
7
|
+
await access(path);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function ensureUniquePath(basePath) {
|
|
18
|
+
let candidate = basePath;
|
|
19
|
+
let attempt = 0;
|
|
20
|
+
const dir = dirname(basePath);
|
|
21
|
+
const original = basename(basePath);
|
|
22
|
+
const extension = extname(original);
|
|
23
|
+
const stem = extension ? original.slice(0, -extension.length) : original;
|
|
24
|
+
while (attempt < 1000) {
|
|
25
|
+
if (!(await pathExists(candidate))) {
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
attempt += 1;
|
|
29
|
+
const suffix = `-${attempt}`;
|
|
30
|
+
const nextName = `${stem}${suffix}${extension}`;
|
|
31
|
+
candidate = join(dir, nextName);
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unable to stage artifact: too many name collisions for ${basePath}`);
|
|
34
|
+
}
|
|
35
|
+
export async function stageArtifacts(params) {
|
|
36
|
+
const { taskId, runId, artifacts, options } = params;
|
|
37
|
+
if (artifacts.length === 0) {
|
|
38
|
+
return artifacts;
|
|
39
|
+
}
|
|
40
|
+
const runsDir = options?.runsDir ?? join(process.cwd(), '.runs');
|
|
41
|
+
const safeTaskId = sanitizeTaskId(taskId);
|
|
42
|
+
const runDir = join(runsDir, safeTaskId, sanitizeRunId(runId));
|
|
43
|
+
const artifactsDir = join(runDir, 'artifacts');
|
|
44
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
45
|
+
const relativeSegments = options?.relativeDir ? sanitizeRelativeDir(options.relativeDir) : [];
|
|
46
|
+
const destinationRoot = relativeSegments.length > 0 ? join(artifactsDir, ...relativeSegments) : artifactsDir;
|
|
47
|
+
await mkdir(destinationRoot, { recursive: true });
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const artifact of artifacts) {
|
|
50
|
+
const sourcePath = resolve(process.cwd(), artifact.path);
|
|
51
|
+
const relativeToArtifacts = relative(artifactsDir, sourcePath);
|
|
52
|
+
const isWithinArtifactsDir = relativeToArtifacts.length > 0 &&
|
|
53
|
+
!relativeToArtifacts.startsWith('..') &&
|
|
54
|
+
!isAbsolute(relativeToArtifacts);
|
|
55
|
+
if (isWithinArtifactsDir) {
|
|
56
|
+
results.push(artifact);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const destinationBase = join(destinationRoot, basename(sourcePath));
|
|
60
|
+
const destinationPath = options?.overwrite
|
|
61
|
+
? await prepareOverwriteDestination(destinationBase)
|
|
62
|
+
: await ensureUniquePath(destinationBase);
|
|
63
|
+
if (options?.keepOriginal) {
|
|
64
|
+
await copyFile(sourcePath, destinationPath);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await rename(sourcePath, destinationPath);
|
|
68
|
+
}
|
|
69
|
+
const relativePath = relative(process.cwd(), destinationPath);
|
|
70
|
+
results.push({
|
|
71
|
+
...artifact,
|
|
72
|
+
path: relativePath
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
async function prepareOverwriteDestination(destinationBase) {
|
|
78
|
+
if (await pathExists(destinationBase)) {
|
|
79
|
+
await rm(destinationBase, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
return destinationBase;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeRelativeDir(relativeDir) {
|
|
84
|
+
const sanitized = relativeDir.replace(/\\+/g, '/');
|
|
85
|
+
const segments = sanitized
|
|
86
|
+
.split('/')
|
|
87
|
+
.map((segment) => segment.trim())
|
|
88
|
+
.filter((segment) => segment.length > 0);
|
|
89
|
+
for (const segment of segments) {
|
|
90
|
+
if (segment === '.' || segment === '..' || segment.includes('..')) {
|
|
91
|
+
throw new Error(`relativeDir contains invalid segment '${segment}'`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return segments;
|
|
95
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFile, mkdir, rm, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { acquireLockWithRetry } from './lockFile.js';
|
|
5
|
+
import { sanitizeTaskId } from './sanitizeTaskId.js';
|
|
6
|
+
import { writeAtomicFile } from './writeAtomicFile.js';
|
|
7
|
+
export class ExperienceStoreLockError extends Error {
|
|
8
|
+
taskId;
|
|
9
|
+
constructor(message, taskId) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.taskId = taskId;
|
|
12
|
+
this.name = 'ExperienceStoreLockError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const DEFAULT_MAX_WORDS = 32;
|
|
16
|
+
const HEX_STAMP_PATTERN = /^[a-f0-9]{64}$/i;
|
|
17
|
+
export class ExperienceStore {
|
|
18
|
+
outDir;
|
|
19
|
+
runsDir;
|
|
20
|
+
maxWords;
|
|
21
|
+
lockRetry;
|
|
22
|
+
now;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.outDir = options.outDir ?? join(process.cwd(), 'out');
|
|
25
|
+
this.runsDir = options.runsDir ?? join(process.cwd(), '.runs');
|
|
26
|
+
this.maxWords = Math.max(1, options.maxSummaryWords ?? DEFAULT_MAX_WORDS);
|
|
27
|
+
const defaults = {
|
|
28
|
+
maxAttempts: 5,
|
|
29
|
+
initialDelayMs: 100,
|
|
30
|
+
backoffFactor: 2,
|
|
31
|
+
maxDelayMs: 1000
|
|
32
|
+
};
|
|
33
|
+
const overrides = options.lockRetry ?? {};
|
|
34
|
+
const sanitizedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
35
|
+
this.lockRetry = { ...defaults, ...sanitizedOverrides };
|
|
36
|
+
this.now = options.now ?? (() => new Date());
|
|
37
|
+
}
|
|
38
|
+
async recordBatch(inputs, manifestPath) {
|
|
39
|
+
if (inputs.length === 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const taskIds = new Set(inputs.map((input) => input.taskId));
|
|
43
|
+
if (taskIds.size !== 1) {
|
|
44
|
+
throw new Error('Experience batches must target a single taskId.');
|
|
45
|
+
}
|
|
46
|
+
const taskId = sanitizeTaskId(inputs[0].taskId);
|
|
47
|
+
const lockPath = this.buildLockPath(taskId);
|
|
48
|
+
await this.acquireLock(taskId, lockPath);
|
|
49
|
+
try {
|
|
50
|
+
const targetDir = join(this.outDir, taskId);
|
|
51
|
+
await mkdir(targetDir, { recursive: true });
|
|
52
|
+
const filePath = join(targetDir, 'experiences.jsonl');
|
|
53
|
+
const existing = await this.readRecords(filePath);
|
|
54
|
+
const nextRecords = inputs.map((input) => this.prepareRecord(input, manifestPath));
|
|
55
|
+
const serialized = [...existing, ...nextRecords].map((record) => JSON.stringify(record)).join('\n');
|
|
56
|
+
await writeAtomicFile(filePath, `${serialized}\n`);
|
|
57
|
+
return nextRecords;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await this.releaseLock(lockPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async fetchTop(params) {
|
|
64
|
+
const safeDomain = params.domain.trim();
|
|
65
|
+
if (!safeDomain) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const taskFilter = params.taskId ? sanitizeTaskId(params.taskId) : null;
|
|
69
|
+
const limit = Math.max(0, params.limit);
|
|
70
|
+
if (limit === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const allRecords = taskFilter
|
|
74
|
+
? await this.readRecords(join(this.outDir, taskFilter, 'experiences.jsonl'))
|
|
75
|
+
: await this.readAllRecords();
|
|
76
|
+
const filtered = allRecords.filter((record) => {
|
|
77
|
+
if (record.domain !== safeDomain) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (taskFilter && record.taskId !== taskFilter) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
const scored = filtered
|
|
86
|
+
.map((record) => ({
|
|
87
|
+
record,
|
|
88
|
+
score: record.reward.gtScore + record.reward.relativeRank
|
|
89
|
+
}))
|
|
90
|
+
.filter((entry) => {
|
|
91
|
+
if (params.minReward === undefined) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return entry.score >= params.minReward;
|
|
95
|
+
})
|
|
96
|
+
.sort((a, b) => {
|
|
97
|
+
if (b.score === a.score) {
|
|
98
|
+
return b.record.createdAt.localeCompare(a.record.createdAt);
|
|
99
|
+
}
|
|
100
|
+
return b.score - a.score;
|
|
101
|
+
});
|
|
102
|
+
return scored.slice(0, limit).map((entry) => entry.record);
|
|
103
|
+
}
|
|
104
|
+
verifyStamp(record) {
|
|
105
|
+
return HEX_STAMP_PATTERN.test(record.stampSignature);
|
|
106
|
+
}
|
|
107
|
+
prepareRecord(input, manifestPath) {
|
|
108
|
+
if (!HEX_STAMP_PATTERN.test(input.stampSignature)) {
|
|
109
|
+
throw new Error(`Experience stamp ${input.stampSignature} is not a valid SHA-256 digest`);
|
|
110
|
+
}
|
|
111
|
+
if (!input.domain || !input.domain.trim()) {
|
|
112
|
+
throw new Error('Experience domain cannot be empty.');
|
|
113
|
+
}
|
|
114
|
+
const summary32 = truncateSummary(input.summary, this.maxWords);
|
|
115
|
+
const reward = {
|
|
116
|
+
gtScore: Number.isFinite(input.reward.gtScore) ? input.reward.gtScore : 0,
|
|
117
|
+
relativeRank: Number.isFinite(input.reward.relativeRank) ? input.reward.relativeRank : 0
|
|
118
|
+
};
|
|
119
|
+
const toolStats = input.toolStats.map((stat) => ({
|
|
120
|
+
tool: stat.tool,
|
|
121
|
+
tokens: normalizeNumber(stat.tokens),
|
|
122
|
+
latencyMs: normalizeNumber(stat.latencyMs),
|
|
123
|
+
costUsd: normalizeNumber(stat.costUsd)
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
id: this.generateId(),
|
|
127
|
+
runId: input.runId,
|
|
128
|
+
taskId: sanitizeTaskId(input.taskId),
|
|
129
|
+
epoch: input.epoch ?? null,
|
|
130
|
+
groupId: input.groupId ?? null,
|
|
131
|
+
summary32,
|
|
132
|
+
reward,
|
|
133
|
+
toolStats,
|
|
134
|
+
stampSignature: input.stampSignature,
|
|
135
|
+
domain: input.domain.trim(),
|
|
136
|
+
createdAt: this.now().toISOString(),
|
|
137
|
+
manifestPath
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
buildLockPath(taskId) {
|
|
141
|
+
return join(this.runsDir, `${taskId}.experiences.lock`);
|
|
142
|
+
}
|
|
143
|
+
async acquireLock(taskId, lockPath) {
|
|
144
|
+
await acquireLockWithRetry({
|
|
145
|
+
taskId,
|
|
146
|
+
lockPath,
|
|
147
|
+
retry: this.lockRetry,
|
|
148
|
+
ensureDirectory: async () => {
|
|
149
|
+
await mkdir(this.runsDir, { recursive: true });
|
|
150
|
+
},
|
|
151
|
+
createError: (id, attempts) => new ExperienceStoreLockError(`Failed to acquire experience lock for ${id} after ${attempts} attempts`, id)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async releaseLock(lockPath) {
|
|
155
|
+
await rm(lockPath, { force: true });
|
|
156
|
+
}
|
|
157
|
+
async readRecords(filePath) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = await readFile(filePath, 'utf8');
|
|
160
|
+
return raw
|
|
161
|
+
.split('\n')
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.map((line) => JSON.parse(line));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error.code === 'ENOENT') {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async readAllRecords() {
|
|
173
|
+
const directories = await listDirectories(this.outDir);
|
|
174
|
+
const all = [];
|
|
175
|
+
for (const dir of directories) {
|
|
176
|
+
const filePath = join(this.outDir, dir, 'experiences.jsonl');
|
|
177
|
+
all.push(...(await this.readRecords(filePath)));
|
|
178
|
+
}
|
|
179
|
+
return all;
|
|
180
|
+
}
|
|
181
|
+
generateId() {
|
|
182
|
+
const suffix = randomBytes(3).toString('hex');
|
|
183
|
+
return `exp-${Date.now().toString(36)}-${suffix}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function listDirectories(path) {
|
|
187
|
+
try {
|
|
188
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
189
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error.code === 'ENOENT') {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function truncateSummary(value, maxWords) {
|
|
199
|
+
const tokens = value.trim().split(/\s+/u).filter(Boolean);
|
|
200
|
+
if (tokens.length <= maxWords) {
|
|
201
|
+
return tokens.join(' ');
|
|
202
|
+
}
|
|
203
|
+
return tokens.slice(0, maxWords).join(' ');
|
|
204
|
+
}
|
|
205
|
+
function normalizeNumber(value) {
|
|
206
|
+
if (!Number.isFinite(value)) {
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|