@shapeshift-labs/frontier-swarm-codex 0.2.0 → 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, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, 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',
@@ -78,11 +86,16 @@ export function coerceCodexSwarmTasksInput(value) {
78
86
  sourceRefs: readStringArray(task.sourceRefs).concat(readStringArray(task.legacySourcePaths)),
79
87
  targetRefs: readStringArray(task.targetRefs).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
80
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),
81
92
  acceptance: readStringArray(task.acceptance),
82
93
  acceptanceChecks: Array.isArray(task.acceptanceChecks) ? task.acceptanceChecks : undefined,
83
94
  verification: Array.isArray(task.verification) ? task.verification : undefined,
84
95
  evidenceCommand: typeof task.evidenceCommand === 'string' ? task.evidenceCommand : undefined,
85
96
  shardCommand: typeof task.shardCommand === 'string' ? task.shardCommand : undefined,
97
+ capabilities: readStringArray(task.capabilities),
98
+ resourceRequirements: isObject(task.resourceRequirements) ? task.resourceRequirements : undefined,
86
99
  tags: readStringArray(task.tags),
87
100
  metadata: { source: task }
88
101
  };
@@ -92,26 +105,87 @@ export async function runCodexSwarm(plan, options) {
92
105
  const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir);
93
106
  await fs.mkdir(outDir, { recursive: true });
94
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);
95
116
  let run = createSwarmRun({ plan, status: 'running', startedAt: Date.now() });
96
- run = recordSwarmEvent(run, { type: 'swarm.started', at: run.startedAt, data: { jobCount: plan.jobs.length } });
97
- const results = await runScheduledJobPool(plan, Math.max(1, options.maxConcurrency ?? 1), (job, lease) => runCodexJob(job, options, outDir, lease));
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
+ }
98
136
  for (const result of results)
99
137
  run = completeSwarmJob(run, result);
100
138
  const proof = createSwarmProof(run, { validation: plan.validation });
101
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
+ });
102
145
  await fs.writeFile(path.join(outDir, 'swarm-results.json'), JSON.stringify({ ok, outDir, run, proof }, null, 2) + '\n');
103
- 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;
104
158
  }
105
159
  export async function runCodexJob(job, options, outDir, lease) {
106
- const paths = await createJobPaths(outDir, job);
160
+ const paths = await createJobPaths(outDir, job, options);
107
161
  const workspace = await prepareCodexWorkspace(job, options);
108
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);
109
172
  const fileSnapshot = shouldSnapshotWorkspaceChanges(workspacePlan, options)
110
173
  ? await snapshotWorkspaceFiles(workspace)
111
174
  : undefined;
112
- 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;
113
179
  await fs.writeFile(paths.promptPath, prompt);
114
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
+ });
115
189
  const startedAt = Date.now();
116
190
  const execution = options.dryRun
117
191
  ? { exitCode: 0, changedPaths: [] }
@@ -125,12 +199,27 @@ export async function runCodexJob(job, options, outDir, lease) {
125
199
  paths,
126
200
  timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
127
201
  });
128
- 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');
129
211
  const ownership = checkSwarmOwnership(job, changedPaths);
130
212
  const verification = options.runVerification ? await runVerification(job.verification, workspace) : [];
131
213
  const failedVerification = verification.some((entry) => entry.required !== false && entry.status !== 0);
132
214
  const status = ownership.ok && execution.exitCode === 0 && !failedVerification ? 'completed' : 'failed';
