@h-rig/blocker-classifier-plugin 0.0.6-alpha.156 → 0.0.6-alpha.158

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.
@@ -1,17 +1,272 @@
1
1
  // @bun
2
- var __require = import.meta.require;
3
-
4
2
  // packages/blocker-classifier-plugin/src/plugin.ts
5
3
  import { definePlugin } from "@rig/core/config";
4
+ import { defineCapability } from "@rig/core/capability";
5
+ import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
6
+ import { RUN_READ_MODEL, TASK_IO_SERVICE_CAPABILITY } from "@rig/contracts";
6
7
 
7
- // packages/blocker-classifier-plugin/src/blockers.ts
8
+ // packages/blocker-classifier-plugin/src/analysis/taskGraphPrimitives.ts
8
9
  import {
9
- computeTaskDependencyBadges,
10
- isTaskTerminalStatus,
11
- latestRunByTaskId,
12
- readTaskMetadataStringList,
13
- toTaskDependencyProjection
10
+ OPERATOR_INACTIVE_RUN_STATUSES
14
11
  } from "@rig/contracts";
12
+ function isObjectRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function readStringList(value) {
16
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
17
+ }
18
+ function unique(values) {
19
+ return Array.from(new Set(values));
20
+ }
21
+ function readTaskMetadataStringList(task, key) {
22
+ const taskRecord = task;
23
+ const topLevel = readStringList(taskRecord[key]);
24
+ if (topLevel.length > 0)
25
+ return topLevel;
26
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
27
+ const metadataList = readStringList(metadata?.[key]);
28
+ if (metadataList.length > 0)
29
+ return metadataList;
30
+ if (key === "dependencies") {
31
+ return readStringList(metadata?.deps);
32
+ }
33
+ return [];
34
+ }
35
+ function readTaskBlockingDependencyRefs(task) {
36
+ return readTaskMetadataStringList(task, "dependencies");
37
+ }
38
+ function readTaskSourceIssueId(task) {
39
+ if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0) {
40
+ return task.sourceIssueId;
41
+ }
42
+ const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
43
+ if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0) {
44
+ return metadata.sourceIssueId;
45
+ }
46
+ const rigMetadata = isObjectRecord(metadata?._rig) ? metadata._rig : null;
47
+ return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
48
+ }
49
+ function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
50
+ if (tasksById.has(ref))
51
+ return ref;
52
+ return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
53
+ }
54
+ function buildTaskReferenceIndex(tasks) {
55
+ return {
56
+ tasksById: new Map(tasks.map((task) => [task.id, task])),
57
+ taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
58
+ taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
59
+ const sourceIssueId = readTaskSourceIssueId(task);
60
+ return sourceIssueId ? [[sourceIssueId, task.id]] : [];
61
+ }))
62
+ };
63
+ }
64
+ function computeTaskBlockingDepths(tasks) {
65
+ const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
66
+ const memo = new Map;
67
+ const visit = (taskId, stack) => {
68
+ const cached = memo.get(taskId);
69
+ if (cached !== undefined)
70
+ return cached;
71
+ if (stack.has(taskId))
72
+ return 0;
73
+ const task = tasksById.get(taskId);
74
+ if (!task)
75
+ return 0;
76
+ stack.add(taskId);
77
+ const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
78
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
79
+ stack.delete(taskId);
80
+ memo.set(taskId, depth);
81
+ return depth;
82
+ };
83
+ for (const task of tasks) {
84
+ visit(task.id, new Set);
85
+ }
86
+ return memo;
87
+ }
88
+ function isTaskTerminalStatus(status) {
89
+ switch (status) {
90
+ case "closed":
91
+ case "completed":
92
+ case "done":
93
+ case "cancelled":
94
+ case "canceled":
95
+ return true;
96
+ default:
97
+ return false;
98
+ }
99
+ }
100
+ function isTaskBlockedStatus(status) {
101
+ return status === "blocked";
102
+ }
103
+ function isTaskRunnableStatus(status) {
104
+ if (status === null || status === undefined || status === "")
105
+ return true;
106
+ if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
107
+ return false;
108
+ switch (status) {
109
+ case "ready":
110
+ case "open":
111
+ case "failed":
112
+ return true;
113
+ default:
114
+ return false;
115
+ }
116
+ }
117
+ function computeTaskDependencyBadges(tasks) {
118
+ const index = buildTaskReferenceIndex(tasks);
119
+ const blockingDepths = computeTaskBlockingDepths(tasks);
120
+ const dependencyIdsByTask = new Map;
121
+ const unresolvedRefsByTask = new Map;
122
+ const blocksByTask = new Map;
123
+ for (const task of tasks) {
124
+ const dependencyIds = [];
125
+ const unresolvedRefs = [];
126
+ for (const ref of readTaskBlockingDependencyRefs(task)) {
127
+ const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
128
+ if (dependencyId && dependencyId !== task.id) {
129
+ dependencyIds.push(dependencyId);
130
+ const blocks = blocksByTask.get(dependencyId);
131
+ if (blocks) {
132
+ blocks.push(task.id);
133
+ } else {
134
+ blocksByTask.set(dependencyId, [task.id]);
135
+ }
136
+ } else {
137
+ unresolvedRefs.push(ref);
138
+ }
139
+ }
140
+ dependencyIdsByTask.set(task.id, unique(dependencyIds));
141
+ unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
142
+ }
143
+ const summaries = new Map;
144
+ for (const task of tasks) {
145
+ const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
146
+ const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
147
+ const blockedBy = dependencyIds.filter((dependencyId) => {
148
+ const dependency = index.tasksById.get(dependencyId);
149
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
150
+ });
151
+ const blocks = unique(blocksByTask.get(task.id) ?? []);
152
+ const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
153
+ const ready = isTaskRunnableStatus(task.status) && !blocked;
154
+ const badges = [];
155
+ if (blocked) {
156
+ badges.push({
157
+ kind: "blocked",
158
+ label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
159
+ description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
160
+ ...blockedBy.length > 0 ? { count: blockedBy.length } : {},
161
+ taskIds: blockedBy
162
+ });
163
+ } else if (ready) {
164
+ badges.push({
165
+ kind: "ready",
166
+ label: "ready",
167
+ description: "No open dependencies block this task."
168
+ });
169
+ }
170
+ if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
171
+ badges.push({
172
+ kind: "dependency",
173
+ label: `deps ${dependencyIds.length}/${blocks.length}`,
174
+ description: [
175
+ dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
176
+ blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
177
+ unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
178
+ ].filter((part) => part !== null).join(" "),
179
+ count: dependencyIds.length + blocks.length,
180
+ taskIds: unique([...dependencyIds, ...blocks])
181
+ });
182
+ }
183
+ summaries.set(task.id, {
184
+ taskId: task.id,
185
+ blockingDepth: blockingDepths.get(task.id) ?? 0,
186
+ dependencyIds,
187
+ unresolvedDependencyRefs,
188
+ blockedBy,
189
+ blocks,
190
+ blocked,
191
+ ready,
192
+ dependencyCount: dependencyIds.length,
193
+ dependentCount: blocks.length,
194
+ badges
195
+ });
196
+ }
197
+ return summaries;
198
+ }
199
+ var TASK_STATUSES = new Set([
200
+ "draft",
201
+ "open",
202
+ "ready",
203
+ "queued",
204
+ "running",
205
+ "in_progress",
206
+ "under_review",
207
+ "blocked",
208
+ "unknown",
209
+ "completed",
210
+ "failed",
211
+ "cancelled",
212
+ "closed"
213
+ ]);
214
+ function stringValue(value) {
215
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
216
+ }
217
+ function stringArray(value) {
218
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()) : [];
219
+ }
220
+ function numberOrNull(value) {
221
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
222
+ }
223
+ function metadataOf(task) {
224
+ return isObjectRecord(task.metadata) ? task.metadata : {};
225
+ }
226
+ function normalizeTaskStatus(status) {
227
+ const token = typeof status === "string" ? status.trim().toLowerCase() : "";
228
+ if (token === "done")
229
+ return "completed";
230
+ if (token === "canceled")
231
+ return "cancelled";
232
+ return TASK_STATUSES.has(token) ? token : "unknown";
233
+ }
234
+ function toTaskDependencyProjection(task) {
235
+ const metadata = metadataOf(task);
236
+ return {
237
+ id: String(task.id),
238
+ title: stringValue(task.title),
239
+ status: normalizeTaskStatus(task.status),
240
+ priority: numberOrNull(task.priority),
241
+ metadata,
242
+ externalId: stringValue(task.externalId),
243
+ sourceIssueId: stringValue(task.sourceIssueId),
244
+ dependencies: stringArray(task.dependencies),
245
+ parentChildDeps: stringArray(task.parentChildDeps),
246
+ createdAt: stringValue(task.createdAt) ?? "",
247
+ updatedAt: stringValue(task.updatedAt) ?? "",
248
+ role: stringValue(task.role),
249
+ scope: stringArray(task.scope),
250
+ validationKeys: stringArray(task.validationKeys),
251
+ labels: stringArray(task.labels),
252
+ assignees: task.assignees ?? null,
253
+ assignedTo: task.assignedTo ?? null
254
+ };
255
+ }
256
+ function latestRunByTaskId(runs) {
257
+ const byTask = new Map;
258
+ const stamp = (run) => Date.parse(run.updatedAt ?? run.startedAt ?? "") || 0;
259
+ for (const run of runs) {
260
+ if (!run.taskId)
261
+ continue;
262
+ const current = byTask.get(run.taskId);
263
+ if (!current || stamp(run) >= stamp(current))
264
+ byTask.set(run.taskId, run);
265
+ }
266
+ return byTask;
267
+ }
268
+
269
+ // packages/blocker-classifier-plugin/src/analysis/blockers.ts
15
270
  var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
