@h-rig/blocker-classifier-plugin 0.0.6-alpha.157 → 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.
- package/dist/src/{blockers.d.ts → analysis/blockers.d.ts} +1 -2
- package/dist/src/analysis/blockers.js +355 -0
- package/dist/src/analysis/taskGraphPrimitives.d.ts +58 -0
- package/dist/src/analysis/taskGraphPrimitives.js +528 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +277 -21
- package/dist/src/plugin.d.ts +1 -7
- package/dist/src/plugin.js +274 -21
- package/package.json +3 -4
- package/dist/src/blockers.js +0 -101
package/dist/src/plugin.js
CHANGED
|
@@ -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/
|
|
8
|
+
// packages/blocker-classifier-plugin/src/analysis/taskGraphPrimitives.ts
|
|
8
9
|
import {
|
|
9
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
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:
|
|
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.
|
|
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.
|
|
29
|
-
"@rig/
|
|
30
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.157"
|
|
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
|
}
|
package/dist/src/blockers.js
DELETED
|
@@ -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
|
-
};
|