@shapeshift-labs/frontier-swarm-codex 0.1.1 → 0.3.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/README.md +38 -4
- package/benchmarks/package-bench.mjs +19 -2
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +175 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +649 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmPlan, createSwarmProof, createSwarmRun, recordSwarmEvent } from '@shapeshift-labs/frontier-swarm';
|
|
4
|
+
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmMergeBundle, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
|
|
5
5
|
export const FRONTIER_SWARM_CODEX_DEFAULT_MODEL = FRONTIER_SWARM_DEFAULT_MODEL;
|
|
6
6
|
export const FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT = FRONTIER_SWARM_DEFAULT_REASONING_EFFORT;
|
|
7
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND = 'frontier.swarm-codex.workspace-manifest';
|
|
8
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION = 1;
|
|
9
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND = 'frontier.swarm-codex.workspace-proof';
|
|
10
|
+
export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION = 1;
|
|
11
|
+
export const FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND = 'frontier.swarm-codex.pid-manifest';
|
|
12
|
+
export const FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION = 1;
|
|
13
|
+
export const FRONTIER_SWARM_CODEX_COLLECTION_KIND = 'frontier.swarm-codex.collection';
|
|
14
|
+
export const FRONTIER_SWARM_CODEX_COLLECTION_VERSION = 1;
|
|
7
15
|
const DEFAULT_WORKSPACE_INCLUDES = ['AGENTS.md', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'config'];
|
|
8
16
|
const DEFAULT_WORKSPACE_EXCLUDES = [
|
|
9
17
|
'.git',
|
|
@@ -70,15 +78,24 @@ export function coerceCodexSwarmTasksInput(value) {
|
|
|
70
78
|
lane: typeof task.lane === 'string' ? task.lane : undefined,
|
|
71
79
|
layer: typeof task.layer === 'string' ? task.layer : undefined,
|
|
72
80
|
compute: typeof task.compute === 'string' ? task.compute : undefined,
|
|
81
|
+
dependsOn: readStringArray(task.dependsOn),
|
|
82
|
+
concurrencyKey: typeof task.concurrencyKey === 'string' ? task.concurrencyKey : undefined,
|
|
83
|
+
budget: isObject(task.budget) ? task.budget : undefined,
|
|
84
|
+
review: isObject(task.review) ? task.review : undefined,
|
|
73
85
|
priority: typeof task.priority === 'number' ? task.priority : undefined,
|
|
74
86
|
sourceRefs: readStringArray(task.sourceRefs).concat(readStringArray(task.legacySourcePaths)),
|
|
75
87
|
targetRefs: readStringArray(task.targetRefs).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
|
|
76
88
|
allowedWrites: readStringArray(task.allowedWrites).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
|
|
89
|
+
ownershipRegions: Array.isArray(task.ownershipRegions) ? task.ownershipRegions : [],
|
|
90
|
+
ownedRegions: readStringArray(task.ownedRegions),
|
|
91
|
+
changedRegions: readStringArray(task.changedRegions),
|
|
77
92
|
acceptance: readStringArray(task.acceptance),
|
|
78
93
|
acceptanceChecks: Array.isArray(task.acceptanceChecks) ? task.acceptanceChecks : undefined,
|
|
79
94
|
verification: Array.isArray(task.verification) ? task.verification : undefined,
|
|
80
95
|
evidenceCommand: typeof task.evidenceCommand === 'string' ? task.evidenceCommand : undefined,
|
|
81
96
|
shardCommand: typeof task.shardCommand === 'string' ? task.shardCommand : undefined,
|
|
97
|
+
capabilities: readStringArray(task.capabilities),
|
|
98
|
+
resourceRequirements: isObject(task.resourceRequirements) ? task.resourceRequirements : undefined,
|
|
82
99
|
tags: readStringArray(task.tags),
|
|
83
100
|
metadata: { source: task }
|
|
84
101
|
};
|
|
@@ -88,26 +105,87 @@ export async function runCodexSwarm(plan, options) {
|
|
|
88
105
|
const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir);
|
|
89
106
|
await fs.mkdir(outDir, { recursive: true });
|
|
90
107
|
await fs.writeFile(path.join(outDir, 'swarm-plan.json'), JSON.stringify(plan, null, 2) + '\n');
|
|
108
|
+
const eventStream = options.eventStream ?? createSwarmEventStream({
|
|
109
|
+
runId: plan.runId,
|
|
110
|
+
root: path.join(outDir, 'streams'),
|
|
111
|
+
lanes: Array.from(new Set(plan.jobs.map((job) => job.lane)))
|
|
112
|
+
});
|
|
113
|
+
await initFileSwarmEventStream(eventStream);
|
|
114
|
+
const pidManifestPath = path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'));
|
|
115
|
+
await appendCodexPidManifest(pidManifestPath, { pid: process.pid, role: 'parent', runId: plan.runId, startedAt: Date.now() }, plan.runId);
|
|
91
116
|
let run = createSwarmRun({ plan, status: 'running', startedAt: Date.now() });
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
const startedEvent = { type: 'swarm.started', runId: run.id, at: run.startedAt, data: { jobCount: plan.jobs.length } };
|
|
118
|
+
run = recordSwarmEvent(run, startedEvent);
|
|
119
|
+
await appendFileSwarmEvent(eventStream, startedEvent);
|
|
120
|
+
const runOptions = { ...options, eventStream, pidManifestPath };
|
|
121
|
+
const results = await runScheduledJobPool(plan, Math.max(1, options.maxConcurrency ?? 1), (job, lease) => runCodexJob(job, runOptions, outDir, lease));
|
|
122
|
+
for (const result of results) {
|
|
123
|
+
const job = plan.jobs.find((entry) => entry.id === result.jobId);
|
|
124
|
+
if (job) {
|
|
125
|
+
await options.onJobFinished?.({ job, result });
|
|
126
|
+
await appendFileSwarmEvent(eventStream, {
|
|
127
|
+
type: 'agent.finished',
|
|
128
|
+
runId: run.id,
|
|
129
|
+
jobId: job.id,
|
|
130
|
+
taskId: job.taskId,
|
|
131
|
+
lane: job.lane,
|
|
132
|
+
data: { status: result.status, mergeReadiness: result.mergeReadiness, changedPathCount: result.changedPaths?.length ?? 0 }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
94
136
|
for (const result of results)
|
|
95
137
|
run = completeSwarmJob(run, result);
|
|
96
138
|
const proof = createSwarmProof(run, { validation: plan.validation });
|
|
97
139
|
const ok = run.summary.failedCount === 0 && run.summary.blockedCount === 0 && run.summary.ownershipViolationCount === 0;
|
|
140
|
+
await appendFileSwarmEvent(eventStream, {
|
|
141
|
+
type: 'swarm.finished',
|
|
142
|
+
runId: run.id,
|
|
143
|
+
data: { ok, summary: run.summary }
|
|
144
|
+
});
|
|
98
145
|
await fs.writeFile(path.join(outDir, 'swarm-results.json'), JSON.stringify({ ok, outDir, run, proof }, null, 2) + '\n');
|
|
99
|
-
|
|
146
|
+
await writeSwarmCoordinatorSnapshot(options.coordinatorSnapshotPath ? path.resolve(options.cwd ?? process.cwd(), options.coordinatorSnapshotPath) : path.join(outDir, 'coordinator-dashboard.json'), {
|
|
147
|
+
ok,
|
|
148
|
+
outDir,
|
|
149
|
+
plan,
|
|
150
|
+
run,
|
|
151
|
+
proof,
|
|
152
|
+
eventStream,
|
|
153
|
+
pidManifestPath
|
|
154
|
+
});
|
|
155
|
+
const result = { ok, outDir, plan, run, proof };
|
|
156
|
+
await options.onSwarmFinished?.({ result });
|
|
157
|
+
return result;
|
|
100
158
|
}
|
|
101
|
-
export async function runCodexJob(job, options, outDir) {
|
|
102
|
-
const paths = await createJobPaths(outDir, job);
|
|
159
|
+
export async function runCodexJob(job, options, outDir, lease) {
|
|
160
|
+
const paths = await createJobPaths(outDir, job, options);
|
|
103
161
|
const workspace = await prepareCodexWorkspace(job, options);
|
|
104
162
|
const workspacePlan = createCodexWorkspacePlan(job, options);
|
|
163
|
+
const hookInput = {
|
|
164
|
+
job,
|
|
165
|
+
cwd: options.cwd ?? process.cwd(),
|
|
166
|
+
outDir,
|
|
167
|
+
workspacePath: workspace,
|
|
168
|
+
workspacePlan,
|
|
169
|
+
paths
|
|
170
|
+
};
|
|
171
|
+
await options.prepareJobWorkspace?.(hookInput);
|
|
105
172
|
const fileSnapshot = shouldSnapshotWorkspaceChanges(workspacePlan, options)
|
|
106
173
|
? await snapshotWorkspaceFiles(workspace)
|
|
107
174
|
: undefined;
|
|
108
|
-
const
|
|
175
|
+
const basePrompt = renderCodexPrompt(job, { workspacePath: workspace, paths });
|
|
176
|
+
const prompt = options.renderJobPrompt
|
|
177
|
+
? await options.renderJobPrompt({ ...hookInput, prompt: basePrompt })
|
|
178
|
+
: basePrompt;
|
|
109
179
|
await fs.writeFile(paths.promptPath, prompt);
|
|
110
180
|
const args = buildCodexArgs(job, { ...options, workspacePath: workspace, paths });
|
|
181
|
+
await options.onJobStarted?.({ ...hookInput, prompt, args });
|
|
182
|
+
await appendFileSwarmEvent(options.eventStream, {
|
|
183
|
+
type: 'agent.scheduled',
|
|
184
|
+
jobId: job.id,
|
|
185
|
+
taskId: job.taskId,
|
|
186
|
+
lane: job.lane,
|
|
187
|
+
data: { workspace: workspacePlan.path, capabilities: job.capabilities, resourceRequirements: job.resourceRequirements }
|
|
188
|
+
});
|
|
111
189
|
const startedAt = Date.now();
|
|
112
190
|
const execution = options.dryRun
|
|
113
191
|
? { exitCode: 0, changedPaths: [] }
|
|
@@ -121,12 +199,27 @@ export async function runCodexJob(job, options, outDir) {
|
|
|
121
199
|
paths,
|
|
122
200
|
timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
|
|
123
201
|
});
|
|
124
|
-
const
|
|
202
|
+
const collected = execution.changedPaths
|
|
203
|
+
? filterWorkspaceChangedPaths(execution.changedPaths, workspacePlan)
|
|
204
|
+
: options.collectGitStatus === false
|
|
205
|
+
? { changedPaths: [], ignoredChangedPaths: [] }
|
|
206
|
+
: await collectChangedPaths(workspace, fileSnapshot, workspacePlan);
|
|
207
|
+
const rawChangedPaths = collected.changedPaths;
|
|
208
|
+
const changedPaths = options.changedPathFilter ? [...options.changedPathFilter(rawChangedPaths, hookInput)] : rawChangedPaths;
|
|
209
|
+
const workspaceProof = await createSwarmWorkspaceProof(workspacePlan, { ignoredChangedPaths: collected.ignoredChangedPaths });
|
|
210
|
+
await fs.writeFile(paths.workspaceProofPath, JSON.stringify(workspaceProof, null, 2) + '\n');
|
|
125
211
|
const ownership = checkSwarmOwnership(job, changedPaths);
|
|
126
212
|
const verification = options.runVerification ? await runVerification(job.verification, workspace) : [];
|
|
127
213
|
const failedVerification = verification.some((entry) => entry.required !== false && entry.status !== 0);
|
|
128
214
|
const status = ownership.ok && execution.exitCode === 0 && !failedVerification ? 'completed' : 'failed';
|
|
129
|
-
|
|
215
|
+
const patchPath = await writeCodexPatchFile({
|
|
216
|
+
workspace,
|
|
217
|
+
sourceRoot: path.resolve(options.cwd ?? process.cwd()),
|
|
218
|
+
paths,
|
|
219
|
+
workspacePlan,
|
|
220
|
+
changedPaths
|
|
221
|
+
});
|
|
222
|
+
const result = {
|
|
130
223
|
jobId: job.id,
|
|
131
224
|
status,
|
|
132
225
|
startedAt,
|
|
@@ -134,18 +227,31 @@ export async function runCodexJob(job, options, outDir) {
|
|
|
134
227
|
exitCode: execution.exitCode,
|
|
135
228
|
signal: execution.signal,
|
|
136
229
|
changedPaths,
|
|
230
|
+
changedRegions: job.changedRegions,
|
|
137
231
|
ownershipViolations: ownership.violations,
|
|
138
|
-
evidencePaths: [paths.evidenceDir],
|
|
232
|
+
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
|
|
233
|
+
...(patchPath ? { patchPath } : {}),
|
|
234
|
+
queueItemIds: [job.taskId],
|
|
139
235
|
verification,
|
|
140
236
|
lastMessage: execution.lastMessage,
|
|
141
|
-
error: execution.error
|
|
237
|
+
error: execution.error,
|
|
238
|
+
metadata: lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : undefined
|
|
142
239
|
};
|
|
240
|
+
const mergeBundle = createSwarmMergeBundle({
|
|
241
|
+
runId: options.eventStream?.runId,
|
|
242
|
+
job,
|
|
243
|
+
result,
|
|
244
|
+
...(patchPath ? { patchPath } : {}),
|
|
245
|
+
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath],
|
|
246
|
+
queueItemIds: [job.taskId]
|
|
247
|
+
});
|
|
248
|
+
await fs.writeFile(paths.mergeBundlePath, JSON.stringify(mergeBundle, null, 2) + '\n');
|
|
249
|
+
return result;
|
|
143
250
|
}
|
|
144
251
|
export function buildCodexArgs(job, input) {
|
|
145
|
-
const model = job
|
|
146
|
-
const effort = job
|
|
252
|
+
const model = resolveCodexModelFlag(job, input);
|
|
253
|
+
const effort = resolveCodexReasoningEffort(job, input);
|
|
147
254
|
const sandbox = job.compute.sandbox ?? input.sandbox ?? 'workspace-write';
|
|
148
|
-
const approval = job.compute.approval ?? input.approval ?? 'never';
|
|
149
255
|
const args = [
|
|
150
256
|
'exec',
|
|
151
257
|
'--cd',
|
|
@@ -154,16 +260,17 @@ export function buildCodexArgs(job, input) {
|
|
|
154
260
|
path.resolve(input.cwd ?? process.cwd(), input.outDir),
|
|
155
261
|
'--sandbox',
|
|
156
262
|
sandbox,
|
|
157
|
-
'--ask-for-approval',
|
|
158
|
-
approval,
|
|
159
263
|
'--json',
|
|
160
264
|
'--output-last-message',
|
|
161
|
-
input.paths.lastMessagePath
|
|
162
|
-
'--model',
|
|
163
|
-
model,
|
|
164
|
-
'-c',
|
|
165
|
-
`model_reasoning_effort="${effort}"`
|
|
265
|
+
input.paths.lastMessagePath
|
|
166
266
|
];
|
|
267
|
+
if (model)
|
|
268
|
+
args.push('--model', model);
|
|
269
|
+
if (effort)
|
|
270
|
+
args.push('-c', `model_reasoning_effort="${effort}"`);
|
|
271
|
+
const approval = normalizeCodexApprovalPolicy(input.approval);
|
|
272
|
+
if (approval)
|
|
273
|
+
args.push('--ask-for-approval', approval);
|
|
167
274
|
if (shouldSkipGitRepoCheck(input))
|
|
168
275
|
args.push('--skip-git-repo-check');
|
|
169
276
|
for (const dir of input.addDirs ?? [])
|
|
@@ -176,6 +283,56 @@ export function buildCodexArgs(job, input) {
|
|
|
176
283
|
args.push('-');
|
|
177
284
|
return args;
|
|
178
285
|
}
|
|
286
|
+
export function normalizeCodexModelFlag(model) {
|
|
287
|
+
if (model === false || model == null)
|
|
288
|
+
return undefined;
|
|
289
|
+
const value = String(model).trim();
|
|
290
|
+
if (!value)
|
|
291
|
+
return undefined;
|
|
292
|
+
const normalized = value.toLowerCase();
|
|
293
|
+
if (normalized === 'auto' || normalized === 'default' || normalized === 'config' || normalized === 'config-default') {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
export function normalizeCodexApprovalPolicy(approval) {
|
|
299
|
+
if (approval === false || approval == null)
|
|
300
|
+
return undefined;
|
|
301
|
+
const value = String(approval).trim().toLowerCase().replaceAll('_', '-');
|
|
302
|
+
if (!value || value === 'default' || value === 'config-default')
|
|
303
|
+
return undefined;
|
|
304
|
+
if (value === 'never' || value === 'none' || value === 'off' || value === 'false' || value === 'full-auto')
|
|
305
|
+
return 'never';
|
|
306
|
+
if (value === 'untrusted')
|
|
307
|
+
return 'untrusted';
|
|
308
|
+
if (value === 'on-failure')
|
|
309
|
+
return 'on-failure';
|
|
310
|
+
if (value === 'on-request' || value === 'request' || value === 'manual')
|
|
311
|
+
return 'on-request';
|
|
312
|
+
throw new Error(`unsupported Codex approval policy "${approval}"; expected untrusted, on-request, on-failure, never, full-auto, none, or default`);
|
|
313
|
+
}
|
|
314
|
+
function resolveCodexModelFlag(job, input) {
|
|
315
|
+
const explicit = normalizeCodexModelFlag(input.model);
|
|
316
|
+
if (explicit || input.model === false)
|
|
317
|
+
return explicit;
|
|
318
|
+
const policy = input.modelPolicy ?? (input.forwardPlanModel ? 'plan' : 'config-default');
|
|
319
|
+
if (policy === 'plan')
|
|
320
|
+
return normalizeCodexModelFlag(job.compute.model ?? FRONTIER_SWARM_CODEX_DEFAULT_MODEL);
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
function resolveCodexReasoningEffort(job, input) {
|
|
324
|
+
if (input.reasoningEffort === false)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (typeof input.reasoningEffort === 'string') {
|
|
327
|
+
const explicit = input.reasoningEffort.trim();
|
|
328
|
+
return explicit && explicit !== 'default' && explicit !== 'config-default' ? explicit : undefined;
|
|
329
|
+
}
|
|
330
|
+
const policy = input.modelPolicy ?? (input.forwardPlanModel || input.forwardPlanReasoningEffort ? 'plan' : 'config-default');
|
|
331
|
+
if (policy !== 'plan')
|
|
332
|
+
return undefined;
|
|
333
|
+
const effort = job.compute.reasoningEffort ?? FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT;
|
|
334
|
+
return effort ? String(effort).trim() : undefined;
|
|
335
|
+
}
|
|
179
336
|
export function renderCodexPrompt(job, input) {
|
|
180
337
|
return [
|
|
181
338
|
'# Frontier Swarm Codex Job',
|
|
@@ -202,6 +359,12 @@ export function renderCodexPrompt(job, input) {
|
|
|
202
359
|
'',
|
|
203
360
|
job.task.objective,
|
|
204
361
|
'',
|
|
362
|
+
'Dependencies:',
|
|
363
|
+
...bullets(job.dependsOn),
|
|
364
|
+
'',
|
|
365
|
+
'Budget:',
|
|
366
|
+
...bullets(formatBudget(job)),
|
|
367
|
+
'',
|
|
205
368
|
'Source refs:',
|
|
206
369
|
...bullets(job.task.sourceRefs),
|
|
207
370
|
'',
|
|
@@ -229,6 +392,15 @@ export async function spawnCodexExecutor(input) {
|
|
|
229
392
|
await fs.writeFile(input.paths.stderrPath, '');
|
|
230
393
|
return new Promise((resolve) => {
|
|
231
394
|
const child = spawn(input.codexPath, input.args, { cwd: input.cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
395
|
+
if (child.pid) {
|
|
396
|
+
appendCodexPidManifest(input.paths.pidManifestPath, {
|
|
397
|
+
pid: child.pid,
|
|
398
|
+
role: 'codex',
|
|
399
|
+
jobId: input.job.id,
|
|
400
|
+
startedAt: Date.now(),
|
|
401
|
+
command: [input.codexPath, ...input.args]
|
|
402
|
+
}).catch(() => { });
|
|
403
|
+
}
|
|
232
404
|
const timer = setTimeout(() => child.kill('SIGTERM'), input.timeoutMs);
|
|
233
405
|
child.stdout.on('data', (chunk) => fs.appendFile(input.paths.eventsPath, chunk).catch(() => { }));
|
|
234
406
|
child.stderr.on('data', (chunk) => fs.appendFile(input.paths.stderrPath, chunk).catch(() => { }));
|
|
@@ -247,7 +419,7 @@ export async function spawnCodexExecutor(input) {
|
|
|
247
419
|
});
|
|
248
420
|
});
|
|
249
421
|
}
|
|
250
|
-
async function createJobPaths(outDir, job) {
|
|
422
|
+
async function createJobPaths(outDir, job, options) {
|
|
251
423
|
const jobDir = path.join(outDir, job.id);
|
|
252
424
|
const paths = {
|
|
253
425
|
jobDir,
|
|
@@ -255,7 +427,11 @@ async function createJobPaths(outDir, job) {
|
|
|
255
427
|
eventsPath: path.join(jobDir, 'codex-events.jsonl'),
|
|
256
428
|
stderrPath: path.join(jobDir, 'codex-stderr.log'),
|
|
257
429
|
lastMessagePath: path.join(jobDir, 'last-message.md'),
|
|
258
|
-
evidenceDir: path.join(jobDir, 'evidence')
|
|
430
|
+
evidenceDir: path.join(jobDir, 'evidence'),
|
|
431
|
+
workspaceProofPath: path.join(jobDir, 'evidence', 'workspace-proof.json'),
|
|
432
|
+
patchPath: path.join(jobDir, 'evidence', 'changes.patch'),
|
|
433
|
+
mergeBundlePath: path.join(jobDir, 'evidence', 'merge.json'),
|
|
434
|
+
pidManifestPath: path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'))
|
|
259
435
|
};
|
|
260
436
|
await fs.mkdir(paths.evidenceDir, { recursive: true });
|
|
261
437
|
return paths;
|
|
@@ -277,6 +453,7 @@ export async function prepareCodexWorkspace(job, options) {
|
|
|
277
453
|
if (await pathExists(plan.path)) {
|
|
278
454
|
if (!plan.replace)
|
|
279
455
|
return plan.path;
|
|
456
|
+
assertGeneratedWorkspacePath(plan);
|
|
280
457
|
await fs.rm(plan.path, { recursive: true, force: true });
|
|
281
458
|
}
|
|
282
459
|
await fs.mkdir(plan.path, { recursive: true });
|
|
@@ -306,6 +483,10 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
306
483
|
excludes: [],
|
|
307
484
|
artifactIncludes: [],
|
|
308
485
|
linkPaths: [],
|
|
486
|
+
requiredIncludes: [],
|
|
487
|
+
optionalIncludes: [],
|
|
488
|
+
strategy: workspace.strategy ?? 'fs-cp',
|
|
489
|
+
...(workspace.guardRoot ? { guardRoot: path.resolve(cwd, workspace.guardRoot) } : {}),
|
|
309
490
|
linkNodeModules: false,
|
|
310
491
|
replace: false,
|
|
311
492
|
skipGitRepoCheck: workspace.skipGitRepoCheck ?? false
|
|
@@ -333,6 +514,16 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
333
514
|
...readStringArray(rawTask.snapshotLinkPaths),
|
|
334
515
|
...readStringArray(rawTask.linkPaths)
|
|
335
516
|
]);
|
|
517
|
+
const requiredIncludes = uniqueWorkspacePaths([
|
|
518
|
+
...readStringArray(workspace.requiredIncludes),
|
|
519
|
+
...readStringArray(rawTask.requiredIncludes),
|
|
520
|
+
...readStringArray(rawTask.snapshotRequiredIncludes)
|
|
521
|
+
]);
|
|
522
|
+
const optionalIncludes = uniqueWorkspacePaths([
|
|
523
|
+
...readStringArray(workspace.optionalIncludes),
|
|
524
|
+
...readStringArray(rawTask.optionalIncludes),
|
|
525
|
+
...readStringArray(rawTask.snapshotOptionalIncludes)
|
|
526
|
+
]);
|
|
336
527
|
return {
|
|
337
528
|
mode,
|
|
338
529
|
root,
|
|
@@ -341,11 +532,231 @@ export function createCodexWorkspacePlan(job, options) {
|
|
|
341
532
|
excludes,
|
|
342
533
|
artifactIncludes,
|
|
343
534
|
linkPaths,
|
|
535
|
+
requiredIncludes,
|
|
536
|
+
optionalIncludes,
|
|
537
|
+
strategy: workspace.strategy ?? 'fs-cp',
|
|
538
|
+
guardRoot: path.resolve(cwd, workspace.guardRoot ?? workspace.root ?? path.join('agent-worktrees', 'frontier-swarm-codex')),
|
|
344
539
|
linkNodeModules: workspace.linkNodeModules ?? (mode !== 'git-worktree'),
|
|
345
540
|
replace: workspace.replace ?? false,
|
|
346
541
|
skipGitRepoCheck: workspace.skipGitRepoCheck ?? (mode === 'copy' || mode === 'snapshot')
|
|
347
542
|
};
|
|
348
543
|
}
|
|
544
|
+
export function createSwarmWorkspaceManifest(plan) {
|
|
545
|
+
return {
|
|
546
|
+
kind: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND,
|
|
547
|
+
version: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION,
|
|
548
|
+
id: 'codex-workspace:' + stableHash([plan.mode, plan.root, plan.path, plan.includes, plan.linkPaths]),
|
|
549
|
+
mode: plan.mode,
|
|
550
|
+
root: plan.root,
|
|
551
|
+
path: plan.path,
|
|
552
|
+
includes: [...plan.includes],
|
|
553
|
+
excludes: [...plan.excludes],
|
|
554
|
+
artifactIncludes: [...plan.artifactIncludes],
|
|
555
|
+
linkPaths: [...plan.linkPaths],
|
|
556
|
+
requiredIncludes: [...plan.requiredIncludes],
|
|
557
|
+
optionalIncludes: [...plan.optionalIncludes],
|
|
558
|
+
strategy: plan.strategy,
|
|
559
|
+
...(plan.guardRoot ? { guardRoot: plan.guardRoot } : {}),
|
|
560
|
+
linkNodeModules: plan.linkNodeModules,
|
|
561
|
+
skipGitRepoCheck: plan.skipGitRepoCheck
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
export async function createSwarmWorkspaceProof(plan, input = {}) {
|
|
565
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
566
|
+
const manifest = createSwarmWorkspaceManifest(plan);
|
|
567
|
+
const copiedCandidates = uniqueWorkspacePaths([...plan.includes, ...plan.artifactIncludes, ...plan.requiredIncludes]);
|
|
568
|
+
const optionalCandidates = uniqueWorkspacePaths(plan.optionalIncludes);
|
|
569
|
+
const copiedPaths = [];
|
|
570
|
+
const missingRequired = [];
|
|
571
|
+
const missingOptional = [];
|
|
572
|
+
for (const include of copiedCandidates) {
|
|
573
|
+
if (await pathExists(path.join(plan.path, include)))
|
|
574
|
+
copiedPaths.push(include);
|
|
575
|
+
else if (plan.requiredIncludes.includes(include))
|
|
576
|
+
missingRequired.push(include);
|
|
577
|
+
}
|
|
578
|
+
for (const include of optionalCandidates) {
|
|
579
|
+
if (await pathExists(path.join(plan.path, include)))
|
|
580
|
+
copiedPaths.push(include);
|
|
581
|
+
else
|
|
582
|
+
missingOptional.push(include);
|
|
583
|
+
}
|
|
584
|
+
const linkedPaths = [];
|
|
585
|
+
for (const linkPath of uniqueWorkspacePaths([...plan.linkPaths, ...(plan.linkNodeModules ? ['node_modules'] : [])])) {
|
|
586
|
+
const stat = await fs.lstat(path.join(plan.path, linkPath)).catch(() => undefined);
|
|
587
|
+
if (stat?.isSymbolicLink())
|
|
588
|
+
linkedPaths.push(linkPath);
|
|
589
|
+
}
|
|
590
|
+
const ignoredChangedPaths = uniqueWorkspacePaths(input.ignoredChangedPaths ?? []);
|
|
591
|
+
return {
|
|
592
|
+
kind: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND,
|
|
593
|
+
version: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION,
|
|
594
|
+
id: 'codex-workspace-proof:' + stableHash([manifest.id, copiedPaths, linkedPaths, missingRequired, missingOptional, generatedAt]),
|
|
595
|
+
generatedAt,
|
|
596
|
+
manifest,
|
|
597
|
+
copiedPaths: uniqueWorkspacePaths(copiedPaths),
|
|
598
|
+
linkedPaths,
|
|
599
|
+
missingRequired,
|
|
600
|
+
missingOptional,
|
|
601
|
+
ignoredChangedPaths,
|
|
602
|
+
summary: {
|
|
603
|
+
copiedCount: uniqueWorkspacePaths(copiedPaths).length,
|
|
604
|
+
linkedCount: linkedPaths.length,
|
|
605
|
+
missingRequiredCount: missingRequired.length,
|
|
606
|
+
missingOptionalCount: missingOptional.length,
|
|
607
|
+
ignoredChangedPathCount: ignoredChangedPaths.length
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export async function initFileSwarmEventStream(stream) {
|
|
612
|
+
if (!stream)
|
|
613
|
+
return;
|
|
614
|
+
const mailboxes = [stream.global, ...Object.values(stream.lanes)];
|
|
615
|
+
await Promise.all(mailboxes.map(async (mailbox) => {
|
|
616
|
+
if (!mailbox.path)
|
|
617
|
+
return;
|
|
618
|
+
await fs.mkdir(path.dirname(mailbox.path), { recursive: true });
|
|
619
|
+
await fs.writeFile(mailbox.path, '');
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
export async function appendFileSwarmEvent(stream, event) {
|
|
623
|
+
if (!stream)
|
|
624
|
+
return;
|
|
625
|
+
const line = JSON.stringify({ at: Date.now(), ...event }) + '\n';
|
|
626
|
+
const paths = routeSwarmEventToMailboxes(stream, event)
|
|
627
|
+
.map((mailbox) => mailbox.path)
|
|
628
|
+
.filter((mailboxPath) => !!mailboxPath);
|
|
629
|
+
await Promise.all(paths.map(async (mailboxPath) => {
|
|
630
|
+
await fs.mkdir(path.dirname(mailboxPath), { recursive: true });
|
|
631
|
+
await fs.appendFile(mailboxPath, line);
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
export async function writeSwarmCoordinatorSnapshot(file, input) {
|
|
635
|
+
const byLane = input.run.jobs.reduce((acc, job) => {
|
|
636
|
+
const current = acc[job.lane] ?? { total: 0, completed: 0, failed: 0, blocked: 0 };
|
|
637
|
+
current.total += 1;
|
|
638
|
+
const result = input.run.results.find((entry) => entry.jobId === job.id);
|
|
639
|
+
if (result?.status === 'completed' || result?.status === 'verified')
|
|
640
|
+
current.completed += 1;
|
|
641
|
+
else if (result?.status === 'failed')
|
|
642
|
+
current.failed += 1;
|
|
643
|
+
else if (result?.status === 'blocked')
|
|
644
|
+
current.blocked += 1;
|
|
645
|
+
acc[job.lane] = current;
|
|
646
|
+
return acc;
|
|
647
|
+
}, {});
|
|
648
|
+
const mergeReadiness = input.run.results.reduce((acc, result) => {
|
|
649
|
+
acc[result.mergeReadiness] = (acc[result.mergeReadiness] ?? 0) + 1;
|
|
650
|
+
return acc;
|
|
651
|
+
}, {});
|
|
652
|
+
const dashboard = {
|
|
653
|
+
kind: 'frontier.swarm-codex.coordinator-dashboard',
|
|
654
|
+
version: 1,
|
|
655
|
+
generatedAt: new Date().toISOString(),
|
|
656
|
+
ok: input.ok,
|
|
657
|
+
outDir: input.outDir,
|
|
658
|
+
runId: input.run.id,
|
|
659
|
+
planId: input.plan.id,
|
|
660
|
+
summary: input.run.summary,
|
|
661
|
+
byLane,
|
|
662
|
+
mergeReadiness,
|
|
663
|
+
eventStream: input.eventStream ?? null,
|
|
664
|
+
pidManifestPath: input.pidManifestPath ?? null,
|
|
665
|
+
proof: input.proof
|
|
666
|
+
};
|
|
667
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
668
|
+
await fs.writeFile(file, JSON.stringify(dashboard, null, 2) + '\n');
|
|
669
|
+
}
|
|
670
|
+
export async function appendCodexPidManifest(file, entry, runId) {
|
|
671
|
+
const manifest = await readCodexPidManifest(file).catch(() => ({
|
|
672
|
+
kind: FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND,
|
|
673
|
+
version: FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION,
|
|
674
|
+
...(runId ? { runId } : {}),
|
|
675
|
+
entries: []
|
|
676
|
+
}));
|
|
677
|
+
const entries = manifest.entries.filter((existing) => existing.pid !== entry.pid || existing.jobId !== entry.jobId);
|
|
678
|
+
entries.push(entry);
|
|
679
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
680
|
+
await fs.writeFile(file, JSON.stringify({ ...manifest, ...(runId ? { runId } : {}), entries }, null, 2) + '\n');
|
|
681
|
+
}
|
|
682
|
+
export async function readCodexPidManifest(file) {
|
|
683
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
684
|
+
}
|
|
685
|
+
export async function stopCodexSwarmRun(input) {
|
|
686
|
+
const signal = input.signal ?? 'SIGTERM';
|
|
687
|
+
const pidManifestPath = await resolvePidManifestPath(input.run);
|
|
688
|
+
const manifest = await readCodexPidManifest(pidManifestPath);
|
|
689
|
+
const stopped = [];
|
|
690
|
+
const missing = [];
|
|
691
|
+
const errors = [];
|
|
692
|
+
for (const entry of manifest.entries.filter((item) => item.pid !== process.pid).sort((left, right) => right.startedAt - left.startedAt)) {
|
|
693
|
+
try {
|
|
694
|
+
process.kill(entry.pid, signal);
|
|
695
|
+
stopped.push(entry.pid);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
|
|
699
|
+
if (code === 'ESRCH')
|
|
700
|
+
missing.push(entry.pid);
|
|
701
|
+
else
|
|
702
|
+
errors.push({ pid: entry.pid, error: error instanceof Error ? error.message : String(error) });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { ok: errors.length === 0, pidManifestPath, signal, stopped, missing, errors };
|
|
706
|
+
}
|
|
707
|
+
export async function collectCodexSwarmRun(input) {
|
|
708
|
+
const generatedAt = Date.now();
|
|
709
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
710
|
+
const runDir = await resolveRunDirectory(input.run);
|
|
711
|
+
const outDir = path.resolve(cwd, input.outDir ?? path.join(runDir, 'collected'));
|
|
712
|
+
const buckets = {
|
|
713
|
+
'ready-to-apply': [],
|
|
714
|
+
'needs-human-port': [],
|
|
715
|
+
'failed-evidence': [],
|
|
716
|
+
'stale-against-head': []
|
|
717
|
+
};
|
|
718
|
+
const mergePaths = await findFilesByName(runDir, 'merge.json');
|
|
719
|
+
for (const mergePath of mergePaths.sort()) {
|
|
720
|
+
const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
|
|
721
|
+
const staleAgainstHead = input.checkStale === false ? false : await bundlePatchIsStale(bundle, mergePath, cwd);
|
|
722
|
+
const bucket = classifyCodexCollectBucket(bundle, staleAgainstHead);
|
|
723
|
+
const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(bundle.jobId)}` : bundle.branchName;
|
|
724
|
+
const nextBundle = {
|
|
725
|
+
...bundle,
|
|
726
|
+
...(branchName ? { branchName } : {}),
|
|
727
|
+
staleAgainstHead: bundle.staleAgainstHead || staleAgainstHead,
|
|
728
|
+
disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
|
|
729
|
+
autoMergeable: bucket === 'ready-to-apply' && bundle.autoMergeable
|
|
730
|
+
};
|
|
731
|
+
const outputDir = path.join(outDir, bucket, slug(bundle.jobId));
|
|
732
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
733
|
+
await fs.writeFile(path.join(outputDir, 'merge.json'), JSON.stringify(nextBundle, null, 2) + '\n');
|
|
734
|
+
const patchPath = resolveBundlePatchPath(nextBundle, mergePath);
|
|
735
|
+
if (patchPath && await pathExists(patchPath))
|
|
736
|
+
await fs.copyFile(patchPath, path.join(outputDir, 'changes.patch')).catch(() => { });
|
|
737
|
+
buckets[bucket].push({ bucket, jobId: bundle.jobId, mergePath, outputDir, bundle: nextBundle });
|
|
738
|
+
}
|
|
739
|
+
const summary = {
|
|
740
|
+
total: mergePaths.length,
|
|
741
|
+
'ready-to-apply': buckets['ready-to-apply'].length,
|
|
742
|
+
'needs-human-port': buckets['needs-human-port'].length,
|
|
743
|
+
'failed-evidence': buckets['failed-evidence'].length,
|
|
744
|
+
'stale-against-head': buckets['stale-against-head'].length
|
|
745
|
+
};
|
|
746
|
+
const result = {
|
|
747
|
+
kind: FRONTIER_SWARM_CODEX_COLLECTION_KIND,
|
|
748
|
+
version: FRONTIER_SWARM_CODEX_COLLECTION_VERSION,
|
|
749
|
+
ok: summary['failed-evidence'] === 0 && summary['stale-against-head'] === 0,
|
|
750
|
+
runDir,
|
|
751
|
+
outDir,
|
|
752
|
+
generatedAt,
|
|
753
|
+
buckets,
|
|
754
|
+
summary
|
|
755
|
+
};
|
|
756
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
757
|
+
await fs.writeFile(path.join(outDir, 'collection.json'), JSON.stringify(result, null, 2) + '\n');
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
349
760
|
async function copyWorkspacePath(cwd, workspacePath, include, excludes) {
|
|
350
761
|
const relative = normalizeWorkspacePath(include);
|
|
351
762
|
if (!relative)
|
|
@@ -384,6 +795,12 @@ function shouldSkipGitRepoCheck(input) {
|
|
|
384
795
|
return workspace.skipGitRepoCheck;
|
|
385
796
|
return workspace.mode === 'copy' || workspace.mode === 'snapshot';
|
|
386
797
|
}
|
|
798
|
+
function assertGeneratedWorkspacePath(plan) {
|
|
799
|
+
const relative = path.relative(plan.guardRoot ?? plan.root, plan.path);
|
|
800
|
+
if (relative.startsWith('..') || path.isAbsolute(relative) || relative === '') {
|
|
801
|
+
throw new Error(`Refusing to replace workspace outside generated root: ${plan.path}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
387
804
|
function readRawTask(job) {
|
|
388
805
|
const metadata = isObject(job.task.metadata) ? job.task.metadata : {};
|
|
389
806
|
return isObject(metadata.source) ? metadata.source : {};
|
|
@@ -418,12 +835,71 @@ async function gitChangedPaths(cwd) {
|
|
|
418
835
|
return value.includes(' -> ') ? value.split(' -> ') : [value];
|
|
419
836
|
});
|
|
420
837
|
}
|
|
421
|
-
async function collectChangedPaths(cwd, baseline) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return gitPaths;
|
|
838
|
+
async function collectChangedPaths(cwd, baseline, plan) {
|
|
839
|
+
if (!baseline)
|
|
840
|
+
return filterWorkspaceChangedPaths(await gitChangedPaths(cwd), plan);
|
|
425
841
|
const after = await snapshotWorkspaceFiles(cwd);
|
|
426
|
-
return diffWorkspaceFiles(baseline, after);
|
|
842
|
+
return filterWorkspaceChangedPaths(diffWorkspaceFiles(baseline, after), plan);
|
|
843
|
+
}
|
|
844
|
+
async function writeCodexPatchFile(input) {
|
|
845
|
+
await fs.mkdir(path.dirname(input.paths.patchPath), { recursive: true });
|
|
846
|
+
const changedPaths = uniqueWorkspacePaths(input.changedPaths);
|
|
847
|
+
if (changedPaths.length === 0) {
|
|
848
|
+
await fs.writeFile(input.paths.patchPath, '');
|
|
849
|
+
return undefined;
|
|
850
|
+
}
|
|
851
|
+
const diff = input.workspacePlan.mode === 'current' || input.workspacePlan.mode === 'git-worktree'
|
|
852
|
+
? await gitDiffPatch(input.workspace, changedPaths)
|
|
853
|
+
: await noIndexWorkspacePatch(input.sourceRoot, input.workspace, changedPaths);
|
|
854
|
+
await fs.writeFile(input.paths.patchPath, diff);
|
|
855
|
+
return diff.trim().length ? input.paths.patchPath : undefined;
|
|
856
|
+
}
|
|
857
|
+
async function gitDiffPatch(workspace, changedPaths) {
|
|
858
|
+
const result = await runProcess('git', ['diff', '--', ...changedPaths], { cwd: workspace, allowFailure: true });
|
|
859
|
+
return result.stdout;
|
|
860
|
+
}
|
|
861
|
+
async function noIndexWorkspacePatch(sourceRoot, workspace, changedPaths) {
|
|
862
|
+
const chunks = [];
|
|
863
|
+
for (const file of changedPaths) {
|
|
864
|
+
const source = path.join(sourceRoot, file);
|
|
865
|
+
const target = path.join(workspace, file);
|
|
866
|
+
const sourceExists = await pathExists(source);
|
|
867
|
+
const targetExists = await pathExists(target);
|
|
868
|
+
if (!sourceExists && !targetExists)
|
|
869
|
+
continue;
|
|
870
|
+
const left = sourceExists ? source : '/dev/null';
|
|
871
|
+
const right = targetExists ? target : '/dev/null';
|
|
872
|
+
const result = await runProcess('git', ['diff', '--no-index', '--', left, right], { cwd: sourceRoot, allowFailure: true });
|
|
873
|
+
if (result.stdout.trim())
|
|
874
|
+
chunks.push(result.stdout);
|
|
875
|
+
}
|
|
876
|
+
return chunks.join('\n');
|
|
877
|
+
}
|
|
878
|
+
function filterWorkspaceChangedPaths(paths, plan) {
|
|
879
|
+
const changedPaths = [];
|
|
880
|
+
const ignoredChangedPaths = [];
|
|
881
|
+
for (const file of uniqueWorkspacePaths(paths)) {
|
|
882
|
+
if (isIgnoredWorkspaceChangedPath(file, plan))
|
|
883
|
+
ignoredChangedPaths.push(file);
|
|
884
|
+
else
|
|
885
|
+
changedPaths.push(file);
|
|
886
|
+
}
|
|
887
|
+
return { changedPaths, ignoredChangedPaths };
|
|
888
|
+
}
|
|
889
|
+
function isIgnoredWorkspaceChangedPath(file, plan) {
|
|
890
|
+
if (plan.mode !== 'copy' && plan.mode !== 'snapshot')
|
|
891
|
+
return false;
|
|
892
|
+
const ignored = [
|
|
893
|
+
...plan.excludes,
|
|
894
|
+
...plan.artifactIncludes,
|
|
895
|
+
...plan.linkPaths,
|
|
896
|
+
...(plan.linkNodeModules ? ['node_modules'] : []),
|
|
897
|
+
'agent-runs',
|
|
898
|
+
'.frontier-framework',
|
|
899
|
+
'dist',
|
|
900
|
+
'coverage'
|
|
901
|
+
];
|
|
902
|
+
return ignored.some((entry) => file === entry || file.startsWith(entry.replace(/\/$/, '') + '/'));
|
|
427
903
|
}
|
|
428
904
|
async function snapshotWorkspaceFiles(root) {
|
|
429
905
|
const snapshot = new Map();
|
|
@@ -488,6 +964,58 @@ async function runVerification(commands, cwd) {
|
|
|
488
964
|
}
|
|
489
965
|
return results;
|
|
490
966
|
}
|
|
967
|
+
async function runScheduledJobPool(plan, concurrency, worker) {
|
|
968
|
+
const results = [];
|
|
969
|
+
const active = new Map();
|
|
970
|
+
const leases = [];
|
|
971
|
+
const completed = new Set();
|
|
972
|
+
const resultByJob = new Map();
|
|
973
|
+
while (resultByJob.size < plan.jobs.length) {
|
|
974
|
+
const run = createSwarmRun({ plan, status: 'running', results });
|
|
975
|
+
run.jobs = run.jobs.map((job) => active.has(job.id) ? { ...job, status: 'running' } : job);
|
|
976
|
+
const schedule = createSwarmSchedule({
|
|
977
|
+
plan,
|
|
978
|
+
run,
|
|
979
|
+
maxReadyJobs: Math.max(0, concurrency - active.size)
|
|
980
|
+
});
|
|
981
|
+
const nextLeases = createSwarmLeases({
|
|
982
|
+
schedule,
|
|
983
|
+
workerId: 'frontier-swarm-codex',
|
|
984
|
+
count: Math.max(0, concurrency - active.size),
|
|
985
|
+
existingLeases: leases
|
|
986
|
+
});
|
|
987
|
+
for (const lease of nextLeases) {
|
|
988
|
+
const job = plan.jobs.find((entry) => entry.id === lease.jobId);
|
|
989
|
+
if (!job || active.has(job.id) || completed.has(job.id))
|
|
990
|
+
continue;
|
|
991
|
+
leases.push(lease);
|
|
992
|
+
active.set(job.id, worker(job, lease));
|
|
993
|
+
}
|
|
994
|
+
if (active.size === 0) {
|
|
995
|
+
for (const blocked of schedule.blocked) {
|
|
996
|
+
if (resultByJob.has(blocked.jobId))
|
|
997
|
+
continue;
|
|
998
|
+
const result = {
|
|
999
|
+
jobId: blocked.jobId,
|
|
1000
|
+
status: 'blocked',
|
|
1001
|
+
startedAt: Date.now(),
|
|
1002
|
+
finishedAt: Date.now(),
|
|
1003
|
+
error: blocked.reasons.join(', '),
|
|
1004
|
+
metadata: { waitingFor: blocked.waitingFor, reasons: blocked.reasons }
|
|
1005
|
+
};
|
|
1006
|
+
results.push(result);
|
|
1007
|
+
resultByJob.set(result.jobId, result);
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
const settled = await Promise.race(Array.from(active.entries()).map(async ([jobId, promise]) => ({ jobId, result: await promise })));
|
|
1012
|
+
active.delete(settled.jobId);
|
|
1013
|
+
completed.add(settled.jobId);
|
|
1014
|
+
results.push(settled.result);
|
|
1015
|
+
resultByJob.set(settled.jobId, settled.result);
|
|
1016
|
+
}
|
|
1017
|
+
return plan.jobs.map((job) => resultByJob.get(job.id)).filter((result) => !!result);
|
|
1018
|
+
}
|
|
491
1019
|
async function runJobPool(jobs, concurrency, worker) {
|
|
492
1020
|
const results = [];
|
|
493
1021
|
const pending = jobs.map((job, index) => ({ job, index }));
|
|
@@ -536,6 +1064,17 @@ function formatCommand(command) {
|
|
|
536
1064
|
function bullets(values) {
|
|
537
1065
|
return values.length ? values.map((value) => `- ${value}`) : ['- none'];
|
|
538
1066
|
}
|
|
1067
|
+
function formatBudget(job) {
|
|
1068
|
+
if (!job.budget)
|
|
1069
|
+
return ['none'];
|
|
1070
|
+
return [
|
|
1071
|
+
job.budget.maxCostUsd === undefined ? undefined : `maxCostUsd=${job.budget.maxCostUsd}`,
|
|
1072
|
+
job.budget.maxInputTokens === undefined ? undefined : `maxInputTokens=${job.budget.maxInputTokens}`,
|
|
1073
|
+
job.budget.maxOutputTokens === undefined ? undefined : `maxOutputTokens=${job.budget.maxOutputTokens}`,
|
|
1074
|
+
job.budget.maxDurationMs === undefined ? undefined : `maxDurationMs=${job.budget.maxDurationMs}`,
|
|
1075
|
+
`maxRetries=${job.budget.maxRetries}`
|
|
1076
|
+
].filter((value) => !!value);
|
|
1077
|
+
}
|
|
539
1078
|
function arrayOfObjects(value) {
|
|
540
1079
|
return Array.isArray(value) ? value.filter(isObject) : [];
|
|
541
1080
|
}
|
|
@@ -554,6 +1093,68 @@ async function pathExists(file) {
|
|
|
554
1093
|
return false;
|
|
555
1094
|
}
|
|
556
1095
|
}
|
|
1096
|
+
async function resolvePidManifestPath(runPath) {
|
|
1097
|
+
const absolute = path.resolve(runPath);
|
|
1098
|
+
const stat = await fs.lstat(absolute).catch(() => undefined);
|
|
1099
|
+
if (stat?.isDirectory())
|
|
1100
|
+
return path.join(absolute, 'pids.json');
|
|
1101
|
+
if (path.basename(absolute) === 'swarm-results.json')
|
|
1102
|
+
return path.join(path.dirname(absolute), 'pids.json');
|
|
1103
|
+
return absolute;
|
|
1104
|
+
}
|
|
1105
|
+
async function resolveRunDirectory(runPath) {
|
|
1106
|
+
const absolute = path.resolve(runPath);
|
|
1107
|
+
const stat = await fs.lstat(absolute).catch(() => undefined);
|
|
1108
|
+
if (stat?.isDirectory())
|
|
1109
|
+
return absolute;
|
|
1110
|
+
if (path.basename(absolute) === 'swarm-results.json' || path.basename(absolute) === 'pids.json')
|
|
1111
|
+
return path.dirname(absolute);
|
|
1112
|
+
return path.dirname(absolute);
|
|
1113
|
+
}
|
|
1114
|
+
async function findFilesByName(root, name) {
|
|
1115
|
+
const out = [];
|
|
1116
|
+
async function walk(current) {
|
|
1117
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1118
|
+
for (const entry of entries) {
|
|
1119
|
+
const absolute = path.join(current, entry.name);
|
|
1120
|
+
if (entry.isDirectory()) {
|
|
1121
|
+
if (entry.name === 'collected' || entry.name === 'node_modules' || entry.name === '.git')
|
|
1122
|
+
continue;
|
|
1123
|
+
await walk(absolute);
|
|
1124
|
+
}
|
|
1125
|
+
else if (entry.isFile() && entry.name === name) {
|
|
1126
|
+
out.push(absolute);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
await walk(root);
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
async function bundlePatchIsStale(bundle, mergePath, cwd) {
|
|
1134
|
+
const patchPath = resolveBundlePatchPath(bundle, mergePath);
|
|
1135
|
+
if (!patchPath || !await pathExists(patchPath))
|
|
1136
|
+
return false;
|
|
1137
|
+
const patch = await fs.readFile(patchPath, 'utf8').catch(() => '');
|
|
1138
|
+
if (!patch.trim())
|
|
1139
|
+
return false;
|
|
1140
|
+
const result = await runProcess('git', ['apply', '--check', patchPath], { cwd, allowFailure: true });
|
|
1141
|
+
return result.status !== 0;
|
|
1142
|
+
}
|
|
1143
|
+
function resolveBundlePatchPath(bundle, mergePath) {
|
|
1144
|
+
if (!bundle.patchPath)
|
|
1145
|
+
return undefined;
|
|
1146
|
+
return path.isAbsolute(bundle.patchPath) ? bundle.patchPath : path.resolve(path.dirname(mergePath), bundle.patchPath);
|
|
1147
|
+
}
|
|
1148
|
+
function classifyCodexCollectBucket(bundle, staleAgainstHead) {
|
|
1149
|
+
if (staleAgainstHead || bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
|
|
1150
|
+
return 'stale-against-head';
|
|
1151
|
+
if (bundle.disposition === 'rejected' || bundle.disposition === 'blocked' || bundle.commandsFailed.length > 0 || bundle.status === 'failed') {
|
|
1152
|
+
return 'failed-evidence';
|
|
1153
|
+
}
|
|
1154
|
+
if (bundle.disposition === 'auto-mergeable' && bundle.autoMergeable)
|
|
1155
|
+
return 'ready-to-apply';
|
|
1156
|
+
return 'needs-human-port';
|
|
1157
|
+
}
|
|
557
1158
|
async function readOptionalText(file) {
|
|
558
1159
|
try {
|
|
559
1160
|
return await fs.readFile(file, 'utf8');
|
|
@@ -591,4 +1192,24 @@ async function runProcess(command, args, options) {
|
|
|
591
1192
|
function tail(text, maxLines = 24) {
|
|
592
1193
|
return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
|
|
593
1194
|
}
|
|
1195
|
+
function stableHash(value) {
|
|
1196
|
+
const text = stableStringify(value);
|
|
1197
|
+
let hash = 2166136261;
|
|
1198
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1199
|
+
hash ^= text.charCodeAt(index);
|
|
1200
|
+
hash = Math.imul(hash, 16777619);
|
|
1201
|
+
}
|
|
1202
|
+
return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
|
|
1203
|
+
}
|
|
1204
|
+
function slug(value) {
|
|
1205
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
|
|
1206
|
+
}
|
|
1207
|
+
function stableStringify(value) {
|
|
1208
|
+
if (value === null || typeof value !== 'object')
|
|
1209
|
+
return JSON.stringify(value);
|
|
1210
|
+
if (Array.isArray(value))
|
|
1211
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
1212
|
+
const object = value;
|
|
1213
|
+
return '{' + Object.keys(object).sort().map((key) => JSON.stringify(key) + ':' + stableStringify(object[key])).join(',') + '}';
|
|
1214
|
+
}
|
|
594
1215
|
//# sourceMappingURL=index.js.map
|