@shapeshift-labs/frontier-swarm-codex 0.5.32 → 0.5.34

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
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
10
10
  import fs from 'node:fs/promises';
11
11
  import os from 'node:os';
12
12
  import path from 'node:path';
13
- import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmCoordinatorDashboard, createSwarmEvidenceIndex, createSwarmMergeAdmission, 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';
13
+ import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmCoordinatorDashboard, createSwarmAdaptiveLoadPlan, createSwarmEvidenceIndex, createSwarmMergeAdmission, FRONTIER_SWARM_MERGE_BUNDLE_KIND, FRONTIER_SWARM_MERGE_BUNDLE_VERSION, createSwarmMergeBundle, createSwarmMergeIndex, createSwarmQueueOverlay, matchesGlob, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, createSwarmScheduleInputFromAdaptiveLoadPlan, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
14
14
  export const FRONTIER_SWARM_CODEX_DEFAULT_MODEL = FRONTIER_SWARM_DEFAULT_MODEL;
15
15
  export const FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT = FRONTIER_SWARM_DEFAULT_REASONING_EFFORT;
16
16
  export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND = 'frontier.swarm-codex.workspace-manifest';
@@ -29,6 +29,12 @@ export const FRONTIER_SWARM_CODEX_SEMANTIC_IMPORT_KIND = 'frontier.swarm-codex.s
29
29
  export const FRONTIER_SWARM_CODEX_SEMANTIC_IMPORT_VERSION = 1;
30
30
  export const FRONTIER_SWARM_CODEX_JOB_EVIDENCE_KIND = 'frontier.swarm-codex.job-evidence';
31
31
  export const FRONTIER_SWARM_CODEX_JOB_EVIDENCE_VERSION = 1;
32
+ export const FRONTIER_SWARM_CODEX_PATCH_INTENT_KIND = 'frontier.swarm-codex.patch-intent';
33
+ export const FRONTIER_SWARM_CODEX_PATCH_INTENT_VERSION = 1;
34
+ export const FRONTIER_SWARM_CODEX_COMPACT_DASHBOARD_KIND = 'frontier.swarm-codex.compact-dashboard';
35
+ export const FRONTIER_SWARM_CODEX_COMPACT_DASHBOARD_VERSION = 1;
36
+ export const FRONTIER_SWARM_CODEX_LINK_REPAIR_KIND = 'frontier.swarm-codex.link-repair';
37
+ export const FRONTIER_SWARM_CODEX_LINK_REPAIR_VERSION = 1;
32
38
  const DEFAULT_WORKSPACE_INCLUDES = ['AGENTS.md', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'config'];
33
39
  const DEFAULT_WORKSPACE_EXCLUDES = [
34
40
  '.git',
@@ -118,6 +124,67 @@ export function coerceCodexSwarmTasksInput(value) {
118
124
  };
119
125
  }).filter((task) => task.id.length > 0);
120
126
  }
127
+ export async function repairCodexWorkspacePackageLinks(input = {}) {
128
+ const root = path.resolve(input.root ?? process.cwd());
129
+ const scope = input.scope ?? '@shapeshift-labs';
130
+ const write = input.write ?? false;
131
+ const replace = input.replace ?? false;
132
+ const packageRoots = (input.packageRoots?.length ? input.packageRoots : [path.join(root, 'packages'), path.dirname(root)])
133
+ .map((entry) => path.resolve(root, entry));
134
+ const excludes = new Set(input.excludePackages ?? []);
135
+ const dependencies = input.packages?.length
136
+ ? new Map(input.packages.map((name) => [name, undefined]))
137
+ : await readWorkspaceScopedDependencies(root, scope);
138
+ const localPackages = await discoverLocalWorkspacePackages(packageRoots, scope);
139
+ const entries = [];
140
+ for (const [packageName, dependencyRange] of Array.from(dependencies.entries()).sort(([a], [b]) => a.localeCompare(b))) {
141
+ const linkPath = path.join(root, 'node_modules', ...packageName.split('/'));
142
+ if (excludes.has(packageName)) {
143
+ entries.push({ packageName, dependencyRange, linkPath, status: 'excluded', reason: 'package excluded from local repair' });
144
+ continue;
145
+ }
146
+ const targetPath = localPackages.get(packageName);
147
+ if (!targetPath) {
148
+ entries.push({ packageName, dependencyRange, linkPath, status: 'missing-local-package', reason: 'no matching local package was found' });
149
+ continue;
150
+ }
151
+ entries.push(await planOrRepairWorkspacePackageLink({
152
+ packageName,
153
+ dependencyRange,
154
+ linkPath,
155
+ targetPath,
156
+ write,
157
+ replace
158
+ }));
159
+ }
160
+ const result = {
161
+ kind: FRONTIER_SWARM_CODEX_LINK_REPAIR_KIND,
162
+ version: FRONTIER_SWARM_CODEX_LINK_REPAIR_VERSION,
163
+ generatedAt: Date.now(),
164
+ root,
165
+ scope,
166
+ packageRoots,
167
+ write,
168
+ replace,
169
+ entries,
170
+ summary: {
171
+ total: entries.length,
172
+ planned: entries.filter((entry) => entry.status === 'planned').length,
173
+ linked: entries.filter((entry) => entry.status === 'linked').length,
174
+ replaced: entries.filter((entry) => entry.status === 'replaced').length,
175
+ alreadyLinked: entries.filter((entry) => entry.status === 'already-linked').length,
176
+ excluded: entries.filter((entry) => entry.status === 'excluded').length,
177
+ missingLocalPackage: entries.filter((entry) => entry.status === 'missing-local-package').length,
178
+ conflicts: entries.filter((entry) => entry.status === 'conflict').length
179
+ },
180
+ ...(input.outFile ? { outFile: path.resolve(root, input.outFile) } : {})
181
+ };
182
+ if (result.outFile) {
183
+ await fs.mkdir(path.dirname(result.outFile), { recursive: true });
184
+ await fs.writeFile(result.outFile, JSON.stringify(result, null, 2) + '\n');
185
+ }
186
+ return result;
187
+ }
121
188
  export async function runCodexSwarm(plan, options) {
122
189
  const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir);
123
190
  await fs.mkdir(outDir, { recursive: true });