133
- 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 = {
134
223
  jobId: job.id,
135
224
  status,
136
225
  startedAt,
@@ -138,17 +227,30 @@ export async function runCodexJob(job, options, outDir, lease) {
138
227
  exitCode: execution.exitCode,
139
228
  signal: execution.signal,
140
229
  changedPaths,
230
+ changedRegions: job.changedRegions,
141
231
  ownershipViolations: ownership.violations,
142
- evidencePaths: [paths.evidenceDir],
232
+ evidencePaths: [paths.evidenceDir, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
233
+ ...(patchPath ? { patchPath } : {}),
234
+ queueItemIds: [job.taskId],
143
235
  verification,
144
236
  lastMessage: execution.lastMessage,
145
237
  error: execution.error,
146
238
  metadata: lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : undefined
147
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;
148
250
  }
149
251
  export function buildCodexArgs(job, input) {
150
- const model = job.compute.model ?? input.model ?? FRONTIER_SWARM_CODEX_DEFAULT_MODEL;
151
- 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);
152
254
  const sandbox = job.compute.sandbox ?? input.sandbox ?? 'workspace-write';
153
255
  const args = [
154
256
  'exec',
@@ -160,12 +262,15 @@ export function buildCodexArgs(job, input) {
160
262
  sandbox,
161
263
  '--json',
162
264
  '--output-last-message',
163
- input.paths.lastMessagePath,
164
- '--model',
165
- model,
166
- '-c',
167
- `model_reasoning_effort="${effort}"`
265
+ input.paths.lastMessagePath
168
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);
169
274
  if (shouldSkipGitRepoCheck(input))
170
275
  args.push('--skip-git-repo-check');
171
276
  for (const dir of input.addDirs ?? [])
@@ -178,6 +283,56 @@ export function buildCodexArgs(job, input) {
178
283
  args.push('-');
179
284
  return args;
180
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
+ }
181
336
  export function renderCodexPrompt(job, input) {
182
337
  return [
183
338
  '# Frontier Swarm Codex Job',
@@ -237,6 +392,15 @@ export async function spawnCodexExecutor(input) {
237
392
  await fs.writeFile(input.paths.stderrPath, '');
238
393
  return new Promise((resolve) => {
239
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
+ }
240
404
  const timer = setTimeout(() => child.kill('SIGTERM'), input.timeoutMs);
241
405
  child.stdout.on('data', (chunk) => fs.appendFile(input.paths.eventsPath, chunk).catch(() => { }));
242
406
  child.stderr.on('data', (chunk) => fs.appendFile(input.paths.stderrPath, chunk).catch(() => { }));
@@ -255,7 +419,7 @@ export async function spawnCodexExecutor(input) {
255
419
  });
256
420
  });
257
421
  }
258
- async function createJobPaths(outDir, job) {
422
+ async function createJobPaths(outDir, job, options) {
259
423
  const jobDir = path.join(outDir, job.id);
260
424
  const paths = {
261
425
  jobDir,
@@ -263,7 +427,11 @@ async function createJobPaths(outDir, job) {
263
427
  eventsPath: path.join(jobDir, 'codex-events.jsonl'),
264
428
  stderrPath: path.join(jobDir, 'codex-stderr.log'),
265
429
  lastMessagePath: path.join(jobDir, 'last-message.md'),
266
- 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'))
267
435
  };
268
436
  await fs.mkdir(paths.evidenceDir, { recursive: true });
269
437
  return paths;
@@ -285,6 +453,7 @@ export async function prepareCodexWorkspace(job, options) {
285
453
  if (await pathExists(plan.path)) {
286
454
  if (!plan.replace)
287
455
  return plan.path;
456
+ assertGeneratedWorkspacePath(plan);
288
457
  await fs.rm(plan.path, { recursive: true, force: true });
289
458
  }
290
459
  await fs.mkdir(plan.path, { recursive: true });
@@ -314,6 +483,10 @@ export function createCodexWorkspacePlan(job, options) {
314
483
  excludes: [],
315
484
  artifactIncludes: [],
316
485
  linkPaths: [],
486
+ requiredIncludes: [],
487
+ optionalIncludes: [],
488
+ strategy: workspace.strategy ?? 'fs-cp',
489
+ ...(workspace.guardRoot ? { guardRoot: path.resolve(cwd, workspace.guardRoot) } : {}),
317
490
  linkNodeModules: false,
318
491
  replace: false,
319
492
  skipGitRepoCheck: workspace.skipGitRepoCheck ?? false
@@ -341,6 +514,16 @@ export function createCodexWorkspacePlan(job, options) {
341
514
  ...readStringArray(rawTask.snapshotLinkPaths),
342
515
  ...readStringArray(rawTask.linkPaths)
343
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
+ ]);
344
527
  return {
345
528
  mode,
346
529
  root,
@@ -349,11 +532,231 @@ export function createCodexWorkspacePlan(job, options) {
349
532
  excludes,
350
533
  artifactIncludes,
351
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')),
352
539
  linkNodeModules: workspace.linkNodeModules ?? (mode !== 'git-worktree'),
353
540
  replace: workspace.replace ?? false,
354
541
  skipGitRepoCheck: workspace.skipGitRepoCheck ?? (mode === 'copy' || mode === 'snapshot')
355
542
  };
356
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
+ }
357
760
  async function copyWorkspacePath(cwd, workspacePath, include, excludes) {
358
761
  const relative = normalizeWorkspacePath(include);
359
762
  if (!relative)
@@ -392,6 +795,12 @@ function shouldSkipGitRepoCheck(input) {
392
795
  return workspace.skipGitRepoCheck;
393
796
  return workspace.mode === 'copy' || workspace.mode === 'snapshot';
394
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
+ }
395
804
  function readRawTask(job) {
396
805
  const metadata = isObject(job.task.metadata) ? job.task.metadata : {};
397
806
  return isObject(metadata.source) ? metadata.source : {};
@@ -426,12 +835,71 @@ async function gitChangedPaths(cwd) {
426
835
  return value.includes(' -> ') ? value.split(' -> ') : [value];
427
836
  });
428
837
  }
429
- async function collectChangedPaths(cwd, baseline) {
430
- const gitPaths = await gitChangedPaths(cwd);
431
- if (gitPaths.length > 0 || !baseline)
432
- return gitPaths;
838
+ async function collectChangedPaths(cwd, baseline, plan) {
839
+ if (!baseline)
840
+ return filterWorkspaceChangedPaths(await gitChangedPaths(cwd), plan);
433
841
  const after = await snapshotWorkspaceFiles(cwd);
434
- 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(/\/$/, '') + '/'));
435
903
  }
436
904
  async function snapshotWorkspaceFiles(root) {
437
905
  const snapshot = new Map();
@@ -625,6 +1093,68 @@ async function pathExists(file) {
625
1093
  return false;
626
1094
  }
627
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
+ }
628
1158
  async function readOptionalText(file) {
629
1159
  try {
630
1160
  return await fs.readFile(file, 'utf8');
@@ -662,4 +1192,24 @@ async function runProcess(command, args, options) {
662
1192
  function tail(text, maxLines = 24) {
663
1193
  return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
664
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
+ }
665
1215
  //# sourceMappingURL=index.js.map