@shapeshift-labs/frontier-swarm-codex 0.2.0 → 0.4.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,22 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
+ import os from 'node:os';
3
4
  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';
5
+ import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, FRONTIER_SWARM_MERGE_BUNDLE_KIND, FRONTIER_SWARM_MERGE_BUNDLE_VERSION, createSwarmMergeBundle, createSwarmMergeIndex, createSwarmQueueOverlay, matchesGlob, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
5
6
  export const FRONTIER_SWARM_CODEX_DEFAULT_MODEL = FRONTIER_SWARM_DEFAULT_MODEL;
6
7
  export const FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT = FRONTIER_SWARM_DEFAULT_REASONING_EFFORT;
8
+ export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND = 'frontier.swarm-codex.workspace-manifest';
9
+ export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION = 1;
10
+ export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND = 'frontier.swarm-codex.workspace-proof';
11
+ export const FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION = 1;
12
+ export const FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND = 'frontier.swarm-codex.pid-manifest';
13
+ export const FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION = 1;
14
+ export const FRONTIER_SWARM_CODEX_COLLECTION_KIND = 'frontier.swarm-codex.collection';
15
+ export const FRONTIER_SWARM_CODEX_COLLECTION_VERSION = 1;
16
+ export const FRONTIER_SWARM_CODEX_APPLY_LEDGER_KIND = 'frontier.swarm-codex.apply-ledger';
17
+ export const FRONTIER_SWARM_CODEX_APPLY_LEDGER_VERSION = 1;
18
+ export const FRONTIER_SWARM_CODEX_PATCH_SCORE_KIND = 'frontier.swarm-codex.patch-score';
19
+ export const FRONTIER_SWARM_CODEX_PATCH_SCORE_VERSION = 1;
7
20
  const DEFAULT_WORKSPACE_INCLUDES = ['AGENTS.md', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'config'];
8
21
  const DEFAULT_WORKSPACE_EXCLUDES = [
9
22
  '.git',
@@ -15,6 +28,7 @@ const DEFAULT_WORKSPACE_EXCLUDES = [
15
28
  'test/roms',
16
29
  'target'
17
30
  ];
31
+ const pidManifestWriteQueues = new Map();
18
32
  export function createCodexSwarmPlan(input) {
19
33
  return createSwarmPlan(coerceCodexSwarmManifestInput(input.manifest), coerceCodexSwarmTasksInput(input.tasks), input.plan ?? {});
20
34
  }
@@ -78,11 +92,16 @@ export function coerceCodexSwarmTasksInput(value) {
78
92
  sourceRefs: readStringArray(task.sourceRefs).concat(readStringArray(task.legacySourcePaths)),
79
93
  targetRefs: readStringArray(task.targetRefs).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
80
94
  allowedWrites: readStringArray(task.allowedWrites).concat(readStringArray(task.ownedFiles), readStringArray(task.files)),
95
+ ownershipRegions: Array.isArray(task.ownershipRegions) ? task.ownershipRegions : [],
96
+ ownedRegions: readStringArray(task.ownedRegions),
97
+ changedRegions: readStringArray(task.changedRegions),
81
98
  acceptance: readStringArray(task.acceptance),
82
99
  acceptanceChecks: Array.isArray(task.acceptanceChecks) ? task.acceptanceChecks : undefined,
83
100
  verification: Array.isArray(task.verification) ? task.verification : undefined,
84
101
  evidenceCommand: typeof task.evidenceCommand === 'string' ? task.evidenceCommand : undefined,
85
102
  shardCommand: typeof task.shardCommand === 'string' ? task.shardCommand : undefined,
103
+ capabilities: readStringArray(task.capabilities),
104
+ resourceRequirements: isObject(task.resourceRequirements) ? task.resourceRequirements : undefined,
86
105
  tags: readStringArray(task.tags),
87
106
  metadata: { source: task }
88
107
  };
@@ -92,26 +111,87 @@ export async function runCodexSwarm(plan, options) {
92
111
  const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir);
93
112
  await fs.mkdir(outDir, { recursive: true });
94
113
  await fs.writeFile(path.join(outDir, 'swarm-plan.json'), JSON.stringify(plan, null, 2) + '\n');
114
+ const eventStream = options.eventStream ?? createSwarmEventStream({
115
+ runId: plan.runId,
116
+ root: path.join(outDir, 'streams'),
117
+ lanes: Array.from(new Set(plan.jobs.map((job) => job.lane)))
118
+ });
119
+ await initFileSwarmEventStream(eventStream);
120
+ const pidManifestPath = path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'));
121
+ await appendCodexPidManifest(pidManifestPath, { pid: process.pid, role: 'parent', runId: plan.runId, startedAt: Date.now() }, plan.runId);
95
122
  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));
123
+ const startedEvent = { type: 'swarm.started', runId: run.id, at: run.startedAt, data: { jobCount: plan.jobs.length } };
124
+ run = recordSwarmEvent(run, startedEvent);
125
+ await appendFileSwarmEvent(eventStream, startedEvent);
126
+ const runOptions = { ...options, eventStream, pidManifestPath };
127
+ const results = await runScheduledJobPool(plan, Math.max(1, options.maxConcurrency ?? 1), (job, lease) => runCodexJob(job, runOptions, outDir, lease));
128
+ for (const result of results) {
129
+ const job = plan.jobs.find((entry) => entry.id === result.jobId);
130
+ if (job) {
131
+ await options.onJobFinished?.({ job, result });
132
+ await appendFileSwarmEvent(eventStream, {
133
+ type: 'agent.finished',
134
+ runId: run.id,
135
+ jobId: job.id,
136
+ taskId: job.taskId,
137
+ lane: job.lane,
138
+ data: { status: result.status, mergeReadiness: result.mergeReadiness, changedPathCount: result.changedPaths?.length ?? 0 }
139
+ });
140
+ }
141
+ }
98
142
  for (const result of results)
99
143
  run = completeSwarmJob(run, result);
100
144
  const proof = createSwarmProof(run, { validation: plan.validation });
101
145
  const ok = run.summary.failedCount === 0 && run.summary.blockedCount === 0 && run.summary.ownershipViolationCount === 0;
146
+ await appendFileSwarmEvent(eventStream, {
147
+ type: 'swarm.finished',
148
+ runId: run.id,
149
+ data: { ok, summary: run.summary }
150
+ });
102
151
  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 };
152
+ await writeSwarmCoordinatorSnapshot(options.coordinatorSnapshotPath ? path.resolve(options.cwd ?? process.cwd(), options.coordinatorSnapshotPath) : path.join(outDir, 'coordinator-dashboard.json'), {
153
+ ok,
154
+ outDir,
155
+ plan,
156
+ run,
157
+ proof,
158
+ eventStream,
159
+ pidManifestPath
160
+ });
161
+ const result = { ok, outDir, plan, run, proof };
162
+ await options.onSwarmFinished?.({ result });
163
+ return result;
104
164
  }