16
271
  function labelsFor(task) {
17
272
  return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
@@ -103,16 +358,19 @@ var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
103
358
  var BLOCKERS_CLI_ID = "blocker-classifier.blockers";
104
359
  var DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
105
360
  var HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
361
+ var RunReadModelCap = defineCapability(RUN_READ_MODEL);
106
362
  function isRecord(value) {
107
363
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
364
  }
109
365
  function panelProjectRoot(context) {
110
366
  return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
111
367
  }
112
- async function loadBlockerClientIo() {
113
- const { listTasks } = await import("@rig/core/task-io");
114
- const { listRuns } = await import("@rig/run-worker/runs");
115
- return { listRuns, listTasks };
368
+ async function loadBlockerClientIo(projectRoot) {
369
+ const [taskIo, runReadModel] = await Promise.all([
370
+ requireCapabilityForRoot(projectRoot, defineCapability(TASK_IO_SERVICE_CAPABILITY), "No task-sources plugin provides task IO for this project root."),
371
+ requireCapabilityForRoot(projectRoot, RunReadModelCap, "No run-worker plugin provides run read-model for this project root.")
372
+ ]);
373
+ return { listRuns: (root) => runReadModel.listRuns({ projectRoot: root }), listTasks: taskIo.listTasks };
116
374
  }
117
375
  function taskEpicKey(task) {
118
376
  const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
@@ -125,15 +383,15 @@ async function produceHumanBlockersPanel(context) {
125
383
  const projectRoot = panelProjectRoot(context);
126
384
  if (!projectRoot)
127
385
  return;
128
- const { listRuns, listTasks } = await loadBlockerClientIo();
129
- const [tasks, blockers2] = await Promise.all([
386
+ const { listRuns, listTasks } = await loadBlockerClientIo(projectRoot);
387
+ const [tasks, blockers] = await Promise.all([
130
388
  listTasks(projectRoot),
131
389
  classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
132
390
  ]);
133
391
  const tasksById = new Map(tasks.map((task) => [task.id, task]));
134
392
  return {
135
393
  humanOnly: true,
136
- items: blockers2.classifications.map((classification) => {
394
+ items: blockers.classifications.map((classification) => {
137
395
  const task = tasksById.get(classification.taskId);
138
396
  return {
139
397
  taskId: classification.taskId,
@@ -170,7 +428,7 @@ async function executeBlockers(context, args) {
170
428
  const json = takeFlag(args, "--json");
171
429
  const humanOnly = takeFlag(json.rest, "--human-only");
172
430
  requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
173
- const { listRuns, listTasks } = await loadBlockerClientIo();
431
+ const { listRuns, listTasks } = await loadBlockerClientIo(context.projectRoot);
174
432
  const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
175
433
  const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
176
434
  if (context.outputMode === "text") {
@@ -223,14 +481,9 @@ function createBlockerClassifierPlugin() {
223
481
  }
224
482
  var plugin_default = blockerClassifierPlugin;
225
483
  export {
226
- tierOf,
227
- isHumanBlockerClass,
228
484
  executeBlockers,
229
485
  plugin_default as default,
230
486
  createBlockerClassifierPlugin,
231
- classifyWorkspaceBlockers,
232
- classifyTasks,
233
- classifyBlocker,
234
487
  blockerClassifierPlugin,
235
488
  blockerClassifierCliCommands,
236
489
  HUMAN_BLOCKERS_PANEL_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/blocker-classifier-plugin",
3
- "version": "0.0.6-alpha.156",
3
+ "version": "0.0.6-alpha.158",
4
4
  "type": "module",
5
5
  "description": "First-party blocker classifier capability plugin for Rig.",
6
6
  "license": "UNLICENSED",
@@ -25,8 +25,7 @@
25
25
  "module": "./dist/src/index.js",
26
26
  "types": "./dist/src/index.d.ts",
27
27
  "dependencies": {
28
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.156",
29
- "@rig/run-worker": "npm:@h-rig/run-worker@0.0.6-alpha.156",
30
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.156"
28
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.158",
29
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.158"
31
30
  }
32
31
  }
@@ -1,101 +0,0 @@
1
- // @bun
2
- // packages/blocker-classifier-plugin/src/blockers.ts
3
- import {
4
- computeTaskDependencyBadges,
5
- isTaskTerminalStatus,
6
- latestRunByTaskId,
7
- readTaskMetadataStringList,
8
- toTaskDependencyProjection
9
- } from "@rig/contracts";
10
- var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
11
- function labelsFor(task) {
12
- return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
13
- }
14
- function configuredTier(task, labels) {
15
- const metadata = task.metadata && typeof task.metadata === "object" && !Array.isArray(task.metadata) ? task.metadata : {};
16
- const tier = metadata.riskTier ?? metadata.actionRiskTier;
17
- if (tier === "t1-read" || tier === "t2-reversible" || tier === "t3-external" || tier === "t4-irreversible")
18
- return tier;
19
- if (labels.some((label) => /prod|deploy|migration|billing|delete|irreversible/.test(label)))
20
- return "t4-irreversible";
21
- if (labels.some((label) => /customer|vendor|external|secret|credential|approval|decision/.test(label)))
22
- return "t3-external";
23
- const scopes = task.scope ?? [];
24
- if (scopes.some((scope) => /docs|readme|test|spec/.test(scope.toLowerCase())))
25
- return "t1-read";
26
- return "t2-reversible";
27
- }
28
- function tierOf(task, labels = []) {
29
- const projection = "metadata" in task && "id" in task && typeof task.id === "string" ? toTaskDependencyProjection(task) : task;
30
- return configuredTier(projection, labels.length > 0 ? labels : labelsFor(projection)) ?? "t2-reversible";
31
- }
32
- function baseClassification(task, blockerClass, source, rationale, labels) {
33
- return {
34
- taskId: task.id,
35
- blockerClass,
36
- actionRiskTier: tierOf(task, labels),
37
- rationale,
38
- source,
39
- autoApplied: source === "llm"
40
- };
41
- }
42
- function normalizeRunStatus(status) {
43
- if (status === "waiting-approval" || status === "waiting-user-input" || status === "needs-attention" || status === "completed" || status === "failed" || status === "stopped" || status === "running")
44
- return status;
45
- return null;
46
- }
47
- function isHumanBlockerClass(blockerClass) {
48
- return HUMAN_BLOCKERS.has(blockerClass);
49
- }
50
- function classifyBlocker(input) {
51
- const labels = input.labels ?? labelsFor(input.task);
52
- const runStatus = normalizeRunStatus(input.run?.status);
53
- if (runStatus === "waiting-approval")
54
- return baseClassification(input.task, "human-approval", "status", "run awaiting approval", labels);
55
- if (runStatus === "waiting-user-input")
56
- return baseClassification(input.task, "external-input", "status", "run awaiting user input", labels);
57
- if (input.task.status !== "blocked")
58
- return baseClassification(input.task, "not-blocked", "elimination", "task is not blocked", labels);
59
- const incomplete = (input.badges.get(input.task.id)?.blockedBy ?? []).filter((dependencyId) => {
60
- const dependency = input.tasksById.get(dependencyId);
61
- return dependency ? !isTaskTerminalStatus(dependency.status) : false;
62
- });
63
- if (incomplete.length > 0)
64
- return baseClassification(input.task, "task-blocked", "elimination", `blocked by ${incomplete.length} incomplete task(s)`, labels);
65
- if (labels.includes("needs-decision"))
66
- return baseClassification(input.task, "human-decision", "label", "needs-decision label", labels);
67
- if (labels.some((label) => /^waiting-on-/.test(label)))
68
- return baseClassification(input.task, "external-input", "label", "waiting-on-* label", labels);
69
- if (input.config?.llm)
70
- return baseClassification(input.task, "human-decision", "llm", "LLM residue classifier marked this as human-gated", labels);
71
- return baseClassification(input.task, "human-decision", "elimination", "blocked, all task dependencies terminal; non-task gate remains", labels);
72
- }
73
- function classifyTasks(tasks, runs = [], options = {}) {
74
- const projected = tasks.map(toTaskDependencyProjection);
75
- const badges = computeTaskDependencyBadges(projected);
76
- const tasksById = new Map(projected.map((task) => [task.id, task]));
77
- const runByTask = latestRunByTaskId(runs);
78
- const classifier = options.classifier ?? classifyBlocker;
79
- const classifications = projected.map((task) => classifier({ task, badges, tasksById, run: runByTask.get(task.id) ?? null, labels: labelsFor(task) })).filter((classification) => options.humanOnly !== true || isHumanBlockerClass(classification.blockerClass));
80
- return {
81
- classifications,
82
- byTaskId: new Map(classifications.map((classification) => [classification.taskId, classification])),
83
- human: classifications.filter((classification) => isHumanBlockerClass(classification.blockerClass)),
84
- machine: classifications.filter((classification) => !isHumanBlockerClass(classification.blockerClass)),
85
- generatedAt: options.generatedAt ?? new Date().toISOString()
86
- };
87
- }
88
- async function classifyWorkspaceBlockers(projectRoot, deps) {
89
- const [tasks, runs] = await Promise.all([
90
- deps.listTasks(projectRoot),
91
- deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
92
- ]);
93
- return classifyTasks(tasks, runs, deps);
94
- }
95
- export {
96
- tierOf,
97
- isHumanBlockerClass,
98
- classifyWorkspaceBlockers,
99
- classifyTasks,
100
- classifyBlocker
101
- };