@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/README.md +18 -5
- package/dist/cli.js +45 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +156 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +621 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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) =>
|
|
776
|
-
child.stderr.on('data', (chunk) =>
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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,
|
|
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
|
|
2339
|
+
const adaptivePlan = adaptiveOptions.enabled ? createSwarmAdaptiveLoadPlan({
|
|
1957
2340
|
plan,
|
|
1958
2341
|
run,
|
|
1959
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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)
|