@shapeshift-labs/frontier-swarm 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/benchmarks/package-bench.mjs +103 -1
- package/dist/index.d.ts +801 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1302 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,22 +8,65 @@ export const FRONTIER_SWARM_RUN_KIND = 'frontier.swarm.run';
|
|
|
8
8
|
export const FRONTIER_SWARM_RUN_VERSION = 1;
|
|
9
9
|
export const FRONTIER_SWARM_EVENT_KIND = 'frontier.swarm.event';
|
|
10
10
|
export const FRONTIER_SWARM_EVENT_VERSION = 1;
|
|
11
|
+
export const FRONTIER_SWARM_EVENT_STREAM_KIND = 'frontier.swarm.event-stream';
|
|
12
|
+
export const FRONTIER_SWARM_EVENT_STREAM_VERSION = 1;
|
|
13
|
+
export const FRONTIER_SWARM_MAILBOX_KIND = 'frontier.swarm.mailbox';
|
|
14
|
+
export const FRONTIER_SWARM_MAILBOX_VERSION = 1;
|
|
11
15
|
export const FRONTIER_SWARM_PROOF_KIND = 'frontier.swarm.proof';
|
|
12
16
|
export const FRONTIER_SWARM_PROOF_VERSION = 1;
|
|
13
17
|
export const FRONTIER_SWARM_SCHEDULE_KIND = 'frontier.swarm.schedule';
|
|
14
18
|
export const FRONTIER_SWARM_SCHEDULE_VERSION = 1;
|
|
15
19
|
export const FRONTIER_SWARM_LEASE_KIND = 'frontier.swarm.lease';
|
|
16
20
|
export const FRONTIER_SWARM_LEASE_VERSION = 1;
|
|
21
|
+
export const FRONTIER_SWARM_QUEUE_SNAPSHOT_KIND = 'frontier.swarm.queue-snapshot';
|
|
22
|
+
export const FRONTIER_SWARM_QUEUE_SNAPSHOT_VERSION = 1;
|
|
23
|
+
export const FRONTIER_SWARM_RUN_CHECKPOINT_KIND = 'frontier.swarm.run-checkpoint';
|
|
24
|
+
export const FRONTIER_SWARM_RUN_CHECKPOINT_VERSION = 1;
|
|
17
25
|
export const FRONTIER_SWARM_ARTIFACT_INDEX_KIND = 'frontier.swarm.artifact-index';
|
|
18
26
|
export const FRONTIER_SWARM_ARTIFACT_INDEX_VERSION = 1;
|
|
19
27
|
export const FRONTIER_SWARM_REVIEW_PLAN_KIND = 'frontier.swarm.review-plan';
|
|
20
28
|
export const FRONTIER_SWARM_REVIEW_PLAN_VERSION = 1;
|
|
21
29
|
export const FRONTIER_SWARM_MERGE_PLAN_KIND = 'frontier.swarm.merge-plan';
|
|
22
30
|
export const FRONTIER_SWARM_MERGE_PLAN_VERSION = 1;
|
|
31
|
+
export const FRONTIER_SWARM_MERGE_BUNDLE_KIND = 'frontier.swarm.merge-bundle';
|
|
32
|
+
export const FRONTIER_SWARM_MERGE_BUNDLE_VERSION = 1;
|
|
33
|
+
export const FRONTIER_SWARM_QUEUE_OVERLAY_KIND = 'frontier.swarm.queue-overlay';
|
|
34
|
+
export const FRONTIER_SWARM_QUEUE_OVERLAY_VERSION = 1;
|
|
35
|
+
export const FRONTIER_SWARM_MERGE_INDEX_KIND = 'frontier.swarm.merge-index';
|
|
36
|
+
export const FRONTIER_SWARM_MERGE_INDEX_VERSION = 1;
|
|
37
|
+
export const FRONTIER_SWARM_HOTSPOT_REPORT_KIND = 'frontier.swarm.hotspot-report';
|
|
38
|
+
export const FRONTIER_SWARM_HOTSPOT_REPORT_VERSION = 1;
|
|
39
|
+
export const FRONTIER_SWARM_REVIEWER_LANE_PLAN_KIND = 'frontier.swarm.reviewer-lane-plan';
|
|
40
|
+
export const FRONTIER_SWARM_REVIEWER_LANE_PLAN_VERSION = 1;
|
|
41
|
+
export const FRONTIER_SWARM_RUN_STORE_SHARDS_KIND = 'frontier.swarm.run-store-shards';
|
|
42
|
+
export const FRONTIER_SWARM_RUN_STORE_SHARDS_VERSION = 1;
|
|
43
|
+
export const FRONTIER_SWARM_MERGE_ADMISSION_KIND = 'frontier.swarm.merge-admission';
|
|
44
|
+
export const FRONTIER_SWARM_MERGE_ADMISSION_VERSION = 1;
|
|
45
|
+
export const FRONTIER_SWARM_CONTEXT_PACK_KIND = 'frontier.swarm.context-pack';
|
|
46
|
+
export const FRONTIER_SWARM_CONTEXT_PACK_VERSION = 1;
|
|
47
|
+
export const FRONTIER_SWARM_ORACLE_CORPUS_KIND = 'frontier.swarm.oracle-corpus';
|
|
48
|
+
export const FRONTIER_SWARM_ORACLE_CORPUS_VERSION = 1;
|
|
49
|
+
export const FRONTIER_SWARM_LANE_PLAYBOOK_KIND = 'frontier.swarm.lane-playbook';
|
|
50
|
+
export const FRONTIER_SWARM_LANE_PLAYBOOK_VERSION = 1;
|
|
51
|
+
export const FRONTIER_SWARM_PATCH_STACK_PLAN_KIND = 'frontier.swarm.patch-stack-plan';
|
|
52
|
+
export const FRONTIER_SWARM_PATCH_STACK_PLAN_VERSION = 1;
|
|
23
53
|
export const FRONTIER_SWARM_DEFAULT_CODEX_COMPUTE_ID = 'codex.gpt-5.5.xhigh';
|
|
24
54
|
export const FRONTIER_SWARM_DEFAULT_MODEL = 'gpt-5.5';
|
|
25
55
|
export const FRONTIER_SWARM_DEFAULT_REASONING_EFFORT = 'xhigh';
|
|
26
56
|
const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
|
|
57
|
+
const DEFAULT_SWARM_EVENT_TYPES = [
|
|
58
|
+
'swarm.started',
|
|
59
|
+
'swarm.finished',
|
|
60
|
+
'agent.scheduled',
|
|
61
|
+
'agent.finished',
|
|
62
|
+
'agent.handoff',
|
|
63
|
+
'agent.blocked',
|
|
64
|
+
'agent.ownership-request',
|
|
65
|
+
'agent.evidence',
|
|
66
|
+
'review.requested',
|
|
67
|
+
'review.completed',
|
|
68
|
+
'merge.proposed'
|
|
69
|
+
];
|
|
27
70
|
export function defineSwarmManifest(input = {}) {
|
|
28
71
|
return createSwarmManifest(input);
|
|
29
72
|
}
|
|
@@ -158,6 +201,41 @@ export function createSwarmPlan(manifestInput, taskInput, options = {}) {
|
|
|
158
201
|
...(toJsonObject(options.metadata) ? { metadata: toJsonObject(options.metadata) } : {})
|
|
159
202
|
};
|
|
160
203
|
}
|
|
204
|
+
export function createSwarmTaskSelection(manifestInput, taskInput, options = {}) {
|
|
205
|
+
const manifest = compileSwarm(manifestInput).manifest;
|
|
206
|
+
const tasks = normalizeTaskList(taskInput);
|
|
207
|
+
const lanes = new Set(options.lanes ?? []);
|
|
208
|
+
const layers = new Set(options.layers ?? []);
|
|
209
|
+
const statuses = new Set(options.statuses ?? []);
|
|
210
|
+
const workKinds = new Set(options.workKinds ?? []);
|
|
211
|
+
const selectors = (options.selectors ?? []).map((selector) => selector.toLowerCase());
|
|
212
|
+
const completed = new Set(manifest.policy.completedStatuses);
|
|
213
|
+
const limit = options.limit === undefined ? tasks.length : Math.max(0, Math.floor(options.limit));
|
|
214
|
+
const candidates = tasks
|
|
215
|
+
.filter((task) => !task.lane || manifest.lanes.some((lane) => lane.id === task.lane))
|
|
216
|
+
.filter((task) => lanes.size === 0 || (task.lane !== undefined && lanes.has(task.lane)))
|
|
217
|
+
.filter((task) => layers.size === 0 || taskLayer(manifest, task) !== undefined && layers.has(taskLayer(manifest, task)))
|
|
218
|
+
.filter((task) => statuses.size === 0 || statuses.has(task.status))
|
|
219
|
+
.filter((task) => workKinds.size === 0 || workKinds.has(task.workKind))
|
|
220
|
+
.filter((task) => options.includeCompleted || !completed.has(task.status))
|
|
221
|
+
.filter((task) => selectors.length === 0 || selectors.some((selector) => searchableTask(task).includes(selector)))
|
|
222
|
+
.map((task) => createSelectionEntry(manifest, task, options.priority))
|
|
223
|
+
.filter((entry) => options.includeOwnershipWarnings || entry.ownershipWarnings.length === 0)
|
|
224
|
+
.sort((left, right) => (left.selectionPriority - right.selectionPriority
|
|
225
|
+
|| left.task.priority - right.task.priority
|
|
226
|
+
|| left.task.id.localeCompare(right.task.id)));
|
|
227
|
+
const ordered = options.spreadLanes ? roundRobinSelectionByLane(candidates) : candidates;
|
|
228
|
+
const entries = ordered.slice(0, limit).map((entry, index) => {
|
|
229
|
+
if (!options.assignSelectionPriority)
|
|
230
|
+
return entry;
|
|
231
|
+
return { ...entry, task: { ...entry.task, priority: index } };
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
tasks: entries.map((entry) => entry.task),
|
|
235
|
+
entries,
|
|
236
|
+
summary: summarizeTaskSelection(entries)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
161
239
|
export function createSwarmRun(input) {
|
|
162
240
|
const results = (input.results ?? []).map(normalizeResult);
|
|
163
241
|
const events = (input.events ?? []).map((event) => normalizeEvent({ ...event, runId: event.runId ?? input.id ?? input.plan.runId }));
|
|
@@ -182,6 +260,67 @@ export function recordSwarmEvent(runInput, eventInput) {
|
|
|
182
260
|
run.events = run.events.concat(normalizeEvent({ ...eventInput, runId: eventInput.runId ?? run.id }));
|
|
183
261
|
return run;
|
|
184
262
|
}
|
|
263
|
+
export function createSwarmMailbox(input = {}) {
|
|
264
|
+
const scope = input.scope ?? (input.lane ? 'lane' : input.jobId ? 'job' : 'global');
|
|
265
|
+
const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
|
|
266
|
+
return {
|
|
267
|
+
kind: FRONTIER_SWARM_MAILBOX_KIND,
|
|
268
|
+
version: FRONTIER_SWARM_MAILBOX_VERSION,
|
|
269
|
+
id: input.id ?? 'swarm-mailbox:' + stableHash([input.runId, scope, input.lane, input.jobId, input.path, eventTypes]),
|
|
270
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
271
|
+
scope,
|
|
272
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
273
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
274
|
+
...(input.path ? { path: input.path } : {}),
|
|
275
|
+
eventTypes,
|
|
276
|
+
appendOnly: input.appendOnly ?? true,
|
|
277
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function createSwarmEventStream(input = {}) {
|
|
281
|
+
const laneIds = uniqueStrings((input.lanes ?? []).map(readLaneId));
|
|
282
|
+
const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
|
|
283
|
+
const appendOnly = input.appendOnly ?? true;
|
|
284
|
+
const global = createSwarmMailbox({
|
|
285
|
+
runId: input.runId,
|
|
286
|
+
scope: 'global',
|
|
287
|
+
path: input.root ? joinPathParts(input.root, 'global.jsonl') : undefined,
|
|
288
|
+
eventTypes,
|
|
289
|
+
appendOnly
|
|
290
|
+
});
|
|
291
|
+
const lanes = Object.fromEntries(laneIds.map((lane) => [lane, createSwarmMailbox({
|
|
292
|
+
runId: input.runId,
|
|
293
|
+
scope: 'lane',
|
|
294
|
+
lane,
|
|
295
|
+
path: input.root ? joinPathParts(input.root, 'lanes', `${lane}.jsonl`) : undefined,
|
|
296
|
+
eventTypes,
|
|
297
|
+
appendOnly
|
|
298
|
+
})]));
|
|
299
|
+
return {
|
|
300
|
+
kind: FRONTIER_SWARM_EVENT_STREAM_KIND,
|
|
301
|
+
version: FRONTIER_SWARM_EVENT_STREAM_VERSION,
|
|
302
|
+
id: input.id ?? 'swarm-event-stream:' + stableHash([input.runId, input.root, laneIds, eventTypes]),
|
|
303
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
304
|
+
...(input.root ? { root: input.root } : {}),
|
|
305
|
+
appendOnly,
|
|
306
|
+
global,
|
|
307
|
+
lanes,
|
|
308
|
+
eventTypes,
|
|
309
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
|
|
310
|
+
summary: {
|
|
311
|
+
mailboxCount: 1 + laneIds.length,
|
|
312
|
+
laneCount: laneIds.length,
|
|
313
|
+
eventTypeCount: eventTypes.length
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
export function routeSwarmEventToMailboxes(stream, eventInput) {
|
|
318
|
+
const event = isSwarmEvent(eventInput) ? eventInput : normalizeEvent(eventInput);
|
|
319
|
+
const mailboxes = [stream.global];
|
|
320
|
+
if (event.lane && stream.lanes[event.lane])
|
|
321
|
+
mailboxes.push(stream.lanes[event.lane]);
|
|
322
|
+
return mailboxes;
|
|
323
|
+
}
|
|
185
324
|
export function completeSwarmJob(runInput, resultInput) {
|
|
186
325
|
const run = cloneJsonValue(runInput);
|
|
187
326
|
const result = normalizeResult(resultInput);
|
|
@@ -211,6 +350,672 @@ export function checkSwarmOwnership(job, changedPaths) {
|
|
|
211
350
|
violations
|
|
212
351
|
};
|
|
213
352
|
}
|
|
353
|
+
export function resolveSwarmChangedRegions(job, changedPaths) {
|
|
354
|
+
const changed = uniqueStrings(changedPaths);
|
|
355
|
+
const regions = new Set(job.changedRegions);
|
|
356
|
+
for (const region of job.ownershipRegions) {
|
|
357
|
+
if (region.globs.some((glob) => changed.some((file) => matchesGlob(file, glob))))
|
|
358
|
+
regions.add(region.id);
|
|
359
|
+
for (const selector of region.selectors) {
|
|
360
|
+
if (changed.includes(selector))
|
|
361
|
+
regions.add(region.id);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return Array.from(regions).sort();
|
|
365
|
+
}
|
|
366
|
+
export function classifySwarmMergeReadiness(result) {
|
|
367
|
+
if (result.mergeReadiness)
|
|
368
|
+
return result.mergeReadiness;
|
|
369
|
+
if (result.status === 'blocked')
|
|
370
|
+
return 'blocked';
|
|
371
|
+
if (result.status === 'failed' || result.exitCode !== undefined && result.exitCode !== 0)
|
|
372
|
+
return 'rejected';
|
|
373
|
+
const changedPaths = result.changedPaths ?? [];
|
|
374
|
+
if (changedPaths.length === 0)
|
|
375
|
+
return 'discovery-only';
|
|
376
|
+
const ownershipViolations = result.ownershipViolations ?? [];
|
|
377
|
+
if (ownershipViolations.length)
|
|
378
|
+
return 'rejected';
|
|
379
|
+
const verification = result.verification ?? [];
|
|
380
|
+
const failedRequired = verification.some((entry) => entry.required !== false && entry.status !== 0);
|
|
381
|
+
if (failedRequired)
|
|
382
|
+
return 'patch-candidate';
|
|
383
|
+
return verification.length > 0 || result.status === 'verified' ? 'verified-patch' : 'patch-candidate';
|
|
384
|
+
}
|
|
385
|
+
export function classifySwarmMergeDisposition(result, input = {}) {
|
|
386
|
+
if (result.mergeDisposition)
|
|
387
|
+
return result.mergeDisposition;
|
|
388
|
+
if (input.staleAgainstHead)
|
|
389
|
+
return 'stale-against-head';
|
|
390
|
+
const readiness = classifySwarmMergeReadiness(result);
|
|
391
|
+
if (readiness === 'discovery-only')
|
|
392
|
+
return 'discovery-only';
|
|
393
|
+
if (readiness === 'blocked')
|
|
394
|
+
return 'blocked';
|
|
395
|
+
if (readiness === 'rejected')
|
|
396
|
+
return 'rejected';
|
|
397
|
+
if (readiness === 'verified-patch')
|
|
398
|
+
return 'auto-mergeable';
|
|
399
|
+
return 'needs-port';
|
|
400
|
+
}
|
|
401
|
+
export function createSwarmMergeBundle(input) {
|
|
402
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
403
|
+
const result = isSwarmJobResult(input.result) ? cloneJsonValue(input.result) : normalizeResult(input.result);
|
|
404
|
+
const job = input.job;
|
|
405
|
+
const changedPaths = uniqueStrings(result.changedPaths);
|
|
406
|
+
const changedRegions = uniqueStrings([
|
|
407
|
+
...result.changedRegions,
|
|
408
|
+
...(job ? resolveSwarmChangedRegions(job, changedPaths) : [])
|
|
409
|
+
]);
|
|
410
|
+
const evidencePaths = uniqueStrings([...(result.evidencePaths ?? []), ...(input.evidencePaths ?? [])]);
|
|
411
|
+
const queueItemIds = uniqueStrings([...(result.queueItemIds ?? []), ...(input.queueItemIds ?? []), ...(job ? [job.taskId] : [])]);
|
|
412
|
+
const disposition = input.disposition ?? classifySwarmMergeDisposition(result, { staleAgainstHead: input.staleAgainstHead });
|
|
413
|
+
const commandsPassed = result.verification.filter((entry) => entry.status === 0 || entry.required === false && entry.status === undefined);
|
|
414
|
+
const commandsFailed = result.verification.filter((entry) => entry.status !== undefined && entry.status !== 0 && entry.required !== false);
|
|
415
|
+
const ownedFilesTouched = job ? changedPaths.filter((file) => job.allowedWrites.some((glob) => matchesGlob(file, glob))) : changedPaths;
|
|
416
|
+
const reasons = mergeBundleReasons(result, disposition, input.staleAgainstHead ?? false);
|
|
417
|
+
return {
|
|
418
|
+
kind: FRONTIER_SWARM_MERGE_BUNDLE_KIND,
|
|
419
|
+
version: FRONTIER_SWARM_MERGE_BUNDLE_VERSION,
|
|
420
|
+
id: input.id ?? 'swarm-merge-bundle:' + stableHash([input.runId, input.planId, result.jobId, changedPaths, changedRegions, disposition, generatedAt]),
|
|
421
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
422
|
+
...(input.planId ? { planId: input.planId } : {}),
|
|
423
|
+
jobId: result.jobId,
|
|
424
|
+
...(job ? { taskId: job.taskId, lane: job.lane, title: job.title } : {}),
|
|
425
|
+
generatedAt,
|
|
426
|
+
status: result.status,
|
|
427
|
+
mergeReadiness: result.mergeReadiness,
|
|
428
|
+
disposition,
|
|
429
|
+
riskLevel: input.riskLevel ?? result.riskLevel ?? inferMergeRisk(result, disposition),
|
|
430
|
+
autoMergeable: disposition === 'auto-mergeable' && reasons.length === 0,
|
|
431
|
+
changedPaths,
|
|
432
|
+
changedRegions,
|
|
433
|
+
ownedFilesTouched,
|
|
434
|
+
allowedWrites: job ? [...job.allowedWrites] : [],
|
|
435
|
+
ownershipViolations: [...result.ownershipViolations],
|
|
436
|
+
...(input.patchPath ?? result.patchPath ? { patchPath: input.patchPath ?? result.patchPath } : {}),
|
|
437
|
+
...(input.patchHash ? { patchHash: input.patchHash } : {}),
|
|
438
|
+
evidencePaths,
|
|
439
|
+
commandsPassed,
|
|
440
|
+
commandsFailed,
|
|
441
|
+
queueItemIds,
|
|
442
|
+
...(input.branchName ? { branchName: input.branchName } : {}),
|
|
443
|
+
...(input.commit ? { commit: input.commit } : {}),
|
|
444
|
+
staleAgainstHead: input.staleAgainstHead ?? false,
|
|
445
|
+
reasons,
|
|
446
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
export function createSwarmQueueOverlay(input = {}) {
|
|
450
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
451
|
+
const entries = [];
|
|
452
|
+
for (const bundle of input.bundles ?? []) {
|
|
453
|
+
const status = queueOverlayStatusFromBundle(bundle);
|
|
454
|
+
const queueItemIds = bundle.queueItemIds.length ? bundle.queueItemIds : [bundle.taskId ?? bundle.jobId];
|
|
455
|
+
for (const queueItemId of queueItemIds) {
|
|
456
|
+
entries.push({
|
|
457
|
+
queueItemId,
|
|
458
|
+
jobId: bundle.jobId,
|
|
459
|
+
status,
|
|
460
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
461
|
+
disposition: bundle.disposition,
|
|
462
|
+
riskLevel: bundle.riskLevel,
|
|
463
|
+
...(bundle.patchPath ? { patchPath: bundle.patchPath } : {}),
|
|
464
|
+
evidencePaths: [...bundle.evidencePaths],
|
|
465
|
+
changedPaths: [...bundle.changedPaths],
|
|
466
|
+
changedRegions: [...bundle.changedRegions],
|
|
467
|
+
reasons: [...bundle.reasons],
|
|
468
|
+
generatedAt: bundle.generatedAt
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const raw of input.results ?? []) {
|
|
473
|
+
const result = isSwarmJobResult(raw) ? cloneJsonValue(raw) : normalizeResult(raw);
|
|
474
|
+
const queueItemIds = result.queueItemIds.length ? result.queueItemIds : [result.jobId];
|
|
475
|
+
for (const queueItemId of queueItemIds) {
|
|
476
|
+
entries.push({
|
|
477
|
+
queueItemId,
|
|
478
|
+
jobId: result.jobId,
|
|
479
|
+
status: queueOverlayStatusFromResult(result),
|
|
480
|
+
mergeReadiness: result.mergeReadiness,
|
|
481
|
+
disposition: result.mergeDisposition,
|
|
482
|
+
riskLevel: result.riskLevel,
|
|
483
|
+
...(result.patchPath ? { patchPath: result.patchPath } : {}),
|
|
484
|
+
evidencePaths: [...result.evidencePaths],
|
|
485
|
+
changedPaths: [...result.changedPaths],
|
|
486
|
+
changedRegions: [...result.changedRegions],
|
|
487
|
+
reasons: result.error ? [result.error] : [],
|
|
488
|
+
generatedAt
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const byQueueItemId = groupOverlayEntries(entries);
|
|
493
|
+
return {
|
|
494
|
+
kind: FRONTIER_SWARM_QUEUE_OVERLAY_KIND,
|
|
495
|
+
version: FRONTIER_SWARM_QUEUE_OVERLAY_VERSION,
|
|
496
|
+
id: input.id ?? 'swarm-queue-overlay:' + stableHash([input.runId, entries, generatedAt]),
|
|
497
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
498
|
+
generatedAt,
|
|
499
|
+
entries,
|
|
500
|
+
byQueueItemId,
|
|
501
|
+
summary: {
|
|
502
|
+
entryCount: entries.length,
|
|
503
|
+
queueItemCount: Object.keys(byQueueItemId).length,
|
|
504
|
+
readyToApplyCount: entries.filter((entry) => entry.status === 'ready-to-apply').length,
|
|
505
|
+
needsHumanPortCount: entries.filter((entry) => entry.status === 'needs-human-port').length,
|
|
506
|
+
failedEvidenceCount: entries.filter((entry) => entry.status === 'failed-evidence').length,
|
|
507
|
+
staleAgainstHeadCount: entries.filter((entry) => entry.status === 'stale-against-head').length,
|
|
508
|
+
discoveryOnlyCount: entries.filter((entry) => entry.status === 'discovery-only').length
|
|
509
|
+
},
|
|
510
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
export function deriveSwarmQueueStatus(input) {
|
|
514
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
515
|
+
const latestByQueueItem = new Map();
|
|
516
|
+
for (const overlay of input.overlays ?? []) {
|
|
517
|
+
for (const entry of overlay.entries) {
|
|
518
|
+
const existing = latestByQueueItem.get(entry.queueItemId);
|
|
519
|
+
if (!existing || entry.generatedAt >= existing.generatedAt)
|
|
520
|
+
latestByQueueItem.set(entry.queueItemId, entry);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const jobs = input.snapshot.jobs.map((job) => {
|
|
524
|
+
const overlay = latestByQueueItem.get(job.taskId ?? job.jobId) ?? latestByQueueItem.get(job.jobId);
|
|
525
|
+
if (!overlay)
|
|
526
|
+
return cloneJsonValue(job);
|
|
527
|
+
return {
|
|
528
|
+
...cloneJsonValue(job),
|
|
529
|
+
status: queueJobStatusFromOverlay(overlay),
|
|
530
|
+
lastError: overlay.status === 'failed-evidence' || overlay.status === 'stale-against-head' ? overlay.reasons.join(', ') : job.lastError,
|
|
531
|
+
metadata: toJsonObject({
|
|
532
|
+
...(job.metadata ?? {}),
|
|
533
|
+
overlayStatus: overlay.status,
|
|
534
|
+
mergeDisposition: overlay.disposition,
|
|
535
|
+
mergeReadiness: overlay.mergeReadiness,
|
|
536
|
+
evidencePaths: overlay.evidencePaths
|
|
537
|
+
})
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
const byStatus = groupIds(jobs, (job) => job.status);
|
|
541
|
+
return {
|
|
542
|
+
generatedAt,
|
|
543
|
+
jobs,
|
|
544
|
+
byStatus,
|
|
545
|
+
summary: {
|
|
546
|
+
jobCount: jobs.length,
|
|
547
|
+
leaseCount: input.snapshot.leases.length,
|
|
548
|
+
readyCount: byStatus.ready?.length ?? 0,
|
|
549
|
+
leasedCount: byStatus.leased?.length ?? 0,
|
|
550
|
+
completedCount: byStatus.completed?.length ?? 0,
|
|
551
|
+
failedCount: byStatus.failed?.length ?? 0,
|
|
552
|
+
deadLetterCount: byStatus['dead-letter']?.length ?? 0
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
export function createSwarmMergeIndex(input) {
|
|
557
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
558
|
+
const entries = input.bundles.map((bundle) => {
|
|
559
|
+
const patchStatus = input.patchStatuses?.[bundle.jobId] ?? (bundle.staleAgainstHead ? 'stale' : bundle.patchPath ? 'unknown' : 'missing');
|
|
560
|
+
const staleAgainstHead = bundle.staleAgainstHead || patchStatus === 'stale' || patchStatus === 'failed-check';
|
|
561
|
+
return {
|
|
562
|
+
jobId: bundle.jobId,
|
|
563
|
+
...(bundle.taskId ? { taskId: bundle.taskId } : {}),
|
|
564
|
+
...(bundle.lane ? { lane: bundle.lane } : {}),
|
|
565
|
+
...(bundle.title ? { title: bundle.title } : {}),
|
|
566
|
+
status: bundle.status,
|
|
567
|
+
mergeReadiness: bundle.mergeReadiness,
|
|
568
|
+
disposition: staleAgainstHead ? 'stale-against-head' : bundle.disposition,
|
|
569
|
+
riskLevel: bundle.riskLevel,
|
|
570
|
+
patchStatus,
|
|
571
|
+
staleAgainstHead,
|
|
572
|
+
autoMergeable: bundle.autoMergeable && !staleAgainstHead,
|
|
573
|
+
changedPaths: [...bundle.changedPaths],
|
|
574
|
+
changedRegions: [...bundle.changedRegions],
|
|
575
|
+
conflictKeys: mergeIndexConflictKeys(bundle),
|
|
576
|
+
conflictingJobIds: [],
|
|
577
|
+
ownedFilesTouched: [...bundle.ownedFilesTouched],
|
|
578
|
+
ownershipViolations: [...bundle.ownershipViolations],
|
|
579
|
+
...(bundle.patchPath ? { patchPath: bundle.patchPath } : {}),
|
|
580
|
+
...(bundle.patchHash ? { patchHash: bundle.patchHash } : {}),
|
|
581
|
+
evidencePaths: [...bundle.evidencePaths],
|
|
582
|
+
queueItemIds: [...bundle.queueItemIds],
|
|
583
|
+
reasons: uniqueStrings([...bundle.reasons, ...(staleAgainstHead ? ['stale-against-head'] : [])]),
|
|
584
|
+
generatedAt: bundle.generatedAt
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
const conflicts = createMergeIndexConflicts(entries);
|
|
588
|
+
const conflictsByJob = new Map();
|
|
589
|
+
for (const conflict of conflicts) {
|
|
590
|
+
for (const jobId of conflict.jobIds) {
|
|
591
|
+
const set = conflictsByJob.get(jobId) ?? new Set();
|
|
592
|
+
for (const other of conflict.jobIds)
|
|
593
|
+
if (other !== jobId)
|
|
594
|
+
set.add(other);
|
|
595
|
+
conflictsByJob.set(jobId, set);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const indexed = entries.map((entry) => ({
|
|
599
|
+
...entry,
|
|
600
|
+
conflictingJobIds: Array.from(conflictsByJob.get(entry.jobId) ?? []).sort()
|
|
601
|
+
}));
|
|
602
|
+
const byDisposition = groupJobIdsBy(indexed, (entry) => entry.disposition);
|
|
603
|
+
const byPath = groupJobIdsByMany(indexed, (entry) => entry.changedPaths);
|
|
604
|
+
const byRegion = groupJobIdsByMany(indexed, (entry) => entry.changedRegions);
|
|
605
|
+
return {
|
|
606
|
+
kind: FRONTIER_SWARM_MERGE_INDEX_KIND,
|
|
607
|
+
version: FRONTIER_SWARM_MERGE_INDEX_VERSION,
|
|
608
|
+
id: input.id ?? 'swarm-merge-index:' + stableHash([input.runId, input.planId, indexed, conflicts, generatedAt]),
|
|
609
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
610
|
+
...(input.planId ? { planId: input.planId } : {}),
|
|
611
|
+
generatedAt,
|
|
612
|
+
entries: indexed,
|
|
613
|
+
conflicts,
|
|
614
|
+
byDisposition,
|
|
615
|
+
byPath,
|
|
616
|
+
byRegion,
|
|
617
|
+
summary: {
|
|
618
|
+
entryCount: indexed.length,
|
|
619
|
+
readyToApplyCount: indexed.filter((entry) => entry.disposition === 'auto-mergeable' && entry.autoMergeable && !entry.conflictingJobIds.length).length,
|
|
620
|
+
needsHumanPortCount: indexed.filter((entry) => entry.disposition === 'needs-port').length,
|
|
621
|
+
failedEvidenceCount: indexed.filter((entry) => entry.disposition === 'rejected' || entry.disposition === 'blocked' || entry.ownershipViolations.length > 0).length,
|
|
622
|
+
staleAgainstHeadCount: indexed.filter((entry) => entry.staleAgainstHead || entry.disposition === 'stale-against-head').length,
|
|
623
|
+
discoveryOnlyCount: indexed.filter((entry) => entry.disposition === 'discovery-only').length,
|
|
624
|
+
conflictCount: conflicts.length,
|
|
625
|
+
conflictedJobCount: conflictsByJob.size
|
|
626
|
+
},
|
|
627
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
export function checkSwarmRegionOwnership(job, input = {}) {
|
|
631
|
+
const changedPaths = uniqueStrings(input.changedPaths ?? []);
|
|
632
|
+
const resolvedRegions = resolveSwarmChangedRegions(job, changedPaths);
|
|
633
|
+
const changedRegions = uniqueStrings([...(input.changedRegions ?? []), ...resolvedRegions]);
|
|
634
|
+
const ownedRegions = new Set(job.ownedRegions);
|
|
635
|
+
const regionViolations = changedRegions.filter((region) => !ownedRegions.has(region));
|
|
636
|
+
const classifiedPaths = new Set();
|
|
637
|
+
for (const region of job.ownershipRegions) {
|
|
638
|
+
for (const file of changedPaths) {
|
|
639
|
+
if (region.globs.some((glob) => matchesGlob(file, glob)))
|
|
640
|
+
classifiedPaths.add(file);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const unclassifiedChangedPaths = changedPaths.filter((file) => !classifiedPaths.has(file));
|
|
644
|
+
return {
|
|
645
|
+
ok: regionViolations.length === 0 && (job.ownershipRegions.length === 0 || unclassifiedChangedPaths.length === 0),
|
|
646
|
+
jobId: job.id,
|
|
647
|
+
changedPaths,
|
|
648
|
+
changedRegions,
|
|
649
|
+
ownedRegions: [...job.ownedRegions],
|
|
650
|
+
regionViolations,
|
|
651
|
+
unclassifiedChangedPaths
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
export function createSwarmHotspotReport(input = {}) {
|
|
655
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
656
|
+
const threshold = Math.max(2, Math.floor(input.threshold ?? 3));
|
|
657
|
+
const byPath = new Map();
|
|
658
|
+
for (const bundle of input.bundles ?? []) {
|
|
659
|
+
for (const file of bundle.changedPaths) {
|
|
660
|
+
const current = byPath.get(file) ?? {
|
|
661
|
+
path: file,
|
|
662
|
+
touchCount: 0,
|
|
663
|
+
jobIds: [],
|
|
664
|
+
regions: [],
|
|
665
|
+
dispositions: [],
|
|
666
|
+
riskLevels: []
|
|
667
|
+
};
|
|
668
|
+
current.touchCount += 1;
|
|
669
|
+
current.jobIds = uniqueStrings([...current.jobIds, bundle.jobId]);
|
|
670
|
+
current.regions = uniqueStrings([...current.regions, ...bundle.changedRegions]);
|
|
671
|
+
current.dispositions = uniqueStrings([...current.dispositions, bundle.disposition]);
|
|
672
|
+
current.riskLevels = uniqueStrings([...current.riskLevels, bundle.riskLevel]);
|
|
673
|
+
byPath.set(file, current);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const raw of input.results ?? []) {
|
|
677
|
+
const result = isSwarmJobResult(raw) ? raw : normalizeResult(raw);
|
|
678
|
+
for (const file of result.changedPaths) {
|
|
679
|
+
const current = byPath.get(file) ?? {
|
|
680
|
+
path: file,
|
|
681
|
+
touchCount: 0,
|
|
682
|
+
jobIds: [],
|
|
683
|
+
regions: [],
|
|
684
|
+
dispositions: [],
|
|
685
|
+
riskLevels: []
|
|
686
|
+
};
|
|
687
|
+
current.touchCount += 1;
|
|
688
|
+
current.jobIds = uniqueStrings([...current.jobIds, result.jobId]);
|
|
689
|
+
current.regions = uniqueStrings([...current.regions, ...result.changedRegions]);
|
|
690
|
+
current.dispositions = uniqueStrings([...current.dispositions, result.mergeDisposition]);
|
|
691
|
+
current.riskLevels = uniqueStrings([...current.riskLevels, result.riskLevel]);
|
|
692
|
+
byPath.set(file, current);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const entries = Array.from(byPath.values()).sort((left, right) => right.touchCount - left.touchCount || left.path.localeCompare(right.path));
|
|
696
|
+
const recommendations = entries
|
|
697
|
+
.filter((entry) => entry.touchCount >= threshold || entry.regions.length > 1)
|
|
698
|
+
.map((entry) => ({
|
|
699
|
+
path: entry.path,
|
|
700
|
+
reason: entry.regions.length > 1 ? 'region-overlap' : 'hot-file',
|
|
701
|
+
suggestedModuleId: suggestedModuleId(entry.path),
|
|
702
|
+
suggestedOwnershipRegions: entry.regions.length ? entry.regions : [`${suggestedModuleId(entry.path)}.*`],
|
|
703
|
+
jobIds: [...entry.jobIds]
|
|
704
|
+
}));
|
|
705
|
+
return {
|
|
706
|
+
kind: FRONTIER_SWARM_HOTSPOT_REPORT_KIND,
|
|
707
|
+
version: FRONTIER_SWARM_HOTSPOT_REPORT_VERSION,
|
|
708
|
+
id: input.id ?? 'swarm-hotspot-report:' + stableHash([entries, threshold, generatedAt]),
|
|
709
|
+
generatedAt,
|
|
710
|
+
threshold,
|
|
711
|
+
entries,
|
|
712
|
+
recommendations,
|
|
713
|
+
summary: {
|
|
714
|
+
pathCount: entries.length,
|
|
715
|
+
hotspotCount: entries.filter((entry) => entry.touchCount >= threshold).length,
|
|
716
|
+
recommendationCount: recommendations.length
|
|
717
|
+
},
|
|
718
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
export function createSwarmReviewerLanePlan(input) {
|
|
722
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
723
|
+
const reviewerLane = input.reviewerLane ?? 'review';
|
|
724
|
+
const reviewers = uniqueStrings(input.reviewers ?? []);
|
|
725
|
+
const deferralsByJob = new Map((input.admission?.deferred ?? []).map((entry) => [entry.jobId, entry.reasons]));
|
|
726
|
+
const candidates = input.index.entries.filter((entry) => input.includeAutoMergeable
|
|
727
|
+
|| deferralsByJob.has(entry.jobId)
|
|
728
|
+
|| entry.conflictingJobIds.length > 0
|
|
729
|
+
|| entry.riskLevel === 'high'
|
|
730
|
+
|| entry.disposition !== 'auto-mergeable'
|
|
731
|
+
|| !entry.autoMergeable);
|
|
732
|
+
const assignments = candidates.map((entry) => ({
|
|
733
|
+
jobId: entry.jobId,
|
|
734
|
+
reviewers: selectReviewers(reviewers, reviewers.length ? 1 : 0, entry.jobId),
|
|
735
|
+
required: deferralsByJob.has(entry.jobId) || entry.conflictingJobIds.length > 0 || entry.riskLevel === 'high' || entry.disposition !== 'auto-mergeable',
|
|
736
|
+
reasons: uniqueStrings([...reviewerLaneReasons(entry), ...(deferralsByJob.get(entry.jobId) ?? [])])
|
|
737
|
+
}));
|
|
738
|
+
const tasks = candidates.map((entry) => ({
|
|
739
|
+
id: `review-${slug(entry.jobId)}`,
|
|
740
|
+
lane: reviewerLane,
|
|
741
|
+
kind: 'review',
|
|
742
|
+
title: `Review ${entry.title ?? entry.jobId}`,
|
|
743
|
+
objective: `Review swarm merge bundle ${entry.jobId}.`,
|
|
744
|
+
sourceRefs: entry.evidencePaths,
|
|
745
|
+
targetRefs: entry.changedPaths,
|
|
746
|
+
ownedRegions: entry.changedRegions,
|
|
747
|
+
acceptance: [
|
|
748
|
+
'Review evidence, patch applicability, ownership, conflicts, and risk.',
|
|
749
|
+
`Merge disposition: ${entry.disposition}.`
|
|
750
|
+
],
|
|
751
|
+
metadata: {
|
|
752
|
+
mergeJobId: entry.jobId,
|
|
753
|
+
conflictingJobIds: entry.conflictingJobIds,
|
|
754
|
+
reasons: uniqueStrings([...reviewerLaneReasons(entry), ...(deferralsByJob.get(entry.jobId) ?? [])])
|
|
755
|
+
}
|
|
756
|
+
}));
|
|
757
|
+
return {
|
|
758
|
+
kind: FRONTIER_SWARM_REVIEWER_LANE_PLAN_KIND,
|
|
759
|
+
version: FRONTIER_SWARM_REVIEWER_LANE_PLAN_VERSION,
|
|
760
|
+
id: input.id ?? 'swarm-reviewer-lane-plan:' + stableHash([input.index.id, assignments, generatedAt]),
|
|
761
|
+
mergeIndexId: input.index.id,
|
|
762
|
+
generatedAt,
|
|
763
|
+
reviewerLane,
|
|
764
|
+
assignments,
|
|
765
|
+
tasks,
|
|
766
|
+
summary: {
|
|
767
|
+
assignmentCount: assignments.length,
|
|
768
|
+
taskCount: tasks.length
|
|
769
|
+
},
|
|
770
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
export function createSwarmRunStoreShards(input = {}) {
|
|
774
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
775
|
+
const root = input.root ?? 'agent-runs/shards';
|
|
776
|
+
const shardSize = Math.max(1, Math.floor(input.shardSize ?? 100));
|
|
777
|
+
const groupBy = input.groupBy ?? 'lane';
|
|
778
|
+
const jobs = input.run?.jobs ?? input.plan?.jobs ?? [];
|
|
779
|
+
const groups = new Map();
|
|
780
|
+
for (const job of jobs) {
|
|
781
|
+
const key = groupBy === 'none' ? 'all' : groupBy === 'hash' ? String(hashBucket(job.id, shardSize)) : job.lane;
|
|
782
|
+
groups.set(key, [...(groups.get(key) ?? []), job]);
|
|
783
|
+
}
|
|
784
|
+
const shards = [];
|
|
785
|
+
for (const [group, groupJobs] of Array.from(groups.entries()).sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
786
|
+
for (let index = 0; index < groupJobs.length; index += shardSize) {
|
|
787
|
+
const slice = groupJobs.slice(index, index + shardSize);
|
|
788
|
+
const suffix = `${slug(group)}-${Math.floor(index / shardSize)}`;
|
|
789
|
+
const shardRoot = joinPathParts(root, suffix);
|
|
790
|
+
shards.push({
|
|
791
|
+
id: 'swarm-run-store-shard:' + stableHash([input.run?.id, input.plan?.id, group, index, slice.map((job) => job.id)]),
|
|
792
|
+
...(groupBy === 'lane' ? { lane: group } : {}),
|
|
793
|
+
path: shardRoot,
|
|
794
|
+
eventPath: joinPathParts(shardRoot, 'events.jsonl'),
|
|
795
|
+
resultPath: joinPathParts(shardRoot, 'results.jsonl'),
|
|
796
|
+
checkpointPath: joinPathParts(shardRoot, 'checkpoint.json'),
|
|
797
|
+
jobIds: slice.map((job) => job.id)
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
kind: FRONTIER_SWARM_RUN_STORE_SHARDS_KIND,
|
|
803
|
+
version: FRONTIER_SWARM_RUN_STORE_SHARDS_VERSION,
|
|
804
|
+
id: input.id ?? 'swarm-run-store-shards:' + stableHash([input.run?.id, input.plan?.id, root, shardSize, groupBy, shards, generatedAt]),
|
|
805
|
+
...(input.run ? { runId: input.run.id } : {}),
|
|
806
|
+
...(input.plan ? { planId: input.plan.id } : {}),
|
|
807
|
+
root,
|
|
808
|
+
generatedAt,
|
|
809
|
+
groupBy,
|
|
810
|
+
shardSize,
|
|
811
|
+
shards,
|
|
812
|
+
summary: {
|
|
813
|
+
shardCount: shards.length,
|
|
814
|
+
jobCount: jobs.length
|
|
815
|
+
},
|
|
816
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
export function createSwarmMergeAdmission(input) {
|
|
820
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
821
|
+
const maxReady = Math.max(0, Math.floor(input.maxReady ?? input.index.entries.length));
|
|
822
|
+
const maxChangedPaths = input.maxChangedPaths === undefined ? undefined : Math.max(0, Math.floor(input.maxChangedPaths));
|
|
823
|
+
const maxChangedRegions = input.maxChangedRegions === undefined ? undefined : Math.max(0, Math.floor(input.maxChangedRegions));
|
|
824
|
+
const maxHighRisk = input.maxHighRisk === undefined ? undefined : Math.max(0, Math.floor(input.maxHighRisk));
|
|
825
|
+
const allowRisks = uniqueStrings(input.allowRisks ?? ['low', 'medium']);
|
|
826
|
+
const admitted = [];
|
|
827
|
+
const deferred = [];
|
|
828
|
+
const usedPaths = new Set();
|
|
829
|
+
const usedRegions = new Set();
|
|
830
|
+
let highRiskCount = 0;
|
|
831
|
+
for (const entry of input.index.entries) {
|
|
832
|
+
const reasons = [];
|
|
833
|
+
if (entry.disposition !== 'auto-mergeable' || !entry.autoMergeable)
|
|
834
|
+
reasons.push('not-auto-mergeable');
|
|
835
|
+
if (entry.staleAgainstHead)
|
|
836
|
+
reasons.push('stale-against-head');
|
|
837
|
+
if (entry.conflictingJobIds.length)
|
|
838
|
+
reasons.push('conflicting-changes');
|
|
839
|
+
if (!allowRisks.includes(entry.riskLevel))
|
|
840
|
+
reasons.push('risk-not-admitted');
|
|
841
|
+
if (admitted.length >= maxReady)
|
|
842
|
+
reasons.push('max-ready');
|
|
843
|
+
const nextPaths = new Set([...usedPaths, ...entry.changedPaths]);
|
|
844
|
+
const nextRegions = new Set([...usedRegions, ...entry.changedRegions]);
|
|
845
|
+
const nextHighRiskCount = highRiskCount + (entry.riskLevel === 'high' ? 1 : 0);
|
|
846
|
+
if (maxChangedPaths !== undefined && nextPaths.size > maxChangedPaths)
|
|
847
|
+
reasons.push('max-changed-paths');
|
|
848
|
+
if (maxChangedRegions !== undefined && nextRegions.size > maxChangedRegions)
|
|
849
|
+
reasons.push('max-changed-regions');
|
|
850
|
+
if (maxHighRisk !== undefined && nextHighRiskCount > maxHighRisk)
|
|
851
|
+
reasons.push('max-high-risk');
|
|
852
|
+
if (reasons.length) {
|
|
853
|
+
deferred.push({ jobId: entry.jobId, reasons: uniqueStrings(reasons) });
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
admitted.push(entry.jobId);
|
|
857
|
+
for (const file of entry.changedPaths)
|
|
858
|
+
usedPaths.add(file);
|
|
859
|
+
for (const region of entry.changedRegions)
|
|
860
|
+
usedRegions.add(region);
|
|
861
|
+
highRiskCount = nextHighRiskCount;
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
kind: FRONTIER_SWARM_MERGE_ADMISSION_KIND,
|
|
865
|
+
version: FRONTIER_SWARM_MERGE_ADMISSION_VERSION,
|
|
866
|
+
id: input.id ?? 'swarm-merge-admission:' + stableHash([input.index.id, admitted, deferred, generatedAt]),
|
|
867
|
+
mergeIndexId: input.index.id,
|
|
868
|
+
generatedAt,
|
|
869
|
+
admitted,
|
|
870
|
+
deferred,
|
|
871
|
+
budget: {
|
|
872
|
+
maxReady,
|
|
873
|
+
...(maxChangedPaths !== undefined ? { maxChangedPaths } : {}),
|
|
874
|
+
...(maxChangedRegions !== undefined ? { maxChangedRegions } : {}),
|
|
875
|
+
...(maxHighRisk !== undefined ? { maxHighRisk } : {}),
|
|
876
|
+
allowRisks
|
|
877
|
+
},
|
|
878
|
+
summary: {
|
|
879
|
+
admittedCount: admitted.length,
|
|
880
|
+
deferredCount: deferred.length,
|
|
881
|
+
changedPathCount: usedPaths.size,
|
|
882
|
+
changedRegionCount: usedRegions.size,
|
|
883
|
+
highRiskCount
|
|
884
|
+
},
|
|
885
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
export function createSwarmContextPack(input = {}) {
|
|
889
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
890
|
+
const task = input.job?.task ?? (input.task ? isSwarmTask(input.task) ? input.task : normalizeTask(input.task) : undefined);
|
|
891
|
+
const files = uniqueStrings([
|
|
892
|
+
...(input.files ?? []),
|
|
893
|
+
...(input.job?.task.sourceRefs ?? []),
|
|
894
|
+
...(input.job?.task.targetRefs ?? []),
|
|
895
|
+
...(task?.sourceRefs ?? []),
|
|
896
|
+
...(task?.targetRefs ?? [])
|
|
897
|
+
]);
|
|
898
|
+
const apiMap = Object.fromEntries(Object.entries(input.apiMap ?? {}).map(([key, values]) => [key, uniqueStrings(values)]));
|
|
899
|
+
const commands = normalizeCommands([
|
|
900
|
+
...(input.commands ?? []),
|
|
901
|
+
...(input.oracleCommands ?? []),
|
|
902
|
+
...(input.job?.verification ?? [])
|
|
903
|
+
]);
|
|
904
|
+
const expectedEvidence = uniqueStrings([
|
|
905
|
+
...(input.expectedEvidence ?? []),
|
|
906
|
+
...(input.job?.evidencePrefix ? [joinPathParts(input.job.evidencePrefix, 'evidence.json')] : [])
|
|
907
|
+
]);
|
|
908
|
+
return {
|
|
909
|
+
kind: FRONTIER_SWARM_CONTEXT_PACK_KIND,
|
|
910
|
+
version: FRONTIER_SWARM_CONTEXT_PACK_VERSION,
|
|
911
|
+
id: input.id ?? 'swarm-context-pack:' + stableHash([input.job?.id, task?.id, files, apiMap, generatedAt]),
|
|
912
|
+
...(input.job ? { jobId: input.job.id } : {}),
|
|
913
|
+
...(task ? { taskId: task.id } : {}),
|
|
914
|
+
...(input.job?.lane ?? task?.lane ? { lane: input.job?.lane ?? task?.lane } : {}),
|
|
915
|
+
title: input.title ?? input.job?.title ?? task?.title ?? 'Swarm Context Pack',
|
|
916
|
+
generatedAt,
|
|
917
|
+
files,
|
|
918
|
+
apiMap,
|
|
919
|
+
knownFailures: uniqueStrings(input.knownFailures ?? []),
|
|
920
|
+
commands,
|
|
921
|
+
oracleCommands: commands,
|
|
922
|
+
...(input.evidenceSchema !== undefined ? { evidenceSchema: toJsonValue(input.evidenceSchema) } : {}),
|
|
923
|
+
expectedEvidence,
|
|
924
|
+
exclusions: uniqueStrings(input.exclusions ?? []),
|
|
925
|
+
avoidInvestigating: uniqueStrings(input.avoidInvestigating ?? []),
|
|
926
|
+
playbookIds: uniqueStrings(input.playbookIds ?? []),
|
|
927
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
export function createSwarmOracleCorpus(input = {}) {
|
|
931
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
932
|
+
const artifacts = (input.artifacts ?? []).map(normalizeOracleArtifact).sort((left, right) => left.id.localeCompare(right.id));
|
|
933
|
+
const byKind = groupArtifactIdsBy(artifacts, (artifact) => [artifact.kind]);
|
|
934
|
+
const byTag = groupArtifactIdsBy(artifacts, (artifact) => artifact.tags);
|
|
935
|
+
return {
|
|
936
|
+
kind: FRONTIER_SWARM_ORACLE_CORPUS_KIND,
|
|
937
|
+
version: FRONTIER_SWARM_ORACLE_CORPUS_VERSION,
|
|
938
|
+
id: input.id ?? 'swarm-oracle-corpus:' + stableHash([artifacts, generatedAt]),
|
|
939
|
+
title: input.title ?? titleFromId(input.id ?? 'oracle corpus'),
|
|
940
|
+
generatedAt,
|
|
941
|
+
artifacts,
|
|
942
|
+
byKind,
|
|
943
|
+
byTag,
|
|
944
|
+
summary: {
|
|
945
|
+
artifactCount: artifacts.length,
|
|
946
|
+
kindCount: Object.keys(byKind).length,
|
|
947
|
+
tagCount: Object.keys(byTag).length
|
|
948
|
+
},
|
|
949
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
export function createSwarmLanePlaybook(input) {
|
|
953
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
954
|
+
const successful = (input.successfulBundles ?? []).filter((bundle) => bundle.status === 'completed' || bundle.status === 'verified' || bundle.autoMergeable);
|
|
955
|
+
const hotPaths = createSwarmHotspotReport({ bundles: successful, threshold: 2, generatedAt }).entries
|
|
956
|
+
.filter((entry) => entry.touchCount >= 2)
|
|
957
|
+
.map((entry) => entry.path);
|
|
958
|
+
return {
|
|
959
|
+
kind: FRONTIER_SWARM_LANE_PLAYBOOK_KIND,
|
|
960
|
+
version: FRONTIER_SWARM_LANE_PLAYBOOK_VERSION,
|
|
961
|
+
id: input.id ?? 'swarm-lane-playbook:' + stableHash([input.lane, successful.map((bundle) => bundle.jobId), input.notes, generatedAt]),
|
|
962
|
+
lane: normalizeId(input.lane, 'playbook lane'),
|
|
963
|
+
title: input.title ?? `${titleFromId(input.lane)} Playbook`,
|
|
964
|
+
generatedAt,
|
|
965
|
+
notes: uniqueStrings(input.notes ?? []),
|
|
966
|
+
commands: normalizeCommands(input.commands ?? []),
|
|
967
|
+
avoidInvestigating: uniqueStrings(input.avoidInvestigating ?? []),
|
|
968
|
+
evidencePatterns: uniqueStrings(input.evidencePatterns ?? successful.flatMap((bundle) => bundle.evidencePaths)),
|
|
969
|
+
successfulJobIds: uniqueStrings(successful.map((bundle) => bundle.jobId)),
|
|
970
|
+
hotPaths,
|
|
971
|
+
changedRegions: uniqueStrings(successful.flatMap((bundle) => bundle.changedRegions)),
|
|
972
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
export function createSwarmPatchStackPlan(input) {
|
|
976
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
977
|
+
const maxStackSize = Math.max(1, Math.floor(input.maxStackSize ?? 8));
|
|
978
|
+
const groups = new Map();
|
|
979
|
+
for (const entry of input.index.entries) {
|
|
980
|
+
const key = patchStackKey(entry);
|
|
981
|
+
groups.set(key, [...(groups.get(key) ?? []), entry]);
|
|
982
|
+
}
|
|
983
|
+
const stacks = [];
|
|
984
|
+
for (const [key, entries] of Array.from(groups.entries()).sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
985
|
+
const sorted = [...entries].sort((left, right) => riskRank(left.riskLevel) - riskRank(right.riskLevel) || left.jobId.localeCompare(right.jobId));
|
|
986
|
+
for (let index = 0; index < sorted.length; index += maxStackSize) {
|
|
987
|
+
const slice = sorted.slice(index, index + maxStackSize);
|
|
988
|
+
const jobIds = slice.map((entry) => entry.jobId);
|
|
989
|
+
const conflicts = input.index.conflicts.filter((conflict) => conflict.jobIds.some((jobId) => jobIds.includes(jobId)));
|
|
990
|
+
stacks.push({
|
|
991
|
+
id: 'swarm-patch-stack:' + stableHash([input.index.id, key, index, jobIds]),
|
|
992
|
+
title: titleFromId(key),
|
|
993
|
+
...(slice[0]?.lane ? { lane: slice[0].lane } : {}),
|
|
994
|
+
jobIds,
|
|
995
|
+
changedPaths: uniqueStrings(slice.flatMap((entry) => entry.changedPaths)),
|
|
996
|
+
changedRegions: uniqueStrings(slice.flatMap((entry) => entry.changedRegions)),
|
|
997
|
+
riskLevels: uniqueStrings(slice.map((entry) => entry.riskLevel)),
|
|
998
|
+
dispositions: uniqueStrings(slice.map((entry) => entry.disposition)),
|
|
999
|
+
conflicts,
|
|
1000
|
+
gateHints: uniqueStrings(slice.flatMap((entry) => entry.evidencePaths.filter((file) => file.endsWith('.json') || file.endsWith('.jsonl'))))
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
kind: FRONTIER_SWARM_PATCH_STACK_PLAN_KIND,
|
|
1006
|
+
version: FRONTIER_SWARM_PATCH_STACK_PLAN_VERSION,
|
|
1007
|
+
id: input.id ?? 'swarm-patch-stack-plan:' + stableHash([input.index.id, stacks, generatedAt]),
|
|
1008
|
+
mergeIndexId: input.index.id,
|
|
1009
|
+
generatedAt,
|
|
1010
|
+
stacks,
|
|
1011
|
+
summary: {
|
|
1012
|
+
stackCount: stacks.length,
|
|
1013
|
+
jobCount: input.index.entries.length,
|
|
1014
|
+
conflictedStackCount: stacks.filter((stack) => stack.conflicts.length > 0).length
|
|
1015
|
+
},
|
|
1016
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
214
1019
|
export function resolveSwarmCompute(manifestInput, taskInput) {
|
|
215
1020
|
const compiled = compileSwarm(manifestInput);
|
|
216
1021
|
const task = isSwarmTask(taskInput) ? taskInput : normalizeTask(taskInput);
|
|
@@ -248,7 +1053,14 @@ export function createSwarmSchedule(input) {
|
|
|
248
1053
|
}
|
|
249
1054
|
const runningJobs = (run?.jobs ?? plan.jobs)
|
|
250
1055
|
.filter((job) => job.status === 'running')
|
|
251
|
-
.map((job) => ({
|
|
1056
|
+
.map((job) => ({
|
|
1057
|
+
jobId: job.id,
|
|
1058
|
+
lane: job.lane,
|
|
1059
|
+
compute: job.compute.id,
|
|
1060
|
+
concurrencyKey: job.concurrencyKey,
|
|
1061
|
+
capabilities: [...job.capabilities],
|
|
1062
|
+
...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
|
|
1063
|
+
}));
|
|
252
1064
|
const runningByLane = countBy(runningJobs.map((job) => job.lane));
|
|
253
1065
|
const runningByKey = countBy(runningJobs.map((job) => job.concurrencyKey));
|
|
254
1066
|
const runningByCompute = countBy(runningJobs.map((job) => job.compute));
|
|
@@ -330,6 +1142,65 @@ export function createSwarmLeases(input) {
|
|
|
330
1142
|
status: 'active'
|
|
331
1143
|
}));
|
|
332
1144
|
}
|
|
1145
|
+
export function renewSwarmLease(input) {
|
|
1146
|
+
const now = input.now ?? Date.now();
|
|
1147
|
+
const leaseMs = Math.max(1, Math.floor(input.leaseMs ?? Math.max(1, input.lease.expiresAt - input.lease.leasedAt)));
|
|
1148
|
+
return {
|
|
1149
|
+
...cloneJsonValue(input.lease),
|
|
1150
|
+
leasedAt: now,
|
|
1151
|
+
expiresAt: now + leaseMs,
|
|
1152
|
+
status: input.status ?? 'active'
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
export function createSwarmQueueSnapshot(input) {
|
|
1156
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
1157
|
+
const leases = [...(input.leases ?? [])].map((lease) => cloneJsonValue(lease));
|
|
1158
|
+
const jobs = (input.jobs ? input.jobs.map(normalizeQueueJob) : queueJobsFromPlan(input.plan, input.run, leases)).sort((left, right) => (left.priority - right.priority
|
|
1159
|
+
|| left.jobId.localeCompare(right.jobId)));
|
|
1160
|
+
const byStatus = groupIds(jobs, (job) => job.status);
|
|
1161
|
+
const byLane = groupIds(jobs, (job) => job.lane ?? 'unassigned');
|
|
1162
|
+
return {
|
|
1163
|
+
kind: FRONTIER_SWARM_QUEUE_SNAPSHOT_KIND,
|
|
1164
|
+
version: FRONTIER_SWARM_QUEUE_SNAPSHOT_VERSION,
|
|
1165
|
+
id: input.id ?? 'swarm-queue-snapshot:' + stableHash([input.plan.id, input.run?.id, jobs, leases, generatedAt]),
|
|
1166
|
+
planId: input.plan.id,
|
|
1167
|
+
runId: input.run?.id ?? input.plan.runId,
|
|
1168
|
+
generatedAt,
|
|
1169
|
+
jobs,
|
|
1170
|
+
byStatus,
|
|
1171
|
+
byLane,
|
|
1172
|
+
leases,
|
|
1173
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
|
|
1174
|
+
summary: {
|
|
1175
|
+
jobCount: jobs.length,
|
|
1176
|
+
leaseCount: leases.length,
|
|
1177
|
+
readyCount: byStatus.ready?.length ?? 0,
|
|
1178
|
+
leasedCount: byStatus.leased?.length ?? 0,
|
|
1179
|
+
completedCount: byStatus.completed?.length ?? 0,
|
|
1180
|
+
failedCount: byStatus.failed?.length ?? 0,
|
|
1181
|
+
deadLetterCount: byStatus['dead-letter']?.length ?? 0
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
export function createSwarmRunCheckpoint(input) {
|
|
1186
|
+
const run = 'kind' in input ? input : input.run;
|
|
1187
|
+
const sequence = 'kind' in input ? run.events.length + run.results.length : input.sequence ?? run.events.length + run.results.length;
|
|
1188
|
+
const savedAt = 'kind' in input ? Date.now() : input.savedAt ?? Date.now();
|
|
1189
|
+
return {
|
|
1190
|
+
kind: FRONTIER_SWARM_RUN_CHECKPOINT_KIND,
|
|
1191
|
+
version: FRONTIER_SWARM_RUN_CHECKPOINT_VERSION,
|
|
1192
|
+
id: 'swarm-run-checkpoint:' + stableHash([run.id, sequence, savedAt, run.summary]),
|
|
1193
|
+
runId: run.id,
|
|
1194
|
+
planId: run.planId,
|
|
1195
|
+
sequence,
|
|
1196
|
+
savedAt,
|
|
1197
|
+
status: run.status,
|
|
1198
|
+
eventCount: run.events.length,
|
|
1199
|
+
resultCount: run.results.length,
|
|
1200
|
+
hash: stableHash(run),
|
|
1201
|
+
...(!('kind' in input) && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
333
1204
|
export function checkSwarmBudget(job, usageInput) {
|
|
334
1205
|
const usage = normalizeUsage(usageInput);
|
|
335
1206
|
const budget = job.budget;
|
|
@@ -524,7 +1395,8 @@ function createSwarmJobGraph(jobs) {
|
|
|
524
1395
|
function normalizeScheduleLimits(manifest, options) {
|
|
525
1396
|
const maxLaneConcurrency = {};
|
|
526
1397
|
for (const lane of manifest.lanes) {
|
|
527
|
-
const
|
|
1398
|
+
const browserMax = lane.resourceRequirements?.browser?.maxConcurrency;
|
|
1399
|
+
const value = options.maxLaneConcurrency?.[lane.id] ?? lane.maxConcurrency ?? browserMax ?? manifest.policy.defaultConcurrency;
|
|
528
1400
|
maxLaneConcurrency[lane.id] = Math.max(1, Math.floor(value));
|
|
529
1401
|
}
|
|
530
1402
|
return {
|
|
@@ -550,7 +1422,9 @@ function scheduleJob(job, dependsOn = job.dependsOn) {
|
|
|
550
1422
|
compute: job.compute.id,
|
|
551
1423
|
concurrencyKey: job.concurrencyKey,
|
|
552
1424
|
priority: job.priority,
|
|
553
|
-
dependsOn: [...dependsOn]
|
|
1425
|
+
dependsOn: [...dependsOn],
|
|
1426
|
+
capabilities: [...job.capabilities],
|
|
1427
|
+
...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
|
|
554
1428
|
};
|
|
555
1429
|
}
|
|
556
1430
|
function normalizeBudget(input = {}) {
|
|
@@ -616,28 +1490,188 @@ function selectReviewers(pool, required, salt) {
|
|
|
616
1490
|
return sorted.slice(0, Math.min(required, sorted.length));
|
|
617
1491
|
}
|
|
618
1492
|
function conflictMap(results) {
|
|
619
|
-
const
|
|
620
|
-
for (
|
|
621
|
-
for (
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
1493
|
+
const conflicts = new Map();
|
|
1494
|
+
for (let leftIndex = 0; leftIndex < results.length; leftIndex += 1) {
|
|
1495
|
+
for (let rightIndex = leftIndex + 1; rightIndex < results.length; rightIndex += 1) {
|
|
1496
|
+
const left = results[leftIndex];
|
|
1497
|
+
const right = results[rightIndex];
|
|
1498
|
+
if (!left || !right || pairConflictKeys(left, right).length === 0)
|
|
1499
|
+
continue;
|
|
1500
|
+
const leftConflicts = conflicts.get(left.jobId) ?? new Set();
|
|
1501
|
+
const rightConflicts = conflicts.get(right.jobId) ?? new Set();
|
|
1502
|
+
leftConflicts.add(right.jobId);
|
|
1503
|
+
rightConflicts.add(left.jobId);
|
|
1504
|
+
conflicts.set(left.jobId, leftConflicts);
|
|
1505
|
+
conflicts.set(right.jobId, rightConflicts);
|
|
625
1506
|
}
|
|
626
1507
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1508
|
+
return conflicts;
|
|
1509
|
+
}
|
|
1510
|
+
function queueOverlayStatusFromBundle(bundle) {
|
|
1511
|
+
if (bundle.staleAgainstHead || bundle.disposition === 'stale-against-head')
|
|
1512
|
+
return 'stale-against-head';
|
|
1513
|
+
if (bundle.disposition === 'rejected' || bundle.disposition === 'blocked' || bundle.status === 'failed' || bundle.commandsFailed.length > 0) {
|
|
1514
|
+
return 'failed-evidence';
|
|
1515
|
+
}
|
|
1516
|
+
if (bundle.disposition === 'auto-mergeable' && bundle.autoMergeable)
|
|
1517
|
+
return 'ready-to-apply';
|
|
1518
|
+
if (bundle.disposition === 'needs-port')
|
|
1519
|
+
return 'needs-human-port';
|
|
1520
|
+
if (bundle.disposition === 'discovery-only')
|
|
1521
|
+
return 'discovery-only';
|
|
1522
|
+
if (bundle.mergeReadiness === 'blocked')
|
|
1523
|
+
return 'blocked';
|
|
1524
|
+
if (bundle.mergeReadiness === 'rejected')
|
|
1525
|
+
return 'rejected';
|
|
1526
|
+
return 'unknown';
|
|
1527
|
+
}
|
|
1528
|
+
function queueOverlayStatusFromResult(result) {
|
|
1529
|
+
if (result.mergeDisposition === 'stale-against-head')
|
|
1530
|
+
return 'stale-against-head';
|
|
1531
|
+
if (result.status === 'failed' || result.exitCode !== undefined && result.exitCode !== 0 || result.ownershipViolations.length > 0)
|
|
1532
|
+
return 'failed-evidence';
|
|
1533
|
+
if (result.mergeDisposition === 'auto-mergeable')
|
|
1534
|
+
return 'ready-to-apply';
|
|
1535
|
+
if (result.mergeDisposition === 'needs-port')
|
|
1536
|
+
return 'needs-human-port';
|
|
1537
|
+
if (result.mergeDisposition === 'discovery-only')
|
|
1538
|
+
return 'discovery-only';
|
|
1539
|
+
if (result.status === 'blocked')
|
|
1540
|
+
return 'blocked';
|
|
1541
|
+
return 'unknown';
|
|
1542
|
+
}
|
|
1543
|
+
function queueJobStatusFromOverlay(entry) {
|
|
1544
|
+
if (entry.status === 'ready-to-apply' || entry.status === 'discovery-only')
|
|
1545
|
+
return 'completed';
|
|
1546
|
+
if (entry.status === 'needs-human-port')
|
|
1547
|
+
return 'blocked';
|
|
1548
|
+
if (entry.status === 'failed-evidence' || entry.status === 'rejected' || entry.status === 'stale-against-head')
|
|
1549
|
+
return 'failed';
|
|
1550
|
+
if (entry.status === 'blocked')
|
|
1551
|
+
return 'blocked';
|
|
1552
|
+
return 'completed';
|
|
1553
|
+
}
|
|
1554
|
+
function groupOverlayEntries(entries) {
|
|
1555
|
+
const out = {};
|
|
1556
|
+
for (const entry of entries)
|
|
1557
|
+
out[entry.queueItemId] = [...(out[entry.queueItemId] ?? []), entry];
|
|
1558
|
+
for (const key of Object.keys(out)) {
|
|
1559
|
+
out[key] = [...(out[key] ?? [])].sort((left, right) => right.generatedAt - left.generatedAt || left.jobId.localeCompare(right.jobId));
|
|
1560
|
+
}
|
|
1561
|
+
return out;
|
|
1562
|
+
}
|
|
1563
|
+
function mergeIndexConflictKeys(bundle) {
|
|
1564
|
+
return bundle.changedRegions.length
|
|
1565
|
+
? bundle.changedRegions.map((region) => `region:${region}`).sort()
|
|
1566
|
+
: bundle.changedPaths.map((file) => `path:${file}`).sort();
|
|
1567
|
+
}
|
|
1568
|
+
function createMergeIndexConflicts(entries) {
|
|
1569
|
+
const conflicts = [];
|
|
1570
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
1571
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
1572
|
+
const left = entries[leftIndex];
|
|
1573
|
+
const right = entries[rightIndex];
|
|
1574
|
+
if (!left || !right)
|
|
1575
|
+
continue;
|
|
1576
|
+
const keys = pairConflictKeys(left, right);
|
|
1577
|
+
for (const key of keys) {
|
|
1578
|
+
const kind = key.startsWith('region:') ? 'region' : 'path';
|
|
1579
|
+
const value = key.slice(key.indexOf(':') + 1);
|
|
1580
|
+
conflicts.push({
|
|
1581
|
+
jobIds: [left.jobId, right.jobId].sort(),
|
|
1582
|
+
key,
|
|
1583
|
+
kind,
|
|
1584
|
+
...(kind === 'region' ? { region: value } : { path: value })
|
|
1585
|
+
});
|
|
636
1586
|
}
|
|
637
|
-
conflicts.set(jobId, set);
|
|
638
1587
|
}
|
|
639
1588
|
}
|
|
640
|
-
|
|
1589
|
+
const deduped = new Map();
|
|
1590
|
+
for (const conflict of conflicts)
|
|
1591
|
+
deduped.set(`${conflict.key}:${conflict.jobIds.join(',')}`, conflict);
|
|
1592
|
+
return Array.from(deduped.values()).sort((left, right) => left.key.localeCompare(right.key) || left.jobIds.join(',').localeCompare(right.jobIds.join(',')));
|
|
1593
|
+
}
|
|
1594
|
+
function pairConflictKeys(left, right) {
|
|
1595
|
+
if (left.changedRegions.length > 0 && right.changedRegions.length > 0) {
|
|
1596
|
+
const rightRegions = new Set(right.changedRegions);
|
|
1597
|
+
return left.changedRegions.filter((region) => rightRegions.has(region)).map((region) => `region:${region}`).sort();
|
|
1598
|
+
}
|
|
1599
|
+
const rightPaths = new Set(right.changedPaths);
|
|
1600
|
+
return left.changedPaths.filter((file) => rightPaths.has(file)).map((file) => `path:${file}`).sort();
|
|
1601
|
+
}
|
|
1602
|
+
function groupJobIdsBy(items, key) {
|
|
1603
|
+
const out = {};
|
|
1604
|
+
for (const item of items)
|
|
1605
|
+
out[key(item)] = uniqueStrings([...(out[key(item)] ?? []), item.jobId]);
|
|
1606
|
+
return out;
|
|
1607
|
+
}
|
|
1608
|
+
function groupJobIdsByMany(items, key) {
|
|
1609
|
+
const out = {};
|
|
1610
|
+
for (const item of items) {
|
|
1611
|
+
for (const value of key(item))
|
|
1612
|
+
out[value] = uniqueStrings([...(out[value] ?? []), item.jobId]);
|
|
1613
|
+
}
|
|
1614
|
+
return out;
|
|
1615
|
+
}
|
|
1616
|
+
function suggestedModuleId(file) {
|
|
1617
|
+
const base = file.split('/').pop()?.replace(/\.[^.]+$/, '') ?? file;
|
|
1618
|
+
return slug(base).replace(/-/g, '.');
|
|
1619
|
+
}
|
|
1620
|
+
function reviewerLaneReasons(entry) {
|
|
1621
|
+
const reasons = [];
|
|
1622
|
+
if (entry.conflictingJobIds.length)
|
|
1623
|
+
reasons.push('conflicting-changes');
|
|
1624
|
+
if (entry.riskLevel === 'high')
|
|
1625
|
+
reasons.push('high-risk');
|
|
1626
|
+
if (entry.disposition !== 'auto-mergeable')
|
|
1627
|
+
reasons.push(entry.disposition);
|
|
1628
|
+
if (!entry.autoMergeable)
|
|
1629
|
+
reasons.push('not-auto-mergeable');
|
|
1630
|
+
if (entry.staleAgainstHead)
|
|
1631
|
+
reasons.push('stale-against-head');
|
|
1632
|
+
return uniqueStrings(reasons);
|
|
1633
|
+
}
|
|
1634
|
+
function hashBucket(value, buckets) {
|
|
1635
|
+
const hex = stableHash(value).split(':')[1] ?? '0';
|
|
1636
|
+
return parseInt(hex, 16) % Math.max(1, buckets);
|
|
1637
|
+
}
|
|
1638
|
+
function normalizeOracleArtifact(input) {
|
|
1639
|
+
return {
|
|
1640
|
+
id: normalizeId(input.id, 'oracle artifact id'),
|
|
1641
|
+
path: normalizeId(input.path, 'oracle artifact path'),
|
|
1642
|
+
kind: input.kind ?? 'oracle',
|
|
1643
|
+
...(input.command ? { command: typeof input.command === 'string' ? normalizeCommands([input.command])[0] : normalizeCommands([input.command])[0] } : {}),
|
|
1644
|
+
...(input.hash ? { hash: input.hash } : {}),
|
|
1645
|
+
...(input.sourceRef ? { sourceRef: input.sourceRef } : {}),
|
|
1646
|
+
tags: uniqueStrings(input.tags ?? []),
|
|
1647
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function groupArtifactIdsBy(artifacts, key) {
|
|
1651
|
+
const out = {};
|
|
1652
|
+
for (const artifact of artifacts) {
|
|
1653
|
+
for (const value of key(artifact))
|
|
1654
|
+
out[value] = uniqueStrings([...(out[value] ?? []), artifact.id]);
|
|
1655
|
+
}
|
|
1656
|
+
return out;
|
|
1657
|
+
}
|
|
1658
|
+
function patchStackKey(entry) {
|
|
1659
|
+
const lane = entry.lane ?? 'unassigned';
|
|
1660
|
+
if (entry.changedRegions.length)
|
|
1661
|
+
return `${lane}:${entry.changedRegions[0]}`;
|
|
1662
|
+
const firstPath = entry.changedPaths[0] ?? 'evidence-only';
|
|
1663
|
+
return `${lane}:${firstPath.split('/').slice(0, 2).join('/') || firstPath}`;
|
|
1664
|
+
}
|
|
1665
|
+
function riskRank(risk) {
|
|
1666
|
+
if (risk === 'low')
|
|
1667
|
+
return 0;
|
|
1668
|
+
if (risk === 'medium')
|
|
1669
|
+
return 1;
|
|
1670
|
+
if (risk === 'unknown')
|
|
1671
|
+
return 2;
|
|
1672
|
+
if (risk === 'high')
|
|
1673
|
+
return 3;
|
|
1674
|
+
return 4;
|
|
641
1675
|
}
|
|
642
1676
|
function groupMergeReadyJobs(ready, results) {
|
|
643
1677
|
const byJob = new Map(results.map((result) => [result.jobId, result]));
|
|
@@ -658,6 +1692,16 @@ function groupArtifacts(artifacts, key) {
|
|
|
658
1692
|
}
|
|
659
1693
|
return out;
|
|
660
1694
|
}
|
|
1695
|
+
function groupIds(items, key) {
|
|
1696
|
+
const out = {};
|
|
1697
|
+
for (const item of items) {
|
|
1698
|
+
const group = key(item);
|
|
1699
|
+
out[group] = [...(out[group] ?? []), item.jobId];
|
|
1700
|
+
}
|
|
1701
|
+
for (const ids of Object.values(out))
|
|
1702
|
+
ids.sort();
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
661
1705
|
function countBy(values) {
|
|
662
1706
|
const out = {};
|
|
663
1707
|
for (const value of values)
|
|
@@ -733,6 +1777,9 @@ function normalizeLane(input) {
|
|
|
733
1777
|
allowedWrites,
|
|
734
1778
|
sharedReadOnly: uniqueStrings(input.sharedReadOnly ?? []),
|
|
735
1779
|
neverEdit: uniqueStrings(input.neverEdit ?? []),
|
|
1780
|
+
ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
|
|
1781
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1782
|
+
...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
|
|
736
1783
|
...(input.worktreePath ? { worktreePath: input.worktreePath } : {}),
|
|
737
1784
|
...(input.evidencePrefix || input.evidenceOutDirPrefix ? { evidencePrefix: input.evidencePrefix ?? input.evidenceOutDirPrefix } : {}),
|
|
738
1785
|
concurrencyKey: input.concurrencyKey ?? input.id,
|
|
@@ -742,6 +1789,68 @@ function normalizeLane(input) {
|
|
|
742
1789
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
743
1790
|
};
|
|
744
1791
|
}
|
|
1792
|
+
function normalizeOwnershipRegions(input = []) {
|
|
1793
|
+
return input.map((region) => {
|
|
1794
|
+
const globs = uniqueStrings([...(region.globs ?? []), ...(region.paths ?? [])]);
|
|
1795
|
+
return {
|
|
1796
|
+
id: normalizeId(region.id, 'ownership region id'),
|
|
1797
|
+
title: region.title ?? titleFromId(region.id),
|
|
1798
|
+
...(region.description ? { description: region.description } : {}),
|
|
1799
|
+
globs,
|
|
1800
|
+
selectors: uniqueStrings(region.selectors ?? []),
|
|
1801
|
+
...(region.owner ? { owner: region.owner } : {}),
|
|
1802
|
+
...(toJsonObject(region.metadata) ? { metadata: toJsonObject(region.metadata) } : {})
|
|
1803
|
+
};
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
function mergeOwnershipRegions(laneRegions, taskRegions) {
|
|
1807
|
+
const byId = new Map();
|
|
1808
|
+
for (const region of laneRegions)
|
|
1809
|
+
byId.set(region.id, cloneJsonValue(region));
|
|
1810
|
+
for (const region of taskRegions)
|
|
1811
|
+
byId.set(region.id, cloneJsonValue(region));
|
|
1812
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
1813
|
+
}
|
|
1814
|
+
function normalizeResourceRequirements(input = {}) {
|
|
1815
|
+
const resources = {};
|
|
1816
|
+
for (const [key, value] of Object.entries(input.resources ?? {})) {
|
|
1817
|
+
if (Number.isFinite(value) && value > 0)
|
|
1818
|
+
resources[key] = value;
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1822
|
+
resources,
|
|
1823
|
+
...(input.browser ? { browser: normalizeBrowserResource(input.browser) } : {}),
|
|
1824
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function normalizeBrowserResource(input) {
|
|
1828
|
+
return {
|
|
1829
|
+
required: input.required ?? true,
|
|
1830
|
+
portPool: uniqueStrings((input.portPool ?? []).map((port) => String(port))),
|
|
1831
|
+
...(input.profileDir ? { profileDir: input.profileDir } : {}),
|
|
1832
|
+
...(input.profileDirPrefix ? { profileDirPrefix: input.profileDirPrefix } : {}),
|
|
1833
|
+
...(positiveNumber(input.maxConcurrency) ? { maxConcurrency: Math.floor(input.maxConcurrency) } : {}),
|
|
1834
|
+
...(input.headless !== undefined ? { headless: input.headless } : {}),
|
|
1835
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function mergeResourceRequirements(lane, task, extraCapabilities = []) {
|
|
1839
|
+
if (!lane && !task && extraCapabilities.length === 0)
|
|
1840
|
+
return undefined;
|
|
1841
|
+
const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...(task?.capabilities ?? []), ...extraCapabilities]);
|
|
1842
|
+
const resources = { ...(lane?.resources ?? {}) };
|
|
1843
|
+
for (const [key, value] of Object.entries(task?.resources ?? {}))
|
|
1844
|
+
resources[key] = Math.max(resources[key] ?? 0, value);
|
|
1845
|
+
const browser = task?.browser ?? lane?.browser;
|
|
1846
|
+
const metadata = toJsonObject({ ...(lane?.metadata ?? {}), ...(task?.metadata ?? {}) });
|
|
1847
|
+
return {
|
|
1848
|
+
capabilities,
|
|
1849
|
+
resources,
|
|
1850
|
+
...(browser ? { browser } : {}),
|
|
1851
|
+
...(metadata && Object.keys(metadata).length ? { metadata } : {})
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
745
1854
|
function normalizePolicy(input, defaultCompute) {
|
|
746
1855
|
return {
|
|
747
1856
|
mode: input?.mode ?? 'hard-file-ownership',
|
|
@@ -778,6 +1887,11 @@ function normalizeTask(input) {
|
|
|
778
1887
|
sourceRefs: uniqueStrings(input.sourceRefs ?? []),
|
|
779
1888
|
targetRefs,
|
|
780
1889
|
allowedWrites: uniqueStrings([...(input.allowedWrites ?? []), ...targetRefs]),
|
|
1890
|
+
ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
|
|
1891
|
+
ownedRegions: uniqueStrings(input.ownedRegions ?? []),
|
|
1892
|
+
changedRegions: uniqueStrings(input.changedRegions ?? []),
|
|
1893
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1894
|
+
...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
|
|
781
1895
|
acceptance: normalizeAcceptance(input),
|
|
782
1896
|
verification: normalizeCommands(input.verification ?? []),
|
|
783
1897
|
...(input.evidenceCommand ? { evidenceCommand: input.evidenceCommand } : {}),
|
|
@@ -827,6 +1941,55 @@ function selectSwarmTasks(manifest, tasks, options) {
|
|
|
827
1941
|
.sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id))
|
|
828
1942
|
.slice(0, limit);
|
|
829
1943
|
}
|
|
1944
|
+
function createSelectionEntry(manifest, task, priority) {
|
|
1945
|
+
const lane = task.lane ? manifest.lanes.find((entry) => entry.id === task.lane) : undefined;
|
|
1946
|
+
return {
|
|
1947
|
+
task,
|
|
1948
|
+
...(lane ? { lane } : {}),
|
|
1949
|
+
ownershipWarnings: selectionOwnershipWarnings(task, lane),
|
|
1950
|
+
selectionPriority: selectionPriority(task, priority)
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function selectionOwnershipWarnings(task, lane) {
|
|
1954
|
+
if (!lane || lane.allowedWrites.length === 0)
|
|
1955
|
+
return [];
|
|
1956
|
+
return task.targetRefs
|
|
1957
|
+
.filter((file) => !lane.allowedWrites.some((glob) => matchesGlob(file, glob)))
|
|
1958
|
+
.map((file) => `${file} is outside allowed write globs for ${lane.id}`);
|
|
1959
|
+
}
|
|
1960
|
+
function selectionPriority(task, input) {
|
|
1961
|
+
const statusRanks = input?.statuses ?? {};
|
|
1962
|
+
const workKindRanks = input?.workKinds ?? {};
|
|
1963
|
+
const statusRank = statusRanks[task.status] ?? input?.defaultStatusRank ?? 100;
|
|
1964
|
+
const workKindRank = workKindRanks[task.workKind] ?? input?.defaultWorkKindRank ?? 100;
|
|
1965
|
+
const statusWeight = input?.statusWeight ?? 1000;
|
|
1966
|
+
const workKindWeight = input?.workKindWeight ?? 1;
|
|
1967
|
+
return statusRank * statusWeight + workKindRank * workKindWeight;
|
|
1968
|
+
}
|
|
1969
|
+
function roundRobinSelectionByLane(entries) {
|
|
1970
|
+
const groups = new Map();
|
|
1971
|
+
for (const entry of entries)
|
|
1972
|
+
groups.set(entry.task.lane ?? 'unassigned', [...(groups.get(entry.task.lane ?? 'unassigned') ?? []), entry]);
|
|
1973
|
+
const selected = [];
|
|
1974
|
+
while (Array.from(groups.values()).some((group) => group.length > 0)) {
|
|
1975
|
+
for (const group of groups.values()) {
|
|
1976
|
+
const next = group.shift();
|
|
1977
|
+
if (next)
|
|
1978
|
+
selected.push(next);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return selected;
|
|
1982
|
+
}
|
|
1983
|
+
function summarizeTaskSelection(entries) {
|
|
1984
|
+
return entries.reduce((summary, entry) => {
|
|
1985
|
+
const lane = entry.task.lane ?? 'unassigned';
|
|
1986
|
+
summary.total += 1;
|
|
1987
|
+
summary.byLane[lane] = (summary.byLane[lane] ?? 0) + 1;
|
|
1988
|
+
summary.byWorkKind[entry.task.workKind] = (summary.byWorkKind[entry.task.workKind] ?? 0) + 1;
|
|
1989
|
+
summary.ownershipWarningCount += entry.ownershipWarnings.length;
|
|
1990
|
+
return summary;
|
|
1991
|
+
}, { total: 0, byLane: {}, byWorkKind: {}, ownershipWarningCount: 0 });
|
|
1992
|
+
}
|
|
830
1993
|
function createJob(compiled, task, options) {
|
|
831
1994
|
const lane = task.lane ? compiled.lanesById.get(task.lane) : undefined;
|
|
832
1995
|
const layer = task.layer ?? lane?.layer ?? compiled.manifest.policy.defaultLayer;
|
|
@@ -842,6 +2005,10 @@ function createJob(compiled, task, options) {
|
|
|
842
2005
|
const ownershipWarnings = task.targetRefs
|
|
843
2006
|
.filter((file) => allowedWrites.length > 0 && !allowedWrites.some((glob) => matchesGlob(file, glob)))
|
|
844
2007
|
.map((file) => `${file} is outside allowed write globs for ${lane?.id ?? 'unassigned'}`);
|
|
2008
|
+
const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...task.capabilities]);
|
|
2009
|
+
const resourceRequirements = mergeResourceRequirements(lane?.resourceRequirements, task.resourceRequirements, capabilities);
|
|
2010
|
+
const ownershipRegions = mergeOwnershipRegions(lane?.ownershipRegions ?? [], task.ownershipRegions);
|
|
2011
|
+
const ownedRegions = uniqueStrings([...task.ownedRegions, ...ownershipRegions.map((region) => region.id)]);
|
|
845
2012
|
return {
|
|
846
2013
|
id: `${lane?.id ?? 'unassigned'}-${slug(task.id)}`,
|
|
847
2014
|
taskId: task.id,
|
|
@@ -855,6 +2022,11 @@ function createJob(compiled, task, options) {
|
|
|
855
2022
|
allowedWrites,
|
|
856
2023
|
sharedReadOnly: uniqueStrings([...(compiled.manifest.policy.sharedReadOnly ?? []), ...(lane?.sharedReadOnly ?? [])]),
|
|
857
2024
|
neverEdit: uniqueStrings([...(compiled.manifest.policy.neverEditWithoutParent ?? []), ...(lane?.neverEdit ?? [])]),
|
|
2025
|
+
ownershipRegions,
|
|
2026
|
+
ownedRegions,
|
|
2027
|
+
changedRegions: uniqueStrings(task.changedRegions),
|
|
2028
|
+
capabilities,
|
|
2029
|
+
...(resourceRequirements ? { resourceRequirements } : {}),
|
|
858
2030
|
...(lane?.worktreePath ? { worktreePath: lane.worktreePath } : {}),
|
|
859
2031
|
...(evidencePrefix ? { evidencePrefix } : {}),
|
|
860
2032
|
concurrencyKey: task.concurrencyKey ?? lane?.concurrencyKey ?? task.lane ?? compute.id,
|
|
@@ -949,26 +2121,121 @@ function normalizeEvent(input) {
|
|
|
949
2121
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
950
2122
|
};
|
|
951
2123
|
}
|
|
2124
|
+
function normalizeQueueJob(input) {
|
|
2125
|
+
return {
|
|
2126
|
+
jobId: input.jobId,
|
|
2127
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
2128
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
2129
|
+
status: input.status ?? 'ready',
|
|
2130
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
2131
|
+
...(input.compute ? { compute: input.compute } : {}),
|
|
2132
|
+
...(input.concurrencyKey ? { concurrencyKey: input.concurrencyKey } : {}),
|
|
2133
|
+
priority: input.priority ?? 100,
|
|
2134
|
+
attempts: Math.max(0, Math.floor(input.attempts ?? 0)),
|
|
2135
|
+
maxAttempts: Math.max(1, Math.floor(input.maxAttempts ?? 1)),
|
|
2136
|
+
...(input.availableAt !== undefined ? { availableAt: input.availableAt } : {}),
|
|
2137
|
+
...(input.lease ? { lease: cloneJsonValue(input.lease) } : {}),
|
|
2138
|
+
...(input.lastError ? { lastError: input.lastError } : {}),
|
|
2139
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function queueJobsFromPlan(plan, run, leases) {
|
|
2143
|
+
const resultsByJob = new Map((run?.results ?? []).map((result) => [result.jobId, result]));
|
|
2144
|
+
const activeLeases = new Map(leases.filter((lease) => lease.status === 'active').map((lease) => [lease.jobId, lease]));
|
|
2145
|
+
return plan.jobs.map((job) => {
|
|
2146
|
+
const result = resultsByJob.get(job.id);
|
|
2147
|
+
const lease = activeLeases.get(job.id);
|
|
2148
|
+
const failed = result?.status === 'failed' || result?.exitCode !== undefined && result.exitCode !== 0;
|
|
2149
|
+
const completed = result?.status === 'completed' || result?.status === 'verified';
|
|
2150
|
+
const status = completed
|
|
2151
|
+
? 'completed'
|
|
2152
|
+
: failed
|
|
2153
|
+
? 'failed'
|
|
2154
|
+
: lease
|
|
2155
|
+
? 'leased'
|
|
2156
|
+
: job.status === 'running'
|
|
2157
|
+
? 'running'
|
|
2158
|
+
: 'ready';
|
|
2159
|
+
return normalizeQueueJob({
|
|
2160
|
+
jobId: job.id,
|
|
2161
|
+
taskId: job.taskId,
|
|
2162
|
+
runId: run?.id ?? plan.runId,
|
|
2163
|
+
status,
|
|
2164
|
+
lane: job.lane,
|
|
2165
|
+
compute: job.compute.id,
|
|
2166
|
+
concurrencyKey: job.concurrencyKey,
|
|
2167
|
+
priority: job.priority,
|
|
2168
|
+
attempts: result?.metadata && typeof result.metadata.attempts === 'number' ? result.metadata.attempts : undefined,
|
|
2169
|
+
maxAttempts: job.budget?.maxRetries !== undefined ? job.budget.maxRetries + 1 : 1,
|
|
2170
|
+
lease,
|
|
2171
|
+
lastError: result?.error
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
952
2175
|
function normalizeResult(input) {
|
|
953
2176
|
const startedAt = input.startedAt;
|
|
954
2177
|
const finishedAt = input.finishedAt;
|
|
2178
|
+
const status = input.status ?? (input.exitCode === 0 || input.exitCode === undefined ? 'completed' : 'failed');
|
|
955
2179
|
return {
|
|
956
2180
|
jobId: input.jobId,
|
|
957
|
-
status
|
|
2181
|
+
status,
|
|
2182
|
+
mergeReadiness: classifySwarmMergeReadiness({ ...input, status }),
|
|
958
2183
|
...(startedAt !== undefined ? { startedAt } : {}),
|
|
959
2184
|
...(finishedAt !== undefined ? { finishedAt } : {}),
|
|
960
2185
|
...(startedAt !== undefined && finishedAt !== undefined ? { durationMs: Math.max(0, finishedAt - startedAt) } : {}),
|
|
961
2186
|
...(input.exitCode !== undefined ? { exitCode: input.exitCode } : {}),
|
|
962
2187
|
...(input.signal ? { signal: input.signal } : {}),
|
|
963
2188
|
changedPaths: uniqueStrings(input.changedPaths ?? []),
|
|
2189
|
+
changedRegions: uniqueStrings(input.changedRegions ?? []),
|
|
964
2190
|
ownershipViolations: uniqueStrings(input.ownershipViolations ?? []),
|
|
965
2191
|
evidencePaths: uniqueStrings(input.evidencePaths ?? []),
|
|
2192
|
+
...(input.patchPath ? { patchPath: input.patchPath } : {}),
|
|
2193
|
+
queueItemIds: uniqueStrings(input.queueItemIds ?? []),
|
|
2194
|
+
riskLevel: input.riskLevel ?? 'unknown',
|
|
2195
|
+
mergeDisposition: input.mergeDisposition ?? classifySwarmMergeDisposition({ ...input, status }),
|
|
966
2196
|
verification: (input.verification ?? []).map(normalizeVerificationResult),
|
|
967
2197
|
...(input.lastMessage ? { lastMessage: input.lastMessage } : {}),
|
|
968
2198
|
...(input.error !== undefined ? { error: stringifyError(input.error) } : {}),
|
|
969
2199
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
970
2200
|
};
|
|
971
2201
|
}
|
|
2202
|
+
function isSwarmJobResult(value) {
|
|
2203
|
+
return Array.isArray(value.changedPaths)
|
|
2204
|
+
&& Array.isArray(value.changedRegions)
|
|
2205
|
+
&& Array.isArray(value.verification)
|
|
2206
|
+
&& Array.isArray(value.queueItemIds)
|
|
2207
|
+
&& typeof value.riskLevel === 'string'
|
|
2208
|
+
&& typeof value.mergeDisposition === 'string';
|
|
2209
|
+
}
|
|
2210
|
+
function mergeBundleReasons(result, disposition, staleAgainstHead) {
|
|
2211
|
+
const reasons = [];
|
|
2212
|
+
if (staleAgainstHead)
|
|
2213
|
+
reasons.push('stale-against-head');
|
|
2214
|
+
if (result.status === 'blocked')
|
|
2215
|
+
reasons.push('blocked');
|
|
2216
|
+
if (result.status === 'failed')
|
|
2217
|
+
reasons.push('failed');
|
|
2218
|
+
if (result.ownershipViolations.length)
|
|
2219
|
+
reasons.push('ownership-violations');
|
|
2220
|
+
if (result.verification.some((entry) => entry.required !== false && entry.status !== 0))
|
|
2221
|
+
reasons.push('failed-verification');
|
|
2222
|
+
if (disposition === 'needs-port')
|
|
2223
|
+
reasons.push('needs-human-port');
|
|
2224
|
+
if (disposition === 'rejected')
|
|
2225
|
+
reasons.push('rejected');
|
|
2226
|
+
return uniqueStrings(reasons);
|
|
2227
|
+
}
|
|
2228
|
+
function inferMergeRisk(result, disposition) {
|
|
2229
|
+
if (disposition === 'discovery-only')
|
|
2230
|
+
return 'low';
|
|
2231
|
+
if (disposition === 'rejected' || disposition === 'blocked' || disposition === 'stale-against-head')
|
|
2232
|
+
return 'high';
|
|
2233
|
+
if (result.changedPaths.length <= 2 && result.ownershipViolations.length === 0)
|
|
2234
|
+
return 'low';
|
|
2235
|
+
if (result.changedPaths.length <= 8)
|
|
2236
|
+
return 'medium';
|
|
2237
|
+
return 'high';
|
|
2238
|
+
}
|
|
972
2239
|
function normalizeVerificationResult(input) {
|
|
973
2240
|
return {
|
|
974
2241
|
name: input.name ?? ((input.command ?? []).join(' ') || 'verification'),
|
|
@@ -1044,6 +2311,20 @@ function isSwarmManifest(value) {
|
|
|
1044
2311
|
function isSwarmTask(value) {
|
|
1045
2312
|
return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_TASK_KIND;
|
|
1046
2313
|
}
|
|
2314
|
+
function isSwarmEvent(value) {
|
|
2315
|
+
return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_EVENT_KIND;
|
|
2316
|
+
}
|
|
2317
|
+
function readLaneId(value) {
|
|
2318
|
+
return typeof value === 'string' ? value : value.id;
|
|
2319
|
+
}
|
|
2320
|
+
function joinPathParts(...parts) {
|
|
2321
|
+
const first = parts[0] ? String(parts[0]) : '';
|
|
2322
|
+
const prefix = first.startsWith('/') ? '/' : '';
|
|
2323
|
+
return prefix + parts
|
|
2324
|
+
.map((part, index) => String(part).replace(index === 0 ? /\/+$/g : /^\/+|\/+$/g, ''))
|
|
2325
|
+
.filter(Boolean)
|
|
2326
|
+
.join('/');
|
|
2327
|
+
}
|
|
1047
2328
|
function normalizeId(value, label) {
|
|
1048
2329
|
const id = String(value || '').trim();
|
|
1049
2330
|
if (!id)
|