@shapeshift-labs/frontier-swarm-codex 0.3.0 → 0.4.1
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 +10 -3
- package/dist/cli.js +51 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +127 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +520 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, createSwarmMergeBundle, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
|
|
5
|
+
import { FRONTIER_SWARM_DEFAULT_MODEL, FRONTIER_SWARM_DEFAULT_REASONING_EFFORT, checkSwarmOwnership, completeSwarmJob, FRONTIER_SWARM_MERGE_BUNDLE_KIND, FRONTIER_SWARM_MERGE_BUNDLE_VERSION, createSwarmMergeBundle, createSwarmMergeIndex, createSwarmQueueOverlay, matchesGlob, createSwarmEventStream, createSwarmLeases, createSwarmPlan, createSwarmProof, createSwarmRun, createSwarmSchedule, recordSwarmEvent, routeSwarmEventToMailboxes } from '@shapeshift-labs/frontier-swarm';
|
|
5
6
|
export const FRONTIER_SWARM_CODEX_DEFAULT_MODEL = FRONTIER_SWARM_DEFAULT_MODEL;
|
|
6
7
|
export const FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT = FRONTIER_SWARM_DEFAULT_REASONING_EFFORT;
|
|
7
8
|
export const FRONTIER_SWARM_CODEX_WORKSPACE_MANIFEST_KIND = 'frontier.swarm-codex.workspace-manifest';
|
|
@@ -12,6 +13,10 @@ export const FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND = 'frontier.swarm-codex.pid-
|
|
|
12
13
|
export const FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION = 1;
|
|
13
14
|
export const FRONTIER_SWARM_CODEX_COLLECTION_KIND = 'frontier.swarm-codex.collection';
|
|
14
15
|
export const FRONTIER_SWARM_CODEX_COLLECTION_VERSION = 1;
|
|
16
|
+
export const FRONTIER_SWARM_CODEX_APPLY_LEDGER_KIND = 'frontier.swarm-codex.apply-ledger';
|
|
17
|
+
export const FRONTIER_SWARM_CODEX_APPLY_LEDGER_VERSION = 1;
|
|
18
|
+
export const FRONTIER_SWARM_CODEX_PATCH_SCORE_KIND = 'frontier.swarm-codex.patch-score';
|
|
19
|
+
export const FRONTIER_SWARM_CODEX_PATCH_SCORE_VERSION = 1;
|
|
15
20
|
const DEFAULT_WORKSPACE_INCLUDES = ['AGENTS.md', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'config'];
|
|
16
21
|
const DEFAULT_WORKSPACE_EXCLUDES = [
|
|
17
22
|
'.git',
|
|
@@ -23,6 +28,7 @@ const DEFAULT_WORKSPACE_EXCLUDES = [
|
|
|
23
28
|
'test/roms',
|
|
24
29
|
'target'
|
|
25
30
|
];
|
|
31
|
+
const pidManifestWriteQueues = new Map();
|
|
26
32
|
export function createCodexSwarmPlan(input) {
|
|
27
33
|
return createSwarmPlan(coerceCodexSwarmManifestInput(input.manifest), coerceCodexSwarmTasksInput(input.tasks), input.plan ?? {});
|
|
28
34
|
}
|
|
@@ -160,19 +166,29 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
160
166
|
const paths = await createJobPaths(outDir, job, options);
|
|
161
167
|
const workspace = await prepareCodexWorkspace(job, options);
|
|
162
168
|
const workspacePlan = createCodexWorkspacePlan(job, options);
|
|
169
|
+
const resourceAllocation = createCodexResourceAllocation(job, {
|
|
170
|
+
cwd: options.cwd ?? process.cwd(),
|
|
171
|
+
outDir,
|
|
172
|
+
workspacePath: workspace,
|
|
173
|
+
lease
|
|
174
|
+
});
|
|
175
|
+
if (resourceAllocation.browser?.profileDir)
|
|
176
|
+
await fs.mkdir(resourceAllocation.browser.profileDir, { recursive: true });
|
|
163
177
|
const hookInput = {
|
|
164
178
|
job,
|
|
165
179
|
cwd: options.cwd ?? process.cwd(),
|
|
166
180
|
outDir,
|
|
167
181
|
workspacePath: workspace,
|
|
168
182
|
workspacePlan,
|
|
169
|
-
paths
|
|
183
|
+
paths,
|
|
184
|
+
resourceAllocation
|
|
170
185
|
};
|
|
171
186
|
await options.prepareJobWorkspace?.(hookInput);
|
|
172
187
|
const fileSnapshot = shouldSnapshotWorkspaceChanges(workspacePlan, options)
|
|
173
188
|
? await snapshotWorkspaceFiles(workspace)
|
|
174
189
|
: undefined;
|
|
175
|
-
|
|
190
|
+
await fs.writeFile(paths.resourceAllocationPath, JSON.stringify(resourceAllocation, null, 2) + '\n');
|
|
191
|
+
const basePrompt = renderCodexPrompt(job, { workspacePath: workspace, paths, resourceAllocation });
|
|
176
192
|
const prompt = options.renderJobPrompt
|
|
177
193
|
? await options.renderJobPrompt({ ...hookInput, prompt: basePrompt })
|
|
178
194
|
: basePrompt;
|
|
@@ -184,7 +200,12 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
184
200
|
jobId: job.id,
|
|
185
201
|
taskId: job.taskId,
|
|
186
202
|
lane: job.lane,
|
|
187
|
-
data: {
|
|
203
|
+
data: {
|
|
204
|
+
workspace: workspacePlan.path,
|
|
205
|
+
capabilities: job.capabilities,
|
|
206
|
+
resourceRequirements: job.resourceRequirements,
|
|
207
|
+
resourceAllocation
|
|
208
|
+
}
|
|
188
209
|
});
|
|
189
210
|
const startedAt = Date.now();
|
|
190
211
|
const execution = options.dryRun
|
|
@@ -197,6 +218,8 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
197
218
|
workspacePath: workspace,
|
|
198
219
|
codexPath: options.codexPath ?? 'codex',
|
|
199
220
|
paths,
|
|
221
|
+
resourceAllocation,
|
|
222
|
+
env: resourceAllocation.env,
|
|
200
223
|
timeoutMs: job.compute.timeoutMs ?? options.jobTimeoutMs ?? 7200000
|
|
201
224
|
});
|
|
202
225
|
const collected = execution.changedPaths
|
|
@@ -229,20 +252,23 @@ export async function runCodexJob(job, options, outDir, lease) {
|
|
|
229
252
|
changedPaths,
|
|
230
253
|
changedRegions: job.changedRegions,
|
|
231
254
|
ownershipViolations: ownership.violations,
|
|
232
|
-
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
|
|
255
|
+
evidencePaths: [paths.evidenceDir, paths.resourceAllocationPath, paths.workspaceProofPath, paths.mergeBundlePath, ...(patchPath ? [patchPath] : [])],
|
|
233
256
|
...(patchPath ? { patchPath } : {}),
|
|
234
257
|
queueItemIds: [job.taskId],
|
|
235
258
|
verification,
|
|
236
259
|
lastMessage: execution.lastMessage,
|
|
237
260
|
error: execution.error,
|
|
238
|
-
metadata:
|
|
261
|
+
metadata: {
|
|
262
|
+
...(lease ? { leaseId: lease.id, leaseToken: lease.token, fencingToken: lease.fencingToken } : {}),
|
|
263
|
+
resourceAllocation
|
|
264
|
+
}
|
|
239
265
|
};
|
|
240
266
|
const mergeBundle = createSwarmMergeBundle({
|
|
241
267
|
runId: options.eventStream?.runId,
|
|
242
268
|
job,
|
|
243
269
|
result,
|
|
244
270
|
...(patchPath ? { patchPath } : {}),
|
|
245
|
-
evidencePaths: [paths.evidenceDir, paths.workspaceProofPath],
|
|
271
|
+
evidencePaths: [paths.evidenceDir, paths.resourceAllocationPath, paths.workspaceProofPath],
|
|
246
272
|
queueItemIds: [job.taskId]
|
|
247
273
|
});
|
|
248
274
|
await fs.writeFile(paths.mergeBundlePath, JSON.stringify(mergeBundle, null, 2) + '\n');
|
|
@@ -252,7 +278,9 @@ export function buildCodexArgs(job, input) {
|
|
|
252
278
|
const model = resolveCodexModelFlag(job, input);
|
|
253
279
|
const effort = resolveCodexReasoningEffort(job, input);
|
|
254
280
|
const sandbox = job.compute.sandbox ?? input.sandbox ?? 'workspace-write';
|
|
281
|
+
const approval = normalizeCodexApprovalPolicy(input.approval);
|
|
255
282
|
const args = [
|
|
283
|
+
...(approval ? ['--ask-for-approval', approval] : []),
|
|
256
284
|
'exec',
|
|
257
285
|
'--cd',
|
|
258
286
|
input.workspacePath,
|
|
@@ -268,9 +296,6 @@ export function buildCodexArgs(job, input) {
|
|
|
268
296
|
args.push('--model', model);
|
|
269
297
|
if (effort)
|
|
270
298
|
args.push('-c', `model_reasoning_effort="${effort}"`);
|
|
271
|
-
const approval = normalizeCodexApprovalPolicy(input.approval);
|
|
272
|
-
if (approval)
|
|
273
|
-
args.push('--ask-for-approval', approval);
|
|
274
299
|
if (shouldSkipGitRepoCheck(input))
|
|
275
300
|
args.push('--skip-git-repo-check');
|
|
276
301
|
for (const dir of input.addDirs ?? [])
|
|
@@ -333,7 +358,48 @@ function resolveCodexReasoningEffort(job, input) {
|
|
|
333
358
|
const effort = job.compute.reasoningEffort ?? FRONTIER_SWARM_CODEX_DEFAULT_REASONING_EFFORT;
|
|
334
359
|
return effort ? String(effort).trim() : undefined;
|
|
335
360
|
}
|
|
361
|
+
export function createCodexResourceAllocation(job, input) {
|
|
362
|
+
const requirements = job.resourceRequirements;
|
|
363
|
+
const capabilities = uniqueStrings([...(job.capabilities ?? []), ...(requirements?.capabilities ?? [])]);
|
|
364
|
+
const resources = { ...(requirements?.resources ?? {}) };
|
|
365
|
+
const env = {
|
|
366
|
+
FRONTIER_SWARM_JOB_ID: job.id,
|
|
367
|
+
FRONTIER_SWARM_TASK_ID: job.taskId,
|
|
368
|
+
FRONTIER_SWARM_LANE: job.lane,
|
|
369
|
+
FRONTIER_SWARM_CAPABILITIES: capabilities.join(',')
|
|
370
|
+
};
|
|
371
|
+
const browser = requirements?.browser;
|
|
372
|
+
if (!browser)
|
|
373
|
+
return { capabilities, resources, env };
|
|
374
|
+
const portPool = uniqueWorkspacePaths(browser.portPool ?? []);
|
|
375
|
+
const port = portPool.length ? portPool[resourceSlot(job, input.lease, portPool.length)] : undefined;
|
|
376
|
+
const profileDir = resolveBrowserProfileDir(job, browser.profileDir, browser.profileDirPrefix, input.cwd ?? process.cwd());
|
|
377
|
+
const browserAllocation = {
|
|
378
|
+
required: browser.required,
|
|
379
|
+
portPool,
|
|
380
|
+
...(port ? { port } : {}),
|
|
381
|
+
...(profileDir ? { profileDir } : {}),
|
|
382
|
+
...(browser.headless !== undefined ? { headless: browser.headless } : {})
|
|
383
|
+
};
|
|
384
|
+
env.FRONTIER_SWARM_BROWSER_REQUIRED = String(browser.required);
|
|
385
|
+
if (port) {
|
|
386
|
+
env.FRONTIER_SWARM_BROWSER_PORT = port;
|
|
387
|
+
env.PORT = port;
|
|
388
|
+
}
|
|
389
|
+
if (profileDir)
|
|
390
|
+
env.FRONTIER_SWARM_BROWSER_PROFILE_DIR = profileDir;
|
|
391
|
+
if (browser.headless !== undefined)
|
|
392
|
+
env.FRONTIER_SWARM_BROWSER_HEADLESS = String(browser.headless);
|
|
393
|
+
env.FRONTIER_SWARM_RESOURCE_ALLOCATION = JSON.stringify({ capabilities, resources, browser: browserAllocation });
|
|
394
|
+
return {
|
|
395
|
+
capabilities,
|
|
396
|
+
resources,
|
|
397
|
+
env,
|
|
398
|
+
browser: browserAllocation
|
|
399
|
+
};
|
|
400
|
+
}
|
|
336
401
|
export function renderCodexPrompt(job, input) {
|
|
402
|
+
const resourceAllocation = input.resourceAllocation ?? createCodexResourceAllocation(job, { outDir: input.paths.jobDir, workspacePath: input.workspacePath });
|
|
337
403
|
return [
|
|
338
404
|
'# Frontier Swarm Codex Job',
|
|
339
405
|
'',
|
|
@@ -365,6 +431,9 @@ export function renderCodexPrompt(job, input) {
|
|
|
365
431
|
'Budget:',
|
|
366
432
|
...bullets(formatBudget(job)),
|
|
367
433
|
'',
|
|
434
|
+
'Resource allocation:',
|
|
435
|
+
...bullets(formatResourceAllocation(resourceAllocation)),
|
|
436
|
+
'',
|
|
368
437
|
'Source refs:',
|
|
369
438
|
...bullets(job.task.sourceRefs),
|
|
370
439
|
'',
|
|
@@ -391,7 +460,11 @@ export async function spawnCodexExecutor(input) {
|
|
|
391
460
|
await fs.writeFile(input.paths.eventsPath, '');
|
|
392
461
|
await fs.writeFile(input.paths.stderrPath, '');
|
|
393
462
|
return new Promise((resolve) => {
|
|
394
|
-
const child = spawn(input.codexPath, input.args, {
|
|
463
|
+
const child = spawn(input.codexPath, input.args, {
|
|
464
|
+
cwd: input.cwd,
|
|
465
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
466
|
+
env: { ...process.env, ...input.env }
|
|
467
|
+
});
|
|
395
468
|
if (child.pid) {
|
|
396
469
|
appendCodexPidManifest(input.paths.pidManifestPath, {
|
|
397
470
|
pid: child.pid,
|
|
@@ -428,6 +501,7 @@ async function createJobPaths(outDir, job, options) {
|
|
|
428
501
|
stderrPath: path.join(jobDir, 'codex-stderr.log'),
|
|
429
502
|
lastMessagePath: path.join(jobDir, 'last-message.md'),
|
|
430
503
|
evidenceDir: path.join(jobDir, 'evidence'),
|
|
504
|
+
resourceAllocationPath: path.join(jobDir, 'evidence', 'resource-allocation.json'),
|
|
431
505
|
workspaceProofPath: path.join(jobDir, 'evidence', 'workspace-proof.json'),
|
|
432
506
|
patchPath: path.join(jobDir, 'evidence', 'changes.patch'),
|
|
433
507
|
mergeBundlePath: path.join(jobDir, 'evidence', 'merge.json'),
|
|
@@ -668,6 +742,20 @@ export async function writeSwarmCoordinatorSnapshot(file, input) {
|
|
|
668
742
|
await fs.writeFile(file, JSON.stringify(dashboard, null, 2) + '\n');
|
|
669
743
|
}
|
|
670
744
|
export async function appendCodexPidManifest(file, entry, runId) {
|
|
745
|
+
const absolute = path.resolve(file);
|
|
746
|
+
const previous = pidManifestWriteQueues.get(absolute) ?? Promise.resolve();
|
|
747
|
+
let next;
|
|
748
|
+
next = previous
|
|
749
|
+
.catch(() => { })
|
|
750
|
+
.then(() => appendCodexPidManifestUnlocked(absolute, entry, runId))
|
|
751
|
+
.finally(() => {
|
|
752
|
+
if (pidManifestWriteQueues.get(absolute) === next)
|
|
753
|
+
pidManifestWriteQueues.delete(absolute);
|
|
754
|
+
});
|
|
755
|
+
pidManifestWriteQueues.set(absolute, next);
|
|
756
|
+
return next;
|
|
757
|
+
}
|
|
758
|
+
async function appendCodexPidManifestUnlocked(file, entry, runId) {
|
|
671
759
|
const manifest = await readCodexPidManifest(file).catch(() => ({
|
|
672
760
|
kind: FRONTIER_SWARM_CODEX_PID_MANIFEST_KIND,
|
|
673
761
|
version: FRONTIER_SWARM_CODEX_PID_MANIFEST_VERSION,
|
|
@@ -677,7 +765,7 @@ export async function appendCodexPidManifest(file, entry, runId) {
|
|
|
677
765
|
const entries = manifest.entries.filter((existing) => existing.pid !== entry.pid || existing.jobId !== entry.jobId);
|
|
678
766
|
entries.push(entry);
|
|
679
767
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
680
|
-
await
|
|
768
|
+
await writeJsonAtomic(file, { ...manifest, ...(runId ? { runId } : {}), entries });
|
|
681
769
|
}
|
|
682
770
|
export async function readCodexPidManifest(file) {
|
|
683
771
|
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
@@ -715,9 +803,29 @@ export async function collectCodexSwarmRun(input) {
|
|
|
715
803
|
'failed-evidence': [],
|
|
716
804
|
'stale-against-head': []
|
|
717
805
|
};
|
|
718
|
-
const
|
|
806
|
+
const collectedBundles = [];
|
|
807
|
+
const patchStatuses = {};
|
|
808
|
+
const mergePaths = (await findFilesByName(runDir, 'merge.json'))
|
|
809
|
+
.filter((mergePath) => !pathHasIgnoredSegment(path.relative(runDir, mergePath), [
|
|
810
|
+
'collected',
|
|
811
|
+
'patch-scores',
|
|
812
|
+
'ready-to-apply',
|
|
813
|
+
'needs-human-port',
|
|
814
|
+
'failed-evidence',
|
|
815
|
+
'stale-against-head'
|
|
816
|
+
]));
|
|
817
|
+
const mergeRecordsByJob = new Map();
|
|
719
818
|
for (const mergePath of mergePaths.sort()) {
|
|
720
|
-
const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
|
|
819
|
+
const bundle = normalizeCollectedMergeBundle(JSON.parse(await fs.readFile(mergePath, 'utf8')), mergePath);
|
|
820
|
+
const existing = mergeRecordsByJob.get(bundle.jobId);
|
|
821
|
+
const next = { mergePath, bundle };
|
|
822
|
+
if (!existing || mergeRecordScore(next) > mergeRecordScore(existing))
|
|
823
|
+
mergeRecordsByJob.set(bundle.jobId, next);
|
|
824
|
+
}
|
|
825
|
+
const mergeRecords = Array.from(mergeRecordsByJob.values()).sort((left, right) => left.bundle.jobId.localeCompare(right.bundle.jobId));
|
|
826
|
+
for (const { mergePath, bundle } of mergeRecords) {
|
|
827
|
+
const patchPath = resolveBundlePatchPath(bundle, mergePath);
|
|
828
|
+
const patchExists = !!patchPath && await pathExists(patchPath);
|
|
721
829
|
const staleAgainstHead = input.checkStale === false ? false : await bundlePatchIsStale(bundle, mergePath, cwd);
|
|
722
830
|
const bucket = classifyCodexCollectBucket(bundle, staleAgainstHead);
|
|
723
831
|
const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(bundle.jobId)}` : bundle.branchName;
|
|
@@ -728,16 +836,26 @@ export async function collectCodexSwarmRun(input) {
|
|
|
728
836
|
disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
|
|
729
837
|
autoMergeable: bucket === 'ready-to-apply' && bundle.autoMergeable
|
|
730
838
|
};
|
|
839
|
+
collectedBundles.push(nextBundle);
|
|
840
|
+
patchStatuses[nextBundle.jobId] = staleAgainstHead ? 'stale' : patchExists ? input.checkStale === false ? 'unknown' : 'applies' : 'missing';
|
|
731
841
|
const outputDir = path.join(outDir, bucket, slug(bundle.jobId));
|
|
732
842
|
await fs.mkdir(outputDir, { recursive: true });
|
|
733
843
|
await fs.writeFile(path.join(outputDir, 'merge.json'), JSON.stringify(nextBundle, null, 2) + '\n');
|
|
734
|
-
const patchPath = resolveBundlePatchPath(nextBundle, mergePath);
|
|
735
844
|
if (patchPath && await pathExists(patchPath))
|
|
736
845
|
await fs.copyFile(patchPath, path.join(outputDir, 'changes.patch')).catch(() => { });
|
|
737
846
|
buckets[bucket].push({ bucket, jobId: bundle.jobId, mergePath, outputDir, bundle: nextBundle });
|
|
738
847
|
}
|
|
848
|
+
const mergeIndex = createSwarmMergeIndex({
|
|
849
|
+
runId: path.basename(runDir),
|
|
850
|
+
bundles: collectedBundles,
|
|
851
|
+
patchStatuses
|
|
852
|
+
});
|
|
853
|
+
const queueOverlay = createSwarmQueueOverlay({
|
|
854
|
+
runId: path.basename(runDir),
|
|
855
|
+
bundles: collectedBundles
|
|
856
|
+
});
|
|
739
857
|
const summary = {
|
|
740
|
-
total:
|
|
858
|
+
total: mergeRecords.length,
|
|
741
859
|
'ready-to-apply': buckets['ready-to-apply'].length,
|
|
742
860
|
'needs-human-port': buckets['needs-human-port'].length,
|
|
743
861
|
'failed-evidence': buckets['failed-evidence'].length,
|
|
@@ -751,12 +869,299 @@ export async function collectCodexSwarmRun(input) {
|
|
|
751
869
|
outDir,
|
|
752
870
|
generatedAt,
|
|
753
871
|
buckets,
|
|
872
|
+
mergeIndex,
|
|
873
|
+
queueOverlay,
|
|
754
874
|
summary
|
|
755
875
|
};
|
|
756
876
|
await fs.mkdir(outDir, { recursive: true });
|
|
757
877
|
await fs.writeFile(path.join(outDir, 'collection.json'), JSON.stringify(result, null, 2) + '\n');
|
|
878
|
+
await fs.writeFile(path.join(outDir, 'merge-index.json'), JSON.stringify(mergeIndex, null, 2) + '\n');
|
|
879
|
+
await fs.writeFile(path.join(outDir, 'queue-overlay.json'), JSON.stringify(queueOverlay, null, 2) + '\n');
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
export async function applyCodexSwarmCollection(input) {
|
|
883
|
+
const generatedAt = Date.now();
|
|
884
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
885
|
+
const dryRun = input.dryRun ?? true;
|
|
886
|
+
if (!input.collection && !input.run)
|
|
887
|
+
throw new Error('apply requires --collection <dir> or --run <run-dir>');
|
|
888
|
+
const collectionDir = input.collection
|
|
889
|
+
? path.resolve(cwd, input.collection)
|
|
890
|
+
: (await collectCodexSwarmRun({ run: String(input.run ?? ''), cwd, outDir: input.outDir })).outDir;
|
|
891
|
+
const outDir = path.resolve(cwd, input.outDir ?? path.join(collectionDir, 'apply-ledger'));
|
|
892
|
+
if (!dryRun && !input.allowDirty) {
|
|
893
|
+
const dirty = await gitDirty(cwd);
|
|
894
|
+
if (dirty.length)
|
|
895
|
+
throw new Error(`refusing to apply into dirty worktree; pass allowDirty to override (${dirty.slice(0, 8).join(', ')})`);
|
|
896
|
+
}
|
|
897
|
+
const bucket = input.bucket ?? 'ready-to-apply';
|
|
898
|
+
const roots = bucket === 'all'
|
|
899
|
+
? ['ready-to-apply', 'needs-human-port', 'failed-evidence', 'stale-against-head'].map((entry) => path.join(collectionDir, entry))
|
|
900
|
+
: [path.join(collectionDir, bucket)];
|
|
901
|
+
const wanted = new Set(input.jobIds ?? []);
|
|
902
|
+
const mergePaths = (await Promise.all(roots.map((root) => findFilesByName(root, 'merge.json')))).flat().sort();
|
|
903
|
+
const entries = [];
|
|
904
|
+
for (const mergePath of mergePaths.slice(0, input.limit ? Math.max(0, Math.floor(input.limit)) : undefined)) {
|
|
905
|
+
const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
|
|
906
|
+
if (wanted.size && !wanted.has(bundle.jobId))
|
|
907
|
+
continue;
|
|
908
|
+
entries.push(await applyCodexMergeBundle({
|
|
909
|
+
cwd,
|
|
910
|
+
bundle,
|
|
911
|
+
mergePath,
|
|
912
|
+
dryRun,
|
|
913
|
+
commit: input.commit ?? false,
|
|
914
|
+
branchPrefix: input.branchPrefix
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
const summary = {
|
|
918
|
+
total: entries.length,
|
|
919
|
+
checked: entries.filter((entry) => entry.status === 'checked').length,
|
|
920
|
+
applied: entries.filter((entry) => entry.status === 'applied').length,
|
|
921
|
+
committed: entries.filter((entry) => entry.status === 'committed').length,
|
|
922
|
+
skipped: entries.filter((entry) => entry.status === 'skipped').length,
|
|
923
|
+
failed: entries.filter((entry) => entry.status === 'failed').length
|
|
924
|
+
};
|
|
925
|
+
const result = {
|
|
926
|
+
kind: FRONTIER_SWARM_CODEX_APPLY_LEDGER_KIND,
|
|
927
|
+
version: FRONTIER_SWARM_CODEX_APPLY_LEDGER_VERSION,
|
|
928
|
+
ok: summary.failed === 0,
|
|
929
|
+
cwd,
|
|
930
|
+
collectionDir,
|
|
931
|
+
outDir,
|
|
932
|
+
generatedAt,
|
|
933
|
+
dryRun,
|
|
934
|
+
entries,
|
|
935
|
+
summary
|
|
936
|
+
};
|
|
937
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
938
|
+
await fs.writeFile(path.join(outDir, 'apply-ledger.json'), JSON.stringify(result, null, 2) + '\n');
|
|
939
|
+
return result;
|
|
940
|
+
}
|
|
941
|
+
export async function scoreCodexSwarmPatches(input) {
|
|
942
|
+
const generatedAt = Date.now();
|
|
943
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
944
|
+
if (!input.collection && !input.run)
|
|
945
|
+
throw new Error('score requires --collection <dir> or --run <run-dir>');
|
|
946
|
+
const collectionDir = input.collection
|
|
947
|
+
? path.resolve(cwd, input.collection)
|
|
948
|
+
: (await collectCodexSwarmRun({ run: String(input.run ?? ''), cwd, outDir: input.outDir })).outDir;
|
|
949
|
+
const outDir = path.resolve(cwd, input.outDir ?? path.join(collectionDir, 'patch-scores'));
|
|
950
|
+
const bucket = input.bucket ?? 'all';
|
|
951
|
+
const roots = bucket === 'all'
|
|
952
|
+
? ['ready-to-apply', 'needs-human-port', 'failed-evidence', 'stale-against-head'].map((entry) => path.join(collectionDir, entry))
|
|
953
|
+
: [path.join(collectionDir, bucket)];
|
|
954
|
+
const wanted = new Set(input.jobIds ?? []);
|
|
955
|
+
const mergePaths = (await Promise.all(roots.map((root) => findFilesByName(root, 'merge.json')))).flat().sort();
|
|
956
|
+
const entries = [];
|
|
957
|
+
for (const mergePath of mergePaths.slice(0, input.limit ? Math.max(0, Math.floor(input.limit)) : undefined)) {
|
|
958
|
+
const bundle = JSON.parse(await fs.readFile(mergePath, 'utf8'));
|
|
959
|
+
if (wanted.size && !wanted.has(bundle.jobId))
|
|
960
|
+
continue;
|
|
961
|
+
entries.push(await scoreCodexMergeBundle({ cwd, mergePath, bundle, outDir, input }));
|
|
962
|
+
}
|
|
963
|
+
const statuses = ['accepted-clean', 'accepted-needs-port', 'conflict', 'test-fail', 'stale', 'evidence-only'];
|
|
964
|
+
const summary = Object.fromEntries(statuses.map((status) => [status, entries.filter((entry) => entry.status === status).length]));
|
|
965
|
+
const result = {
|
|
966
|
+
kind: FRONTIER_SWARM_CODEX_PATCH_SCORE_KIND,
|
|
967
|
+
version: FRONTIER_SWARM_CODEX_PATCH_SCORE_VERSION,
|
|
968
|
+
ok: entries.every((entry) => entry.status === 'accepted-clean' || entry.status === 'accepted-needs-port' || entry.status === 'evidence-only'),
|
|
969
|
+
cwd,
|
|
970
|
+
collectionDir,
|
|
971
|
+
outDir,
|
|
972
|
+
generatedAt,
|
|
973
|
+
entries: entries.sort((left, right) => right.score - left.score || left.jobId.localeCompare(right.jobId)),
|
|
974
|
+
summary: { ...summary, total: entries.length }
|
|
975
|
+
};
|
|
976
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
977
|
+
await fs.writeFile(path.join(outDir, 'patch-score.json'), JSON.stringify(result, null, 2) + '\n');
|
|
758
978
|
return result;
|
|
759
979
|
}
|
|
980
|
+
async function applyCodexMergeBundle(input) {
|
|
981
|
+
const commands = [];
|
|
982
|
+
const patchPath = await resolveApplyPatchPath(input.bundle, input.mergePath);
|
|
983
|
+
const branchName = input.branchPrefix ? `${input.branchPrefix}/${slug(input.bundle.jobId)}` : input.bundle.branchName;
|
|
984
|
+
const base = {
|
|
985
|
+
jobId: input.bundle.jobId,
|
|
986
|
+
bundlePath: input.mergePath,
|
|
987
|
+
...(patchPath ? { patchPath } : {}),
|
|
988
|
+
...(branchName ? { branchName } : {}),
|
|
989
|
+
dryRun: input.dryRun,
|
|
990
|
+
commands
|
|
991
|
+
};
|
|
992
|
+
if (!patchPath) {
|
|
993
|
+
return {
|
|
994
|
+
...base,
|
|
995
|
+
status: input.bundle.disposition === 'discovery-only' ? 'skipped' : 'failed',
|
|
996
|
+
error: 'missing patch'
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
const check = await runLoggedProcess('git', ['apply', '--check', patchPath], input.cwd);
|
|
1000
|
+
commands.push(check);
|
|
1001
|
+
if (check.status !== 0)
|
|
1002
|
+
return { ...base, status: 'failed', error: 'git apply --check failed' };
|
|
1003
|
+
if (input.dryRun)
|
|
1004
|
+
return { ...base, status: 'checked' };
|
|
1005
|
+
if (branchName) {
|
|
1006
|
+
const branch = await runLoggedProcess('git', ['switch', '-c', branchName], input.cwd);
|
|
1007
|
+
commands.push(branch);
|
|
1008
|
+
if (branch.status !== 0)
|
|
1009
|
+
return { ...base, status: 'failed', error: 'git switch -c failed' };
|
|
1010
|
+
}
|
|
1011
|
+
const apply = await runLoggedProcess('git', ['apply', patchPath], input.cwd);
|
|
1012
|
+
commands.push(apply);
|
|
1013
|
+
if (apply.status !== 0)
|
|
1014
|
+
return { ...base, status: 'failed', error: 'git apply failed' };
|
|
1015
|
+
if (!input.commit)
|
|
1016
|
+
return { ...base, status: 'applied' };
|
|
1017
|
+
const add = await runLoggedProcess('git', ['add', '--', ...input.bundle.changedPaths], input.cwd);
|
|
1018
|
+
commands.push(add);
|
|
1019
|
+
if (add.status !== 0)
|
|
1020
|
+
return { ...base, status: 'failed', error: 'git add failed' };
|
|
1021
|
+
const commit = await runLoggedProcess('git', ['commit', '-m', `Apply swarm bundle ${input.bundle.jobId}`], input.cwd);
|
|
1022
|
+
commands.push(commit);
|
|
1023
|
+
if (commit.status !== 0)
|
|
1024
|
+
return { ...base, status: 'failed', error: 'git commit failed' };
|
|
1025
|
+
const rev = await runLoggedProcess('git', ['rev-parse', 'HEAD'], input.cwd);
|
|
1026
|
+
commands.push(rev);
|
|
1027
|
+
return {
|
|
1028
|
+
...base,
|
|
1029
|
+
status: 'committed',
|
|
1030
|
+
commit: rev.stdoutTail[0]
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
async function scoreCodexMergeBundle(input) {
|
|
1034
|
+
const commands = [];
|
|
1035
|
+
const patchPath = await resolveApplyPatchPath(input.bundle, input.mergePath);
|
|
1036
|
+
const base = {
|
|
1037
|
+
jobId: input.bundle.jobId,
|
|
1038
|
+
bundlePath: input.mergePath,
|
|
1039
|
+
...(patchPath ? { patchPath } : {}),
|
|
1040
|
+
changedPaths: [...input.bundle.changedPaths],
|
|
1041
|
+
commands
|
|
1042
|
+
};
|
|
1043
|
+
if (!patchPath || input.bundle.disposition === 'discovery-only' || input.bundle.changedPaths.length === 0) {
|
|
1044
|
+
return { ...base, status: 'evidence-only', score: 20, reasons: ['no patch to apply'] };
|
|
1045
|
+
}
|
|
1046
|
+
if (input.bundle.staleAgainstHead || input.bundle.disposition === 'stale-against-head') {
|
|
1047
|
+
return { ...base, status: 'stale', score: 0, reasons: ['stale-against-head'] };
|
|
1048
|
+
}
|
|
1049
|
+
const workspacePath = await createScoreWorkspace(input.cwd, input.bundle.jobId, input.input);
|
|
1050
|
+
try {
|
|
1051
|
+
const check = await runLoggedProcess('git', ['apply', '--check', patchPath], workspacePath);
|
|
1052
|
+
commands.push(check);
|
|
1053
|
+
if (check.status !== 0)
|
|
1054
|
+
return { ...base, workspacePath, status: 'conflict', score: 0, reasons: ['git apply --check failed'] };
|
|
1055
|
+
const apply = await runLoggedProcess('git', ['apply', patchPath], workspacePath);
|
|
1056
|
+
commands.push(apply);
|
|
1057
|
+
if (apply.status !== 0)
|
|
1058
|
+
return { ...base, workspacePath, status: 'conflict', score: 0, reasons: ['git apply failed'] };
|
|
1059
|
+
const gates = scoreCommands(input.bundle, input.input);
|
|
1060
|
+
for (const gate of gates) {
|
|
1061
|
+
const run = await runLoggedProcess(gate.command, gate.args, gate.cwd ? path.resolve(workspacePath, gate.cwd) : workspacePath);
|
|
1062
|
+
commands.push(run);
|
|
1063
|
+
if (run.status !== 0 && gate.required !== false) {
|
|
1064
|
+
return { ...base, workspacePath, status: 'test-fail', score: 10, reasons: [`gate failed: ${gate.name}`] };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const clean = input.bundle.disposition === 'auto-mergeable' && input.bundle.autoMergeable;
|
|
1068
|
+
return {
|
|
1069
|
+
...base,
|
|
1070
|
+
workspacePath,
|
|
1071
|
+
status: clean ? 'accepted-clean' : 'accepted-needs-port',
|
|
1072
|
+
score: clean ? 100 : 70,
|
|
1073
|
+
reasons: clean ? [] : ['patch applies but bundle is not auto-mergeable']
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
finally {
|
|
1077
|
+
if (!input.input.keepWorkspaces)
|
|
1078
|
+
await fs.rm(workspacePath, { recursive: true, force: true }).catch(() => { });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function createScoreWorkspace(cwd, jobId, input) {
|
|
1082
|
+
const workspacePath = await fs.mkdtemp(path.join(os.tmpdir(), `frontier-swarm-score-${slug(jobId)}-`));
|
|
1083
|
+
const excludes = uniqueWorkspacePaths([
|
|
1084
|
+
'.git',
|
|
1085
|
+
'node_modules',
|
|
1086
|
+
'dist',
|
|
1087
|
+
'coverage',
|
|
1088
|
+
'agent-runs',
|
|
1089
|
+
'.frontier-framework',
|
|
1090
|
+
...(input.workspaceExcludes ?? [])
|
|
1091
|
+
]);
|
|
1092
|
+
const includes = uniqueWorkspacePaths(input.workspaceIncludes ?? []);
|
|
1093
|
+
if (includes.length) {
|
|
1094
|
+
for (const include of includes)
|
|
1095
|
+
await copyWorkspacePath(cwd, workspacePath, include, excludes);
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
await fs.cp(cwd, workspacePath, {
|
|
1099
|
+
recursive: true,
|
|
1100
|
+
force: true,
|
|
1101
|
+
filter: (source) => {
|
|
1102
|
+
if (source === cwd)
|
|
1103
|
+
return true;
|
|
1104
|
+
const relative = path.relative(cwd, source).replace(/\\/g, '/');
|
|
1105
|
+
if (!relative)
|
|
1106
|
+
return true;
|
|
1107
|
+
if (pathHasIgnoredSegment(relative, excludes))
|
|
1108
|
+
return false;
|
|
1109
|
+
return !excludes.some((entry) => relative === entry || relative.startsWith(entry.replace(/\/$/, '') + '/'));
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return workspacePath;
|
|
1114
|
+
}
|
|
1115
|
+
function scoreCommands(bundle, input) {
|
|
1116
|
+
const focused = normalizeScoreCommands(input.focusedCommands ?? []);
|
|
1117
|
+
const global = bundle.changedPaths.some((file) => (input.globalGlobs ?? []).some((glob) => matchesGlob(file, glob)))
|
|
1118
|
+
? normalizeScoreCommands(input.globalCommands ?? [])
|
|
1119
|
+
: [];
|
|
1120
|
+
return [...focused, ...global];
|
|
1121
|
+
}
|
|
1122
|
+
function normalizeScoreCommands(input) {
|
|
1123
|
+
return input.map((entry) => {
|
|
1124
|
+
if (typeof entry === 'string')
|
|
1125
|
+
return { name: entry, command: 'sh', args: ['-c', entry], required: true };
|
|
1126
|
+
return {
|
|
1127
|
+
name: entry.name,
|
|
1128
|
+
command: entry.command,
|
|
1129
|
+
args: [...entry.args],
|
|
1130
|
+
required: entry.required,
|
|
1131
|
+
...(entry.cwd ? { cwd: entry.cwd } : {}),
|
|
1132
|
+
...(entry.metadata ? { metadata: entry.metadata } : {})
|
|
1133
|
+
};
|
|
1134
|
+
}).filter((entry) => entry.command.length > 0);
|
|
1135
|
+
}
|
|
1136
|
+
async function resolveApplyPatchPath(bundle, mergePath) {
|
|
1137
|
+
const sibling = path.join(path.dirname(mergePath), 'changes.patch');
|
|
1138
|
+
if (await pathExists(sibling))
|
|
1139
|
+
return sibling;
|
|
1140
|
+
const patchPath = resolveBundlePatchPath(bundle, mergePath);
|
|
1141
|
+
if (patchPath && await pathExists(patchPath))
|
|
1142
|
+
return patchPath;
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
async function runLoggedProcess(command, args, cwd) {
|
|
1146
|
+
const result = await runProcess(command, args, { cwd, allowFailure: true });
|
|
1147
|
+
return {
|
|
1148
|
+
command: [command, ...args],
|
|
1149
|
+
status: result.status,
|
|
1150
|
+
stdoutTail: tail(result.stdout),
|
|
1151
|
+
stderrTail: tail(result.stderr)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
async function writeJsonAtomic(file, value) {
|
|
1155
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
1156
|
+
await fs.writeFile(tmp, JSON.stringify(value, null, 2) + '\n');
|
|
1157
|
+
await fs.rename(tmp, file);
|
|
1158
|
+
}
|
|
1159
|
+
async function gitDirty(cwd) {
|
|
1160
|
+
const result = await runProcess('git', ['status', '--porcelain'], { cwd, allowFailure: true });
|
|
1161
|
+
if (result.status !== 0)
|
|
1162
|
+
return [];
|
|
1163
|
+
return result.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.slice(3));
|
|
1164
|
+
}
|
|
760
1165
|
async function copyWorkspacePath(cwd, workspacePath, include, excludes) {
|
|
761
1166
|
const relative = normalizeWorkspacePath(include);
|
|
762
1167
|
if (!relative)
|
|
@@ -889,6 +1294,8 @@ function filterWorkspaceChangedPaths(paths, plan) {
|
|
|
889
1294
|
function isIgnoredWorkspaceChangedPath(file, plan) {
|
|
890
1295
|
if (plan.mode !== 'copy' && plan.mode !== 'snapshot')
|
|
891
1296
|
return false;
|
|
1297
|
+
if (pathHasIgnoredSegment(file, ['node_modules', 'dist', 'coverage', '.frontier-framework', 'agent-runs']))
|
|
1298
|
+
return true;
|
|
892
1299
|
const ignored = [
|
|
893
1300
|
...plan.excludes,
|
|
894
1301
|
...plan.artifactIncludes,
|
|
@@ -901,6 +1308,10 @@ function isIgnoredWorkspaceChangedPath(file, plan) {
|
|
|
901
1308
|
];
|
|
902
1309
|
return ignored.some((entry) => file === entry || file.startsWith(entry.replace(/\/$/, '') + '/'));
|
|
903
1310
|
}
|
|
1311
|
+
function pathHasIgnoredSegment(file, segments) {
|
|
1312
|
+
const parts = file.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
1313
|
+
return parts.some((part) => segments.includes(part));
|
|
1314
|
+
}
|
|
904
1315
|
async function snapshotWorkspaceFiles(root) {
|
|
905
1316
|
const snapshot = new Map();
|
|
906
1317
|
await walkWorkspaceFiles(root, root, snapshot);
|
|
@@ -1075,6 +1486,45 @@ function formatBudget(job) {
|
|
|
1075
1486
|
`maxRetries=${job.budget.maxRetries}`
|
|
1076
1487
|
].filter((value) => !!value);
|
|
1077
1488
|
}
|
|
1489
|
+
function formatResourceAllocation(allocation) {
|
|
1490
|
+
const entries = [
|
|
1491
|
+
allocation.capabilities.length ? `capabilities=${allocation.capabilities.join(',')}` : undefined,
|
|
1492
|
+
Object.keys(allocation.resources).length ? `resources=${JSON.stringify(allocation.resources)}` : undefined,
|
|
1493
|
+
allocation.browser ? `browser.required=${allocation.browser.required}` : undefined,
|
|
1494
|
+
allocation.browser?.port ? `browser.port=${allocation.browser.port}` : undefined,
|
|
1495
|
+
allocation.browser?.profileDir ? `browser.profileDir=${allocation.browser.profileDir}` : undefined,
|
|
1496
|
+
allocation.browser?.headless === undefined ? undefined : `browser.headless=${allocation.browser.headless}`,
|
|
1497
|
+
Object.keys(allocation.env).length ? `env=${Object.keys(allocation.env).sort().join(',')}` : undefined
|
|
1498
|
+
].filter((value) => !!value);
|
|
1499
|
+
return entries.length ? entries : ['none'];
|
|
1500
|
+
}
|
|
1501
|
+
function resourceSlot(job, lease, count) {
|
|
1502
|
+
if (count <= 1)
|
|
1503
|
+
return 0;
|
|
1504
|
+
const seed = lease ? lease.fencingToken - 1 : Number.parseInt(stableHash(job.id).slice(0, 8), 16);
|
|
1505
|
+
return Math.abs(seed) % count;
|
|
1506
|
+
}
|
|
1507
|
+
function resolveBrowserProfileDir(job, profileDir, profileDirPrefix, cwd) {
|
|
1508
|
+
const raw = profileDir ?? (profileDirPrefix ? path.join(profileDirPrefix, safePathSegment(job.id)) : undefined);
|
|
1509
|
+
if (!raw)
|
|
1510
|
+
return undefined;
|
|
1511
|
+
return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
1512
|
+
}
|
|
1513
|
+
function safePathSegment(value) {
|
|
1514
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'job';
|
|
1515
|
+
}
|
|
1516
|
+
function uniqueStrings(values) {
|
|
1517
|
+
const out = [];
|
|
1518
|
+
const seen = new Set();
|
|
1519
|
+
for (const value of values) {
|
|
1520
|
+
const normalized = String(value).trim();
|
|
1521
|
+
if (!normalized || seen.has(normalized))
|
|
1522
|
+
continue;
|
|
1523
|
+
seen.add(normalized);
|
|
1524
|
+
out.push(normalized);
|
|
1525
|
+
}
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1078
1528
|
function arrayOfObjects(value) {
|
|
1079
1529
|
return Array.isArray(value) ? value.filter(isObject) : [];
|
|
1080
1530
|
}
|
|
@@ -1145,6 +1595,60 @@ function resolveBundlePatchPath(bundle, mergePath) {
|
|
|
1145
1595
|
return undefined;
|
|
1146
1596
|
return path.isAbsolute(bundle.patchPath) ? bundle.patchPath : path.resolve(path.dirname(mergePath), bundle.patchPath);
|
|
1147
1597
|
}
|
|
1598
|
+
function normalizeCollectedMergeBundle(value, mergePath) {
|
|
1599
|
+
const input = typeof value === 'object' && value !== null ? value : {};
|
|
1600
|
+
const jobId = typeof input.jobId === 'string' && input.jobId ? input.jobId : path.basename(path.dirname(mergePath));
|
|
1601
|
+
const changedPaths = stringArray(input.changedPaths);
|
|
1602
|
+
const status = typeof input.status === 'string' ? input.status : 'completed';
|
|
1603
|
+
const autoMergeable = Boolean(input.autoMergeable);
|
|
1604
|
+
const disposition = typeof input.disposition === 'string'
|
|
1605
|
+
? input.disposition
|
|
1606
|
+
: autoMergeable ? 'auto-mergeable' : status === 'failed' ? 'rejected' : 'needs-port';
|
|
1607
|
+
return {
|
|
1608
|
+
kind: typeof input.kind === 'string' ? input.kind : FRONTIER_SWARM_MERGE_BUNDLE_KIND,
|
|
1609
|
+
version: typeof input.version === 'number' ? input.version : FRONTIER_SWARM_MERGE_BUNDLE_VERSION,
|
|
1610
|
+
id: typeof input.id === 'string' && input.id ? input.id : `swarm-merge-bundle:${jobId}`,
|
|
1611
|
+
...(typeof input.runId === 'string' ? { runId: input.runId } : {}),
|
|
1612
|
+
...(typeof input.planId === 'string' ? { planId: input.planId } : {}),
|
|
1613
|
+
jobId,
|
|
1614
|
+
...(typeof input.taskId === 'string' ? { taskId: input.taskId } : {}),
|
|
1615
|
+
...(typeof input.lane === 'string' ? { lane: input.lane } : {}),
|
|
1616
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
1617
|
+
generatedAt: typeof input.generatedAt === 'number' ? input.generatedAt : Date.now(),
|
|
1618
|
+
status,
|
|
1619
|
+
mergeReadiness: typeof input.mergeReadiness === 'string'
|
|
1620
|
+
? input.mergeReadiness
|
|
1621
|
+
: changedPaths.length ? 'patch-candidate' : 'discovery-only',
|
|
1622
|
+
disposition,
|
|
1623
|
+
riskLevel: typeof input.riskLevel === 'string' ? input.riskLevel : 'unknown',
|
|
1624
|
+
autoMergeable,
|
|
1625
|
+
changedPaths,
|
|
1626
|
+
changedRegions: stringArray(input.changedRegions),
|
|
1627
|
+
ownedFilesTouched: stringArray(input.ownedFilesTouched),
|
|
1628
|
+
allowedWrites: stringArray(input.allowedWrites),
|
|
1629
|
+
ownershipViolations: stringArray(input.ownershipViolations),
|
|
1630
|
+
...(typeof input.patchPath === 'string' ? { patchPath: input.patchPath } : {}),
|
|
1631
|
+
...(typeof input.patchHash === 'string' ? { patchHash: input.patchHash } : {}),
|
|
1632
|
+
evidencePaths: stringArray(input.evidencePaths),
|
|
1633
|
+
commandsPassed: Array.isArray(input.commandsPassed) ? input.commandsPassed : [],
|
|
1634
|
+
commandsFailed: Array.isArray(input.commandsFailed) ? input.commandsFailed : [],
|
|
1635
|
+
queueItemIds: stringArray(input.queueItemIds),
|
|
1636
|
+
...(typeof input.branchName === 'string' ? { branchName: input.branchName } : {}),
|
|
1637
|
+
...(typeof input.commit === 'string' ? { commit: input.commit } : {}),
|
|
1638
|
+
staleAgainstHead: Boolean(input.staleAgainstHead),
|
|
1639
|
+
reasons: stringArray(input.reasons)
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function mergeRecordScore(record) {
|
|
1643
|
+
return (record.mergePath.includes('/evidence/') ? 100 : 0)
|
|
1644
|
+
+ record.bundle.changedPaths.length
|
|
1645
|
+
+ record.bundle.evidencePaths.length
|
|
1646
|
+
+ record.bundle.commandsPassed.length
|
|
1647
|
+
+ record.bundle.commandsFailed.length;
|
|
1648
|
+
}
|
|
1649
|
+
function stringArray(value) {
|
|
1650
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
1651
|
+
}
|
|
1148
1652
|
function classifyCodexCollectBucket(bundle, staleAgainstHead) {
|
|
1149
1653
|
if (staleAgainstHead || bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
|
|
1150
1654
|
return 'stale-against-head';
|