@@ -135,7 +202,12 @@ export async function runCodexSwarm(plan, options) {
135
202
  run = recordSwarmEvent(run, startedEvent);
136
203
  await appendFileSwarmEvent(eventStream, startedEvent);
137
204
  const runOptions = { ...options, eventStream, pidManifestPath };
138
- const results = await runScheduledJobPool(plan, Math.max(1, options.maxConcurrency ?? 1), (job, lease) => runCodexJob(job, runOptions, outDir, lease));
205
+ const results = await runScheduledJobPool(plan, {
206
+ concurrency: Math.max(1, options.maxConcurrency ?? 1),
207
+ adaptive: options.adaptiveConcurrency,
208
+ outDir,
209
+ eventStream
210
+ }, (job, lease) => runCodexJob(job, runOptions, outDir, lease));
139
211
  for (const result of results) {
140
212
  const job = plan.jobs.find((entry) => entry.id === result.jobId);
141
213
  if (job) {
@@ -231,8 +303,12 @@ export async function runCodexJob(job, options, outDir, lease) {
231
303
  paths,
232
304
  resourceAllocation,
233
305
  env: resourceAllocation.env,
234
- timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
306
+ timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000,
307
+ compactLogs: normalizeCompactLogOptions(options.compactLogs)
235
308
  });
309
+ const logSummary = execution.logSummary ?? createEmptyCodexLogSummary(paths);
310
+ if (!execution.logSummary)
311
+ await fs.writeFile(paths.logSummaryPath, JSON.stringify(logSummary, null, 2) + '\n');
236
312
  const collected = execution.changedPaths
237
313
  ? filterWorkspaceChangedPaths(execution.changedPaths, workspacePlan)
238
314
  : options.collectGitStatus === false
@@ -270,6 +346,8 @@ export async function runCodexJob(job, options, outDir, lease) {
270
346
  paths.mergeBundlePath,
271
347
  ...(patchPath ? [patchPath] : []),
272
348
  ...(semanticImport ? [semanticImport.path] : []),
349
+ paths.patchIntentPath,
350
+ paths.logSummaryPath,
273
351
  ...handoffArtifacts.map((artifact) => artifact.path)
274
352
  ]);
275
353
  const result = {
@@ -292,6 +370,7 @@ export async function runCodexJob(job, options, outDir, lease) {
292
370
  metadata: {
293
371
  ...(lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : {}),
294
372
  resourceAllocation,
373
+ logSummary,
295
374
  ...(semanticImport ? { semanticImport: semanticImport.sidecar.summary } : {}),
296
375
  codexHandoffArtifacts: handoffArtifacts
297
376
  }
@@ -306,6 +385,8 @@ export async function runCodexJob(job, options, outDir, lease) {
306
385
  evidenceSummaryPath,
307
386
  paths.resourceAllocationPath,
308
387
  paths.workspaceProofPath,
388
+ paths.patchIntentPath,
389
+ paths.logSummaryPath,
309
390
  ...(semanticImport ? [semanticImport.path] : []),
310
391
  ...handoffArtifacts.map((artifact) => artifact.path)
311
392
  ]),
@@ -314,6 +395,16 @@ export async function runCodexJob(job, options, outDir, lease) {
314
395
  ...(semanticImport ? { metadata: { semanticImport: semanticImport.sidecar.summary } } : {})
315
396
  });
316
397
  await fs.writeFile(paths.mergeBundlePath, JSON.stringify(mergeBundle, null, 2) + '\n');
398
+ await writeCodexPatchIntent({
399
+ file: paths.patchIntentPath,
400
+ job,
401
+ result,
402
+ mergeBundle,
403
+ patchPath,
404
+ semanticImport: semanticImport?.sidecar,
405
+ semanticImportExpected: options.semanticImportExpected ?? semanticImportEnabled(options.semanticImport),
406
+ evidencePaths
407
+ });
317
408
  await writeCodexJobEvidenceSummary({
318
409
  file: evidenceSummaryPath,
319
410
  job,
@@ -321,6 +412,8 @@ export async function runCodexJob(job, options, outDir, lease) {
321
412
  mergeBundle,
322
413
  mergeBundlePath: paths.mergeBundlePath,
323
414
  patchPath,
415
+ patchIntentPath: paths.patchIntentPath,
416
+ logSummary,
324
417
  semanticImportPath: semanticImport?.path,
325
418
  semanticImport: semanticImport?.sidecar,
326
419
  handoffArtifacts
@@ -346,6 +439,7 @@ async function writeCodexJobEvidenceSummary(input) {
346
439
  ownershipViolations: [...input.mergeBundle.ownershipViolations],
347
440
  ...(input.patchPath ? { patchPath: input.patchPath } : {}),
348
441
  mergeBundlePath: input.mergeBundlePath,
442
+ ...(input.patchIntentPath ? { patchIntentPath: input.patchIntentPath } : {}),
349
443
  ...(input.semanticImportPath ? { semanticImportPath: input.semanticImportPath } : {}),
350
444
  evidencePaths: uniqueStrings(input.mergeBundle.evidencePaths),
351
445
  handoffArtifacts: input.handoffArtifacts.map((artifact) => ({ ...artifact })),
@@ -368,11 +462,56 @@ async function writeCodexJobEvidenceSummary(input) {
368
462
  metadata: {
369
463
  autoMergeable: input.mergeBundle.autoMergeable,
370
464
  staleAgainstHead: input.mergeBundle.staleAgainstHead,
465
+ ...(input.logSummary ? { logSummary: input.logSummary } : {}),
371
466
  reasons: input.mergeBundle.reasons
372
467
  }
373
468
  };
374
469
  await fs.writeFile(input.file, JSON.stringify(evidence, null, 2) + '\n');
375
470
  }
471
+ async function writeCodexPatchIntent(input) {
472
+ const patchHunks = input.patchPath ? await readPatchHunks(input.patchPath) : [];
473
+ const semanticImportQuality = summarizeCodexSemanticImportQuality(input.semanticImport?.summary, input.semanticImportExpected);
474
+ const warnings = uniqueStrings([
475
+ ...semanticImportQuality.warnings,
476
+ ...(input.mergeBundle.staleAgainstHead ? ['stale against coordinator head'] : []),
477
+ ...(input.mergeBundle.ownershipViolations.length ? ['ownership violations present'] : []),
478
+ ...(input.mergeBundle.commandsFailed.length ? ['verification commands failed'] : []),
479
+ ...(input.mergeBundle.disposition === 'discovery-only' ? ['discovery-only output'] : [])
480
+ ]);
481
+ const intent = {
482
+ kind: FRONTIER_SWARM_CODEX_PATCH_INTENT_KIND,
483
+ version: FRONTIER_SWARM_CODEX_PATCH_INTENT_VERSION,
484
+ generatedAt: Date.now(),
485
+ jobId: input.job.id,
486
+ taskId: input.job.taskId,
487
+ lane: input.job.lane,
488
+ changedPaths: [...input.mergeBundle.changedPaths],
489
+ changedRegions: [...input.mergeBundle.changedRegions],
490
+ intent: input.mergeBundle.changedPaths.length
491
+ ? `Patch ${input.mergeBundle.changedPaths.slice(0, 5).join(', ')}`
492
+ : 'No source patch produced',
493
+ why: input.result.lastMessage ? firstNonEmptyLine(input.result.lastMessage) ?? input.job.task.objective : input.job.task.objective,
494
+ riskLevel: input.mergeBundle.riskLevel,
495
+ mergeReadiness: input.mergeBundle.mergeReadiness,
496
+ disposition: input.mergeBundle.disposition,
497
+ safeToPortManually: input.mergeBundle.commandsFailed.length === 0
498
+ && input.mergeBundle.ownershipViolations.length === 0
499
+ && !input.mergeBundle.staleAgainstHead
500
+ && input.mergeBundle.disposition !== 'rejected'
501
+ && input.mergeBundle.disposition !== 'blocked',
502
+ verification: input.mergeBundle.commandsPassed.concat(input.mergeBundle.commandsFailed).map((command) => ({
503
+ name: command.name,
504
+ command: [...command.command],
505
+ ...(command.status !== undefined ? { status: command.status } : {}),
506
+ required: command.required
507
+ })),
508
+ evidencePaths: uniqueStrings(input.evidencePaths),
509
+ semanticImportQuality,
510
+ patchHunks,
511
+ warnings
512
+ };
513
+ await fs.writeFile(input.file, JSON.stringify(intent, null, 2) + '\n');
514
+ }
376
515
  async function readPatchHunks(file) {
377
516
  const text = await fs.readFile(file, 'utf8').catch(() => '');
378
517
  if (!text.trim())
@@ -423,7 +562,7 @@ async function createCodexSemanticImportSidecar(input) {
423
562
  const options = normalizeSemanticImportOptions(input.options);
424
563
  if (!options)
425
564
  return undefined;
426
- const selection = selectSemanticImportPaths(input.changedPaths, options);
565
+ const selection = selectSemanticImportPaths(semanticImportCandidatePaths(input.job, input.changedPaths), options);
427
566
  const selected = selection.selected;
428
567
  const records = [];
429
568
  const importPath = path.join(input.evidenceDir, 'semantic-imports.json');
@@ -756,6 +895,19 @@ export function renderCodexPrompt(job, input) {
756
895
  export async function spawnCodexExecutor(input) {
757
896
  await fs.writeFile(input.paths.eventsPath, '');
758
897
  await fs.writeFile(input.paths.stderrPath, '');
898
+ const logOptions = normalizeCompactLogOptions(input.compactLogs);
899
+ const eventLimit = logOptions.enabled === false ? Number.POSITIVE_INFINITY : logOptions.maxEventBytes ?? 1_000_000;
900
+ const stderrLimit = logOptions.enabled === false ? Number.POSITIVE_INFINITY : logOptions.maxStderrBytes ?? 256_000;
901
+ const logSummary = {
902
+ eventsPath: input.paths.eventsPath,
903
+ stderrPath: input.paths.stderrPath,
904
+ eventBytes: 0,
905
+ stderrBytes: 0,
906
+ eventBytesWritten: 0,
907
+ stderrBytesWritten: 0,
908
+ eventBytesTruncated: 0,
909
+ stderrBytesTruncated: 0
910
+ };
759
911
  return new Promise((resolve) => {
760
912
  const child = spawn(input.codexPath, input.args, {
761
913
  cwd: input.cwd,
@@ -772,23 +924,52 @@ export async function spawnCodexExecutor(input) {
772
924
  }).catch(() => { });
773
925
  }
774
926
  const timer = setTimeout(() => child.kill('SIGTERM'), input.timeoutMs);
775
- child.stdout.on('data', (chunk) => fs.appendFile(input.paths.eventsPath, chunk).catch(() => { }));
776
- child.stderr.on('data', (chunk) => fs.appendFile(input.paths.stderrPath, chunk).catch(() => { }));
927
+ child.stdout.on('data', (chunk) => appendLimitedLogChunk(input.paths.eventsPath, chunk, eventLimit, logSummary, 'event').catch(() => { }));
928
+ child.stderr.on('data', (chunk) => appendLimitedLogChunk(input.paths.stderrPath, chunk, stderrLimit, logSummary, 'stderr').catch(() => { }));
777
929
  child.stdin.end(input.prompt);
778
930
  child.on('close', async (code, signal) => {
779
931
  clearTimeout(timer);
932
+ await fs.writeFile(input.paths.logSummaryPath, JSON.stringify(logSummary, null, 2) + '\n').catch(() => { });
780
933
  resolve({
781
934
  exitCode: code ?? 1,
782
935
  ...(signal ? { signal } : {}),
783
- lastMessage: await readOptionalText(input.paths.lastMessagePath)
936
+ lastMessage: await readOptionalText(input.paths.lastMessagePath),
937
+ logSummary
784
938
  });
785
939
  });
786
940
  child.on('error', (error) => {
787
941
  clearTimeout(timer);
788
- resolve({ exitCode: 1, error });
942
+ fs.writeFile(input.paths.logSummaryPath, JSON.stringify(logSummary, null, 2) + '\n').catch(() => { });
943
+ resolve({ exitCode: 1, logSummary, error });
789
944
  });
790
945
  });
791
946
  }
947
+ async function appendLimitedLogChunk(file, chunk, limit, summary, kind) {
948
+ const bytes = chunk.byteLength;
949
+ if (kind === 'event')
950
+ summary.eventBytes += bytes;
951
+ else
952
+ summary.stderrBytes += bytes;
953
+ const written = kind === 'event' ? summary.eventBytesWritten : summary.stderrBytesWritten;
954
+ const available = Math.max(0, limit - written);
955
+ if (available <= 0) {
956
+ if (kind === 'event')
957
+ summary.eventBytesTruncated += bytes;
958
+ else
959
+ summary.stderrBytesTruncated += bytes;
960
+ return;
961
+ }
962
+ const slice = bytes > available ? chunk.subarray(0, available) : chunk;
963
+ await fs.appendFile(file, slice);
964
+ if (kind === 'event') {
965
+ summary.eventBytesWritten += slice.byteLength;
966
+ summary.eventBytesTruncated += bytes - slice.byteLength;
967
+ }
968
+ else {
969
+ summary.stderrBytesWritten += slice.byteLength;
970
+ summary.stderrBytesTruncated += bytes - slice.byteLength;
971
+ }
972
+ }
792
973
  async function createJobPaths(outDir, job, options) {
793
974
  const jobDir = path.join(outDir, job.id);
794
975
  const paths = {
@@ -802,6 +983,8 @@ async function createJobPaths(outDir, job, options) {
802
983
  workspaceProofPath: path.join(jobDir, 'evidence', 'workspace-proof.json'),
803
984
  patchPath: path.join(jobDir, 'evidence', 'changes.patch'),
804
985
  mergeBundlePath: path.join(jobDir, 'evidence', 'merge.json'),
986
+ patchIntentPath: path.join(jobDir, 'evidence', 'patch-intent.json'),
987
+ logSummaryPath: path.join(jobDir, 'evidence', 'log-summary.json'),
805
988
  pidManifestPath: path.resolve(options.cwd ?? process.cwd(), options.pidManifestPath ?? path.join(outDir, 'pids.json'))
806
989
  };
807
990
  await fs.mkdir(paths.evidenceDir, { recursive: true });
@@ -1106,21 +1289,35 @@ export async function collectCodexSwarmRun(input) {
1106
1289
  for (const { mergePath, bundle } of mergeRecords) {
1107
1290
  const patchPath = resolveBundlePatchPath(bundle, mergePath);
1108
1291
  const patchExists = !!patchPath && await pathExists(patchPath);
1109
- const staleAgainstHead = input.checkStale === false ? false : await bundlePatchIsStale(bundle, mergePath, cwd);
1292
+ const staleness = input.checkStale === false
1293
+ ? { stale: false, patchStatus: patchExists ? 'unknown' : 'missing', reasons: ['stale check disabled'] }
1294
+ : await bundlePatchStaleness(bundle, mergePath, cwd);
1295
+ const staleAgainstHead = staleness.stale;
1110
1296
  const bucket = classifyCodexCollectBucket(bundle, staleAgainstHead);
1111
1297
  const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(bundle.jobId)}` : bundle.branchName;
1112
1298
  const outputDir = path.join(outDir, bucket, slug(bundle.jobId));
1113
1299
  const collectedEvidencePath = path.join(outputDir, 'evidence.json');
1300
+ const collectReasons = staleness.patchStatus === 'applies'
1301
+ ? bundle.reasons
1302
+ : uniqueStrings([...bundle.reasons, ...staleness.reasons]);
1114
1303
  const nextBundle = {
1115
1304
  ...bundle,
1116
1305
  ...(branchName ? { branchName } : {}),
1117
1306
  staleAgainstHead: bundle.staleAgainstHead || staleAgainstHead,
1118
1307
  disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
1119
1308
  autoMergeable: bucket === 'ready-to-apply' && bundle.autoMergeable,
1309
+ reasons: collectReasons,
1310
+ metadata: {
1311
+ ...(isObject(bundle.metadata) ? bundle.metadata : {}),
1312
+ collect: {
1313
+ patchStatus: staleness.patchStatus,
1314
+ staleReasons: staleness.reasons
1315
+ }
1316
+ },
1120
1317
  evidencePaths: uniqueStrings([...bundle.evidencePaths, collectedEvidencePath])
1121
1318
  };
1122
1319
  collectedBundles.push(nextBundle);
1123
- patchStatuses[nextBundle.jobId] = staleAgainstHead ? 'stale' : patchExists ? input.checkStale === false ? 'unknown' : 'applies' : 'missing';
1320
+ patchStatuses[nextBundle.jobId] = staleness.patchStatus;
1124
1321
  await fs.mkdir(outputDir, { recursive: true });
1125
1322
  await fs.writeFile(path.join(outputDir, 'merge.json'), JSON.stringify(nextBundle, null, 2) + '\n');
1126
1323
  if (patchPath && await pathExists(patchPath))
@@ -1131,7 +1328,8 @@ export async function collectCodexSwarmRun(input) {
1131
1328
  bucket,
1132
1329
  mergePath,
1133
1330
  patchPath,
1134
- patchStatus: patchStatuses[nextBundle.jobId]
1331
+ patchStatus: patchStatuses[nextBundle.jobId],
1332
+ staleReasons: staleness.reasons
1135
1333
  });
1136
1334
  evidenceEntries.push(...createCollectedEvidenceEntries(nextBundle, collectedEvidencePath, bucket));
1137
1335
  buckets[bucket].push({ bucket, jobId: bundle.jobId, mergePath, outputDir, bundle: nextBundle });
@@ -1166,6 +1364,12 @@ export async function collectCodexSwarmRun(input) {
1166
1364
  generatedAt,
1167
1365
  metadata: { runDir, outDir }
1168
1366
  });
1367
+ const compactDashboard = createCodexCompactDashboard({
1368
+ runDir,
1369
+ dashboard,
1370
+ semanticImportExpected: input.semanticImportExpected ?? false,
1371
+ generatedAt
1372
+ });
1169
1373
  const summary = {
1170
1374
  total: mergeRecords.length,
1171
1375
  'ready-to-apply': buckets['ready-to-apply'].length,
@@ -1186,6 +1390,7 @@ export async function collectCodexSwarmRun(input) {
1186
1390
  evidenceIndex,
1187
1391
  admission,
1188
1392
  dashboard,
1393
+ compactDashboard,
1189
1394
  summary
1190
1395
  };
1191
1396
  await fs.mkdir(outDir, { recursive: true });
@@ -1195,6 +1400,7 @@ export async function collectCodexSwarmRun(input) {
1195
1400
  await fs.writeFile(path.join(outDir, 'evidence-index.json'), JSON.stringify(evidenceIndex, null, 2) + '\n');
1196
1401
  await fs.writeFile(path.join(outDir, 'merge-admission.json'), JSON.stringify(admission, null, 2) + '\n');
1197
1402
  await fs.writeFile(path.join(outDir, 'coordinator-query.json'), JSON.stringify(dashboard, null, 2) + '\n');
1403
+ await fs.writeFile(path.join(outDir, 'compact-dashboard.json'), JSON.stringify(compactDashboard, null, 2) + '\n');
1198
1404
  return result;
1199
1405
  }
1200
1406
  async function readCodexPidProcesses(file) {
@@ -1256,6 +1462,7 @@ async function copyOrWriteCollectedEvidenceSummary(input) {
1256
1462
  metadata: {
1257
1463
  bucket: input.bucket,
1258
1464
  patchStatus: input.patchStatus,
1465
+ staleReasons: input.staleReasons ?? [],
1259
1466
  autoMergeable: input.bundle.autoMergeable,
1260
1467
  staleAgainstHead: input.bundle.staleAgainstHead,
1261
1468
  reasons: input.bundle.reasons
@@ -1303,6 +1510,58 @@ function createCollectedEvidenceEntries(bundle, collectedEvidencePath, bucket) {
1303
1510
  }
1304
1511
  return entries;
1305
1512
  }
1513
+ function createCodexCompactDashboard(input) {
1514
+ const qualities = new Map(input.dashboard.jobs.map((job) => [
1515
+ job.jobId,
1516
+ summarizeCodexSemanticImportQuality(job.semanticImport, input.semanticImportExpected)
1517
+ ]));
1518
+ const semanticQualities = Array.from(qualities.values());
1519
+ const usefulPatchJobs = input.dashboard.jobs.filter((job) => ((job.disposition === 'auto-mergeable' || job.disposition === 'needs-port')
1520
+ && job.changedPaths.length > 0
1521
+ && job.tests.requiredFailed === 0));
1522
+ const topJobs = [...input.dashboard.jobs]
1523
+ .filter((job) => job.changedPaths.length > 0 || job.evidencePaths.length > 0)
1524
+ .sort((left, right) => right.mergeScore - left.mergeScore || left.jobId.localeCompare(right.jobId))
1525
+ .slice(0, 20)
1526
+ .map((job) => ({
1527
+ jobId: job.jobId,
1528
+ ...(job.lane ? { lane: job.lane } : {}),
1529
+ disposition: job.disposition,
1530
+ mergeScore: job.mergeScore,
1531
+ changedPaths: job.changedPaths.slice(0, 12),
1532
+ semanticImportQuality: qualities.get(job.jobId),
1533
+ staleAgainstHead: job.staleAgainstHead,
1534
+ ...(job.duplicateGroupId ? { duplicateGroupId: job.duplicateGroupId } : {}),
1535
+ evidencePaths: job.evidencePaths.slice(0, 12)
1536
+ }));
1537
+ return {
1538
+ kind: FRONTIER_SWARM_CODEX_COMPACT_DASHBOARD_KIND,
1539
+ version: FRONTIER_SWARM_CODEX_COMPACT_DASHBOARD_VERSION,
1540
+ generatedAt: input.generatedAt,
1541
+ runDir: input.runDir,
1542
+ total: input.dashboard.summary.jobCount,
1543
+ activeJobs: input.dashboard.jobs.filter((job) => job.liveness === 'running').length,
1544
+ usefulPatchCount: usefulPatchJobs.length,
1545
+ stalePatchCount: input.dashboard.summary.staleAgainstHeadCount,
1546
+ duplicateDiscoveryCount: input.dashboard.duplicateGroups.length,
1547
+ semanticImport: {
1548
+ expected: input.semanticImportExpected,
1549
+ presentCount: semanticQualities.filter((entry) => entry.present).length,
1550
+ emptyCount: semanticQualities.filter((entry) => entry.empty).length,
1551
+ weakCount: semanticQualities.filter((entry) => entry.present && entry.warnings.length > 0).length,
1552
+ symbolCount: semanticQualities.reduce((sum, entry) => sum + entry.symbols, 0),
1553
+ ownershipRegionCount: semanticQualities.reduce((sum, entry) => sum + entry.ownershipRegions, 0),
1554
+ patchHintCount: semanticQualities.reduce((sum, entry) => sum + entry.patchHints, 0)
1555
+ },
1556
+ evidence: {
1557
+ readyToApply: input.dashboard.summary.readyToApplyCount,
1558
+ needsHumanPort: input.dashboard.summary.needsHumanPortCount,
1559
+ failedEvidence: input.dashboard.summary.failedEvidenceCount,
1560
+ averageMergeScore: input.dashboard.summary.averageMergeScore
1561
+ },
1562
+ topJobs
1563
+ };
1564
+ }
1306
1565
  export async function applyCodexSwarmCollection(input) {
1307
1566
  const generatedAt = Date.now();
1308
1567
  const cwd = path.resolve(input.cwd ?? process.cwd());
@@ -1623,12 +1882,132 @@ function summarizePatchScoreSemanticEvidence(bundle) {
1623
1882
  reasons: uniqueStrings(reasons)
1624
1883
  };
1625
1884
  }
1885
+ function summarizeCodexSemanticImportQuality(summary, expected = false) {
1886
+ const selected = nonNegativeNumber(summary?.selected);
1887
+ const eligible = nonNegativeNumber(summary?.eligible);
1888
+ const imported = nonNegativeNumber(summary?.imported);
1889
+ const symbols = nonNegativeNumber(summary?.semanticIndex?.symbols);
1890
+ const ownershipRegions = nonNegativeNumber(summary?.semanticSidecars?.ownershipRegions);
1891
+ const patchHints = nonNegativeNumber(summary?.semanticSidecars?.patchHints);
1892
+ const sourceMapMappings = nonNegativeNumber(summary?.sourceMapMappingCount);
1893
+ const present = !!summary;
1894
+ const empty = present && (nonNegativeNumber(summary?.total) === 0 || selected === 0 && eligible === 0 && imported === 0 && symbols === 0);
1895
+ const warnings = [];
1896
+ if (expected && !present)
1897
+ warnings.push('semantic import expected but missing');
1898
+ if (expected && empty)
1899
+ warnings.push('semantic import expected but empty');
1900
+ if (present && imported === 0)
1901
+ warnings.push('semantic import imported no files');
1902
+ if (present && selected > 0 && symbols === 0)
1903
+ warnings.push('semantic import has no symbols');
1904
+ if (present && selected > 0 && ownershipRegions === 0)
1905
+ warnings.push('semantic import has no ownership regions');
1906
+ if (present && selected > 0 && sourceMapMappings === 0)
1907
+ warnings.push('semantic import has no source-map mappings');
1908
+ return {
1909
+ expected,
1910
+ present,
1911
+ empty,
1912
+ selected,
1913
+ eligible,
1914
+ imported,
1915
+ symbols,
1916
+ ownershipRegions,
1917
+ patchHints,
1918
+ sourceMapMappings,
1919
+ warnings: uniqueStrings(warnings)
1920
+ };
1921
+ }
1626
1922
  function semanticImportSummaryFromBundle(bundle) {
1627
1923
  if (bundle.semanticImport)
1628
1924
  return bundle.semanticImport;
1629
1925
  const metadata = bundle.metadata;
1630
1926
  return metadata?.semanticImport;
1631
1927
  }
1928
+ function semanticImportEnabled(input) {
1929
+ if (input === true)
1930
+ return true;
1931
+ if (!input)
1932
+ return false;
1933
+ return input.enabled !== false;
1934
+ }
1935
+ function normalizeCompactLogOptions(input) {
1936
+ if (input === false)
1937
+ return { enabled: false };
1938
+ if (input === true || input === undefined)
1939
+ return { enabled: true, maxEventBytes: 1_000_000, maxStderrBytes: 256_000 };
1940
+ return {
1941
+ enabled: input.enabled ?? true,
1942
+ maxEventBytes: positiveInteger(input.maxEventBytes, 1_000_000),
1943
+ maxStderrBytes: positiveInteger(input.maxStderrBytes, 256_000)
1944
+ };
1945
+ }
1946
+ function normalizeAdaptiveConcurrencyOptions(input, maxConcurrency) {
1947
+ if (input === false || input === undefined) {
1948
+ return { enabled: false, mode: 'balanced', minConcurrency: 1, maxConcurrency, writePlan: true };
1949
+ }
1950
+ if (input === true) {
1951
+ return { enabled: true, mode: 'balanced', minConcurrency: 1, maxConcurrency, writePlan: true };
1952
+ }
1953
+ return {
1954
+ enabled: input.enabled ?? true,
1955
+ mode: input.mode ?? 'balanced',
1956
+ minConcurrency: Math.max(1, Math.min(maxConcurrency, Math.floor(input.minConcurrency ?? 1))),
1957
+ maxConcurrency: Math.max(1, Math.min(maxConcurrency, Math.floor(input.maxConcurrency ?? maxConcurrency))),
1958
+ writePlan: input.writePlan ?? true
1959
+ };
1960
+ }
1961
+ function createCodexAdaptiveObservations(results) {
1962
+ const observations = [];
1963
+ for (const result of results) {
1964
+ const metadata = result.metadata && typeof result.metadata === 'object' ? result.metadata : {};
1965
+ const logSummary = metadata.logSummary;
1966
+ if (logSummary && (logSummary.eventBytesTruncated > 0 || logSummary.stderrBytesTruncated > 0 || logSummary.eventBytes > 1_000_000 || logSummary.stderrBytes > 256_000)) {
1967
+ observations.push({
1968
+ kind: 'log-noise',
1969
+ severity: logSummary.eventBytesTruncated > 0 || logSummary.stderrBytesTruncated > 0 ? 'warning' : 'info',
1970
+ jobId: result.jobId,
1971
+ value: logSummary.eventBytes + logSummary.stderrBytes,
1972
+ reason: 'worker output exceeded compact log threshold',
1973
+ metadata: logSummary
1974
+ });
1975
+ }
1976
+ if (result.mergeDisposition === 'stale-against-head') {
1977
+ observations.push({ kind: 'stale-patch', severity: 'warning', jobId: result.jobId, reason: 'worker result is stale against head' });
1978
+ }
1979
+ if (result.mergeDisposition === 'discovery-only' || result.mergeReadiness === 'discovery-only') {
1980
+ observations.push({ kind: 'discovery-only-output', severity: 'info', jobId: result.jobId, reason: 'worker produced discovery-only output' });
1981
+ }
1982
+ const semanticQuality = summarizeCodexSemanticImportQuality(result.semanticImport ?? metadata.semanticImport, false);
1983
+ if (semanticQuality.present && semanticQuality.empty) {
1984
+ observations.push({ kind: 'semantic-empty', severity: 'warning', jobId: result.jobId, reason: 'worker semantic sidecar is empty' });
1985
+ }
1986
+ else if (semanticQuality.present && semanticQuality.warnings.length > 0) {
1987
+ observations.push({ kind: 'semantic-weak', severity: 'info', jobId: result.jobId, reasons: semanticQuality.warnings });
1988
+ }
1989
+ }
1990
+ return observations;
1991
+ }
1992
+ function createEmptyCodexLogSummary(paths) {
1993
+ return {
1994
+ eventsPath: paths.eventsPath,
1995
+ stderrPath: paths.stderrPath,
1996
+ eventBytes: 0,
1997
+ stderrBytes: 0,
1998
+ eventBytesWritten: 0,
1999
+ stderrBytesWritten: 0,
2000
+ eventBytesTruncated: 0,
2001
+ stderrBytesTruncated: 0
2002
+ };
2003
+ }
2004
+ function positiveInteger(value, fallback) {
2005
+ const number = Number(value);
2006
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
2007
+ }
2008
+ function firstNonEmptyLine(text) {
2009
+ return text.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
2010
+ }
1632
2011
  function numberRecord(value) {
1633
2012
  if (!value || typeof value !== 'object')
1634
2013
  return {};
@@ -1944,24 +2323,65 @@ async function runVerification(commands, cwd) {
1944
2323
  }
1945
2324
  return results;
1946
2325
  }
1947
- async function runScheduledJobPool(plan, concurrency, worker) {
2326
+ async function runScheduledJobPool(plan, input, worker) {
2327
+ const concurrency = Math.max(1, Math.floor(input.concurrency));
2328
+ const adaptiveOptions = normalizeAdaptiveConcurrencyOptions(input.adaptive, concurrency);
1948
2329
  const results = [];
1949
2330
  const active = new Map();
1950
2331
  const leases = [];
1951
2332
  const completed = new Set();
1952
2333
  const resultByJob = new Map();
2334
+ const adaptiveHistory = [];
2335
+ let currentAdaptiveLimits;
1953
2336
  while (resultByJob.size < plan.jobs.length) {
1954
2337
  const run = createSwarmRun({ plan, status: 'running', results });
1955
2338
  run.jobs = run.jobs.map((job) => active.has(job.id) ? { ...job, status: 'running' } : job);
1956
- const schedule = createSwarmSchedule({
2339
+ const adaptivePlan = adaptiveOptions.enabled ? createSwarmAdaptiveLoadPlan({
1957
2340
  plan,
1958
2341
  run,
1959
- maxReadyJobs: Math.max(0, concurrency - active.size)
2342
+ mode: adaptiveOptions.mode,
2343
+ maxLimits: { maxReadyJobs: adaptiveOptions.maxConcurrency },
2344
+ minLimits: { maxReadyJobs: adaptiveOptions.minConcurrency },
2345
+ currentLimits: currentAdaptiveLimits ?? { maxReadyJobs: adaptiveOptions.maxConcurrency },
2346
+ observations: createCodexAdaptiveObservations(results)
2347
+ }) : undefined;
2348
+ if (adaptivePlan) {
2349
+ currentAdaptiveLimits = adaptivePlan.effectiveLimits;
2350
+ adaptiveHistory.push(adaptivePlan);
2351
+ if (adaptiveOptions.writePlan !== false && input.outDir) {
2352
+ await writeJsonAtomic(path.join(input.outDir, 'adaptive-load.json'), {
2353
+ latest: adaptivePlan,
2354
+ history: adaptiveHistory.slice(-50)
2355
+ }).catch(() => { });
2356
+ }
2357
+ await appendFileSwarmEvent(input.eventStream, {
2358
+ type: 'swarm.adaptive-load',
2359
+ runId: run.id,
2360
+ data: {
2361
+ mode: adaptivePlan.mode,
2362
+ effectiveMaxReadyJobs: adaptivePlan.effectiveLimits.maxReadyJobs,
2363
+ bottleneckCount: adaptivePlan.summary.bottleneckCount,
2364
+ decisions: adaptivePlan.decisions.map((decision) => ({
2365
+ action: decision.action,
2366
+ target: decision.target,
2367
+ key: decision.key,
2368
+ previous: decision.previous,
2369
+ next: decision.next,
2370
+ reason: decision.reason
2371
+ }))
2372
+ }
2373
+ });
2374
+ }
2375
+ const effectiveConcurrency = Math.max(1, Math.min(concurrency, adaptivePlan?.effectiveLimits.maxReadyJobs ?? concurrency));
2376
+ const readyWindow = Math.max(0, effectiveConcurrency - active.size);
2377
+ const schedule = createSwarmSchedule({
2378
+ ...(adaptivePlan ? createSwarmScheduleInputFromAdaptiveLoadPlan(plan, adaptivePlan, { run }) : { plan, run }),
2379
+ maxReadyJobs: readyWindow
1960
2380
  });
1961
2381
  const nextLeases = createSwarmLeases({
1962
2382
  schedule,
1963
2383
  workerId: 'frontier-swarm-codex',
1964
- count: Math.max(0, concurrency - active.size),
2384
+ count: readyWindow,
1965
2385
  existingLeases: leases
1966
2386
  });
1967
2387
  for (const lease of nextLeases) {
@@ -2117,6 +2537,15 @@ function selectSemanticImportPaths(changedPaths, options) {
2117
2537
  maxFiles
2118
2538
  };
2119
2539
  }
2540
+ function semanticImportCandidatePaths(job, changedPaths) {
2541
+ const concreteRefs = job.task.sourceRefs.concat(job.task.targetRefs).filter((file) => {
2542
+ const normalized = normalizeWorkspacePath(file);
2543
+ return normalized
2544
+ && !normalized.includes('*')
2545
+ && path.extname(normalized).length > 0;
2546
+ });
2547
+ return uniqueWorkspacePaths([...changedPaths, ...concreteRefs]);
2548
+ }
2120
2549
  function inferSemanticImportLanguage(file, overrides) {
2121
2550
  const ext = path.extname(file).toLowerCase();
2122
2551
  return overrides?.[file] ?? overrides?.[ext] ?? {
@@ -2398,6 +2827,95 @@ async function pathExists(file) {
2398
2827
  return false;
2399
2828
  }
2400
2829
  }
2830
+ async function readWorkspaceScopedDependencies(root, scope) {
2831
+ const packageJson = await readJsonObject(path.join(root, 'package.json'));
2832
+ const dependencies = new Map();
2833
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
2834
+ const value = packageJson?.[section];
2835
+ if (!isObject(value))
2836
+ continue;
2837
+ for (const [name, range] of Object.entries(value)) {
2838
+ if (name === scope || name.startsWith(scope + '/'))
2839
+ dependencies.set(name, typeof range === 'string' ? range : undefined);
2840
+ }
2841
+ }
2842
+ return dependencies;
2843
+ }
2844
+ async function discoverLocalWorkspacePackages(packageRoots, scope) {
2845
+ const packages = new Map();
2846
+ for (const root of uniqueStrings(packageRoots)) {
2847
+ await addLocalWorkspacePackage(packages, root, scope);
2848
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
2849
+ for (const entry of entries) {
2850
+ if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name === '.git')
2851
+ continue;
2852
+ const child = path.join(root, entry.name);
2853
+ if (entry.name.startsWith('@')) {
2854
+ const scopedEntries = await fs.readdir(child, { withFileTypes: true }).catch(() => []);
2855
+ for (const scopedEntry of scopedEntries) {
2856
+ if (scopedEntry.isDirectory())
2857
+ await addLocalWorkspacePackage(packages, path.join(child, scopedEntry.name), scope);
2858
+ }
2859
+ }
2860
+ else {
2861
+ await addLocalWorkspacePackage(packages, child, scope);
2862
+ }
2863
+ }
2864
+ }
2865
+ return packages;
2866
+ }
2867
+ async function addLocalWorkspacePackage(packages, packageDir, scope) {
2868
+ const packageJson = await readJsonObject(path.join(packageDir, 'package.json'));
2869
+ const name = typeof packageJson?.name === 'string' ? packageJson.name : undefined;
2870
+ if (!name || name !== scope && !name.startsWith(scope + '/'))
2871
+ return;
2872
+ if (!packages.has(name))
2873
+ packages.set(name, path.resolve(packageDir));
2874
+ }
2875
+ async function readJsonObject(file) {
2876
+ try {
2877
+ const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
2878
+ return isObject(parsed) ? parsed : undefined;
2879
+ }
2880
+ catch {
2881
+ return undefined;
2882
+ }
2883
+ }
2884
+ async function planOrRepairWorkspacePackageLink(input) {
2885
+ const base = {
2886
+ packageName: input.packageName,
2887
+ dependencyRange: input.dependencyRange,
2888
+ linkPath: input.linkPath,
2889
+ targetPath: input.targetPath
2890
+ };
2891
+ const stat = await fs.lstat(input.linkPath).catch(() => undefined);
2892
+ const relativeTarget = path.relative(path.dirname(input.linkPath), input.targetPath) || '.';
2893
+ if (stat?.isSymbolicLink()) {
2894
+ const currentTarget = path.resolve(path.dirname(input.linkPath), await fs.readlink(input.linkPath));
2895
+ if (currentTarget === input.targetPath)
2896
+ return { ...base, status: 'already-linked' };
2897
+ if (!input.write)
2898
+ return { ...base, status: 'planned', reason: 'existing symlink points at a different package' };
2899
+ await fs.unlink(input.linkPath);
2900
+ await fs.symlink(relativeTarget, input.linkPath, 'dir');
2901
+ return { ...base, status: 'linked', reason: 'updated existing symlink' };
2902
+ }
2903
+ if (stat) {
2904
+ if (!input.replace)
2905
+ return { ...base, status: 'conflict', reason: 'existing node_modules entry is not a symlink' };
2906
+ if (!input.write)
2907
+ return { ...base, status: 'planned', reason: 'would replace existing node_modules entry' };
2908
+ await fs.rm(input.linkPath, { recursive: true, force: true });
2909
+ await fs.mkdir(path.dirname(input.linkPath), { recursive: true });
2910
+ await fs.symlink(relativeTarget, input.linkPath, 'dir');
2911
+ return { ...base, status: 'replaced', reason: 'replaced existing node_modules entry with a symlink' };
2912
+ }
2913
+ if (!input.write)
2914
+ return { ...base, status: 'planned', reason: 'missing symlink' };
2915
+ await fs.mkdir(path.dirname(input.linkPath), { recursive: true });
2916
+ await fs.symlink(relativeTarget, input.linkPath, 'dir');
2917
+ return { ...base, status: 'linked', reason: 'created symlink' };
2918
+ }
2401
2919
  async function resolvePidManifestPath(runPath) {
2402
2920
  const absolute = path.resolve(runPath);
2403
2921
  const stat = await fs.lstat(absolute).catch(() => undefined);
@@ -2435,15 +2953,99 @@ async function findFilesByName(root, name) {
2435
2953
  await walk(root);
2436
2954
  return out;
2437
2955
  }
2438
- async function bundlePatchIsStale(bundle, mergePath, cwd) {
2956
+ async function bundlePatchStaleness(bundle, mergePath, cwd) {
2439
2957
  const patchPath = resolveBundlePatchPath(bundle, mergePath);
2440
2958
  if (!patchPath || !await pathExists(patchPath))
2441
- return false;
2959
+ return { stale: false, patchStatus: 'missing', reasons: ['missing patch'] };
2442
2960
  const patch = await fs.readFile(patchPath, 'utf8').catch(() => '');
2443
2961
  if (!patch.trim())
2444
- return false;
2962
+ return { stale: false, patchStatus: 'missing', reasons: ['empty patch'] };
2445
2963
  const result = await runProcess('git', ['apply', '--check', patchPath], { cwd, allowFailure: true });
2446
- return result.status !== 0;
2964
+ if (result.status === 0)
2965
+ return { stale: false, patchStatus: 'applies', reasons: ['patch applies to working tree'] };
2966
+ const cached = await runProcess('git', ['apply', '--check', '--cached', patchPath], { cwd, allowFailure: true });
2967
+ if (cached.status === 0) {
2968
+ return {
2969
+ stale: false,
2970
+ patchStatus: 'dirty-workspace-conflict',
2971
+ reasons: ['patch applies to index but not dirty working tree']
2972
+ };
2973
+ }
2974
+ const baseStatus = await patchBaseHashStatus(patch, cwd);
2975
+ if (!baseStatus.known) {
2976
+ return {
2977
+ stale: false,
2978
+ patchStatus: 'needs-port',
2979
+ reasons: ['patch does not expose comparable base hashes; coordinator review must port it', ...baseStatus.reasons]
2980
+ };
2981
+ }
2982
+ if (baseStatus.known && baseStatus.mismatched === 0) {
2983
+ return {
2984
+ stale: false,
2985
+ patchStatus: 'needs-port',
2986
+ reasons: ['patch base hashes match HEAD but textual apply failed', ...baseStatus.reasons]
2987
+ };
2988
+ }
2989
+ return {
2990
+ stale: true,
2991
+ patchStatus: 'stale',
2992
+ reasons: uniqueStrings(['git apply --check failed', ...baseStatus.reasons, ...tail(result.stderr || result.stdout, 3)])
2993
+ };
2994
+ }
2995
+ async function patchBaseHashStatus(patch, cwd) {
2996
+ const entries = parsePatchBaseHashes(patch, cwd);
2997
+ if (entries.length === 0)
2998
+ return { known: false, mismatched: 0, reasons: ['no patch base hashes available'] };
2999
+ let mismatched = 0;
3000
+ const reasons = [];
3001
+ for (const entry of entries) {
3002
+ const head = await runProcess('git', ['rev-parse', `HEAD:${entry.path}`], { cwd, allowFailure: true });
3003
+ if (head.status !== 0) {
3004
+ mismatched += 1;
3005
+ reasons.push(`missing HEAD blob for ${entry.path}`);
3006
+ continue;
3007
+ }
3008
+ const headHash = head.stdout.trim();
3009
+ if (!headHash.startsWith(entry.oldHash)) {
3010
+ mismatched += 1;
3011
+ reasons.push(`base hash mismatch for ${entry.path}`);
3012
+ }
3013
+ }
3014
+ return { known: true, mismatched, reasons };
3015
+ }
3016
+ function parsePatchBaseHashes(patch, cwd) {
3017
+ const lines = patch.split(/\r?\n/);
3018
+ const entries = [];
3019
+ let currentPath;
3020
+ for (const line of lines) {
3021
+ if (line.startsWith('diff --git ')) {
3022
+ const parts = line.split(/\s+/);
3023
+ currentPath = normalizePatchBasePath(parts[2], cwd) ?? normalizePatchBasePath(parts[3], cwd);
3024
+ continue;
3025
+ }
3026
+ if (!currentPath || !line.startsWith('index '))
3027
+ continue;
3028
+ const match = /^index\s+([0-9a-f]+)\.\.([0-9a-f]+)/i.exec(line);
3029
+ if (match?.[1] && match[1] !== '0000000')
3030
+ entries.push({ path: currentPath, oldHash: match[1] });
3031
+ }
3032
+ return entries;
3033
+ }
3034
+ function normalizePatchBasePath(token, cwd) {
3035
+ if (!token || token === '/dev/null')
3036
+ return undefined;
3037
+ let value = token;
3038
+ if (value.startsWith('a/') || value.startsWith('b/'))
3039
+ value = value.slice(2);
3040
+ if (value === '/dev/null')
3041
+ return undefined;
3042
+ if (path.isAbsolute(value)) {
3043
+ const relative = path.relative(cwd, value);
3044
+ if (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
3045
+ return relative.replace(/\\/g, '/');
3046
+ return undefined;
3047
+ }
3048
+ return value.replace(/\\/g, '/');
2447
3049
  }
2448
3050
  function resolveBundlePatchPath(bundle, mergePath) {
2449
3051
  if (!bundle.patchPath)