@h-rig/dependency-graph-plugin 0.0.6-alpha.155 → 0.0.6-alpha.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -3,6 +3,1038 @@ var __require = import.meta.require;
3
3
 
4
4
  // packages/dependency-graph-plugin/src/plugin.ts
5
5
  import { definePlugin } from "@rig/core/config";
6
+ import { classifyWorkspaceBlockers } from "@rig/blocker-classifier-plugin";
7
+
8
+ // packages/dependency-graph-plugin/src/graph-model/dependencyGraph.ts
9
+ import { computeTaskDependencyBadges, readTaskMetadataStringList as readTaskMetadataStringList2, resolveTaskReference as resolveTaskReference2, buildTaskReferenceIndex as buildTaskReferenceIndex2 } from "@rig/contracts";
10
+
11
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphCodes.ts
12
+ var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
13
+ function extractTaskCode(title) {
14
+ const match = title.match(TASK_CODE_RE);
15
+ return match?.[1] ?? null;
16
+ }
17
+ function extractTaskGroupKey(title) {
18
+ const code = extractTaskCode(title);
19
+ if (!code)
20
+ return null;
21
+ const parts = code.split("-");
22
+ const suffix = parts.at(-1) ?? "";
23
+ if (/^\d+$/.test(suffix)) {
24
+ return parts.slice(0, -1).join("-");
25
+ }
26
+ return parts[0] ?? code;
27
+ }
28
+ function stripTaskCode(label) {
29
+ return label.replace(TASK_CODE_RE, "");
30
+ }
31
+
32
+ // packages/dependency-graph-plugin/src/graph-model/taskGraphLayout.ts
33
+ import {
34
+ buildTaskReferenceIndex,
35
+ readTaskMetadataStringList,
36
+ resolveTaskReference
37
+ } from "@rig/contracts";
38
+ import {
39
+ selectPendingApprovals,
40
+ selectRunsByTask,
41
+ selectUserInputsForRun
42
+ } from "@rig/read-model-plugin";
43
+ var CARD_WIDTH = 200;
44
+ var CARD_HEIGHT = 110;
45
+ var CELL_V_PAD = 12;
46
+ var CELL_H_PAD = 12;
47
+ var ROW_GAP = 28;
48
+ var COL_GAP = 40;
49
+ var LANE_LABEL_W = 120;
50
+ var STAGE_HDR_H = 32;
51
+ var PALETTE = [
52
+ { bg: "#3a2d12", border: "#8d6b19", edge: "#d6a11d" },
53
+ { bg: "#102642", border: "#245fbf", edge: "#66a2ff" },
54
+ { bg: "#2c173f", border: "#7b39d4", edge: "#a76df5" },
55
+ { bg: "#112d1c", border: "#2d8f4e", edge: "#62d882" },
56
+ { bg: "#3a2314", border: "#c86d1c", edge: "#e69654" },
57
+ { bg: "#31152b", border: "#bf3d88", edge: "#f07ebb" },
58
+ { bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
59
+ { bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
60
+ ];
61
+ function isObjectRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function readIssueType(task) {
65
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
66
+ if (typeof metadata?.issueType === "string")
67
+ return metadata.issueType;
68
+ const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
69
+ return typeof raw?.issueType === "string" ? raw.issueType : null;
70
+ }
71
+ function isGraphTask(task) {
72
+ return readIssueType(task) !== "epic";
73
+ }
74
+ function findEpicAncestor(task, resolve, tasksById) {
75
+ const visited = new Set;
76
+ let current = task;
77
+ let epic = null;
78
+ while (!visited.has(current.id)) {
79
+ visited.add(current.id);
80
+ const parentRef = readTaskMetadataStringList(current, "parentChildDeps")[0];
81
+ if (!parentRef)
82
+ break;
83
+ const parentId = resolve(parentRef);
84
+ if (!parentId)
85
+ break;
86
+ const parent = tasksById.get(parentId);
87
+ if (!parent)
88
+ break;
89
+ if (readIssueType(parent) === "epic") {
90
+ epic = parent;
91
+ }
92
+ current = parent;
93
+ }
94
+ return epic;
95
+ }
96
+ function getRowKey(task, resolve, tasksById) {
97
+ const epic = findEpicAncestor(task, resolve, tasksById);
98
+ if (epic) {
99
+ return `group:${epic.id}`;
100
+ }
101
+ const codeGroup = extractTaskGroupKey(task.title);
102
+ if (codeGroup) {
103
+ return `code:${codeGroup}`;
104
+ }
105
+ return `type:${readIssueType(task) ?? "task"}`;
106
+ }
107
+ function getRowLabel(task, rowKey, resolve, tasksById) {
108
+ if (rowKey.startsWith("group:")) {
109
+ const groupId = rowKey.slice("group:".length);
110
+ return tasksById.get(groupId)?.title ?? groupId;
111
+ }
112
+ if (rowKey.startsWith("code:")) {
113
+ return rowKey.slice("code:".length);
114
+ }
115
+ const code = extractTaskCode(task.title);
116
+ if (code)
117
+ return code;
118
+ const issueType = readIssueType(task);
119
+ if (issueType === "task")
120
+ return "Tasks";
121
+ if (issueType)
122
+ return `${issueType[0]?.toUpperCase() ?? ""}${issueType.slice(1)}`;
123
+ const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
124
+ if (parentRef) {
125
+ const parentId = resolve(parentRef);
126
+ if (parentId) {
127
+ return tasksById.get(parentId)?.title ?? "Tasks";
128
+ }
129
+ }
130
+ return "Tasks";
131
+ }
132
+ function computeDepths(ids, edges) {
133
+ const blockers = new Map;
134
+ for (const edge of edges) {
135
+ if (!ids.has(edge.source) || !ids.has(edge.target))
136
+ continue;
137
+ const current = blockers.get(edge.target) ?? [];
138
+ current.push(edge.source);
139
+ blockers.set(edge.target, current);
140
+ }
141
+ const memo = new Map;
142
+ const visit = (id, stack) => {
143
+ const cached = memo.get(id);
144
+ if (cached !== undefined)
145
+ return cached;
146
+ if (stack.has(id))
147
+ return 0;
148
+ stack.add(id);
149
+ const deps = blockers.get(id);
150
+ if (!deps || deps.length === 0) {
151
+ memo.set(id, 0);
152
+ return 0;
153
+ }
154
+ let maxDepth = 0;
155
+ for (const dep of deps) {
156
+ maxDepth = Math.max(maxDepth, visit(dep, stack) + 1);
157
+ }
158
+ stack.delete(id);
159
+ memo.set(id, maxDepth);
160
+ return maxDepth;
161
+ };
162
+ for (const id of ids) {
163
+ visit(id, new Set);
164
+ }
165
+ return memo;
166
+ }
167
+ function buildTaskGraphLayout(snapshot, tasks, options) {
168
+ const showParentChild = options?.showParentChild ?? false;
169
+ const graphTasks = tasks.filter(isGraphTask);
170
+ if (graphTasks.length === 0) {
171
+ return {
172
+ lanes: [],
173
+ stages: [],
174
+ nodes: [],
175
+ edges: [],
176
+ totalWidth: 0,
177
+ totalHeight: 0,
178
+ taskCount: 0
179
+ };
180
+ }
181
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
182
+ const resolve = (ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId);
183
+ const rows = new Map;
184
+ const rowLabelByKey = new Map;
185
+ for (const task of graphTasks) {
186
+ const rowKey = getRowKey(task, resolve, tasksById);
187
+ const current = rows.get(rowKey) ?? [];
188
+ current.push(task);
189
+ rows.set(rowKey, current);
190
+ if (!rowLabelByKey.has(rowKey)) {
191
+ rowLabelByKey.set(rowKey, getRowLabel(task, rowKey, resolve, tasksById));
192
+ }
193
+ }
194
+ const orderedRows = [...rows.entries()].sort((left, right) => {
195
+ if (left[1].length !== right[1].length)
196
+ return right[1].length - left[1].length;
197
+ return (rowLabelByKey.get(left[0]) ?? left[0]).localeCompare(rowLabelByKey.get(right[0]) ?? right[0]);
198
+ });
199
+ const rowIndexByKey = new Map(orderedRows.map(([key], index) => [key, index]));
200
+ const rowIndexByTaskId = new Map;
201
+ for (const task of graphTasks) {
202
+ rowIndexByTaskId.set(task.id, rowIndexByKey.get(getRowKey(task, resolve, tasksById)) ?? 0);
203
+ }
204
+ const blockingEdgesRaw = [];
205
+ const depsIn = new Map;
206
+ const depsOut = new Map;
207
+ const edges = [];
208
+ for (const task of graphTasks) {
209
+ for (const ref of readTaskMetadataStringList(task, "dependencies")) {
210
+ const sourceId = resolve(ref);
211
+ if (!sourceId)
212
+ continue;
213
+ blockingEdgesRaw.push({ source: sourceId, target: task.id });
214
+ depsOut.set(sourceId, (depsOut.get(sourceId) ?? 0) + 1);
215
+ depsIn.set(task.id, (depsIn.get(task.id) ?? 0) + 1);
216
+ const color = PALETTE[(rowIndexByTaskId.get(sourceId) ?? 0) % PALETTE.length].edge;
217
+ edges.push({
218
+ id: `blocking:${sourceId}:${task.id}`,
219
+ sourceId,
220
+ targetId: task.id,
221
+ color,
222
+ kind: "blocking"
223
+ });
224
+ }
225
+ if (!showParentChild)
226
+ continue;
227
+ for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
228
+ const sourceId = resolve(ref);
229
+ if (!sourceId)
230
+ continue;
231
+ edges.push({
232
+ id: `parent:${sourceId}:${task.id}`,
233
+ sourceId,
234
+ targetId: task.id,
235
+ color: "#5b6472",
236
+ kind: "parent-child"
237
+ });
238
+ }
239
+ }
240
+ const graphTaskIds = new Set(graphTasks.map((task) => task.id));
241
+ const depths = computeDepths(graphTaskIds, blockingEdgesRaw);
242
+ const maxStage = Math.max(0, ...depths.values());
243
+ const rowMaxStack = new Array(orderedRows.length).fill(0);
244
+ const cells = new Map;
245
+ for (const task of graphTasks) {
246
+ const rowIndex = rowIndexByTaskId.get(task.id) ?? 0;
247
+ const stage = depths.get(task.id) ?? 0;
248
+ const key = `${rowIndex}:${stage}`;
249
+ const current = cells.get(key) ?? [];
250
+ current.push(task);
251
+ cells.set(key, current);
252
+ }
253
+ for (const [cellKey, cellTasks] of cells) {
254
+ const [rowIndexText] = cellKey.split(":");
255
+ const rowIndex = Number.parseInt(rowIndexText ?? "0", 10) || 0;
256
+ cellTasks.sort((left, right) => {
257
+ const leftFanout = depsOut.get(left.id) ?? 0;
258
+ const rightFanout = depsOut.get(right.id) ?? 0;
259
+ if (leftFanout !== rightFanout)
260
+ return rightFanout - leftFanout;
261
+ return left.title.localeCompare(right.title);
262
+ });
263
+ rowMaxStack[rowIndex] = Math.max(rowMaxStack[rowIndex] ?? 0, cellTasks.length);
264
+ }
265
+ const rowHeights = rowMaxStack.map((count) => Math.max(count, 1) * (CARD_HEIGHT + CELL_V_PAD) - CELL_V_PAD + CELL_V_PAD * 2);
266
+ const colWidths = new Array(maxStage + 1).fill(CARD_WIDTH + CELL_H_PAD * 2);
267
+ const colX = [];
268
+ let currentX = LANE_LABEL_W;
269
+ for (let index = 0;index <= maxStage; index += 1) {
270
+ colX.push(currentX);
271
+ currentX += (colWidths[index] ?? 0) + COL_GAP;
272
+ }
273
+ const totalWidth = currentX - COL_GAP;
274
+ const rowY = [];
275
+ let currentY = STAGE_HDR_H;
276
+ for (let rowIndex = 0;rowIndex < orderedRows.length; rowIndex += 1) {
277
+ rowY.push(currentY);
278
+ currentY += (rowHeights[rowIndex] ?? 0) + ROW_GAP;
279
+ }
280
+ const totalHeight = currentY - ROW_GAP;
281
+ const lanes = orderedRows.map(([rowKey, rowTasks], rowIndex) => ({
282
+ key: rowKey,
283
+ label: rowLabelByKey.get(rowKey) ?? rowKey,
284
+ rowIndex,
285
+ x: LANE_LABEL_W - 6,
286
+ y: (rowY[rowIndex] ?? 0) - 6,
287
+ width: totalWidth - LANE_LABEL_W + 12,
288
+ height: (rowHeights[rowIndex] ?? 0) + 12,
289
+ color: PALETTE[rowIndex % PALETTE.length].border,
290
+ taskCount: rowTasks.length
291
+ }));
292
+ const stages = Array.from({ length: maxStage + 1 }, (_, index) => ({
293
+ index,
294
+ label: index === 0 ? "Roots" : `Stage ${index}`,
295
+ x: (colX[index] ?? 0) + CELL_H_PAD,
296
+ width: CARD_WIDTH
297
+ }));
298
+ const pendingApprovalRunIds = new Set(selectPendingApprovals(snapshot).map((approval) => approval.runId));
299
+ const nodes = [];
300
+ for (const [rowIndex, [rowKey]] of orderedRows.entries()) {
301
+ for (let stage = 0;stage <= maxStage; stage += 1) {
302
+ const cellTasks = cells.get(`${rowIndex}:${stage}`) ?? [];
303
+ const baseX = (colX[stage] ?? 0) + CELL_H_PAD;
304
+ const baseY = (rowY[rowIndex] ?? 0) + CELL_V_PAD;
305
+ const palette = PALETTE[rowIndex % PALETTE.length];
306
+ for (const [stackIndex, task] of cellTasks.entries()) {
307
+ const runs = selectRunsByTask(snapshot, task.id);
308
+ const runIds = new Set(runs.map((run) => run.id));
309
+ const hasApprovals = runs.some((run) => pendingApprovalRunIds.has(run.id));
310
+ const hasPendingUserInput = runs.some((run) => selectUserInputsForRun(snapshot, run.id).some((request) => request.status === "pending"));
311
+ const hasRejectedReview = (snapshot?.reviews ?? []).some((review) => runIds.has(review.runId) && review.status === "rejected");
312
+ const hasFailedValidations = (snapshot?.validations ?? []).some((validation) => runIds.has(validation.runId) && validation.status === "failed");
313
+ const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
314
+ nodes.push({
315
+ id: task.id,
316
+ taskId: task.id,
317
+ task,
318
+ rowKey,
319
+ rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
320
+ rowIndex,
321
+ stage,
322
+ x: baseX,
323
+ y: baseY + stackIndex * (CARD_HEIGHT + CELL_V_PAD),
324
+ width: CARD_WIDTH,
325
+ height: CARD_HEIGHT,
326
+ color: palette.border,
327
+ taskCode: extractTaskCode(task.title),
328
+ strippedTitle: stripTaskCode(task.title),
329
+ depsIn: depsIn.get(task.id) ?? 0,
330
+ depsOut: depsOut.get(task.id) ?? 0,
331
+ runCount: runs.length,
332
+ hasApprovals,
333
+ hasPendingUserInput,
334
+ hasRejectedReview,
335
+ hasFailedValidations,
336
+ artifactCount
337
+ });
338
+ }
339
+ }
340
+ }
341
+ return {
342
+ lanes,
343
+ stages,
344
+ nodes,
345
+ edges,
346
+ totalWidth,
347
+ totalHeight,
348
+ taskCount: graphTasks.length
349
+ };
350
+ }
351
+
352
+ // packages/dependency-graph-plugin/src/graph-model/dependencyGraph.ts
353
+ import { readTaskAssigneeLogins } from "@rig/read-model-plugin";
354
+ function uniqueSorted(values) {
355
+ return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
356
+ }
357
+ function deriveGraphId(tasks) {
358
+ const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
359
+ let hash = 2166136261;
360
+ for (let index = 0;index < basis.length; index += 1) {
361
+ hash ^= basis.charCodeAt(index);
362
+ hash = Math.imul(hash, 16777619);
363
+ }
364
+ return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
365
+ }
366
+ function dedupeEdges(edges) {
367
+ const byKey = new Map;
368
+ for (const edge of edges) {
369
+ byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
370
+ }
371
+ return [...byKey.values()].toSorted((left, right) => {
372
+ const typeDelta = left.type.localeCompare(right.type);
373
+ if (typeDelta !== 0)
374
+ return typeDelta;
375
+ const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
376
+ if (fromDelta !== 0)
377
+ return fromDelta;
378
+ return left.toTaskId.localeCompare(right.toTaskId);
379
+ });
380
+ }
381
+ function detectBlockingCycles(tasks, edges) {
382
+ const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
383
+ const adjacency = new Map;
384
+ for (const taskId of taskIds)
385
+ adjacency.set(taskId, []);
386
+ for (const edge of edges) {
387
+ if (edge.type === "blocks")
388
+ adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
389
+ }
390
+ for (const targets of adjacency.values())
391
+ targets.sort((left, right) => left.localeCompare(right));
392
+ let nextIndex = 0;
393
+ const indexByNode = new Map;
394
+ const lowByNode = new Map;
395
+ const stack = [];
396
+ const onStack = new Set;
397
+ const cycles = [];
398
+ const visit = (node) => {
399
+ indexByNode.set(node, nextIndex);
400
+ lowByNode.set(node, nextIndex);
401
+ nextIndex += 1;
402
+ stack.push(node);
403
+ onStack.add(node);
404
+ for (const target of adjacency.get(node) ?? []) {
405
+ if (!indexByNode.has(target)) {
406
+ visit(target);
407
+ lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
408
+ } else if (onStack.has(target)) {
409
+ lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
410
+ }
411
+ }
412
+ if (lowByNode.get(node) !== indexByNode.get(node))
413
+ return;
414
+ const component = [];
415
+ let current;
416
+ do {
417
+ current = stack.pop();
418
+ if (current) {
419
+ onStack.delete(current);
420
+ component.push(current);
421
+ }
422
+ } while (current && current !== node);
423
+ const selfLoop = (adjacency.get(node) ?? []).includes(node);
424
+ if (component.length > 1 || selfLoop)
425
+ cycles.push(component.sort((left, right) => left.localeCompare(right)));
426
+ };
427
+ for (const taskId of taskIds) {
428
+ if (!indexByNode.has(taskId))
429
+ visit(taskId);
430
+ }
431
+ return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
432
+ }
433
+ function buildDependencyGraphModel(tasks, options = {}) {
434
+ const badges = computeTaskDependencyBadges(tasks);
435
+ const index = buildTaskReferenceIndex2(tasks);
436
+ const edges = [];
437
+ const unresolvedRefs = [];
438
+ for (const task of tasks) {
439
+ for (const ref of readTaskMetadataStringList2(task, "dependencies")) {
440
+ const dependencyId = resolveTaskReference2(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
441
+ if (dependencyId)
442
+ edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
443
+ else
444
+ unresolvedRefs.push(ref);
445
+ }
446
+ for (const ref of readTaskMetadataStringList2(task, "parentChildDeps")) {
447
+ const parentId = resolveTaskReference2(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
448
+ if (parentId)
449
+ edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
450
+ else
451
+ unresolvedRefs.push(ref);
452
+ }
453
+ }
454
+ const dedupedEdges = dedupeEdges(edges);
455
+ const nodes = tasks.map((task) => {
456
+ const summary = badges.get(task.id);
457
+ const assignees = readTaskAssigneeLogins(task);
458
+ const groupKey = extractTaskGroupKey(task.title);
459
+ return {
460
+ taskId: String(task.id),
461
+ title: task.title,
462
+ status: task.status,
463
+ priority: task.priority,
464
+ assignee: assignees[0] ?? null,
465
+ blockedBy: summary?.blockedBy ?? [],
466
+ blocks: summary?.blocks ?? [],
467
+ blockingDepth: summary?.blockingDepth ?? 0,
468
+ blockerClass: null,
469
+ actionRiskTier: null,
470
+ epicKey: groupKey,
471
+ groupKey,
472
+ externalId: task.externalId,
473
+ sourceIssueId: task.sourceIssueId ?? null,
474
+ scope: task.scope,
475
+ validationKeys: task.validationKeys
476
+ };
477
+ }).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
478
+ return {
479
+ graphId: options.graphId ?? deriveGraphId(tasks),
480
+ nodes,
481
+ edges: dedupedEdges,
482
+ layout: buildTaskGraphLayout(options.snapshot ?? null, tasks, { showParentChild: options.showParentChild ?? true }),
483
+ cycles: detectBlockingCycles(tasks, dedupedEdges),
484
+ unresolvedRefs: uniqueSorted(unresolvedRefs),
485
+ degraded: false,
486
+ generatedAt: options.generatedAt ?? new Date().toISOString()
487
+ };
488
+ }
489
+
490
+ // packages/dependency-graph-plugin/src/graph.ts
491
+ import { toTaskSummary } from "@rig/contracts";
492
+ import { classifyTasks } from "@rig/blocker-classifier-plugin";
493
+ function overlayLayoutRunState(model, runs) {
494
+ if (runs.length === 0)
495
+ return model.layout;
496
+ const runsByTask = new Map;
497
+ for (const run of runs) {
498
+ if (!run.taskId)
499
+ continue;
500
+ runsByTask.set(run.taskId, [...runsByTask.get(run.taskId) ?? [], run]);
501
+ }
502
+ return {
503
+ ...model.layout,
504
+ nodes: model.layout.nodes.map((node) => {
505
+ const taskRuns = runsByTask.get(node.taskId) ?? [];
506
+ return {
507
+ ...node,
508
+ runCount: taskRuns.length,
509
+ hasApprovals: taskRuns.some((run) => run.pendingApprovals > 0),
510
+ hasPendingUserInput: taskRuns.some((run) => run.pendingInputs > 0),
511
+ hasRejectedReview: taskRuns.some((run) => run.status === "reviewing" && Boolean(run.errorSummary)),
512
+ hasFailedValidations: taskRuns.some((run) => run.status === "failed")
513
+ };
514
+ })
515
+ };
516
+ }
517
+ function buildWorkspaceDependencyGraph(input) {
518
+ const summaries = input.tasks.map((task) => toTaskSummary(task, input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}));
519
+ const model = (input.projectDependencyGraph ?? buildDependencyGraphModel)(summaries, {
520
+ ...input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {},
521
+ ...input.graphId !== undefined ? { graphId: input.graphId } : {},
522
+ ...input.showParentChild !== undefined ? { showParentChild: input.showParentChild } : {}
523
+ });
524
+ const classifications = input.classifications ?? classifyTasks(input.tasks, input.runs ?? [], input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {}).byTaskId;
525
+ return {
526
+ ...model,
527
+ layout: overlayLayoutRunState(model, input.runs ?? []),
528
+ nodes: model.nodes.map((node) => {
529
+ const classification = classifications.get(node.taskId);
530
+ return {
531
+ ...node,
532
+ blockerClass: classification?.blockerClass ?? node.blockerClass,
533
+ actionRiskTier: classification?.actionRiskTier ?? null
534
+ };
535
+ })
536
+ };
537
+ }
538
+ async function getWorkspaceDependencyGraph(projectRoot, deps) {
539
+ const [tasks, runs, blockers] = await Promise.all([
540
+ deps.listTasks(projectRoot),
541
+ deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([]),
542
+ deps.classifyBlockers ? deps.classifyBlockers(projectRoot) : Promise.resolve(null)
543
+ ]);
544
+ return buildWorkspaceDependencyGraph({
545
+ tasks,
546
+ runs,
547
+ ...blockers?.byTaskId !== undefined ? { classifications: blockers.byTaskId } : {},
548
+ ...deps.generatedAt !== undefined ? { generatedAt: deps.generatedAt } : {},
549
+ ...deps.graphId !== undefined ? { graphId: deps.graphId } : {},
550
+ ...deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {},
551
+ ...deps.projectDependencyGraph !== undefined ? { projectDependencyGraph: deps.projectDependencyGraph } : {}
552
+ });
553
+ }
554
+ function formatDependencyGraphDot(model) {
555
+ const lines = ["digraph RigDependencies {", " rankdir=LR;"];
556
+ for (const node of model.nodes) {
557
+ const color = node.blockerClass === "task-blocked" ? "orange" : node.blockerClass && node.blockerClass !== "not-blocked" ? "red" : "gray";
558
+ lines.push(` "${escapeDot(node.taskId)}" [label="${escapeDot(node.title)}", color="${color}"];`);
559
+ }
560
+ for (const edge of model.edges) {
561
+ lines.push(` "${escapeDot(edge.fromTaskId)}" -> "${escapeDot(edge.toTaskId)}" [label="${edge.type}", color="${edgeColor(edge)}"];`);
562
+ }
563
+ lines.push("}");
564
+ return `${lines.join(`
565
+ `)}
566
+ `;
567
+ }
568
+ function edgeColor(edge) {
569
+ return edge.type === "blocks" ? "black" : edge.type === "parent-child" ? "blue" : "gray";
570
+ }
571
+ function escapeDot(value) {
572
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
573
+ }
574
+
575
+ // packages/dependency-graph-plugin/src/graph-model/rollups.ts
576
+ import {
577
+ isOperatorActiveRunStatus,
578
+ buildTaskReferenceIndex as buildTaskReferenceIndex3,
579
+ computeTaskDependencyBadges as computeTaskDependencyBadges2,
580
+ isTaskTerminalStatus,
581
+ readTaskMetadataStringList as readTaskMetadataStringList3,
582
+ resolveTaskReference as resolveTaskReference3
583
+ } from "@rig/contracts";
584
+ import { readTaskAssigneeLogins as readTaskAssigneeLogins2 } from "@rig/read-model-plugin";
585
+ var UNASSIGNED_EPIC = "(unassigned-epic)";
586
+ var UNASSIGNED_ASSIGNEE = "(unassigned)";
587
+ var HUMAN_BLOCKER_CLASSES = {
588
+ "human-decision": true,
589
+ "human-approval": true,
590
+ "external-input": true,
591
+ unknown: true
592
+ };
593
+ function isObjectRecord2(value) {
594
+ return typeof value === "object" && value !== null && !Array.isArray(value);
595
+ }
596
+ function readIssueType2(task) {
597
+ const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
598
+ const raw = isObjectRecord2(metadata?.raw) ? metadata.raw : null;
599
+ const value = raw?.issueType ?? metadata?.issueType;
600
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
601
+ }
602
+ function isEpicTask(task) {
603
+ return readIssueType2(task) === "epic";
604
+ }
605
+ function isInFlightTaskStatus(status) {
606
+ switch (status) {
607
+ case "queued":
608
+ case "running":
609
+ case "in_progress":
610
+ case "under_review":
611
+ return true;
612
+ default:
613
+ return false;
614
+ }
615
+ }
616
+ function epicKeyForTask(task, tasksById, resolve) {
617
+ const parentRef = readTaskMetadataStringList3(task, "parentChildDeps")[0];
618
+ const parentId = parentRef ? resolve(parentRef) : null;
619
+ const parent = parentId ? tasksById.get(parentId) : null;
620
+ if (parent)
621
+ return extractTaskGroupKey(parent.title) ?? parent.title ?? parent.id;
622
+ return extractTaskGroupKey(task.title) ?? UNASSIGNED_EPIC;
623
+ }
624
+ function rollupByEpic(tasks, classifications = new Map) {
625
+ const index = buildTaskReferenceIndex3(tasks);
626
+ const badges = computeTaskDependencyBadges2(tasks);
627
+ const resolve = (ref) => resolveTaskReference3(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
628
+ const buckets = new Map;
629
+ for (const task of tasks) {
630
+ if (isEpicTask(task))
631
+ continue;
632
+ const epicKey = epicKeyForTask(task, index.tasksById, resolve);
633
+ const current = buckets.get(epicKey) ?? {
634
+ total: 0,
635
+ completed: 0,
636
+ blockedCount: 0,
637
+ humanBlockedCount: 0,
638
+ inFlightCount: 0,
639
+ byStatus: {}
640
+ };
641
+ current.total += 1;
642
+ if (isTaskTerminalStatus(task.status))
643
+ current.completed += 1;
644
+ if (badges.get(task.id)?.blocked === true)
645
+ current.blockedCount += 1;
646
+ if (isInFlightTaskStatus(task.status))
647
+ current.inFlightCount += 1;
648
+ const classification = classifications.get(task.id);
649
+ if (classification && HUMAN_BLOCKER_CLASSES[classification])
650
+ current.humanBlockedCount += 1;
651
+ current.byStatus[task.status] = (current.byStatus[task.status] ?? 0) + 1;
652
+ buckets.set(epicKey, current);
653
+ }
654
+ return [...buckets.entries()].map(([epicKey, bucket]) => ({
655
+ epicKey,
656
+ total: bucket.total,
657
+ percentComplete: bucket.total === 0 ? 0 : Math.round(100 * bucket.completed / bucket.total),
658
+ blockedCount: bucket.blockedCount,
659
+ humanBlockedCount: bucket.humanBlockedCount,
660
+ inFlightCount: bucket.inFlightCount,
661
+ byStatus: Object.fromEntries(Object.entries(bucket.byStatus).sort(([left], [right]) => left.localeCompare(right)))
662
+ })).toSorted((left, right) => left.epicKey.localeCompare(right.epicKey));
663
+ }
664
+ function assigneesForTask(task) {
665
+ const assignees = readTaskAssigneeLogins2(task);
666
+ return assignees.length > 0 ? assignees : [UNASSIGNED_ASSIGNEE];
667
+ }
668
+ function rollupByAssignee(tasks, runs) {
669
+ const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
670
+ const badges = computeTaskDependencyBadges2(tasks);
671
+ const buckets = new Map;
672
+ const ensureBucket = (assignee) => {
673
+ const existing = buckets.get(assignee);
674
+ if (existing)
675
+ return existing;
676
+ const created = { openTaskCount: 0, inFlightRunIds: new Set, prsAwaitingReview: 0, blockers: new Set };
677
+ buckets.set(assignee, created);
678
+ return created;
679
+ };
680
+ for (const task of tasks) {
681
+ if (isEpicTask(task))
682
+ continue;
683
+ for (const assignee of assigneesForTask(task)) {
684
+ const bucket = ensureBucket(assignee);
685
+ if (!isTaskTerminalStatus(task.status))
686
+ bucket.openTaskCount += 1;
687
+ if (badges.get(task.id)?.blocked === true)
688
+ bucket.blockers.add(task.id);
689
+ if (task.status === "under_review")
690
+ bucket.prsAwaitingReview += 1;
691
+ }
692
+ }
693
+ for (const run of runs) {
694
+ const taskId = run.record.taskId;
695
+ const task = taskId ? tasksById.get(taskId) : null;
696
+ if (!task)
697
+ continue;
698
+ if (isEpicTask(task))
699
+ continue;
700
+ for (const assignee of assigneesForTask(task)) {
701
+ const bucket = ensureBucket(assignee);
702
+ if (run.status && isOperatorActiveRunStatus(run.status))
703
+ bucket.inFlightRunIds.add(run.record.runId ?? `${String(task.id)}:${run.lastSeq}`);
704
+ if (run.status === "reviewing" && run.record.prUrl)
705
+ bucket.prsAwaitingReview += 1;
706
+ }
707
+ }
708
+ return [...buckets.entries()].map(([assignee, bucket]) => ({
709
+ assignee,
710
+ openTaskCount: bucket.openTaskCount,
711
+ inFlightRunCount: bucket.inFlightRunIds.size,
712
+ prsAwaitingReview: bucket.prsAwaitingReview,
713
+ blockers: [...bucket.blockers].toSorted((left, right) => String(left).localeCompare(String(right)))
714
+ })).toSorted((left, right) => left.assignee.localeCompare(right.assignee));
715
+ }
716
+
717
+ // packages/dependency-graph-plugin/src/rollups.ts
718
+ import { toTaskSummary as toTaskSummary2 } from "@rig/contracts";
719
+ import { classifyTasks as classifyTasks2 } from "@rig/blocker-classifier-plugin";
720
+ function blockerClassMap(classifications) {
721
+ return new Map([...classifications.entries()].map(([taskId, classification]) => [taskId, classification.blockerClass]));
722
+ }
723
+ function selectWorkspaceRollups(input) {
724
+ const tasks = input.tasks.map((task) => toTaskSummary2(task, input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}));
725
+ const runs = input.runs ?? [];
726
+ const classifications = input.classifications ?? classifyTasks2(input.tasks, runs, input.generatedAt !== undefined ? { generatedAt: input.generatedAt } : {}).byTaskId;
727
+ return {
728
+ epics: [...rollupByEpic(tasks, blockerClassMap(classifications))],
729
+ assignees: [...rollupByAssignee(tasks, runs.map((run) => run.projection))],
730
+ generatedAt: input.generatedAt ?? new Date().toISOString()
731
+ };
732
+ }
733
+ async function getWorkspaceRollups(projectRoot, deps) {
734
+ const [tasks, runs, blockers] = await Promise.all([
735
+ deps.listTasks(projectRoot),
736
+ deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([]),
737
+ deps.classifyBlockers ? deps.classifyBlockers(projectRoot) : Promise.resolve(null)
738
+ ]);
739
+ return selectWorkspaceRollups({
740
+ tasks,
741
+ runs,
742
+ ...blockers?.byTaskId !== undefined ? { classifications: blockers.byTaskId } : {},
743
+ ...deps.generatedAt !== undefined ? { generatedAt: deps.generatedAt } : {},
744
+ ...deps.workspaceId !== undefined ? { workspaceId: deps.workspaceId } : {}
745
+ });
746
+ }
747
+ var getWorkspaceStatus = getWorkspaceRollups;
748
+ // packages/dependency-graph-plugin/src/taskScore.ts
749
+ var ROLE_WEIGHT = {
750
+ architect: 25,
751
+ extractor: 10
752
+ };
753
+ var CRITICALITY_WEIGHT = {
754
+ core: 20,
755
+ high: 10,
756
+ normal: 0
757
+ };
758
+ function finiteNumber(value, fallback) {
759
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
760
+ }
761
+ function scoreTask(input) {
762
+ const priority = finiteNumber(input.priority, 3);
763
+ const unblockCount = Math.max(0, finiteNumber(input.unblockCount, 0));
764
+ const roleWeight = input.role ? ROLE_WEIGHT[input.role] ?? 0 : 0;
765
+ const criticalityWeight = input.criticality ? CRITICALITY_WEIGHT[input.criticality] ?? 0 : 0;
766
+ const validationWeight = (input.validation ?? []).some((entry) => entry.includes("test-contract") || entry.includes("test-boundary")) ? 8 : 0;
767
+ const queueWeight = finiteNumber(input.queueWeight, 0);
768
+ return unblockCount * 100 + (10 - priority) + roleWeight + criticalityWeight + validationWeight + queueWeight;
769
+ }
770
+ function rankTasks(tasks, scoreInput, idOf) {
771
+ return tasks.map((task) => {
772
+ const input = scoreInput(task);
773
+ return {
774
+ task,
775
+ score: scoreTask(input),
776
+ priority: finiteNumber(input.priority, 3),
777
+ unblockCount: Math.max(0, finiteNumber(input.unblockCount, 0))
778
+ };
779
+ }).toSorted((left, right) => {
780
+ const scoreDelta = right.score - left.score;
781
+ if (scoreDelta !== 0)
782
+ return scoreDelta;
783
+ const unblockDelta = right.unblockCount - left.unblockCount;
784
+ if (unblockDelta !== 0)
785
+ return unblockDelta;
786
+ const priorityDelta = left.priority - right.priority;
787
+ if (priorityDelta !== 0)
788
+ return priorityDelta;
789
+ return idOf(left.task).localeCompare(idOf(right.task));
790
+ });
791
+ }
792
+ // packages/dependency-graph-plugin/src/taskRanking.ts
793
+ import {
794
+ computeTaskDependencyBadges as computeTaskDependencyBadges3,
795
+ isTaskTerminalStatus as isTaskTerminalStatus2,
796
+ readTaskScope
797
+ } from "@rig/contracts";
798
+ function isObjectRecord3(value) {
799
+ return typeof value === "object" && value !== null && !Array.isArray(value);
800
+ }
801
+ function readStringList(value) {
802
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
803
+ }
804
+ function priorityValue(task) {
805
+ return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
806
+ }
807
+ function readTaskRole(task) {
808
+ if (typeof task.role === "string" && task.role.trim())
809
+ return task.role.trim();
810
+ const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
811
+ return typeof metadata?.role === "string" && metadata.role.trim() ? metadata.role.trim() : null;
812
+ }
813
+ function readTaskCriticality(task) {
814
+ const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
815
+ return typeof metadata?.criticality === "string" && metadata.criticality.trim() ? metadata.criticality.trim() : null;
816
+ }
817
+ function readTaskValidationKeys(task) {
818
+ const taskRecord = task;
819
+ const topLevel = readStringList(taskRecord.validationKeys);
820
+ if (topLevel.length > 0)
821
+ return topLevel;
822
+ const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
823
+ return readStringList(metadata?.validation);
824
+ }
825
+ function readTaskQueueWeight(task) {
826
+ const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
827
+ const queueWeight = metadata?.queueWeight ?? metadata?.queue_weight;
828
+ return typeof queueWeight === "number" && Number.isFinite(queueWeight) ? queueWeight : 0;
829
+ }
830
+ function scoreInputForTask(task, unblockCount) {
831
+ return {
832
+ priority: task.priority,
833
+ unblockCount,
834
+ role: readTaskRole(task),
835
+ criticality: readTaskCriticality(task),
836
+ validation: readTaskValidationKeys(task),
837
+ queueWeight: readTaskQueueWeight(task)
838
+ };
839
+ }
840
+ function selectNextReadyTaskByPriority(tasks, options = {}) {
841
+ const excluded = new Set(options.excludeTaskIds ?? []);
842
+ const badges = computeTaskDependencyBadges3(tasks);
843
+ const candidates = tasks.filter((task) => !excluded.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true).toSorted((left, right) => {
844
+ const priorityDelta = priorityValue(left) - priorityValue(right);
845
+ if (priorityDelta !== 0)
846
+ return priorityDelta;
847
+ const createdDelta = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
848
+ if (createdDelta !== 0)
849
+ return createdDelta;
850
+ return left.id.localeCompare(right.id);
851
+ });
852
+ return candidates[0] ?? null;
853
+ }
854
+ function rankReadyTasks(tasks, options = {}) {
855
+ const excluded = new Set(options.excludeTaskIds ?? []);
856
+ const activeTaskIds = new Set(options.activeTaskIds ?? []);
857
+ const badges = computeTaskDependencyBadges3(tasks);
858
+ const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
859
+ const openBlockCount = (task) => (badges.get(task.id)?.blocks ?? []).filter((blockedTaskId) => {
860
+ const blockedTask = tasksById.get(blockedTaskId);
861
+ return blockedTask ? !isTaskTerminalStatus2(blockedTask.status) : false;
862
+ }).length;
863
+ const readyTasks = tasks.filter((task) => !excluded.has(task.id)).filter((task) => !activeTaskIds.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true);
864
+ const maxUnblockCount = Math.max(0, ...readyTasks.map(openBlockCount));
865
+ const selectedByMode = readyTasks.filter((task) => {
866
+ const unblockCount = openBlockCount(task);
867
+ if (options.selection === "blocking-only")
868
+ return unblockCount > 0;
869
+ if (options.selection === "max-unblock")
870
+ return maxUnblockCount > 0 && unblockCount === maxUnblockCount;
871
+ return true;
872
+ });
873
+ return rankTasks(selectedByMode, (task) => scoreInputForTask(task, openBlockCount(task)), (task) => task.id).map((entry) => ({
874
+ task: entry.task,
875
+ score: entry.score,
876
+ priority: entry.priority,
877
+ unblockCount: entry.unblockCount,
878
+ scope: readTaskScope(entry.task)
879
+ }));
880
+ }
881
+ function selectRankedReadyTasks(tasks, options = {}) {
882
+ const ranked = rankReadyTasks(tasks, options);
883
+ if (options.requireDisjointScopes !== true && !options.disjointWithScopes) {
884
+ return ranked.slice(0, options.limit).map((entry) => entry.task);
885
+ }
886
+ const occupiedScopes = new Set(options.disjointWithScopes ?? []);
887
+ const selected = [];
888
+ for (const entry of ranked) {
889
+ if (entry.scope.some((scope) => occupiedScopes.has(scope)))
890
+ continue;
891
+ selected.push(entry.task);
892
+ for (const scope of entry.scope)
893
+ occupiedScopes.add(scope);
894
+ if (options.limit !== undefined && selected.length >= options.limit)
895
+ break;
896
+ }
897
+ return selected;
898
+ }
899
+ // packages/dependency-graph-plugin/src/taskSelection.ts
900
+ import {
901
+ getTaskForCommand,
902
+ listTasksForCommand,
903
+ normalizeTaskId
904
+ } from "@rig/core/task-io";
905
+ var CLOSED_STATUSES = {
906
+ closed: true,
907
+ completed: true,
908
+ complete: true,
909
+ done: true,
910
+ cancelled: true,
911
+ canceled: true,
912
+ merged: true,
913
+ resolved: true
914
+ };
915
+ var NOT_READY_STATUSES = {
916
+ ...CLOSED_STATUSES,
917
+ blocked: true,
918
+ in_progress: true,
919
+ "in-progress": true,
920
+ running: true,
921
+ active: true
922
+ };
923
+ function isRecord(value) {
924
+ return typeof value === "object" && value !== null && !Array.isArray(value);
925
+ }
926
+ function readTaskStatus(task) {
927
+ const value = task.status;
928
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
929
+ }
930
+ function isReadyTask(task) {
931
+ const status = readTaskStatus(task);
932
+ return status.length === 0 || NOT_READY_STATUSES[status] !== true;
933
+ }
934
+ function asDependencyProjection(task) {
935
+ const record = task;
936
+ return {
937
+ ...record,
938
+ status: typeof record.status === "string" && record.status.trim() ? record.status : "open",
939
+ priority: record.priority ?? "P2",
940
+ metadata: isRecord(record.metadata) ? record.metadata : {}
941
+ };
942
+ }
943
+ function currentLogin(env = process.env) {
944
+ for (const key of ["RIG_GITHUB_LOGIN", "GITHUB_ACTOR", "GH_USER", "USER", "USERNAME"]) {
945
+ const value = env[key]?.trim();
946
+ if (value)
947
+ return value;
948
+ }
949
+ return;
950
+ }
951
+ function resolveAssignee(value, env = process.env) {
952
+ const trimmed = value?.trim();
953
+ if (!trimmed)
954
+ return;
955
+ const lowered = trimmed.toLowerCase();
956
+ if (lowered === "me" || lowered === "@me")
957
+ return currentLogin(env) ?? "@me";
958
+ return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
959
+ }
960
+ var normalizeAssigneeFilter = resolveAssignee;
961
+ function taskMatchesState(task, state) {
962
+ if (!state)
963
+ return true;
964
+ const closed = CLOSED_STATUSES[readTaskStatus(task)] === true;
965
+ return state === "closed" ? closed : !closed;
966
+ }
967
+ function taskMatchesSearch(task, search) {
968
+ const needle = search?.trim().toLowerCase();
969
+ if (!needle)
970
+ return true;
971
+ const title = typeof task.title === "string" ? task.title : "";
972
+ const body = typeof task.body === "string" ? task.body : "";
973
+ const description = typeof task.description === "string" ? task.description : "";
974
+ return `${task.id}
975
+ ${title}
976
+ ${body}
977
+ ${description}`.toLowerCase().includes(needle);
978
+ }
979
+ function collectAssigneeLogins(value) {
980
+ if (typeof value === "string")
981
+ return [value];
982
+ if (Array.isArray(value))
983
+ return value.flatMap((entry) => collectAssigneeLogins(entry));
984
+ if (isRecord(value))
985
+ return [value.login, value.username, value.name, value.id].flatMap((entry) => collectAssigneeLogins(entry));
986
+ return [];
987
+ }
988
+ function taskAssignees(task) {
989
+ const raw = isRecord(task.raw) ? task.raw : null;
990
+ return [
991
+ task.assignee,
992
+ task.assignees,
993
+ task.assignedTo,
994
+ task.owner,
995
+ raw?.assignee,
996
+ raw?.assignees,
997
+ raw?.assignedTo,
998
+ raw?.owner
999
+ ].flatMap((value) => collectAssigneeLogins(value));
1000
+ }
1001
+ function taskMatchesAssignee(task, assignee) {
1002
+ if (!assignee)
1003
+ return true;
1004
+ const needle = assignee.startsWith("@") ? assignee.slice(1) : assignee;
1005
+ return taskAssignees(task).some((login) => {
1006
+ const normalized = login.startsWith("@") ? login.slice(1) : login;
1007
+ return normalized.localeCompare(needle, undefined, { sensitivity: "accent" }) === 0;
1008
+ });
1009
+ }
1010
+ function applyFilters(tasks, filters = {}) {
1011
+ const filtered = tasks.filter((task) => taskMatchesAssignee(task, resolveAssignee(filters.assignee))).filter((task) => taskMatchesState(task, filters.state)).filter((task) => taskMatchesSearch(task, filters.search));
1012
+ return filters.limit === undefined ? filtered : filtered.slice(0, filters.limit);
1013
+ }
1014
+ function selectNextReadyTask(tasks, filters = {}) {
1015
+ const matching = applyFilters(tasks, { ...filters, limit: undefined }).filter(isReadyTask);
1016
+ const selected = selectNextReadyTaskByPriority(matching.map(asDependencyProjection), {
1017
+ filter: (task) => isReadyTask(task)
1018
+ });
1019
+ return selected ?? matching[0] ?? null;
1020
+ }
1021
+ var selectNextTask = selectNextReadyTask;
1022
+ async function resolveStartTask(input) {
1023
+ if (input.next) {
1024
+ const task2 = selectNextReadyTask(await listTasksForCommand(input.projectRoot, { listTasks: input.listTasks }), input.filters ?? {});
1025
+ const taskId2 = normalizeTaskId(task2?.id);
1026
+ if (!task2 || !taskId2)
1027
+ throw new Error("No ready task found.");
1028
+ return { taskId: taskId2, task: task2 };
1029
+ }
1030
+ const taskId = normalizeTaskId(input.taskId ?? undefined);
1031
+ if (!taskId)
1032
+ throw new Error("Missing task id.");
1033
+ const task = await getTaskForCommand(input.projectRoot, taskId, { getTask: input.getTask }).catch(() => null);
1034
+ return { taskId, task };
1035
+ }
1036
+
1037
+ // packages/dependency-graph-plugin/src/plugin.ts
6
1038
  var DEPENDENCY_GRAPH_PLUGIN_NAME = "@rig/dependency-graph-plugin";
7
1039
  var DEPENDENCY_GRAPH_CLI_ID = "dependency-graph.graph";
8
1040
  var WORKSPACE_STATUS_CLI_ID = "dependency-graph.status";
@@ -10,14 +1042,16 @@ var WORKSPACE_SUMMARY_CLI_ID = "dependency-graph.summary";
10
1042
  var DEPENDENCY_GRAPH_PANEL_ID = "dependency-graph";
11
1043
  var EPICS_PANEL_ID = "epics";
12
1044
  var PEOPLE_PANEL_ID = "people";
13
- function isRecord(value) {
1045
+ function isRecord2(value) {
14
1046
  return typeof value === "object" && value !== null && !Array.isArray(value);
15
1047
  }
16
1048
  function panelProjectRoot(context) {
17
- return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
1049
+ return isRecord2(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
18
1050
  }
19
- async function loadDependencyGraphClient() {
20
- return await import("@rig/client");
1051
+ async function loadDependencyGraphClientIo() {
1052
+ const { listTasks } = await import("@rig/core/task-io");
1053
+ const { listRuns } = await import("@rig/run-worker/runs");
1054
+ return { listRuns, listTasks };
21
1055
  }
22
1056
  function toDependencyGraphPanelPayload(model) {
23
1057
  const layoutByTaskId = new Map(model.layout.nodes.map((node) => [node.taskId, node]));
@@ -46,9 +1080,9 @@ function toDependencyGraphPanelPayload(model) {
46
1080
  degraded: model.degraded ? "dependency graph projection degraded" : null
47
1081
  };
48
1082
  }
49
- function toWorkspaceRollupsPanelPayload(rollups) {
1083
+ function toWorkspaceRollupsPanelPayload(rollups2) {
50
1084
  return {
51
- epics: rollups.epics.map((epic) => ({
1085
+ epics: rollups2.epics.map((epic) => ({
52
1086
  epicKey: epic.epicKey,
53
1087
  title: epic.epicKey,
54
1088
  total: epic.total,
@@ -57,7 +1091,7 @@ function toWorkspaceRollupsPanelPayload(rollups) {
57
1091
  inFlightCount: epic.inFlightCount,
58
1092
  humanBlockedCount: epic.humanBlockedCount
59
1093
  })),
60
- people: rollups.assignees.map((assignee) => ({
1094
+ people: rollups2.assignees.map((assignee) => ({
61
1095
  assignee: assignee.assignee,
62
1096
  openCount: assignee.openTaskCount,
63
1097
  inFlightCount: assignee.inFlightRunCount,
@@ -71,7 +1105,7 @@ async function produceDependencyGraphPanel(context) {
71
1105
  const projectRoot = panelProjectRoot(context);
72
1106
  if (!projectRoot)
73
1107
  return;
74
- const { getWorkspaceDependencyGraph, classifyWorkspaceBlockers, listRuns, listTasks } = await loadDependencyGraphClient();
1108
+ const { listRuns, listTasks } = await loadDependencyGraphClientIo();
75
1109
  const model = await getWorkspaceDependencyGraph(projectRoot, {
76
1110
  listTasks,
77
1111
  listRuns,
@@ -83,13 +1117,13 @@ async function produceWorkspaceRollupsPanel(context) {
83
1117
  const projectRoot = panelProjectRoot(context);
84
1118
  if (!projectRoot)
85
1119
  return;
86
- const { getWorkspaceRollups, classifyWorkspaceBlockers, listRuns, listTasks } = await loadDependencyGraphClient();
87
- const rollups = await getWorkspaceRollups(projectRoot, {
1120
+ const { listRuns, listTasks } = await loadDependencyGraphClientIo();
1121
+ const rollups2 = await getWorkspaceRollups(projectRoot, {
88
1122
  listTasks,
89
1123
  listRuns,
90
1124
  classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
91
1125
  });
92
- return toWorkspaceRollupsPanelPayload(rollups);
1126
+ return toWorkspaceRollupsPanelPayload(rollups2);
93
1127
  }
94
1128
  function printJson(value) {
95
1129
  console.log(JSON.stringify(value, null, 2));
@@ -137,7 +1171,7 @@ async function executeGraph(context, args) {
137
1171
  requireNoExtraArgs(json.rest, "rig graph [--json|--dot]");
138
1172
  if (dot.value && json.value)
139
1173
  throw new Error("Pass only one of --json or --dot.");
140
- const { getWorkspaceDependencyGraph, classifyWorkspaceBlockers, formatDependencyGraphDot, listRuns, listTasks } = await loadDependencyGraphClient();
1174
+ const { listRuns, listTasks } = await loadDependencyGraphClientIo();
141
1175
  const model = await getWorkspaceDependencyGraph(context.projectRoot, {
142
1176
  listTasks,
143
1177
  listRuns,
@@ -158,24 +1192,24 @@ async function executeStatus(context, args) {
158
1192
  const json = takeFlag(args, "--json");
159
1193
  const epic = takeOption(json.rest, "--epic");
160
1194
  requireNoExtraArgs(epic.rest, "rig status [--epic <key>] [--json]");
161
- const { getWorkspaceRollups, classifyWorkspaceBlockers, listRuns, listTasks } = await loadDependencyGraphClient();
162
- const [rollups, blockers, runs] = await Promise.all([
1195
+ const { listRuns, listTasks } = await loadDependencyGraphClientIo();
1196
+ const [rollups2, blockers, runs] = await Promise.all([
163
1197
  getWorkspaceRollups(context.projectRoot, { listTasks, listRuns }),
164
1198
  classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns }),
165
1199
  listRuns(context.projectRoot)
166
1200
  ]);
167
- const filteredEpics = epic.value ? rollups.epics.filter((entry) => entry.epicKey === epic.value) : rollups.epics;
1201
+ const filteredEpics = epic.value ? rollups2.epics.filter((entry) => entry.epicKey === epic.value) : rollups2.epics;
168
1202
  const details = {
169
1203
  epic: epic.value ?? null,
170
1204
  epicRollups: filteredEpics,
171
1205
  epics: filteredEpics.length,
172
- assignees: rollups.assignees.length,
1206
+ assignees: rollups2.assignees.length,
173
1207
  humanBlockers: blockers.human.length,
174
1208
  machineBlockers: blockers.machine.length,
175
1209
  runs: runs.length,
176
1210
  activeRuns: runs.filter((run) => run.live && !run.stale).length,
177
1211
  needsAttention: runs.filter((run) => run.status === "needs-attention" || run.pendingApprovals > 0 || run.pendingInputs > 0).length,
178
- generatedAt: rollups.generatedAt
1212
+ generatedAt: rollups2.generatedAt
179
1213
  };
180
1214
  if (context.outputMode === "text") {
181
1215
  if (json.value)
@@ -190,9 +1224,9 @@ async function executeSummary(context, args) {
190
1224
  const json = takeFlag(args, "--json");
191
1225
  const epicOpt = takeOption(json.rest, "--epic");
192
1226
  requireNoExtraArgs(epicOpt.rest, "rig summary [--epic <key>] [--json]");
193
- const { getWorkspaceRollups, listRuns, listTasks } = await loadDependencyGraphClient();
194
- const rollups = await getWorkspaceRollups(context.projectRoot, { listTasks, listRuns });
195
- const details = epicOpt.value ? { ...rollups, epics: rollups.epics.filter((epic) => epic.epicKey === epicOpt.value) } : rollups;
1227
+ const { listRuns, listTasks } = await loadDependencyGraphClientIo();
1228
+ const rollups2 = await getWorkspaceRollups(context.projectRoot, { listTasks, listRuns });
1229
+ const details = epicOpt.value ? { ...rollups2, epics: rollups2.epics.filter((epic) => epic.epicKey === epicOpt.value) } : rollups2;
196
1230
  if (context.outputMode === "text") {
197
1231
  if (json.value)
198
1232
  printJson(details);
@@ -244,34 +1278,49 @@ var dependencyGraphPlugin = definePlugin({
244
1278
  { id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
245
1279
  ],
246
1280
  panels: [
247
- { id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph" },
248
- { id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups" },
1281
+ { id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
1282
+ { id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel },
249
1283
  { id: PEOPLE_PANEL_ID, slot: "capability", title: "People", capabilityId: "workspace.rollups" }
250
1284
  ],
251
- cliCommands: dependencyGraphCliCommands.map(({ run: _run, ...metadata }) => metadata)
252
- }
253
- }, {
254
- featureCapabilities: [
255
- { id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
256
- { id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
257
- { id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
258
- ],
259
- panels: [
260
- { id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
261
- { id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel }
262
- ],
263
- cliCommands: dependencyGraphCliCommands
1285
+ cliCommands: dependencyGraphCliCommands
1286
+ }
264
1287
  });
265
1288
  function createDependencyGraphPlugin() {
266
1289
  return dependencyGraphPlugin;
267
1290
  }
268
1291
  export {
1292
+ taskMatchesState,
1293
+ taskMatchesSearch,
1294
+ taskMatchesAssignee,
1295
+ taskAssignees,
1296
+ selectWorkspaceRollups,
1297
+ selectRankedReadyTasks,
1298
+ selectNextTask,
1299
+ selectNextReadyTaskByPriority,
1300
+ selectNextReadyTask,
1301
+ scoreTask,
1302
+ resolveStartTask,
1303
+ resolveAssignee,
1304
+ readTaskStatus,
1305
+ rankTasks,
1306
+ rankReadyTasks,
1307
+ normalizeAssigneeFilter,
1308
+ isReadyTask,
1309
+ getWorkspaceStatus,
1310
+ getWorkspaceRollups,
1311
+ getWorkspaceDependencyGraph,
1312
+ formatDependencyGraphDot,
269
1313
  executeSummary,
270
1314
  executeStatus,
271
1315
  executeGraph,
272
1316
  dependencyGraphPlugin,
273
1317
  dependencyGraphCliCommands,
1318
+ currentLogin,
274
1319
  createDependencyGraphPlugin,
1320
+ collectAssigneeLogins,
1321
+ buildWorkspaceDependencyGraph,
1322
+ asDependencyProjection,
1323
+ applyFilters,
275
1324
  WORKSPACE_SUMMARY_CLI_ID,
276
1325
  WORKSPACE_STATUS_CLI_ID,
277
1326
  PEOPLE_PANEL_ID,