105
165
  export async function runCodexJob(job, options, outDir, lease) {
106
- const paths = await createJobPaths(outDir, job);
166
+ const paths = await createJobPaths(outDir, job, options);
107
167
  const workspace = await prepareCodexWorkspace(job, options);
108
168
  const workspacePlan = createCodexWorkspacePlan(job, options);
169
+ const hookInput = {
170
+ job,
171
+ cwd: options.cwd ?? process.cwd(),
172
+ outDir,
173
+ workspacePath: workspace,
174
+ workspacePlan,
175
+ paths
176
+ };
177
+ await options.prepareJobWorkspace?.(hookInput);
109
178
  const fileSnapshot = shouldSnapshotWorkspaceChanges(workspacePlan, options)
110
179
  ? await snapshotWorkspaceFiles(workspace)
111
180
  : undefined;
112
- const prompt = renderCodexPrompt(job, { workspacePath: workspace, paths });
181
+ const basePrompt = renderCodexPrompt(job, { workspacePath: workspace, paths });
182
+ const prompt = options.renderJobPrompt
183
+ ? await options.renderJobPrompt({ ...hookInput, prompt: basePrompt })
184
+ : basePrompt;
113
185
  await fs.writeFile(paths.promptPath, prompt);
114
186
  const args = buildCodexArgs(job, { ...options, workspacePath: workspace, paths });
187
+ await options.onJobStarted?.({ ...hookInput, prompt, args });
188
+ await appendFileSwarmEvent(options.eventStream, {
189
+ type: 'agent.scheduled',
190
+ jobId: job.id,
191
+ taskId: job.taskId,
192
+ lane: job.lane,
193
+ data: { workspace: workspacePlan.path, capabilities: job.capabilities, resourceRequirements: job.resourceRequirements }
194
+ });
115
195
  const startedAt = Date.now();
116
196
  const execution = options.dryRun
117
197
  ? { exitCode: 0, changedPaths: [] }
@@ -125,12 +205,27 @@ export async function runCodexJob(job, options, outDir, lease) {
125
205
  paths,
126
206
  timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
127
207
  });
128
- const changedPaths = execution.changedPaths ?? (options.collectGitStatus === false ? [] : await collectChangedPaths(workspace, fileSnapshot));
208
+ const collected = execution.changedPaths
209
+ ? filterWorkspaceChangedPaths(execution.changedPaths, workspacePlan)
210
+ : options.collectGitStatus === false
211
+ ? { changedPaths: [], ignoredChangedPaths: [] }
212
+ : await collectChangedPaths(workspace, fileSnapshot, workspacePlan);
213
+ const rawChangedPaths = collected.changedPaths;
214
+ const changedPaths = options.changedPathFilter ? [...options.changedPathFilter(rawChangedPaths, hookInput)] : rawChangedPaths;
215
+ const workspaceProof = await createSwarmWorkspaceProof(workspacePlan, { ignoredChangedPaths: collected.ignoredChangedPaths });
216
+ await fs.writeFile(paths.workspaceProofPath, JSON.stringify(workspaceProof, null, 2) + '\n');
129
217
  const ownership = checkSwarmOwnership(job, changedPaths);
130
218
  const verification = options.runVerification ? await runVerification(job.verification, workspace) : [];
131
219
  const failedVerification = verification.some((entry) => entry.required !== false && entry.status !== 0);
132
220
  const status = ownership.ok && execution.exitCode === 0 && !failedVerification ? 'completed' : 'failed';
