@shapeshift-labs/frontier-swarm 0.2.0 → 0.3.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 +18 -3
- package/benchmarks/package-bench.mjs +35 -1
- package/dist/index.d.ts +350 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +541 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,22 +8,45 @@ 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;
|
|
23
33
|
export const FRONTIER_SWARM_DEFAULT_CODEX_COMPUTE_ID = 'codex.gpt-5.5.xhigh';
|
|
24
34
|
export const FRONTIER_SWARM_DEFAULT_MODEL = 'gpt-5.5';
|
|
25
35
|
export const FRONTIER_SWARM_DEFAULT_REASONING_EFFORT = 'xhigh';
|
|
26
36
|
const DEFAULT_COMPLETED_STATUSES = ['completed', 'verified', 'done', 'verified-local-harness'];
|
|
37
|
+
const DEFAULT_SWARM_EVENT_TYPES = [
|
|
38
|
+
'swarm.started',
|
|
39
|
+
'swarm.finished',
|
|
40
|
+
'agent.scheduled',
|
|
41
|
+
'agent.finished',
|
|
42
|
+
'agent.handoff',
|
|
43
|
+
'agent.blocked',
|
|
44
|
+
'agent.ownership-request',
|
|
45
|
+
'agent.evidence',
|
|
46
|
+
'review.requested',
|
|
47
|
+
'review.completed',
|
|
48
|
+
'merge.proposed'
|
|
49
|
+
];
|
|
27
50
|
export function defineSwarmManifest(input = {}) {
|
|
28
51
|
return createSwarmManifest(input);
|
|
29
52
|
}
|
|
@@ -158,6 +181,41 @@ export function createSwarmPlan(manifestInput, taskInput, options = {}) {
|
|
|
158
181
|
...(toJsonObject(options.metadata) ? { metadata: toJsonObject(options.metadata) } : {})
|
|
159
182
|
};
|
|
160
183
|
}
|
|
184
|
+
export function createSwarmTaskSelection(manifestInput, taskInput, options = {}) {
|
|
185
|
+
const manifest = compileSwarm(manifestInput).manifest;
|
|
186
|
+
const tasks = normalizeTaskList(taskInput);
|
|
187
|
+
const lanes = new Set(options.lanes ?? []);
|
|
188
|
+
const layers = new Set(options.layers ?? []);
|
|
189
|
+
const statuses = new Set(options.statuses ?? []);
|
|
190
|
+
const workKinds = new Set(options.workKinds ?? []);
|
|
191
|
+
const selectors = (options.selectors ?? []).map((selector) => selector.toLowerCase());
|
|
192
|
+
const completed = new Set(manifest.policy.completedStatuses);
|
|
193
|
+
const limit = options.limit === undefined ? tasks.length : Math.max(0, Math.floor(options.limit));
|
|
194
|
+
const candidates = tasks
|
|
195
|
+
.filter((task) => !task.lane || manifest.lanes.some((lane) => lane.id === task.lane))
|
|
196
|
+
.filter((task) => lanes.size === 0 || (task.lane !== undefined && lanes.has(task.lane)))
|
|
197
|
+
.filter((task) => layers.size === 0 || taskLayer(manifest, task) !== undefined && layers.has(taskLayer(manifest, task)))
|
|
198
|
+
.filter((task) => statuses.size === 0 || statuses.has(task.status))
|
|
199
|
+
.filter((task) => workKinds.size === 0 || workKinds.has(task.workKind))
|
|
200
|
+
.filter((task) => options.includeCompleted || !completed.has(task.status))
|
|
201
|
+
.filter((task) => selectors.length === 0 || selectors.some((selector) => searchableTask(task).includes(selector)))
|
|
202
|
+
.map((task) => createSelectionEntry(manifest, task, options.priority))
|
|
203
|
+
.filter((entry) => options.includeOwnershipWarnings || entry.ownershipWarnings.length === 0)
|
|
204
|
+
.sort((left, right) => (left.selectionPriority - right.selectionPriority
|
|
205
|
+
|| left.task.priority - right.task.priority
|
|
206
|
+
|| left.task.id.localeCompare(right.task.id)));
|
|
207
|
+
const ordered = options.spreadLanes ? roundRobinSelectionByLane(candidates) : candidates;
|
|
208
|
+
const entries = ordered.slice(0, limit).map((entry, index) => {
|
|
209
|
+
if (!options.assignSelectionPriority)
|
|
210
|
+
return entry;
|
|
211
|
+
return { ...entry, task: { ...entry.task, priority: index } };
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
tasks: entries.map((entry) => entry.task),
|
|
215
|
+
entries,
|
|
216
|
+
summary: summarizeTaskSelection(entries)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
161
219
|
export function createSwarmRun(input) {
|
|
162
220
|
const results = (input.results ?? []).map(normalizeResult);
|
|
163
221
|
const events = (input.events ?? []).map((event) => normalizeEvent({ ...event, runId: event.runId ?? input.id ?? input.plan.runId }));
|
|
@@ -182,6 +240,67 @@ export function recordSwarmEvent(runInput, eventInput) {
|
|
|
182
240
|
run.events = run.events.concat(normalizeEvent({ ...eventInput, runId: eventInput.runId ?? run.id }));
|
|
183
241
|
return run;
|
|
184
242
|
}
|
|
243
|
+
export function createSwarmMailbox(input = {}) {
|
|
244
|
+
const scope = input.scope ?? (input.lane ? 'lane' : input.jobId ? 'job' : 'global');
|
|
245
|
+
const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
|
|
246
|
+
return {
|
|
247
|
+
kind: FRONTIER_SWARM_MAILBOX_KIND,
|
|
248
|
+
version: FRONTIER_SWARM_MAILBOX_VERSION,
|
|
249
|
+
id: input.id ?? 'swarm-mailbox:' + stableHash([input.runId, scope, input.lane, input.jobId, input.path, eventTypes]),
|
|
250
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
251
|
+
scope,
|
|
252
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
253
|
+
...(input.jobId ? { jobId: input.jobId } : {}),
|
|
254
|
+
...(input.path ? { path: input.path } : {}),
|
|
255
|
+
eventTypes,
|
|
256
|
+
appendOnly: input.appendOnly ?? true,
|
|
257
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function createSwarmEventStream(input = {}) {
|
|
261
|
+
const laneIds = uniqueStrings((input.lanes ?? []).map(readLaneId));
|
|
262
|
+
const eventTypes = uniqueStrings(input.eventTypes ?? DEFAULT_SWARM_EVENT_TYPES);
|
|
263
|
+
const appendOnly = input.appendOnly ?? true;
|
|
264
|
+
const global = createSwarmMailbox({
|
|
265
|
+
runId: input.runId,
|
|
266
|
+
scope: 'global',
|
|
267
|
+
path: input.root ? joinPathParts(input.root, 'global.jsonl') : undefined,
|
|
268
|
+
eventTypes,
|
|
269
|
+
appendOnly
|
|
270
|
+
});
|
|
271
|
+
const lanes = Object.fromEntries(laneIds.map((lane) => [lane, createSwarmMailbox({
|
|
272
|
+
runId: input.runId,
|
|
273
|
+
scope: 'lane',
|
|
274
|
+
lane,
|
|
275
|
+
path: input.root ? joinPathParts(input.root, 'lanes', `${lane}.jsonl`) : undefined,
|
|
276
|
+
eventTypes,
|
|
277
|
+
appendOnly
|
|
278
|
+
})]));
|
|
279
|
+
return {
|
|
280
|
+
kind: FRONTIER_SWARM_EVENT_STREAM_KIND,
|
|
281
|
+
version: FRONTIER_SWARM_EVENT_STREAM_VERSION,
|
|
282
|
+
id: input.id ?? 'swarm-event-stream:' + stableHash([input.runId, input.root, laneIds, eventTypes]),
|
|
283
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
284
|
+
...(input.root ? { root: input.root } : {}),
|
|
285
|
+
appendOnly,
|
|
286
|
+
global,
|
|
287
|
+
lanes,
|
|
288
|
+
eventTypes,
|
|
289
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
|
|
290
|
+
summary: {
|
|
291
|
+
mailboxCount: 1 + laneIds.length,
|
|
292
|
+
laneCount: laneIds.length,
|
|
293
|
+
eventTypeCount: eventTypes.length
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export function routeSwarmEventToMailboxes(stream, eventInput) {
|
|
298
|
+
const event = isSwarmEvent(eventInput) ? eventInput : normalizeEvent(eventInput);
|
|
299
|
+
const mailboxes = [stream.global];
|
|
300
|
+
if (event.lane && stream.lanes[event.lane])
|
|
301
|
+
mailboxes.push(stream.lanes[event.lane]);
|
|
302
|
+
return mailboxes;
|
|
303
|
+
}
|
|
185
304
|
export function completeSwarmJob(runInput, resultInput) {
|
|
186
305
|
const run = cloneJsonValue(runInput);
|
|
187
306
|
const result = normalizeResult(resultInput);
|
|
@@ -211,6 +330,102 @@ export function checkSwarmOwnership(job, changedPaths) {
|
|
|
211
330
|
violations
|
|
212
331
|
};
|
|
213
332
|
}
|
|
333
|
+
export function resolveSwarmChangedRegions(job, changedPaths) {
|
|
334
|
+
const changed = uniqueStrings(changedPaths);
|
|
335
|
+
const regions = new Set(job.changedRegions);
|
|
336
|
+
for (const region of job.ownershipRegions) {
|
|
337
|
+
if (region.globs.some((glob) => changed.some((file) => matchesGlob(file, glob))))
|
|
338
|
+
regions.add(region.id);
|
|
339
|
+
for (const selector of region.selectors) {
|
|
340
|
+
if (changed.includes(selector))
|
|
341
|
+
regions.add(region.id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return Array.from(regions).sort();
|
|
345
|
+
}
|
|
346
|
+
export function classifySwarmMergeReadiness(result) {
|
|
347
|
+
if (result.mergeReadiness)
|
|
348
|
+
return result.mergeReadiness;
|
|
349
|
+
if (result.status === 'blocked')
|
|
350
|
+
return 'blocked';
|
|
351
|
+
if (result.status === 'failed' || result.exitCode !== undefined && result.exitCode !== 0)
|
|
352
|
+
return 'rejected';
|
|
353
|
+
const changedPaths = result.changedPaths ?? [];
|
|
354
|
+
if (changedPaths.length === 0)
|
|
355
|
+
return 'discovery-only';
|
|
356
|
+
const ownershipViolations = result.ownershipViolations ?? [];
|
|
357
|
+
if (ownershipViolations.length)
|
|
358
|
+
return 'rejected';
|
|
359
|
+
const verification = result.verification ?? [];
|
|
360
|
+
const failedRequired = verification.some((entry) => entry.required !== false && entry.status !== 0);
|
|
361
|
+
if (failedRequired)
|
|
362
|
+
return 'patch-candidate';
|
|
363
|
+
return verification.length > 0 || result.status === 'verified' ? 'verified-patch' : 'patch-candidate';
|
|
364
|
+
}
|
|
365
|
+
export function classifySwarmMergeDisposition(result, input = {}) {
|
|
366
|
+
if (result.mergeDisposition)
|
|
367
|
+
return result.mergeDisposition;
|
|
368
|
+
if (input.staleAgainstHead)
|
|
369
|
+
return 'stale-against-head';
|
|
370
|
+
const readiness = classifySwarmMergeReadiness(result);
|
|
371
|
+
if (readiness === 'discovery-only')
|
|
372
|
+
return 'discovery-only';
|
|
373
|
+
if (readiness === 'blocked')
|
|
374
|
+
return 'blocked';
|
|
375
|
+
if (readiness === 'rejected')
|
|
376
|
+
return 'rejected';
|
|
377
|
+
if (readiness === 'verified-patch')
|
|
378
|
+
return 'auto-mergeable';
|
|
379
|
+
return 'needs-port';
|
|
380
|
+
}
|
|
381
|
+
export function createSwarmMergeBundle(input) {
|
|
382
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
383
|
+
const result = isSwarmJobResult(input.result) ? cloneJsonValue(input.result) : normalizeResult(input.result);
|
|
384
|
+
const job = input.job;
|
|
385
|
+
const changedPaths = uniqueStrings(result.changedPaths);
|
|
386
|
+
const changedRegions = uniqueStrings([
|
|
387
|
+
...result.changedRegions,
|
|
388
|
+
...(job ? resolveSwarmChangedRegions(job, changedPaths) : [])
|
|
389
|
+
]);
|
|
390
|
+
const evidencePaths = uniqueStrings([...(result.evidencePaths ?? []), ...(input.evidencePaths ?? [])]);
|
|
391
|
+
const queueItemIds = uniqueStrings([...(result.queueItemIds ?? []), ...(input.queueItemIds ?? []), ...(job ? [job.taskId] : [])]);
|
|
392
|
+
const disposition = input.disposition ?? classifySwarmMergeDisposition(result, { staleAgainstHead: input.staleAgainstHead });
|
|
393
|
+
const commandsPassed = result.verification.filter((entry) => entry.status === 0 || entry.required === false && entry.status === undefined);
|
|
394
|
+
const commandsFailed = result.verification.filter((entry) => entry.status !== undefined && entry.status !== 0 && entry.required !== false);
|
|
395
|
+
const ownedFilesTouched = job ? changedPaths.filter((file) => job.allowedWrites.some((glob) => matchesGlob(file, glob))) : changedPaths;
|
|
396
|
+
const reasons = mergeBundleReasons(result, disposition, input.staleAgainstHead ?? false);
|
|
397
|
+
return {
|
|
398
|
+
kind: FRONTIER_SWARM_MERGE_BUNDLE_KIND,
|
|
399
|
+
version: FRONTIER_SWARM_MERGE_BUNDLE_VERSION,
|
|
400
|
+
id: input.id ?? 'swarm-merge-bundle:' + stableHash([input.runId, input.planId, result.jobId, changedPaths, changedRegions, disposition, generatedAt]),
|
|
401
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
402
|
+
...(input.planId ? { planId: input.planId } : {}),
|
|
403
|
+
jobId: result.jobId,
|
|
404
|
+
...(job ? { taskId: job.taskId, lane: job.lane, title: job.title } : {}),
|
|
405
|
+
generatedAt,
|
|
406
|
+
status: result.status,
|
|
407
|
+
mergeReadiness: result.mergeReadiness,
|
|
408
|
+
disposition,
|
|
409
|
+
riskLevel: input.riskLevel ?? result.riskLevel ?? inferMergeRisk(result, disposition),
|
|
410
|
+
autoMergeable: disposition === 'auto-mergeable' && reasons.length === 0,
|
|
411
|
+
changedPaths,
|
|
412
|
+
changedRegions,
|
|
413
|
+
ownedFilesTouched,
|
|
414
|
+
allowedWrites: job ? [...job.allowedWrites] : [],
|
|
415
|
+
ownershipViolations: [...result.ownershipViolations],
|
|
416
|
+
...(input.patchPath ?? result.patchPath ? { patchPath: input.patchPath ?? result.patchPath } : {}),
|
|
417
|
+
...(input.patchHash ? { patchHash: input.patchHash } : {}),
|
|
418
|
+
evidencePaths,
|
|
419
|
+
commandsPassed,
|
|
420
|
+
commandsFailed,
|
|
421
|
+
queueItemIds,
|
|
422
|
+
...(input.branchName ? { branchName: input.branchName } : {}),
|
|
423
|
+
...(input.commit ? { commit: input.commit } : {}),
|
|
424
|
+
staleAgainstHead: input.staleAgainstHead ?? false,
|
|
425
|
+
reasons,
|
|
426
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
427
|
+
};
|
|
428
|
+
}
|
|
214
429
|
export function resolveSwarmCompute(manifestInput, taskInput) {
|
|
215
430
|
const compiled = compileSwarm(manifestInput);
|
|
216
431
|
const task = isSwarmTask(taskInput) ? taskInput : normalizeTask(taskInput);
|
|
@@ -248,7 +463,14 @@ export function createSwarmSchedule(input) {
|
|
|
248
463
|
}
|
|
249
464
|
const runningJobs = (run?.jobs ?? plan.jobs)
|
|
250
465
|
.filter((job) => job.status === 'running')
|
|
251
|
-
.map((job) => ({
|
|
466
|
+
.map((job) => ({
|
|
467
|
+
jobId: job.id,
|
|
468
|
+
lane: job.lane,
|
|
469
|
+
compute: job.compute.id,
|
|
470
|
+
concurrencyKey: job.concurrencyKey,
|
|
471
|
+
capabilities: [...job.capabilities],
|
|
472
|
+
...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
|
|
473
|
+
}));
|
|
252
474
|
const runningByLane = countBy(runningJobs.map((job) => job.lane));
|
|
253
475
|
const runningByKey = countBy(runningJobs.map((job) => job.concurrencyKey));
|
|
254
476
|
const runningByCompute = countBy(runningJobs.map((job) => job.compute));
|
|
@@ -330,6 +552,65 @@ export function createSwarmLeases(input) {
|
|
|
330
552
|
status: 'active'
|
|
331
553
|
}));
|
|
332
554
|
}
|
|
555
|
+
export function renewSwarmLease(input) {
|
|
556
|
+
const now = input.now ?? Date.now();
|
|
557
|
+
const leaseMs = Math.max(1, Math.floor(input.leaseMs ?? Math.max(1, input.lease.expiresAt - input.lease.leasedAt)));
|
|
558
|
+
return {
|
|
559
|
+
...cloneJsonValue(input.lease),
|
|
560
|
+
leasedAt: now,
|
|
561
|
+
expiresAt: now + leaseMs,
|
|
562
|
+
status: input.status ?? 'active'
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
export function createSwarmQueueSnapshot(input) {
|
|
566
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
567
|
+
const leases = [...(input.leases ?? [])].map((lease) => cloneJsonValue(lease));
|
|
568
|
+
const jobs = (input.jobs ? input.jobs.map(normalizeQueueJob) : queueJobsFromPlan(input.plan, input.run, leases)).sort((left, right) => (left.priority - right.priority
|
|
569
|
+
|| left.jobId.localeCompare(right.jobId)));
|
|
570
|
+
const byStatus = groupIds(jobs, (job) => job.status);
|
|
571
|
+
const byLane = groupIds(jobs, (job) => job.lane ?? 'unassigned');
|
|
572
|
+
return {
|
|
573
|
+
kind: FRONTIER_SWARM_QUEUE_SNAPSHOT_KIND,
|
|
574
|
+
version: FRONTIER_SWARM_QUEUE_SNAPSHOT_VERSION,
|
|
575
|
+
id: input.id ?? 'swarm-queue-snapshot:' + stableHash([input.plan.id, input.run?.id, jobs, leases, generatedAt]),
|
|
576
|
+
planId: input.plan.id,
|
|
577
|
+
runId: input.run?.id ?? input.plan.runId,
|
|
578
|
+
generatedAt,
|
|
579
|
+
jobs,
|
|
580
|
+
byStatus,
|
|
581
|
+
byLane,
|
|
582
|
+
leases,
|
|
583
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {}),
|
|
584
|
+
summary: {
|
|
585
|
+
jobCount: jobs.length,
|
|
586
|
+
leaseCount: leases.length,
|
|
587
|
+
readyCount: byStatus.ready?.length ?? 0,
|
|
588
|
+
leasedCount: byStatus.leased?.length ?? 0,
|
|
589
|
+
completedCount: byStatus.completed?.length ?? 0,
|
|
590
|
+
failedCount: byStatus.failed?.length ?? 0,
|
|
591
|
+
deadLetterCount: byStatus['dead-letter']?.length ?? 0
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
export function createSwarmRunCheckpoint(input) {
|
|
596
|
+
const run = 'kind' in input ? input : input.run;
|
|
597
|
+
const sequence = 'kind' in input ? run.events.length + run.results.length : input.sequence ?? run.events.length + run.results.length;
|
|
598
|
+
const savedAt = 'kind' in input ? Date.now() : input.savedAt ?? Date.now();
|
|
599
|
+
return {
|
|
600
|
+
kind: FRONTIER_SWARM_RUN_CHECKPOINT_KIND,
|
|
601
|
+
version: FRONTIER_SWARM_RUN_CHECKPOINT_VERSION,
|
|
602
|
+
id: 'swarm-run-checkpoint:' + stableHash([run.id, sequence, savedAt, run.summary]),
|
|
603
|
+
runId: run.id,
|
|
604
|
+
planId: run.planId,
|
|
605
|
+
sequence,
|
|
606
|
+
savedAt,
|
|
607
|
+
status: run.status,
|
|
608
|
+
eventCount: run.events.length,
|
|
609
|
+
resultCount: run.results.length,
|
|
610
|
+
hash: stableHash(run),
|
|
611
|
+
...(!('kind' in input) && toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
612
|
+
};
|
|
613
|
+
}
|
|
333
614
|
export function checkSwarmBudget(job, usageInput) {
|
|
334
615
|
const usage = normalizeUsage(usageInput);
|
|
335
616
|
const budget = job.budget;
|
|
@@ -524,7 +805,8 @@ function createSwarmJobGraph(jobs) {
|
|
|
524
805
|
function normalizeScheduleLimits(manifest, options) {
|
|
525
806
|
const maxLaneConcurrency = {};
|
|
526
807
|
for (const lane of manifest.lanes) {
|
|
527
|
-
const
|
|
808
|
+
const browserMax = lane.resourceRequirements?.browser?.maxConcurrency;
|
|
809
|
+
const value = options.maxLaneConcurrency?.[lane.id] ?? lane.maxConcurrency ?? browserMax ?? manifest.policy.defaultConcurrency;
|
|
528
810
|
maxLaneConcurrency[lane.id] = Math.max(1, Math.floor(value));
|
|
529
811
|
}
|
|
530
812
|
return {
|
|
@@ -550,7 +832,9 @@ function scheduleJob(job, dependsOn = job.dependsOn) {
|
|
|
550
832
|
compute: job.compute.id,
|
|
551
833
|
concurrencyKey: job.concurrencyKey,
|
|
552
834
|
priority: job.priority,
|
|
553
|
-
dependsOn: [...dependsOn]
|
|
835
|
+
dependsOn: [...dependsOn],
|
|
836
|
+
capabilities: [...job.capabilities],
|
|
837
|
+
...(job.resourceRequirements ? { resourceRequirements: cloneJsonValue(job.resourceRequirements) } : {})
|
|
554
838
|
};
|
|
555
839
|
}
|
|
556
840
|
function normalizeBudget(input = {}) {
|
|
@@ -618,10 +902,13 @@ function selectReviewers(pool, required, salt) {
|
|
|
618
902
|
function conflictMap(results) {
|
|
619
903
|
const byPath = new Map();
|
|
620
904
|
for (const result of results) {
|
|
621
|
-
|
|
622
|
-
|
|
905
|
+
const keys = result.changedRegions.length
|
|
906
|
+
? result.changedRegions.map((region) => `region:${region}`)
|
|
907
|
+
: result.changedPaths.map((file) => `file:${file}`);
|
|
908
|
+
for (const key of keys) {
|
|
909
|
+
const list = byPath.get(key) ?? [];
|
|
623
910
|
list.push(result.jobId);
|
|
624
|
-
byPath.set(
|
|
911
|
+
byPath.set(key, list);
|
|
625
912
|
}
|
|
626
913
|
}
|
|
627
914
|
const conflicts = new Map();
|
|
@@ -658,6 +945,16 @@ function groupArtifacts(artifacts, key) {
|
|
|
658
945
|
}
|
|
659
946
|
return out;
|
|
660
947
|
}
|
|
948
|
+
function groupIds(items, key) {
|
|
949
|
+
const out = {};
|
|
950
|
+
for (const item of items) {
|
|
951
|
+
const group = key(item);
|
|
952
|
+
out[group] = [...(out[group] ?? []), item.jobId];
|
|
953
|
+
}
|
|
954
|
+
for (const ids of Object.values(out))
|
|
955
|
+
ids.sort();
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
661
958
|
function countBy(values) {
|
|
662
959
|
const out = {};
|
|
663
960
|
for (const value of values)
|
|
@@ -733,6 +1030,9 @@ function normalizeLane(input) {
|
|
|
733
1030
|
allowedWrites,
|
|
734
1031
|
sharedReadOnly: uniqueStrings(input.sharedReadOnly ?? []),
|
|
735
1032
|
neverEdit: uniqueStrings(input.neverEdit ?? []),
|
|
1033
|
+
ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
|
|
1034
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1035
|
+
...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
|
|
736
1036
|
...(input.worktreePath ? { worktreePath: input.worktreePath } : {}),
|
|
737
1037
|
...(input.evidencePrefix || input.evidenceOutDirPrefix ? { evidencePrefix: input.evidencePrefix ?? input.evidenceOutDirPrefix } : {}),
|
|
738
1038
|
concurrencyKey: input.concurrencyKey ?? input.id,
|
|
@@ -742,6 +1042,68 @@ function normalizeLane(input) {
|
|
|
742
1042
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
743
1043
|
};
|
|
744
1044
|
}
|
|
1045
|
+
function normalizeOwnershipRegions(input = []) {
|
|
1046
|
+
return input.map((region) => {
|
|
1047
|
+
const globs = uniqueStrings([...(region.globs ?? []), ...(region.paths ?? [])]);
|
|
1048
|
+
return {
|
|
1049
|
+
id: normalizeId(region.id, 'ownership region id'),
|
|
1050
|
+
title: region.title ?? titleFromId(region.id),
|
|
1051
|
+
...(region.description ? { description: region.description } : {}),
|
|
1052
|
+
globs,
|
|
1053
|
+
selectors: uniqueStrings(region.selectors ?? []),
|
|
1054
|
+
...(region.owner ? { owner: region.owner } : {}),
|
|
1055
|
+
...(toJsonObject(region.metadata) ? { metadata: toJsonObject(region.metadata) } : {})
|
|
1056
|
+
};
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
function mergeOwnershipRegions(laneRegions, taskRegions) {
|
|
1060
|
+
const byId = new Map();
|
|
1061
|
+
for (const region of laneRegions)
|
|
1062
|
+
byId.set(region.id, cloneJsonValue(region));
|
|
1063
|
+
for (const region of taskRegions)
|
|
1064
|
+
byId.set(region.id, cloneJsonValue(region));
|
|
1065
|
+
return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
1066
|
+
}
|
|
1067
|
+
function normalizeResourceRequirements(input = {}) {
|
|
1068
|
+
const resources = {};
|
|
1069
|
+
for (const [key, value] of Object.entries(input.resources ?? {})) {
|
|
1070
|
+
if (Number.isFinite(value) && value > 0)
|
|
1071
|
+
resources[key] = value;
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1075
|
+
resources,
|
|
1076
|
+
...(input.browser ? { browser: normalizeBrowserResource(input.browser) } : {}),
|
|
1077
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function normalizeBrowserResource(input) {
|
|
1081
|
+
return {
|
|
1082
|
+
required: input.required ?? true,
|
|
1083
|
+
portPool: uniqueStrings((input.portPool ?? []).map((port) => String(port))),
|
|
1084
|
+
...(input.profileDir ? { profileDir: input.profileDir } : {}),
|
|
1085
|
+
...(input.profileDirPrefix ? { profileDirPrefix: input.profileDirPrefix } : {}),
|
|
1086
|
+
...(positiveNumber(input.maxConcurrency) ? { maxConcurrency: Math.floor(input.maxConcurrency) } : {}),
|
|
1087
|
+
...(input.headless !== undefined ? { headless: input.headless } : {}),
|
|
1088
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function mergeResourceRequirements(lane, task, extraCapabilities = []) {
|
|
1092
|
+
if (!lane && !task && extraCapabilities.length === 0)
|
|
1093
|
+
return undefined;
|
|
1094
|
+
const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...(task?.capabilities ?? []), ...extraCapabilities]);
|
|
1095
|
+
const resources = { ...(lane?.resources ?? {}) };
|
|
1096
|
+
for (const [key, value] of Object.entries(task?.resources ?? {}))
|
|
1097
|
+
resources[key] = Math.max(resources[key] ?? 0, value);
|
|
1098
|
+
const browser = task?.browser ?? lane?.browser;
|
|
1099
|
+
const metadata = toJsonObject({ ...(lane?.metadata ?? {}), ...(task?.metadata ?? {}) });
|
|
1100
|
+
return {
|
|
1101
|
+
capabilities,
|
|
1102
|
+
resources,
|
|
1103
|
+
...(browser ? { browser } : {}),
|
|
1104
|
+
...(metadata && Object.keys(metadata).length ? { metadata } : {})
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
745
1107
|
function normalizePolicy(input, defaultCompute) {
|
|
746
1108
|
return {
|
|
747
1109
|
mode: input?.mode ?? 'hard-file-ownership',
|
|
@@ -778,6 +1140,11 @@ function normalizeTask(input) {
|
|
|
778
1140
|
sourceRefs: uniqueStrings(input.sourceRefs ?? []),
|
|
779
1141
|
targetRefs,
|
|
780
1142
|
allowedWrites: uniqueStrings([...(input.allowedWrites ?? []), ...targetRefs]),
|
|
1143
|
+
ownershipRegions: normalizeOwnershipRegions(input.ownershipRegions ?? []),
|
|
1144
|
+
ownedRegions: uniqueStrings(input.ownedRegions ?? []),
|
|
1145
|
+
changedRegions: uniqueStrings(input.changedRegions ?? []),
|
|
1146
|
+
capabilities: uniqueStrings(input.capabilities ?? []),
|
|
1147
|
+
...(input.resourceRequirements ? { resourceRequirements: normalizeResourceRequirements(input.resourceRequirements) } : {}),
|
|
781
1148
|
acceptance: normalizeAcceptance(input),
|
|
782
1149
|
verification: normalizeCommands(input.verification ?? []),
|
|
783
1150
|
...(input.evidenceCommand ? { evidenceCommand: input.evidenceCommand } : {}),
|
|
@@ -827,6 +1194,55 @@ function selectSwarmTasks(manifest, tasks, options) {
|
|
|
827
1194
|
.sort((left, right) => left.priority - right.priority || left.id.localeCompare(right.id))
|
|
828
1195
|
.slice(0, limit);
|
|
829
1196
|
}
|
|
1197
|
+
function createSelectionEntry(manifest, task, priority) {
|
|
1198
|
+
const lane = task.lane ? manifest.lanes.find((entry) => entry.id === task.lane) : undefined;
|
|
1199
|
+
return {
|
|
1200
|
+
task,
|
|
1201
|
+
...(lane ? { lane } : {}),
|
|
1202
|
+
ownershipWarnings: selectionOwnershipWarnings(task, lane),
|
|
1203
|
+
selectionPriority: selectionPriority(task, priority)
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function selectionOwnershipWarnings(task, lane) {
|
|
1207
|
+
if (!lane || lane.allowedWrites.length === 0)
|
|
1208
|
+
return [];
|
|
1209
|
+
return task.targetRefs
|
|
1210
|
+
.filter((file) => !lane.allowedWrites.some((glob) => matchesGlob(file, glob)))
|
|
1211
|
+
.map((file) => `${file} is outside allowed write globs for ${lane.id}`);
|
|
1212
|
+
}
|
|
1213
|
+
function selectionPriority(task, input) {
|
|
1214
|
+
const statusRanks = input?.statuses ?? {};
|
|
1215
|
+
const workKindRanks = input?.workKinds ?? {};
|
|
1216
|
+
const statusRank = statusRanks[task.status] ?? input?.defaultStatusRank ?? 100;
|
|
1217
|
+
const workKindRank = workKindRanks[task.workKind] ?? input?.defaultWorkKindRank ?? 100;
|
|
1218
|
+
const statusWeight = input?.statusWeight ?? 1000;
|
|
1219
|
+
const workKindWeight = input?.workKindWeight ?? 1;
|
|
1220
|
+
return statusRank * statusWeight + workKindRank * workKindWeight;
|
|
1221
|
+
}
|
|
1222
|
+
function roundRobinSelectionByLane(entries) {
|
|
1223
|
+
const groups = new Map();
|
|
1224
|
+
for (const entry of entries)
|
|
1225
|
+
groups.set(entry.task.lane ?? 'unassigned', [...(groups.get(entry.task.lane ?? 'unassigned') ?? []), entry]);
|
|
1226
|
+
const selected = [];
|
|
1227
|
+
while (Array.from(groups.values()).some((group) => group.length > 0)) {
|
|
1228
|
+
for (const group of groups.values()) {
|
|
1229
|
+
const next = group.shift();
|
|
1230
|
+
if (next)
|
|
1231
|
+
selected.push(next);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return selected;
|
|
1235
|
+
}
|
|
1236
|
+
function summarizeTaskSelection(entries) {
|
|
1237
|
+
return entries.reduce((summary, entry) => {
|
|
1238
|
+
const lane = entry.task.lane ?? 'unassigned';
|
|
1239
|
+
summary.total += 1;
|
|
1240
|
+
summary.byLane[lane] = (summary.byLane[lane] ?? 0) + 1;
|
|
1241
|
+
summary.byWorkKind[entry.task.workKind] = (summary.byWorkKind[entry.task.workKind] ?? 0) + 1;
|
|
1242
|
+
summary.ownershipWarningCount += entry.ownershipWarnings.length;
|
|
1243
|
+
return summary;
|
|
1244
|
+
}, { total: 0, byLane: {}, byWorkKind: {}, ownershipWarningCount: 0 });
|
|
1245
|
+
}
|
|
830
1246
|
function createJob(compiled, task, options) {
|
|
831
1247
|
const lane = task.lane ? compiled.lanesById.get(task.lane) : undefined;
|
|
832
1248
|
const layer = task.layer ?? lane?.layer ?? compiled.manifest.policy.defaultLayer;
|
|
@@ -842,6 +1258,10 @@ function createJob(compiled, task, options) {
|
|
|
842
1258
|
const ownershipWarnings = task.targetRefs
|
|
843
1259
|
.filter((file) => allowedWrites.length > 0 && !allowedWrites.some((glob) => matchesGlob(file, glob)))
|
|
844
1260
|
.map((file) => `${file} is outside allowed write globs for ${lane?.id ?? 'unassigned'}`);
|
|
1261
|
+
const capabilities = uniqueStrings([...(lane?.capabilities ?? []), ...task.capabilities]);
|
|
1262
|
+
const resourceRequirements = mergeResourceRequirements(lane?.resourceRequirements, task.resourceRequirements, capabilities);
|
|
1263
|
+
const ownershipRegions = mergeOwnershipRegions(lane?.ownershipRegions ?? [], task.ownershipRegions);
|
|
1264
|
+
const ownedRegions = uniqueStrings([...task.ownedRegions, ...ownershipRegions.map((region) => region.id)]);
|
|
845
1265
|
return {
|
|
846
1266
|
id: `${lane?.id ?? 'unassigned'}-${slug(task.id)}`,
|
|
847
1267
|
taskId: task.id,
|
|
@@ -855,6 +1275,11 @@ function createJob(compiled, task, options) {
|
|
|
855
1275
|
allowedWrites,
|
|
856
1276
|
sharedReadOnly: uniqueStrings([...(compiled.manifest.policy.sharedReadOnly ?? []), ...(lane?.sharedReadOnly ?? [])]),
|
|
857
1277
|
neverEdit: uniqueStrings([...(compiled.manifest.policy.neverEditWithoutParent ?? []), ...(lane?.neverEdit ?? [])]),
|
|
1278
|
+
ownershipRegions,
|
|
1279
|
+
ownedRegions,
|
|
1280
|
+
changedRegions: uniqueStrings(task.changedRegions),
|
|
1281
|
+
capabilities,
|
|
1282
|
+
...(resourceRequirements ? { resourceRequirements } : {}),
|
|
858
1283
|
...(lane?.worktreePath ? { worktreePath: lane.worktreePath } : {}),
|
|
859
1284
|
...(evidencePrefix ? { evidencePrefix } : {}),
|
|
860
1285
|
concurrencyKey: task.concurrencyKey ?? lane?.concurrencyKey ?? task.lane ?? compute.id,
|
|
@@ -949,26 +1374,121 @@ function normalizeEvent(input) {
|
|
|
949
1374
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
950
1375
|
};
|
|
951
1376
|
}
|
|
1377
|
+
function normalizeQueueJob(input) {
|
|
1378
|
+
return {
|
|
1379
|
+
jobId: input.jobId,
|
|
1380
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
1381
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
1382
|
+
status: input.status ?? 'ready',
|
|
1383
|
+
...(input.lane ? { lane: input.lane } : {}),
|
|
1384
|
+
...(input.compute ? { compute: input.compute } : {}),
|
|
1385
|
+
...(input.concurrencyKey ? { concurrencyKey: input.concurrencyKey } : {}),
|
|
1386
|
+
priority: input.priority ?? 100,
|
|
1387
|
+
attempts: Math.max(0, Math.floor(input.attempts ?? 0)),
|
|
1388
|
+
maxAttempts: Math.max(1, Math.floor(input.maxAttempts ?? 1)),
|
|
1389
|
+
...(input.availableAt !== undefined ? { availableAt: input.availableAt } : {}),
|
|
1390
|
+
...(input.lease ? { lease: cloneJsonValue(input.lease) } : {}),
|
|
1391
|
+
...(input.lastError ? { lastError: input.lastError } : {}),
|
|
1392
|
+
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function queueJobsFromPlan(plan, run, leases) {
|
|
1396
|
+
const resultsByJob = new Map((run?.results ?? []).map((result) => [result.jobId, result]));
|
|
1397
|
+
const activeLeases = new Map(leases.filter((lease) => lease.status === 'active').map((lease) => [lease.jobId, lease]));
|
|
1398
|
+
return plan.jobs.map((job) => {
|
|
1399
|
+
const result = resultsByJob.get(job.id);
|
|
1400
|
+
const lease = activeLeases.get(job.id);
|
|
1401
|
+
const failed = result?.status === 'failed' || result?.exitCode !== undefined && result.exitCode !== 0;
|
|
1402
|
+
const completed = result?.status === 'completed' || result?.status === 'verified';
|
|
1403
|
+
const status = completed
|
|
1404
|
+
? 'completed'
|
|
1405
|
+
: failed
|
|
1406
|
+
? 'failed'
|
|
1407
|
+
: lease
|
|
1408
|
+
? 'leased'
|
|
1409
|
+
: job.status === 'running'
|
|
1410
|
+
? 'running'
|
|
1411
|
+
: 'ready';
|
|
1412
|
+
return normalizeQueueJob({
|
|
1413
|
+
jobId: job.id,
|
|
1414
|
+
taskId: job.taskId,
|
|
1415
|
+
runId: run?.id ?? plan.runId,
|
|
1416
|
+
status,
|
|
1417
|
+
lane: job.lane,
|
|
1418
|
+
compute: job.compute.id,
|
|
1419
|
+
concurrencyKey: job.concurrencyKey,
|
|
1420
|
+
priority: job.priority,
|
|
1421
|
+
attempts: result?.metadata && typeof result.metadata.attempts === 'number' ? result.metadata.attempts : undefined,
|
|
1422
|
+
maxAttempts: job.budget?.maxRetries !== undefined ? job.budget.maxRetries + 1 : 1,
|
|
1423
|
+
lease,
|
|
1424
|
+
lastError: result?.error
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
952
1428
|
function normalizeResult(input) {
|
|
953
1429
|
const startedAt = input.startedAt;
|
|
954
1430
|
const finishedAt = input.finishedAt;
|
|
1431
|
+
const status = input.status ?? (input.exitCode === 0 || input.exitCode === undefined ? 'completed' : 'failed');
|
|
955
1432
|
return {
|
|
956
1433
|
jobId: input.jobId,
|
|
957
|
-
status
|
|
1434
|
+
status,
|
|
1435
|
+
mergeReadiness: classifySwarmMergeReadiness({ ...input, status }),
|
|
958
1436
|
...(startedAt !== undefined ? { startedAt } : {}),
|
|
959
1437
|
...(finishedAt !== undefined ? { finishedAt } : {}),
|
|
960
1438
|
...(startedAt !== undefined && finishedAt !== undefined ? { durationMs: Math.max(0, finishedAt - startedAt) } : {}),
|
|
961
1439
|
...(input.exitCode !== undefined ? { exitCode: input.exitCode } : {}),
|
|
962
1440
|
...(input.signal ? { signal: input.signal } : {}),
|
|
963
1441
|
changedPaths: uniqueStrings(input.changedPaths ?? []),
|
|
1442
|
+
changedRegions: uniqueStrings(input.changedRegions ?? []),
|
|
964
1443
|
ownershipViolations: uniqueStrings(input.ownershipViolations ?? []),
|
|
965
1444
|
evidencePaths: uniqueStrings(input.evidencePaths ?? []),
|
|
1445
|
+
...(input.patchPath ? { patchPath: input.patchPath } : {}),
|
|
1446
|
+
queueItemIds: uniqueStrings(input.queueItemIds ?? []),
|
|
1447
|
+
riskLevel: input.riskLevel ?? 'unknown',
|
|
1448
|
+
mergeDisposition: input.mergeDisposition ?? classifySwarmMergeDisposition({ ...input, status }),
|
|
966
1449
|
verification: (input.verification ?? []).map(normalizeVerificationResult),
|
|
967
1450
|
...(input.lastMessage ? { lastMessage: input.lastMessage } : {}),
|
|
968
1451
|
...(input.error !== undefined ? { error: stringifyError(input.error) } : {}),
|
|
969
1452
|
...(toJsonObject(input.metadata) ? { metadata: toJsonObject(input.metadata) } : {})
|
|
970
1453
|
};
|
|
971
1454
|
}
|
|
1455
|
+
function isSwarmJobResult(value) {
|
|
1456
|
+
return Array.isArray(value.changedPaths)
|
|
1457
|
+
&& Array.isArray(value.changedRegions)
|
|
1458
|
+
&& Array.isArray(value.verification)
|
|
1459
|
+
&& Array.isArray(value.queueItemIds)
|
|
1460
|
+
&& typeof value.riskLevel === 'string'
|
|
1461
|
+
&& typeof value.mergeDisposition === 'string';
|
|
1462
|
+
}
|
|
1463
|
+
function mergeBundleReasons(result, disposition, staleAgainstHead) {
|
|
1464
|
+
const reasons = [];
|
|
1465
|
+
if (staleAgainstHead)
|
|
1466
|
+
reasons.push('stale-against-head');
|
|
1467
|
+
if (result.status === 'blocked')
|
|
1468
|
+
reasons.push('blocked');
|
|
1469
|
+
if (result.status === 'failed')
|
|
1470
|
+
reasons.push('failed');
|
|
1471
|
+
if (result.ownershipViolations.length)
|
|
1472
|
+
reasons.push('ownership-violations');
|
|
1473
|
+
if (result.verification.some((entry) => entry.required !== false && entry.status !== 0))
|
|
1474
|
+
reasons.push('failed-verification');
|
|
1475
|
+
if (disposition === 'needs-port')
|
|
1476
|
+
reasons.push('needs-human-port');
|
|
1477
|
+
if (disposition === 'rejected')
|
|
1478
|
+
reasons.push('rejected');
|
|
1479
|
+
return uniqueStrings(reasons);
|
|
1480
|
+
}
|
|
1481
|
+
function inferMergeRisk(result, disposition) {
|
|
1482
|
+
if (disposition === 'discovery-only')
|
|
1483
|
+
return 'low';
|
|
1484
|
+
if (disposition === 'rejected' || disposition === 'blocked' || disposition === 'stale-against-head')
|
|
1485
|
+
return 'high';
|
|
1486
|
+
if (result.changedPaths.length <= 2 && result.ownershipViolations.length === 0)
|
|
1487
|
+
return 'low';
|
|
1488
|
+
if (result.changedPaths.length <= 8)
|
|
1489
|
+
return 'medium';
|
|
1490
|
+
return 'high';
|
|
1491
|
+
}
|
|
972
1492
|
function normalizeVerificationResult(input) {
|
|
973
1493
|
return {
|
|
974
1494
|
name: input.name ?? ((input.command ?? []).join(' ') || 'verification'),
|
|
@@ -1044,6 +1564,20 @@ function isSwarmManifest(value) {
|
|
|
1044
1564
|
function isSwarmTask(value) {
|
|
1045
1565
|
return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_TASK_KIND;
|
|
1046
1566
|
}
|
|
1567
|
+
function isSwarmEvent(value) {
|
|
1568
|
+
return !!value && typeof value === 'object' && value.kind === FRONTIER_SWARM_EVENT_KIND;
|
|
1569
|
+
}
|
|
1570
|
+
function readLaneId(value) {
|
|
1571
|
+
return typeof value === 'string' ? value : value.id;
|
|
1572
|
+
}
|
|
1573
|
+
function joinPathParts(...parts) {
|
|
1574
|
+
const first = parts[0] ? String(parts[0]) : '';
|
|
1575
|
+
const prefix = first.startsWith('/') ? '/' : '';
|
|
1576
|
+
return prefix + parts
|
|
1577
|
+
.map((part, index) => String(part).replace(index === 0 ? /\/+$/g : /^\/+|\/+$/g, ''))
|
|
1578
|
+
.filter(Boolean)
|
|
1579
|
+
.join('/');
|
|
1580
|
+
}
|
|
1047
1581
|
function normalizeId(value, label) {
|
|
1048
1582
|
const id = String(value || '').trim();
|
|
1049
1583
|
if (!id)
|