@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/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
- run = recordSwarmEvent(run, { type: 'swarm.started', at: run.startedAt, data: { jobCount: plan.jobs.length } });
93
- const results = await runJobPool(plan.jobs, Math.max(1, options.maxConcurrency ?? 1), (job) => runCodexJob(job, options, outDir));
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
- return { ok, outDir, plan, run, proof };
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 prompt = renderCodexPrompt(job, { workspacePath: workspace, paths });
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 changedPaths = execution.changedPaths ?? (options.collectGitStatus === false ? [] : await collectChangedPaths(workspace, fileSnapshot));
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
- return {
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.compute.model ?? input.model ?? FRONTIER_SWARM_CODEX_DEFAULT_MODEL;
146
- const effort = job.compute.reasoningEffort ?? input.reasoningEffort ?? FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT;
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
- const gitPaths = await gitChangedPaths(cwd);
423
- if (gitPaths.length > 0 || !baseline)
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