133
- return {
221
+ const patchPath = await writeCodexPatchFile({
222
+ workspace,
223
+ sourceRoot: path.resolve(options.cwd ?? process.cwd()),
224
+ paths,
225
+ workspacePlan,
226
+ changedPaths
227
+ });
228
+ const result = {
134
229
  jobId: job.id,
135
230
  status,
136
231
  startedAt,
@@ -138,19 +233,34 @@ export async function runCodexJob(job, options, outDir, lease) {
138
233
  exitCode: execution.exitCode,
139
234
  signal: execution.signal,
140
235
  changedPaths,
236
+ changedRegions: job.changedRegions,
141
237
  ownershipViolations: ownership.violations,
142
- evidencePaths: [paths.evidenceDir],
238
+ evidencePaths: [paths.evidenceDir, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
239
+ ...(patchPath ? { patchPath } : {}),
240
+ queueItemIds: [job.taskId],
143
241
  verification,
144
242
  lastMessage: execution.lastMessage,
145
243
  error: execution.error,
146
244
  metadata: lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : undefined
147
245
  };
246
+ const mergeBundle = createSwarmMergeBundle({
247
+ runId: options.eventStream?.runId,
248
+ job,
249
+ result,
250
+ ...(patchPath ? { patchPath } : {}),
251
+ evidencePaths: [paths.evidenceDir, paths.workspaceProofPath],
252
+ queueItemIds: [job.taskId]
253
+ });
254
+ await fs.writeFile(paths.mergeBundlePath, JSON.stringify(mergeBundle, null, 2) + '\n');
255
+ return result;
148
256
  }
149
257
  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;
258
+ const model = resolveCodexModelFlag(job, input);
259
+ const effort = resolveCodexReasoningEffort(job, input);
152
260
  const sandbox = job.compute.sandbox ?? input.sandbox ?? 'workspace-write';
261
+ const approval = normalizeCodexApprovalPolicy(input.approval);
153
262
  const args = [
263
+ ...(approval ? ['--ask-for-approval', approval] : []),
154
264
  'exec',
155
265
  '--cd',
156
266
  input.workspacePath,
@@ -160,12 +270,12 @@ export function buildCodexArgs(job, input) {
160
270
  sandbox,
161
271
  '--json',
162
272
  '--output-last-message',
163
- input.paths.lastMessagePath,
164
- '--model',
165
- model,
166
- '-c',
167
- `model_reasoning_effort="${effort}"`
273
+ input.paths.lastMessagePath
168
274
  ];
275
+ if (model)
276
+ args.push('--model', model);
277
+ if (effort)
278
+ args.push('-c', `model_reasoning_effort="${effort}"`);
169
279
  if (shouldSkipGitRepoCheck(input))
170
280
  args.push('--skip-git-repo-check');
171
281
  for (const dir of input.addDirs ?? [])
@@ -178,6 +288,56 @@ export function buildCodexArgs(job, input) {
178
288
  args.push('-');
179
289
  return args;
180
290
  }
291
+ export function normalizeCodexModelFlag(model) {
292
+ if (model === false || model == null)
293
+ return undefined;
294
+ const value = String(model).trim();
295
+ if (!value)
296
+ return undefined;
297
+ const normalized = value.toLowerCase();
298
+ if (normalized === 'auto' || normalized === 'default' || normalized === 'config' || normalized === 'config-default') {
299
+ return undefined;
300
+ }
301
+ return value;
302
+ }
303
+ export function normalizeCodexApprovalPolicy(approval) {
304
+ if (approval === false || approval == null)
305
+ return undefined;
306
+ const value = String(approval).trim().toLowerCase().replaceAll('_', '-');
307
+ if (!value || value === 'default' || value === 'config-default')
308
+ return undefined;
309
+ if (value === 'never' || value === 'none' || value === 'off' || value === 'false' || value === 'full-auto')
310
+ return 'never';
311
+ if (value === 'untrusted')
312
+ return 'untrusted';
313
+ if (value === 'on-failure')
314
+ return 'on-failure';
315
+ if (value === 'on-request' || value === 'request' || value === 'manual')
316
+ return 'on-request';
317
+ throw new Error(`unsupported Codex approval policy "${approval}"; expected untrusted, on-request, on-failure, never, full-auto, none, or default`);
318
+ }
319
+ function resolveCodexModelFlag(job, input) {
320
+ const explicit = normalizeCodexModelFlag(input.model);
321
+ if (explicit || input.model === false)
322
+ return explicit;
323
+ const policy = input.modelPolicy ?? (input.forwardPlanModel ? 'plan' : 'config-default');
324
+ if (policy === 'plan')
325
+ return normalizeCodexModelFlag(job.compute.model ?? FRONTIER_SWARM_CODEX_DEFAULT_MODEL);
326
+ return undefined;
327
+ }
328
+ function resolveCodexReasoningEffort(job, input) {
329
+ if (input.reasoningEffort === false)
330
+ return undefined;
331
+ if (typeof input.reasoningEffort === 'string') {
332
+ const explicit = input.reasoningEffort.trim();
333
+ return explicit && explicit !== 'default' && explicit !== 'config-default' ? explicit : undefined;
334
+ }
335
+ const policy = input.modelPolicy ?? (input.forwardPlanModel || input.forwardPlanReasoningEffort ? 'plan' : 'config-default');
336
+ if (policy !== 'plan')
337
+ return undefined;
338
+ const effort = job.compute.reasoningEffort ?? FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT;
339
+ return effort ? String(effort).trim() : undefined;
340
+ }
181
341
  export function renderCodexPrompt(job, input) {
182
342
  return [
183
343
  '# Frontier Swarm Codex Job',
@@ -237,6 +397,15 @@ export async function spawnCodexExecutor(input) {
237
397
  await fs.writeFile(input.paths.stderrPath, '');
238
398
  return new Promise((resolve) => {
239
399
  const child = spawn(input.codexPath, input.args, { cwd: input.cwd, stdio: ['pipe', 'pipe', 'pipe'] });
400
+ if (child.pid) {
401
+ appendCodexPidManifest(input.paths.pidManifestPath, {
402
+ pid: child.pid,
403
+ role: 'codex',
404
+ jobId: input.job.id,
405
+ startedAt: Date.now(),
406
+ command: [input.codexPath, ...input.args]
407
+ }).catch(() => { });
408
+ }
240
409
  const timer = setTimeout(() => child.kill('SIGTERM'), input.timeoutMs);
241
410
  child.stdout.on('data', (chunk) => fs.appendFile(input.paths.eventsPath, chunk).catch(() => { }));
242
411
  child.stderr.on('data', (chunk) => fs.appendFile(input.paths.stderrPath, chunk).catch(() => { }));
@@ -255,7 +424,7 @@ export async function spawnCodexExecutor(input) {
255
424
  });
256
425
  });
257
426
  }
258
- async function createJobPaths(outDir, job) {
427
+ async function createJobPaths(outDir, job, options) {
259
428
  const jobDir = path.join(outDir, job.id);
260
429
  const paths = {
261
430
  jobDir,
@@ -263,7 +432,11 @@ async function createJobPaths(outDir, job) {
263
432
  eventsPath: path.join(jobDir, 'codex-events.jsonl'),
264
433
  stderrPath: path.join(jobDir, 'codex-stderr.log'),
265
434
  lastMessagePath: path.join(jobDir, 'last-message.md'),
266
- evidenceDir: path.join(jobDir, 'evidence')
435
+ evidenceDir: path.join(jobDir, 'evidence'),
436
+ workspaceProofPath: path.join(jobDir, 'evidence', 'workspace-proof.json'),
437
+ patchPath: path.join(jobDir, 'evidence', 'changes.patch'),
438
+ mergeBundlePath: path.join(jobDir, 'evidence', 'merge.json'),
439
+ pidManifestPath: path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'))
267
440
  };
268
441
  await fs.mkdir(paths.evidenceDir, { recursive: true });
269
442
  return paths;
@@ -285,6 +458,7 @@ export async function prepareCodexWorkspace(job, options) {
285
458
  if (await pathExists(plan.path)) {
286
459
  if (!plan.replace)
287
460
  return plan.path;
461
+ assertGeneratedWorkspacePath(plan);
288
462
  await fs.rm(plan.path, { recursive: true, force: true });
289
463
  }
290
464
  await fs.mkdir(plan.path, { recursive: true });
@@ -314,6 +488,10 @@ export function createCodexWorkspacePlan(job, options) {
314
488
  excludes: [],
315
489
  artifactIncludes: [],
316
490
  linkPaths: [],
491
+ requiredIncludes: [],
492
+ optionalIncludes: [],
493
+ strategy: workspace.strategy ?? 'fs-cp',
494
+ ...(workspace.guardRoot ? { guardRoot: path.resolve(cwd, workspace.guardRoot) } : {}),
317
495
  linkNodeModules: false,
318
496
  replace: false,
319
497
  skipGitRepoCheck: workspace.skipGitRepoCheck ?? false
@@ -341,6 +519,16 @@ export function createCodexWorkspacePlan(job, options) {
341
519
  ...readStringArray(rawTask.snapshotLinkPaths),
342
520
  ...readStringArray(rawTask.linkPaths)
343
521
  ]);
522
+ const requiredIncludes = uniqueWorkspacePaths([
523
+ ...readStringArray(workspace.requiredIncludes),
524
+ ...readStringArray(rawTask.requiredIncludes),
525
+ ...readStringArray(rawTask.snapshotRequiredIncludes)
526
+ ]);
527
+ const optionalIncludes = uniqueWorkspacePaths([
528
+ ...readStringArray(workspace.optionalIncludes),
529
+ ...readStringArray(rawTask.optionalIncludes),
530
+ ...readStringArray(rawTask.snapshotOptionalIncludes)
531
+ ]);
344
532
  return {
345
533
  mode,
346
534
  root,
@@ -349,11 +537,562 @@ export function createCodexWorkspacePlan(job, options) {
349
537
  excludes,
350
538
  artifactIncludes,
351
539
  linkPaths,
540
+ requiredIncludes,
541
+ optionalIncludes,
542
+ strategy: workspace.strategy ?? 'fs-cp',
543
+ guardRoot: path.resolve(cwd, workspace.guardRoot ?? workspace.root ?? path.join('agent-worktrees', 'frontier-swarm-codex')),
352
544
  linkNodeModules: workspace.linkNodeModules ?? (mode !== 'git-worktree'),
353
545
  replace: workspace.replace ?? false,
354
546
  skipGitRepoCheck: workspace.skipGitRepoCheck ?? (mode === 'copy' || mode === 'snapshot')
355
547
  };
356
548
  }
549
+ export function createSwarmWorkspaceManifest(plan) {
550
+ return {
551
+ kind: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND,
552
+ version: FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_VERSION,
553
+ id: 'codex-workspace:' + stableHash([plan.mode, plan.root, plan.path, plan.includes, plan.linkPaths]),
554
+ mode: plan.mode,
555
+ root: plan.root,
556
+ path: plan.path,
557
+ includes: [...plan.includes],
558
+ excludes: [...plan.excludes],
559
+ artifactIncludes: [...plan.artifactIncludes],
560
+ linkPaths: [...plan.linkPaths],
561
+ requiredIncludes: [...plan.requiredIncludes],
562
+ optionalIncludes: [...plan.optionalIncludes],
563
+ strategy: plan.strategy,
564
+ ...(plan.guardRoot ? { guardRoot: plan.guardRoot } : {}),
565
+ linkNodeModules: plan.linkNodeModules,
566
+ skipGitRepoCheck: plan.skipGitRepoCheck
567
+ };
568
+ }
569
+ export async function createSwarmWorkspaceProof(plan, input = {}) {
570
+ const generatedAt = input.generatedAt ?? Date.now();
571
+ const manifest = createSwarmWorkspaceManifest(plan);
572
+ const copiedCandidates = uniqueWorkspacePaths([...plan.includes, ...plan.artifactIncludes, ...plan.requiredIncludes]);
573
+ const optionalCandidates = uniqueWorkspacePaths(plan.optionalIncludes);
574
+ const copiedPaths = [];
575
+ const missingRequired = [];
576
+ const missingOptional = [];
577
+ for (const include of copiedCandidates) {
578
+ if (await pathExists(path.join(plan.path, include)))
579
+ copiedPaths.push(include);
580
+ else if (plan.requiredIncludes.includes(include))
581
+ missingRequired.push(include);
582
+ }
583
+ for (const include of optionalCandidates) {
584
+ if (await pathExists(path.join(plan.path, include)))
585
+ copiedPaths.push(include);
586
+ else
587
+ missingOptional.push(include);
588
+ }
589
+ const linkedPaths = [];
590
+ for (const linkPath of uniqueWorkspacePaths([...plan.linkPaths, ...(plan.linkNodeModules ? ['node_modules'] : [])])) {
591
+ const stat = await fs.lstat(path.join(plan.path, linkPath)).catch(() => undefined);
592
+ if (stat?.isSymbolicLink())
593
+ linkedPaths.push(linkPath);
594
+ }
595
+ const ignoredChangedPaths = uniqueWorkspacePaths(input.ignoredChangedPaths ?? []);
596
+ return {
597
+ kind: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_KIND,
598
+ version: FRONTIER_SWARM_CODEX_WORKSPACE_PROOF_VERSION,
599
+ id: 'codex-workspace-proof:' + stableHash([manifest.id, copiedPaths, linkedPaths, missingRequired, missingOptional, generatedAt]),
600
+ generatedAt,
601
+ manifest,
602
+ copiedPaths: uniqueWorkspacePaths(copiedPaths),
603
+ linkedPaths,
604
+ missingRequired,
605
+ missingOptional,
606
+ ignoredChangedPaths,
607
+ summary: {
608
+ copiedCount: uniqueWorkspacePaths(copiedPaths).length,
609
+ linkedCount: linkedPaths.length,
610
+ missingRequiredCount: missingRequired.length,
611
+ missingOptionalCount: missingOptional.length,
612
+ ignoredChangedPathCount: ignoredChangedPaths.length
613
+ }
614
+ };
615
+ }
616
+ export async function initFileSwarmEventStream(stream) {
617
+ if (!stream)
618
+ return;
619
+ const mailboxes = [stream.global, ...Object.values(stream.lanes)];
620
+ await Promise.all(mailboxes.map(async (mailbox) => {
621
+ if (!mailbox.path)
622
+ return;
623
+ await fs.mkdir(path.dirname(mailbox.path), { recursive: true });
624
+ await fs.writeFile(mailbox.path, '');
625
+ }));
626
+ }
627
+ export async function appendFileSwarmEvent(stream, event) {
628
+ if (!stream)
629
+ return;
630
+ const line = JSON.stringify({ at: Date.now(), ...event }) + '\n';
631
+ const paths = routeSwarmEventToMailboxes(stream, event)
632
+ .map((mailbox) => mailbox.path)
633
+ .filter((mailboxPath) => !!mailboxPath);
634
+ await Promise.all(paths.map(async (mailboxPath) => {
635
+ await fs.mkdir(path.dirname(mailboxPath), { recursive: true });
636
+ await fs.appendFile(mailboxPath, line);
637
+ }));
638
+ }
639
+ export async function writeSwarmCoordinatorSnapshot(file, input) {
640
+ const byLane = input.run.jobs.reduce((acc, job) => {
641
+ const current = acc[job.lane] ?? { total: 0, completed: 0, failed: 0, blocked: 0 };
642
+ current.total += 1;
643
+ const result = input.run.results.find((entry) => entry.jobId === job.id);
644
+ if (result?.status === 'completed' || result?.status === 'verified')
645
+ current.completed += 1;
646
+ else if (result?.status === 'failed')
647
+ current.failed += 1;
648
+ else if (result?.status === 'blocked')
649
+ current.blocked += 1;
650
+ acc[job.lane] = current;
651
+ return acc;
652
+ }, {});
653
+ const mergeReadiness = input.run.results.reduce((acc, result) => {
654
+ acc[result.mergeReadiness] = (acc[result.mergeReadiness] ?? 0) + 1;
655
+ return acc;
656
+ }, {});
657
+ const dashboard = {
658
+ kind: 'frontier.swarm-codex.coordinator-dashboard',
659
+ version: 1,
660
+ generatedAt: new Date().toISOString(),
661
+ ok: input.ok,
662
+ outDir: input.outDir,
663
+ runId: input.run.id,
664
+ planId: input.plan.id,
665
+ summary: input.run.summary,
666
+ byLane,
667
+ mergeReadiness,
668
+ eventStream: input.eventStream ?? null,
669
+ pidManifestPath: input.pidManifestPath ?? null,
670
+ proof: input.proof
671
+ };
672
+ await fs.mkdir(path.dirname(file), { recursive: true });
673
+ await fs.writeFile(file, JSON.stringify(dashboard, null, 2) + '\n');
674
+ }
675
+ export async function appendCodexPidManifest(file, entry, runId) {
676
+ const absolute = path.resolve(file);
677
+ const previous = pidManifestWriteQueues.get(absolute) ?? Promise.resolve();
678
+ let next;
679
+ next = previous
680
+ .catch(() => { })
681
+ .then(() => appendCodexPidManifestUnlocked(absolute, entry, runId))
682
+ .finally(() => {
683
+ if (pidManifestWriteQueues.get(absolute) === next)
684
+ pidManifestWriteQueues.delete(absolute);
685
+ });
686
+ pidManifestWriteQueues.set(absolute, next);
687
+ return next;
688
+ }
689
+ async function appendCodexPidManifestUnlocked(file, entry, runId) {
690
+ const manifest = await readCodexPidManifest(file).catch(() => ({
691
+ kind: FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND,
692
+ version: FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION,
693
+ ...(runId ? { runId } : {}),
694
+ entries: []
695
+ }));
696
+ const entries = manifest.entries.filter((existing) => existing.pid !== entry.pid || existing.jobId !== entry.jobId);
697
+ entries.push(entry);
698
+ await fs.mkdir(path.dirname(file), { recursive: true });
699
+ await writeJsonAtomic(file, { ...manifest, ...(runId ? { runId } : {}), entries });
700
+ }
701
+ export async function readCodexPidManifest(file) {
702
+ return JSON.parse(await fs.readFile(file, 'utf8'));
703
+ }
704
+ export async function stopCodexSwarmRun(input) {
705
+ const signal = input.signal ?? 'SIGTERM';
706
+ const pidManifestPath = await resolvePidManifestPath(input.run);
707
+ const manifest = await readCodexPidManifest(pidManifestPath);
708
+ const stopped = [];
709
+ const missing = [];
710
+ const errors = [];
711
+ for (const entry of manifest.entries.filter((item) => item.pid !== process.pid).sort((left, right) => right.startedAt - left.startedAt)) {
712
+ try {
713
+ process.kill(entry.pid, signal);
714
+ stopped.push(entry.pid);
715
+ }
716
+ catch (error) {
717
+ const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
718
+ if (code === 'ESRCH')
719
+ missing.push(entry.pid);
720
+ else
721
+ errors.push({ pid: entry.pid, error: error instanceof Error ? error.message : String(error) });
722
+ }
723
+ }
724
+ return { ok: errors.length === 0, pidManifestPath, signal, stopped, missing, errors };
725
+ }
726
+ export async function collectCodexSwarmRun(input) {
727
+ const generatedAt = Date.now();
728
+ const cwd = path.resolve(input.cwd ?? process.cwd());
729
+ const runDir = await resolveRunDirectory(input.run);
730
+ const outDir = path.resolve(cwd, input.outDir ?? path.join(runDir, 'collected'));
731
+ const buckets = {
732
+ 'ready-to-apply': [],
733
+ 'needs-human-port': [],
734
+ 'failed-evidence': [],
735
+ 'stale-against-head': []
736
+ };
737
+ const collectedBundles = [];
738
+ const patchStatuses = {};
739
+ const mergePaths = (await findFilesByName(runDir, 'merge.json'))
740
+ .filter((mergePath) => !pathHasIgnoredSegment(path.relative(runDir, mergePath), [
741
+ 'collected',
742
+ 'patch-scores',
743
+ 'ready-to-apply',
744
+ 'needs-human-port',
745
+ 'failed-evidence',
746
+ 'stale-against-head'
747
+ ]));
748
+ const mergeRecordsByJob = new Map();
749
+ for (const mergePath of mergePaths.sort()) {
750
+ const bundle = normalizeCollectedMergeBundle(JSON.parse(await fs.readFile(mergePath, 'utf8')), mergePath);
751
+ const existing = mergeRecordsByJob.get(bundle.jobId);
752
+ const next = { mergePath, bundle };
753
+ if (!existing || mergeRecordScore(next) > mergeRecordScore(existing))
754
+ mergeRecordsByJob.set(bundle.jobId, next);
755
+ }
756
+ const mergeRecords = Array.from(mergeRecordsByJob.values()).sort((left, right) => left.bundle.jobId.localeCompare(right.bundle.jobId));
757
+ for (const { mergePath, bundle } of mergeRecords) {
758
+ const patchPath = resolveBundlePatchPath(bundle, mergePath);
759
+ const patchExists = !!patchPath && await pathExists(patchPath);
760
+ const staleAgainstHead = input.checkStale === false ? false : await bundlePatchIsStale(bundle, mergePath, cwd);
761
+ const bucket = classifyCodexCollectBucket(bundle, staleAgainstHead);
762
+ const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(bundle.jobId)}` : bundle.branchName;
763
+ const nextBundle = {
764
+ ...bundle,
765
+ ...(branchName ? { branchName } : {}),
766
+ staleAgainstHead: bundle.staleAgainstHead || staleAgainstHead,
767
+ disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
768
+ autoMergeable: bucket === 'ready-to-apply' && bundle.autoMergeable
769
+ };
770
+ collectedBundles.push(nextBundle);
771
+ patchStatuses[nextBundle.jobId] = staleAgainstHead ? 'stale' : patchExists ? input.checkStale === false ? 'unknown' : 'applies' : 'missing';
772
+ const outputDir = path.join(outDir, bucket, slug(bundle.jobId));
773
+ await fs.mkdir(outputDir, { recursive: true });
774
+ await fs.writeFile(path.join(outputDir, 'merge.json'), JSON.stringify(nextBundle, null, 2) + '\n');
775
+ if (patchPath && await pathExists(patchPath))
776
+ await fs.copyFile(patchPath, path.join(outputDir, 'changes.patch')).catch(() => { });
777
+ buckets[bucket].push({ bucket, jobId: bundle.jobId, mergePath, outputDir, bundle: nextBundle });
778
+ }
779
+ const mergeIndex = createSwarmMergeIndex({
780
+ runId: path.basename(runDir),
781
+ bundles: collectedBundles,
782
+ patchStatuses
783
+ });
784
+ const queueOverlay = createSwarmQueueOverlay({
785
+ runId: path.basename(runDir),
786
+ bundles: collectedBundles
787
+ });
788
+ const summary = {
789
+ total: mergeRecords.length,
790
+ 'ready-to-apply': buckets['ready-to-apply'].length,
791
+ 'needs-human-port': buckets['needs-human-port'].length,
792
+ 'failed-evidence': buckets['failed-evidence'].length,
793
+ 'stale-against-head': buckets['stale-against-head'].length
794
+ };
795
+ const result = {
796
+ kind: FRONTIER_SWARM_CODEX_COLLECTION_KIND,
797
+ version: FRONTIER_SWARM_CODEX_COLLECTION_VERSION,
798
+ ok: summary['failed-evidence'] === 0 && summary['stale-against-head'] === 0,
799
+ runDir,
800
+ outDir,
801
+ generatedAt,
802
+ buckets,
803
+ mergeIndex,
804
+ queueOverlay,
805
+ summary
806
+ };
807
+ await fs.mkdir(outDir, { recursive: true });
808
+ await fs.writeFile(path.join(outDir, 'collection.json'), JSON.stringify(result, null, 2) + '\n');
809
+ await fs.writeFile(path.join(outDir, 'merge-index.json'), JSON.stringify(mergeIndex, null, 2) + '\n');
810
+ await fs.writeFile(path.join(outDir, 'queue-overlay.json'), JSON.stringify(queueOverlay, null, 2) + '\n');
811
+ return result;
812
+ }
813
+ export async function applyCodexSwarmCollection(input) {
814
+ const generatedAt = Date.now();
815
+ const cwd = path.resolve(input.cwd ?? process.cwd());
816
+ const dryRun = input.dryRun ?? true;
817
+ if (!input.collection && !input.run)
818
+ throw new Error('apply requires --collection <dir> or --run <run-dir>');
819
+ const collectionDir = input.collection
820
+ ? path.resolve(cwd, input.collection)
821
+ : (await collectCodexSwarmRun({ run: String(input.run ?? ''), cwd, outDir: input.outDir })).outDir;
822
+ const outDir = path.resolve(cwd, input.outDir ?? path.join(collectionDir, 'apply-ledger'));
823
+ if (!dryRun && !input.allowDirty) {
824
+ const dirty = await gitDirty(cwd);
825
+ if (dirty.length)
826
+ throw new Error(`refusing to apply into dirty worktree; pass allowDirty to override (${dirty.slice(0, 8).join(', ')})`);
827
+ }
828
+ const bucket = input.bucket ?? 'ready-to-apply';
829
+ const roots = bucket === 'all'
830
+ ? ['ready-to-apply', 'needs-human-port', 'failed-evidence', 'stale-against-head'].map((entry) => path.join(collectionDir, entry))
831
+ : [path.join(collectionDir, bucket)];
832
+ const wanted = new Set(input.jobIds ?? []);
833
+ const mergePaths = (await Promise.all(roots.map((root) => findFilesByName(root, 'merge.json')))).flat().sort();
834
+ const entries = [];
835
+ for (const mergePath of mergePaths.slice(0, input.limit ? Math.max(0, Math.floor(input.limit)) : undefined)) {
836
+ const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
837
+ if (wanted.size && !wanted.has(bundle.jobId))
838
+ continue;
839
+ entries.push(await applyCodexMergeBundle({
840
+ cwd,
841
+ bundle,
842
+ mergePath,
843
+ dryRun,
844
+ commit: input.commit ?? false,
845
+ branchPrefix: input.branchPrefix
846
+ }));
847
+ }
848
+ const summary = {
849
+ total: entries.length,
850
+ checked: entries.filter((entry) => entry.status === 'checked').length,
851
+ applied: entries.filter((entry) => entry.status === 'applied').length,
852
+ committed: entries.filter((entry) => entry.status === 'committed').length,
853
+ skipped: entries.filter((entry) => entry.status === 'skipped').length,
854
+ failed: entries.filter((entry) => entry.status === 'failed').length
855
+ };
856
+ const result = {
857
+ kind: FRONTIER_SWARM_CODEX_APPLY_LEDGER_KIND,
858
+ version: FRONTIER_SWARM_CODEX_APPLY_LEDGER_VERSION,
859
+ ok: summary.failed === 0,
860
+ cwd,
861
+ collectionDir,
862
+ outDir,
863
+ generatedAt,
864
+ dryRun,
865
+ entries,
866
+ summary
867
+ };
868
+ await fs.mkdir(outDir, { recursive: true });
869
+ await fs.writeFile(path.join(outDir, 'apply-ledger.json'), JSON.stringify(result, null, 2) + '\n');
870
+ return result;
871
+ }
872
+ export async function scoreCodexSwarmPatches(input) {
873
+ const generatedAt = Date.now();
874
+ const cwd = path.resolve(input.cwd ?? process.cwd());
875
+ if (!input.collection && !input.run)
876
+ throw new Error('score requires --collection <dir> or --run <run-dir>');
877
+ const collectionDir = input.collection
878
+ ? path.resolve(cwd, input.collection)
879
+ : (await collectCodexSwarmRun({ run: String(input.run ?? ''), cwd, outDir: input.outDir })).outDir;
880
+ const outDir = path.resolve(cwd, input.outDir ?? path.join(collectionDir, 'patch-scores'));
881
+ const bucket = input.bucket ?? 'all';
882
+ const roots = bucket === 'all'
883
+ ? ['ready-to-apply', 'needs-human-port', 'failed-evidence', 'stale-against-head'].map((entry) => path.join(collectionDir, entry))
884
+ : [path.join(collectionDir, bucket)];
885
+ const wanted = new Set(input.jobIds ?? []);
886
+ const mergePaths = (await Promise.all(roots.map((root) => findFilesByName(root, 'merge.json')))).flat().sort();
887
+ const entries = [];
888
+ for (const mergePath of mergePaths.slice(0, input.limit ? Math.max(0, Math.floor(input.limit)) : undefined)) {
889
+ const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
890
+ if (wanted.size && !wanted.has(bundle.jobId))
891
+ continue;
892
+ entries.push(await scoreCodexMergeBundle({ cwd, mergePath, bundle, outDir, input }));
893
+ }
894
+ const statuses = ['accepted-clean', 'accepted-needs-port', 'conflict', 'test-fail', 'stale', 'evidence-only'];
895
+ const summary = Object.fromEntries(statuses.map((status) => [status, entries.filter((entry) => entry.status === status).length]));
896
+ const result = {
897
+ kind: FRONTIER_SWARM_CODEX_PATCH_SCORE_KIND,
898
+ version: FRONTIER_SWARM_CODEX_PATCH_SCORE_VERSION,
899
+ ok: entries.every((entry) => entry.status === 'accepted-clean' || entry.status === 'accepted-needs-port' || entry.status === 'evidence-only'),
900
+ cwd,
901
+ collectionDir,
902
+ outDir,
903
+ generatedAt,
904
+ entries: entries.sort((left, right) => right.score - left.score || left.jobId.localeCompare(right.jobId)),
905
+ summary: { ...summary, total: entries.length }
906
+ };
907
+ await fs.mkdir(outDir, { recursive: true });
908
+ await fs.writeFile(path.join(outDir, 'patch-score.json'), JSON.stringify(result, null, 2) + '\n');
909
+ return result;
910
+ }
911
+ async function applyCodexMergeBundle(input) {
912
+ const commands = [];
913
+ const patchPath = await resolveApplyPatchPath(input.bundle, input.mergePath);
914
+ const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(input.bundle.jobId)}` : input.bundle.branchName;
915
+ const base = {
916
+ jobId: input.bundle.jobId,
917
+ bundlePath: input.mergePath,
918
+ ...(patchPath ? { patchPath } : {}),
919
+ ...(branchName ? { branchName } : {}),
920
+ dryRun: input.dryRun,
921
+ commands
922
+ };
923
+ if (!patchPath) {
924
+ return {
925
+ ...base,
926
+ status: input.bundle.disposition === 'discovery-only' ? 'skipped' : 'failed',
927
+ error: 'missing patch'
928
+ };
929
+ }
930
+ const check = await runLoggedProcess('git', ['apply', '--check', patchPath], input.cwd);
931
+ commands.push(check);
932
+ if (check.status !== 0)
933
+ return { ...base, status: 'failed', error: 'git apply --check failed' };
934
+ if (input.dryRun)
935
+ return { ...base, status: 'checked' };
936
+ if (branchName) {
937
+ const branch = await runLoggedProcess('git', ['switch', '-c', branchName], input.cwd);
938
+ commands.push(branch);
939
+ if (branch.status !== 0)
940
+ return { ...base, status: 'failed', error: 'git switch -c failed' };
941
+ }
942
+ const apply = await runLoggedProcess('git', ['apply', patchPath], input.cwd);
943
+ commands.push(apply);
944
+ if (apply.status !== 0)
945
+ return { ...base, status: 'failed', error: 'git apply failed' };
946
+ if (!input.commit)
947
+ return { ...base, status: 'applied' };
948
+ const add = await runLoggedProcess('git', ['add', '--', ...input.bundle.changedPaths], input.cwd);
949
+ commands.push(add);
950
+ if (add.status !== 0)
951
+ return { ...base, status: 'failed', error: 'git add failed' };
952
+ const commit = await runLoggedProcess('git', ['commit', '-m', `Apply swarm bundle ${input.bundle.jobId}`], input.cwd);
953
+ commands.push(commit);
954
+ if (commit.status !== 0)
955
+ return { ...base, status: 'failed', error: 'git commit failed' };
956
+ const rev = await runLoggedProcess('git', ['rev-parse', 'HEAD'], input.cwd);
957
+ commands.push(rev);
958
+ return {
959
+ ...base,
960
+ status: 'committed',
961
+ commit: rev.stdoutTail[0]
962
+ };
963
+ }
964
+ async function scoreCodexMergeBundle(input) {
965
+ const commands = [];
966
+ const patchPath = await resolveApplyPatchPath(input.bundle, input.mergePath);
967
+ const base = {
968
+ jobId: input.bundle.jobId,
969
+ bundlePath: input.mergePath,
970
+ ...(patchPath ? { patchPath } : {}),
971
+ changedPaths: [...input.bundle.changedPaths],
972
+ commands
973
+ };
974
+ if (!patchPath || input.bundle.disposition === 'discovery-only' || input.bundle.changedPaths.length === 0) {
975
+ return { ...base, status: 'evidence-only', score: 20, reasons: ['no patch to apply'] };
976
+ }
977
+ if (input.bundle.staleAgainstHead || input.bundle.disposition === 'stale-against-head') {
978
+ return { ...base, status: 'stale', score: 0, reasons: ['stale-against-head'] };
979
+ }
980
+ const workspacePath = await createScoreWorkspace(input.cwd, input.bundle.jobId, input.input);
981
+ try {
982
+ const check = await runLoggedProcess('git', ['apply', '--check', patchPath], workspacePath);
983
+ commands.push(check);
984
+ if (check.status !== 0)
985
+ return { ...base, workspacePath, status: 'conflict', score: 0, reasons: ['git apply --check failed'] };
986
+ const apply = await runLoggedProcess('git', ['apply', patchPath], workspacePath);
987
+ commands.push(apply);
988
+ if (apply.status !== 0)
989
+ return { ...base, workspacePath, status: 'conflict', score: 0, reasons: ['git apply failed'] };
990
+ const gates = scoreCommands(input.bundle, input.input);
991
+ for (const gate of gates) {
992
+ const run = await runLoggedProcess(gate.command, gate.args, gate.cwd ? path.resolve(workspacePath, gate.cwd) : workspacePath);
993
+ commands.push(run);
994
+ if (run.status !== 0 && gate.required !== false) {
995
+ return { ...base, workspacePath, status: 'test-fail', score: 10, reasons: [`gate failed: ${gate.name}`] };
996
+ }
997
+ }
998
+ const clean = input.bundle.disposition === 'auto-mergeable' && input.bundle.autoMergeable;
999
+ return {
1000
+ ...base,
1001
+ workspacePath,
1002
+ status: clean ? 'accepted-clean' : 'accepted-needs-port',
1003
+ score: clean ? 100 : 70,
1004
+ reasons: clean ? [] : ['patch applies but bundle is not auto-mergeable']
1005
+ };
1006
+ }
1007
+ finally {
1008
+ if (!input.input.keepWorkspaces)
1009
+ await fs.rm(workspacePath, { recursive: true, force: true }).catch(() => { });
1010
+ }
1011
+ }
1012
+ async function createScoreWorkspace(cwd, jobId, input) {
1013
+ const workspacePath = await fs.mkdtemp(path.join(os.tmpdir(), `frontier-swarm-score-${slug(jobId)}-`));
1014
+ const excludes = uniqueWorkspacePaths([
1015
+ '.git',
1016
+ 'node_modules',
1017
+ 'dist',
1018
+ 'coverage',
1019
+ 'agent-runs',
1020
+ '.frontier-framework',
1021
+ ...(input.workspaceExcludes ?? [])
1022
+ ]);
1023
+ const includes = uniqueWorkspacePaths(input.workspaceIncludes ?? []);
1024
+ if (includes.length) {
1025
+ for (const include of includes)
1026
+ await copyWorkspacePath(cwd, workspacePath, include, excludes);
1027
+ }
1028
+ else {
1029
+ await fs.cp(cwd, workspacePath, {
1030
+ recursive: true,
1031
+ force: true,
1032
+ filter: (source) => {
1033
+ if (source === cwd)
1034
+ return true;
1035
+ const relative = path.relative(cwd, source).replace(/\\/g, '/');
1036
+ if (!relative)
1037
+ return true;
1038
+ if (pathHasIgnoredSegment(relative, excludes))
1039
+ return false;
1040
+ return !excludes.some((entry) => relative === entry || relative.startsWith(entry.replace(/\/$/, '') + '/'));
1041
+ }
1042
+ });
1043
+ }
1044
+ return workspacePath;
1045
+ }
1046
+ function scoreCommands(bundle, input) {
1047
+ const focused = normalizeScoreCommands(input.focusedCommands ?? []);
1048
+ const global = bundle.changedPaths.some((file) => (input.globalGlobs ?? []).some((glob) => matchesGlob(file, glob)))
1049
+ ? normalizeScoreCommands(input.globalCommands ?? [])
1050
+ : [];
1051
+ return [...focused, ...global];
1052
+ }
1053
+ function normalizeScoreCommands(input) {
1054
+ return input.map((entry) => {
1055
+ if (typeof entry === 'string')
1056
+ return { name: entry, command: 'sh', args: ['-c', entry], required: true };
1057
+ return {
1058
+ name: entry.name,
1059
+ command: entry.command,
1060
+ args: [...entry.args],
1061
+ required: entry.required,
1062
+ ...(entry.cwd ? { cwd: entry.cwd } : {}),
1063
+ ...(entry.metadata ? { metadata: entry.metadata } : {})
1064
+ };
1065
+ }).filter((entry) => entry.command.length > 0);
1066
+ }
1067
+ async function resolveApplyPatchPath(bundle, mergePath) {
1068
+ const sibling = path.join(path.dirname(mergePath), 'changes.patch');
1069
+ if (await pathExists(sibling))
1070
+ return sibling;
1071
+ const patchPath = resolveBundlePatchPath(bundle, mergePath);
1072
+ if (patchPath && await pathExists(patchPath))
1073
+ return patchPath;
1074
+ return undefined;
1075
+ }
1076
+ async function runLoggedProcess(command, args, cwd) {
1077
+ const result = await runProcess(command, args, { cwd, allowFailure: true });
1078
+ return {
1079
+ command: [command, ...args],
1080
+ status: result.status,
1081
+ stdoutTail: tail(result.stdout),
1082
+ stderrTail: tail(result.stderr)
1083
+ };
1084
+ }
1085
+ async function writeJsonAtomic(file, value) {
1086
+ const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
1087
+ await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\n');
1088
+ await fs.rename(tmp, file);
1089
+ }
1090
+ async function gitDirty(cwd) {
1091
+ const result = await runProcess('git', ['status', '--porcelain'], { cwd, allowFailure: true });
1092
+ if (result.status !== 0)
1093
+ return [];
1094
+ return result.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.slice(3));
1095
+ }
357
1096
  async function copyWorkspacePath(cwd, workspacePath, include, excludes) {
358
1097
  const relative = normalizeWorkspacePath(include);
359
1098
  if (!relative)
@@ -392,6 +1131,12 @@ function shouldSkipGitRepoCheck(input) {
392
1131
  return workspace.skipGitRepoCheck;
393
1132
  return workspace.mode === 'copy' || workspace.mode === 'snapshot';
394
1133
  }
1134
+ function assertGeneratedWorkspacePath(plan) {
1135
+ const relative = path.relative(plan.guardRoot ?? plan.root, plan.path);
1136
+ if (relative.startsWith('..') || path.isAbsolute(relative) || relative === '') {
1137
+ throw new Error(`Refusing to replace workspace outside generated root: ${plan.path}`);
1138
+ }
1139
+ }
395
1140
  function readRawTask(job) {
396
1141
  const metadata = isObject(job.task.metadata) ? job.task.metadata : {};
397
1142
  return isObject(metadata.source) ? metadata.source : {};
@@ -426,12 +1171,77 @@ async function gitChangedPaths(cwd) {
426
1171
  return value.includes(' -> ') ? value.split(' -> ') : [value];
427
1172
  });
428
1173
  }
429
- async function collectChangedPaths(cwd, baseline) {
430
- const gitPaths = await gitChangedPaths(cwd);
431
- if (gitPaths.length > 0 || !baseline)
432
- return gitPaths;
1174
+ async function collectChangedPaths(cwd, baseline, plan) {
1175
+ if (!baseline)
1176
+ return filterWorkspaceChangedPaths(await gitChangedPaths(cwd), plan);
433
1177
  const after = await snapshotWorkspaceFiles(cwd);
434
- return diffWorkspaceFiles(baseline, after);
1178
+ return filterWorkspaceChangedPaths(diffWorkspaceFiles(baseline, after), plan);
1179
+ }
1180
+ async function writeCodexPatchFile(input) {
1181
+ await fs.mkdir(path.dirname(input.paths.patchPath), { recursive: true });
1182
+ const changedPaths = uniqueWorkspacePaths(input.changedPaths);
1183
+ if (changedPaths.length === 0) {
1184
+ await fs.writeFile(input.paths.patchPath, '');
1185
+ return undefined;
1186
+ }
1187
+ const diff = input.workspacePlan.mode === 'current' || input.workspacePlan.mode === 'git-worktree'
1188
+ ? await gitDiffPatch(input.workspace, changedPaths)
1189
+ : await noIndexWorkspacePatch(input.sourceRoot, input.workspace, changedPaths);
1190
+ await fs.writeFile(input.paths.patchPath, diff);
1191
+ return diff.trim().length ? input.paths.patchPath : undefined;
1192
+ }
1193
+ async function gitDiffPatch(workspace, changedPaths) {
1194
+ const result = await runProcess('git', ['diff', '--', ...changedPaths], { cwd: workspace, allowFailure: true });
1195
+ return result.stdout;
1196
+ }
1197
+ async function noIndexWorkspacePatch(sourceRoot, workspace, changedPaths) {
1198
+ const chunks = [];
1199
+ for (const file of changedPaths) {
1200
+ const source = path.join(sourceRoot, file);
1201
+ const target = path.join(workspace, file);
1202
+ const sourceExists = await pathExists(source);
1203
+ const targetExists = await pathExists(target);
1204
+ if (!sourceExists && !targetExists)
1205
+ continue;
1206
+ const left = sourceExists ? source : '/dev/null';
1207
+ const right = targetExists ? target : '/dev/null';
1208
+ const result = await runProcess('git', ['diff', '--no-index', '--', left, right], { cwd: sourceRoot, allowFailure: true });
1209
+ if (result.stdout.trim())
1210
+ chunks.push(result.stdout);
1211
+ }
1212
+ return chunks.join('\n');
1213
+ }
1214
+ function filterWorkspaceChangedPaths(paths, plan) {
1215
+ const changedPaths = [];
1216
+ const ignoredChangedPaths = [];
1217
+ for (const file of uniqueWorkspacePaths(paths)) {
1218
+ if (isIgnoredWorkspaceChangedPath(file, plan))
1219
+ ignoredChangedPaths.push(file);
1220
+ else
1221
+ changedPaths.push(file);
1222
+ }
1223
+ return { changedPaths, ignoredChangedPaths };
1224
+ }
1225
+ function isIgnoredWorkspaceChangedPath(file, plan) {
1226
+ if (plan.mode !== 'copy' && plan.mode !== 'snapshot')
1227
+ return false;
1228
+ if (pathHasIgnoredSegment(file, ['node_modules', 'dist', 'coverage', '.frontier-framework', 'agent-runs']))
1229
+ return true;
1230
+ const ignored = [
1231
+ ...plan.excludes,
1232
+ ...plan.artifactIncludes,
1233
+ ...plan.linkPaths,
1234
+ ...(plan.linkNodeModules ? ['node_modules'] : []),
1235
+ 'agent-runs',
1236
+ '.frontier-framework',
1237
+ 'dist',
1238
+ 'coverage'
1239
+ ];
1240
+ return ignored.some((entry) => file === entry || file.startsWith(entry.replace(/\/$/, '') + '/'));
1241
+ }
1242
+ function pathHasIgnoredSegment(file, segments) {
1243
+ const parts = file.replace(/\\/g, '/').split('/').filter(Boolean);
1244
+ return parts.some((part) => segments.includes(part));
435
1245
  }
436
1246
  async function snapshotWorkspaceFiles(root) {
437
1247
  const snapshot = new Map();
@@ -625,6 +1435,122 @@ async function pathExists(file) {
625
1435
  return false;
626
1436
  }
627
1437
  }
1438
+ async function resolvePidManifestPath(runPath) {
1439
+ const absolute = path.resolve(runPath);
1440
+ const stat = await fs.lstat(absolute).catch(() => undefined);
1441
+ if (stat?.isDirectory())
1442
+ return path.join(absolute, 'pids.json');
1443
+ if (path.basename(absolute) === 'swarm-results.json')
1444
+ return path.join(path.dirname(absolute), 'pids.json');
1445
+ return absolute;
1446
+ }
1447
+ async function resolveRunDirectory(runPath) {
1448
+ const absolute = path.resolve(runPath);
1449
+ const stat = await fs.lstat(absolute).catch(() => undefined);
1450
+ if (stat?.isDirectory())
1451
+ return absolute;
1452
+ if (path.basename(absolute) === 'swarm-results.json' || path.basename(absolute) === 'pids.json')
1453
+ return path.dirname(absolute);
1454
+ return path.dirname(absolute);
1455
+ }
1456
+ async function findFilesByName(root, name) {
1457
+ const out = [];
1458
+ async function walk(current) {
1459
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
1460
+ for (const entry of entries) {
1461
+ const absolute = path.join(current, entry.name);
1462
+ if (entry.isDirectory()) {
1463
+ if (entry.name === 'collected' || entry.name === 'node_modules' || entry.name === '.git')
1464
+ continue;
1465
+ await walk(absolute);
1466
+ }
1467
+ else if (entry.isFile() && entry.name === name) {
1468
+ out.push(absolute);
1469
+ }
1470
+ }
1471
+ }
1472
+ await walk(root);
1473
+ return out;
1474
+ }
1475
+ async function bundlePatchIsStale(bundle, mergePath, cwd) {
1476
+ const patchPath = resolveBundlePatchPath(bundle, mergePath);
1477
+ if (!patchPath || !await pathExists(patchPath))
1478
+ return false;
1479
+ const patch = await fs.readFile(patchPath, 'utf8').catch(() => '');
1480
+ if (!patch.trim())
1481
+ return false;
1482
+ const result = await runProcess('git', ['apply', '--check', patchPath], { cwd, allowFailure: true });
1483
+ return result.status !== 0;
1484
+ }
1485
+ function resolveBundlePatchPath(bundle, mergePath) {
1486
+ if (!bundle.patchPath)
1487
+ return undefined;
1488
+ return path.isAbsolute(bundle.patchPath) ? bundle.patchPath : path.resolve(path.dirname(mergePath), bundle.patchPath);
1489
+ }
1490
+ function normalizeCollectedMergeBundle(value, mergePath) {
1491
+ const input = typeof value === 'object' && value !== null ? value : {};
1492
+ const jobId = typeof input.jobId === 'string' && input.jobId ? input.jobId : path.basename(path.dirname(mergePath));
1493
+ const changedPaths = stringArray(input.changedPaths);
1494
+ const status = typeof input.status === 'string' ? input.status : 'completed';
1495
+ const autoMergeable = Boolean(input.autoMergeable);
1496
+ const disposition = typeof input.disposition === 'string'
1497
+ ? input.disposition
1498
+ : autoMergeable ? 'auto-mergeable' : status === 'failed' ? 'rejected' : 'needs-port';
1499
+ return {
1500
+ kind: typeof input.kind === 'string' ? input.kind : FRONTIER_SWARM_MERGE_BUNDLE_KIND,
1501
+ version: typeof input.version === 'number' ? input.version : FRONTIER_SWARM_MERGE_BUNDLE_VERSION,
1502
+ id: typeof input.id === 'string' && input.id ? input.id : `swarm-merge-bundle:${jobId}`,
1503
+ ...(typeof input.runId === 'string' ? { runId: input.runId } : {}),
1504
+ ...(typeof input.planId === 'string' ? { planId: input.planId } : {}),
1505
+ jobId,
1506
+ ...(typeof input.taskId === 'string' ? { taskId: input.taskId } : {}),
1507
+ ...(typeof input.lane === 'string' ? { lane: input.lane } : {}),
1508
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
1509
+ generatedAt: typeof input.generatedAt === 'number' ? input.generatedAt : Date.now(),
1510
+ status,
1511
+ mergeReadiness: typeof input.mergeReadiness === 'string'
1512
+ ? input.mergeReadiness
1513
+ : changedPaths.length ? 'patch-candidate' : 'discovery-only',
1514
+ disposition,
1515
+ riskLevel: typeof input.riskLevel === 'string' ? input.riskLevel : 'unknown',
1516
+ autoMergeable,
1517
+ changedPaths,
1518
+ changedRegions: stringArray(input.changedRegions),
1519
+ ownedFilesTouched: stringArray(input.ownedFilesTouched),
1520
+ allowedWrites: stringArray(input.allowedWrites),
1521
+ ownershipViolations: stringArray(input.ownershipViolations),
1522
+ ...(typeof input.patchPath === 'string' ? { patchPath: input.patchPath } : {}),
1523
+ ...(typeof input.patchHash === 'string' ? { patchHash: input.patchHash } : {}),
1524
+ evidencePaths: stringArray(input.evidencePaths),
1525
+ commandsPassed: Array.isArray(input.commandsPassed) ? input.commandsPassed : [],
1526
+ commandsFailed: Array.isArray(input.commandsFailed) ? input.commandsFailed : [],
1527
+ queueItemIds: stringArray(input.queueItemIds),
1528
+ ...(typeof input.branchName === 'string' ? { branchName: input.branchName } : {}),
1529
+ ...(typeof input.commit === 'string' ? { commit: input.commit } : {}),
1530
+ staleAgainstHead: Boolean(input.staleAgainstHead),
1531
+ reasons: stringArray(input.reasons)
1532
+ };
1533
+ }
1534
+ function mergeRecordScore(record) {
1535
+ return (record.mergePath.includes('/evidence/') ? 100 : 0)
1536
+ + record.bundle.changedPaths.length
1537
+ + record.bundle.evidencePaths.length
1538
+ + record.bundle.commandsPassed.length
1539
+ + record.bundle.commandsFailed.length;
1540
+ }
1541
+ function stringArray(value) {
1542
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
1543
+ }
1544
+ function classifyCodexCollectBucket(bundle, staleAgainstHead) {
1545
+ if (staleAgainstHead || bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
1546
+ return 'stale-against-head';
1547
+ if (bundle.disposition === 'rejected' || bundle.disposition === 'blocked' || bundle.commandsFailed.length > 0 || bundle.status === 'failed') {
1548
+ return 'failed-evidence';
1549
+ }
1550
+ if (bundle.disposition === 'auto-mergeable' && bundle.autoMergeable)
1551
+ return 'ready-to-apply';
1552
+ return 'needs-human-port';
1553
+ }
628
1554
  async function readOptionalText(file) {
629
1555
  try {
630
1556
  return await fs.readFile(file, 'utf8');
@@ -662,4 +1588,24 @@ async function runProcess(command, args, options) {
662
1588
  function tail(text, maxLines = 24) {
663
1589
  return text.trim().split(/\r?\n/).filter(Boolean).slice(-maxLines);
664
1590
  }
1591
+ function stableHash(value) {
1592
+ const text = stableStringify(value);
1593
+ let hash = 2166136261;
1594
+ for (let index = 0; index < text.length; index += 1) {
1595
+ hash ^= text.charCodeAt(index);
1596
+ hash = Math.imul(hash, 16777619);
1597
+ }
1598
+ return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
1599
+ }
1600
+ function slug(value) {
1601
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
1602
+ }
1603
+ function stableStringify(value) {
1604
+ if (value === null || typeof value !== 'object')
1605
+ return JSON.stringify(value);
1606
+ if (Array.isArray(value))
1607
+ return '[' + value.map(stableStringify).join(',') + ']';
1608
+ const object = value;
1609
+ return '{' + Object.keys(object).sort().map((key) => JSON.stringify(key) + ':' + stableStringify(object[key])).join(',') + '}';
1610
+ }
665
1611
  //# sourceMappingURL=index.js.map