@h-rig/blocker-classifier-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.
@@ -0,0 +1,36 @@
1
+ import type { TaskLike } from "@rig/core/task-io";
2
+ import type { ClientTaskProjection, TaskDependencyBadgeSummary } from "@rig/contracts";
3
+ import type { BlockerClass, BlockerClassification, ActionRiskTier, RunRecord } from "@rig/contracts";
4
+ export type BlockerClassifier = (input: BlockerClassifierInput) => BlockerClassification;
5
+ export interface BlockerClassifierInput {
6
+ readonly task: ClientTaskProjection;
7
+ readonly badges: ReadonlyMap<string, TaskDependencyBadgeSummary>;
8
+ readonly tasksById: ReadonlyMap<string, ClientTaskProjection>;
9
+ readonly run?: Pick<RunRecord, "status"> | null;
10
+ readonly labels?: readonly string[];
11
+ readonly config?: {
12
+ readonly llm?: boolean;
13
+ };
14
+ }
15
+ export interface WorkspaceBlockers {
16
+ readonly classifications: readonly BlockerClassification[];
17
+ readonly byTaskId: ReadonlyMap<string, BlockerClassification>;
18
+ readonly human: readonly BlockerClassification[];
19
+ readonly machine: readonly BlockerClassification[];
20
+ readonly generatedAt: string;
21
+ }
22
+ export declare function tierOf(task: ClientTaskProjection | TaskLike, labels?: readonly string[]): ActionRiskTier;
23
+ export declare function isHumanBlockerClass(blockerClass: BlockerClass): boolean;
24
+ export declare function classifyBlocker(input: BlockerClassifierInput): BlockerClassification;
25
+ export declare function classifyTasks(tasks: readonly TaskLike[], runs?: readonly Pick<RunRecord, "taskId" | "status" | "updatedAt" | "startedAt">[], options?: {
26
+ readonly classifier?: BlockerClassifier;
27
+ readonly generatedAt?: string;
28
+ readonly humanOnly?: boolean;
29
+ }): WorkspaceBlockers;
30
+ export declare function classifyWorkspaceBlockers(projectRoot: string, deps: {
31
+ readonly listTasks: (projectRoot: string) => Promise<readonly TaskLike[]>;
32
+ readonly listRuns?: (projectRoot: string) => Promise<readonly RunRecord[]>;
33
+ readonly classifier?: BlockerClassifier;
34
+ readonly humanOnly?: boolean;
35
+ readonly generatedAt?: string;
36
+ }): Promise<WorkspaceBlockers>;
@@ -0,0 +1,101 @@
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
+ };
@@ -1 +1,2 @@
1
+ export * from "./blockers";
1
2
  export * from "./plugin";
package/dist/src/index.js CHANGED
@@ -1,6 +1,99 @@
1
1
  // @bun
2
2
  var __require = import.meta.require;
3
3
 
4
+ // packages/blocker-classifier-plugin/src/blockers.ts
5
+ import {
6
+ computeTaskDependencyBadges,
7
+ isTaskTerminalStatus,
8
+ latestRunByTaskId,
9
+ readTaskMetadataStringList,
10
+ toTaskDependencyProjection
11
+ } from "@rig/contracts";
12
+ var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
13
+ function labelsFor(task) {
14
+ return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
15
+ }
16
+ function configuredTier(task, labels) {
17
+ const metadata = task.metadata && typeof task.metadata === "object" && !Array.isArray(task.metadata) ? task.metadata : {};
18
+ const tier = metadata.riskTier ?? metadata.actionRiskTier;
19
+ if (tier === "t1-read" || tier === "t2-reversible" || tier === "t3-external" || tier === "t4-irreversible")
20
+ return tier;
21
+ if (labels.some((label) => /prod|deploy|migration|billing|delete|irreversible/.test(label)))
22
+ return "t4-irreversible";
23
+ if (labels.some((label) => /customer|vendor|external|secret|credential|approval|decision/.test(label)))
24
+ return "t3-external";
25
+ const scopes = task.scope ?? [];
26
+ if (scopes.some((scope) => /docs|readme|test|spec/.test(scope.toLowerCase())))
27
+ return "t1-read";
28
+ return "t2-reversible";
29
+ }
30
+ function tierOf(task, labels = []) {
31
+ const projection = "metadata" in task && "id" in task && typeof task.id === "string" ? toTaskDependencyProjection(task) : task;
32
+ return configuredTier(projection, labels.length > 0 ? labels : labelsFor(projection)) ?? "t2-reversible";
33
+ }
34
+ function baseClassification(task, blockerClass, source, rationale, labels) {
35
+ return {
36
+ taskId: task.id,
37
+ blockerClass,
38
+ actionRiskTier: tierOf(task, labels),
39
+ rationale,
40
+ source,
41
+ autoApplied: source === "llm"
42
+ };
43
+ }
44
+ function normalizeRunStatus(status) {
45
+ if (status === "waiting-approval" || status === "waiting-user-input" || status === "needs-attention" || status === "completed" || status === "failed" || status === "stopped" || status === "running")
46
+ return status;
47
+ return null;
48
+ }
49
+ function isHumanBlockerClass(blockerClass) {
50
+ return HUMAN_BLOCKERS.has(blockerClass);
51
+ }
52
+ function classifyBlocker(input) {
53
+ const labels = input.labels ?? labelsFor(input.task);
54
+ const runStatus = normalizeRunStatus(input.run?.status);
55
+ if (runStatus === "waiting-approval")
56
+ return baseClassification(input.task, "human-approval", "status", "run awaiting approval", labels);
57
+ if (runStatus === "waiting-user-input")
58
+ return baseClassification(input.task, "external-input", "status", "run awaiting user input", labels);
59
+ if (input.task.status !== "blocked")
60
+ return baseClassification(input.task, "not-blocked", "elimination", "task is not blocked", labels);
61
+ const incomplete = (input.badges.get(input.task.id)?.blockedBy ?? []).filter((dependencyId) => {
62
+ const dependency = input.tasksById.get(dependencyId);
63
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
64
+ });
65
+ if (incomplete.length > 0)
66
+ return baseClassification(input.task, "task-blocked", "elimination", `blocked by ${incomplete.length} incomplete task(s)`, labels);
67
+ if (labels.includes("needs-decision"))
68
+ return baseClassification(input.task, "human-decision", "label", "needs-decision label", labels);
69
+ if (labels.some((label) => /^waiting-on-/.test(label)))
70
+ return baseClassification(input.task, "external-input", "label", "waiting-on-* label", labels);
71
+ if (input.config?.llm)
72
+ return baseClassification(input.task, "human-decision", "llm", "LLM residue classifier marked this as human-gated", labels);
73
+ return baseClassification(input.task, "human-decision", "elimination", "blocked, all task dependencies terminal; non-task gate remains", labels);
74
+ }
75
+ function classifyTasks(tasks, runs = [], options = {}) {
76
+ const projected = tasks.map(toTaskDependencyProjection);
77
+ const badges = computeTaskDependencyBadges(projected);
78
+ const tasksById = new Map(projected.map((task) => [task.id, task]));
79
+ const runByTask = latestRunByTaskId(runs);
80
+ const classifier = options.classifier ?? classifyBlocker;
81
+ 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));
82
+ return {
83
+ classifications,
84
+ byTaskId: new Map(classifications.map((classification) => [classification.taskId, classification])),
85
+ human: classifications.filter((classification) => isHumanBlockerClass(classification.blockerClass)),
86
+ machine: classifications.filter((classification) => !isHumanBlockerClass(classification.blockerClass)),
87
+ generatedAt: options.generatedAt ?? new Date().toISOString()
88
+ };
89
+ }
90
+ async function classifyWorkspaceBlockers(projectRoot, deps) {
91
+ const [tasks, runs] = await Promise.all([
92
+ deps.listTasks(projectRoot),
93
+ deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
94
+ ]);
95
+ return classifyTasks(tasks, runs, deps);
96
+ }
4
97
  // packages/blocker-classifier-plugin/src/plugin.ts
5
98
  import { definePlugin } from "@rig/core/config";
6
99
  var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
@@ -13,8 +106,10 @@ function isRecord(value) {
13
106
  function panelProjectRoot(context) {
14
107
  return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
15
108
  }
16
- async function loadBlockerClient() {
17
- return await import("@rig/client");
109
+ async function loadBlockerClientIo() {
110
+ const { listTasks } = await import("@rig/core/task-io");
111
+ const { listRuns } = await import("@rig/run-worker/runs");
112
+ return { listRuns, listTasks };
18
113
  }
19
114
  function taskEpicKey(task) {
20
115
  const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
@@ -27,15 +122,15 @@ async function produceHumanBlockersPanel(context) {
27
122
  const projectRoot = panelProjectRoot(context);
28
123
  if (!projectRoot)
29
124
  return;
30
- const { classifyWorkspaceBlockers, listRuns, listTasks } = await loadBlockerClient();
31
- const [tasks, blockers] = await Promise.all([
125
+ const { listRuns, listTasks } = await loadBlockerClientIo();
126
+ const [tasks, blockers2] = await Promise.all([
32
127
  listTasks(projectRoot),
33
128
  classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
34
129
  ]);
35
130
  const tasksById = new Map(tasks.map((task) => [task.id, task]));
36
131
  return {
37
132
  humanOnly: true,
38
- items: blockers.classifications.map((classification) => {
133
+ items: blockers2.classifications.map((classification) => {
39
134
  const task = tasksById.get(classification.taskId);
40
135
  return {
41
136
  taskId: classification.taskId,
@@ -72,7 +167,7 @@ async function executeBlockers(context, args) {
72
167
  const json = takeFlag(args, "--json");
73
168
  const humanOnly = takeFlag(json.rest, "--human-only");
74
169
  requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
75
- const { classifyWorkspaceBlockers, listRuns, listTasks } = await loadBlockerClient();
170
+ const { listRuns, listTasks } = await loadBlockerClientIo();
76
171
  const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
77
172
  const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
78
173
  if (context.outputMode === "text") {
@@ -103,41 +198,34 @@ var blockerClassifierPlugin = definePlugin({
103
198
  { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
104
199
  ],
105
200
  blockerClassifiers: [
106
- { id: DEFAULT_BLOCKER_CLASSIFIER_ID, description: "Default deterministic Rig blocker classifier.", priority: 0 }
201
+ {
202
+ id: DEFAULT_BLOCKER_CLASSIFIER_ID,
203
+ description: "Default deterministic Rig blocker classifier.",
204
+ priority: 0,
205
+ async classify(input) {
206
+ if (!isBlockerClassifierInput(input))
207
+ throw new Error("blocker classifier input must include task, badges, and tasksById.");
208
+ return classifyBlocker(input);
209
+ }
210
+ }
107
211
  ],
108
212
  panels: [
109
- { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers" }
213
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
110
214
  ],
111
- cliCommands: blockerClassifierCliCommands.map(({ run: _run, ...metadata }) => metadata)
215
+ cliCommands: blockerClassifierCliCommands
112
216
  }
113
- }, {
114
- featureCapabilities: [
115
- { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
116
- ],
117
- panels: [
118
- { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
119
- ],
120
- blockerClassifiers: [
121
- {
122
- id: DEFAULT_BLOCKER_CLASSIFIER_ID,
123
- description: "Default deterministic Rig blocker classifier.",
124
- priority: 0,
125
- async classify(input) {
126
- if (!isBlockerClassifierInput(input))
127
- throw new Error("blocker classifier input must include task, badges, and tasksById.");
128
- const { classifyBlocker } = await loadBlockerClient();
129
- return classifyBlocker(input);
130
- }
131
- }
132
- ],
133
- cliCommands: blockerClassifierCliCommands
134
217
  });
135
218
  function createBlockerClassifierPlugin() {
136
219
  return blockerClassifierPlugin;
137
220
  }
138
221
  export {
222
+ tierOf,
223
+ isHumanBlockerClass,
139
224
  executeBlockers,
140
225
  createBlockerClassifierPlugin,
226
+ classifyWorkspaceBlockers,
227
+ classifyTasks,
228
+ classifyBlocker,
141
229
  blockerClassifierPlugin,
142
230
  blockerClassifierCliCommands,
143
231
  HUMAN_BLOCKERS_PANEL_ID,
@@ -1,4 +1,5 @@
1
1
  import { type RuntimeCliContext } from "@rig/core/config";
2
+ export * from "./blockers";
2
3
  export declare const BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
3
4
  export declare const BLOCKERS_CLI_ID = "blocker-classifier.blockers";
4
5
  export declare const DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
@@ -19,6 +20,6 @@ export declare const blockerClassifierCliCommands: readonly [{
19
20
  readonly projectRequired: true;
20
21
  readonly run: typeof executeBlockers;
21
22
  }];
22
- export declare const blockerClassifierPlugin: import("@rig/core").RigPluginWithRuntime;
23
- export declare function createBlockerClassifierPlugin(): import("@rig/core").RigPluginWithRuntime;
23
+ export declare const blockerClassifierPlugin: import("@rig/core/config").RigPlugin;
24
+ export declare function createBlockerClassifierPlugin(): import("@rig/core/config").RigPlugin;
24
25
  export default blockerClassifierPlugin;
@@ -3,6 +3,102 @@ var __require = import.meta.require;
3
3
 
4
4
  // packages/blocker-classifier-plugin/src/plugin.ts
5
5
  import { definePlugin } from "@rig/core/config";
6
+
7
+ // packages/blocker-classifier-plugin/src/blockers.ts
8
+ import {
9
+ computeTaskDependencyBadges,
10
+ isTaskTerminalStatus,
11
+ latestRunByTaskId,
12
+ readTaskMetadataStringList,
13
+ toTaskDependencyProjection
14
+ } from "@rig/contracts";
15
+ var HUMAN_BLOCKERS = new Set(["human-decision", "human-approval", "external-input"]);
16
+ function labelsFor(task) {
17
+ return readTaskMetadataStringList(task, "labels").map((label) => label.toLowerCase());
18
+ }
19
+ function configuredTier(task, labels) {
20
+ const metadata = task.metadata && typeof task.metadata === "object" && !Array.isArray(task.metadata) ? task.metadata : {};
21
+ const tier = metadata.riskTier ?? metadata.actionRiskTier;
22
+ if (tier === "t1-read" || tier === "t2-reversible" || tier === "t3-external" || tier === "t4-irreversible")
23
+ return tier;
24
+ if (labels.some((label) => /prod|deploy|migration|billing|delete|irreversible/.test(label)))
25
+ return "t4-irreversible";
26
+ if (labels.some((label) => /customer|vendor|external|secret|credential|approval|decision/.test(label)))
27
+ return "t3-external";
28
+ const scopes = task.scope ?? [];
29
+ if (scopes.some((scope) => /docs|readme|test|spec/.test(scope.toLowerCase())))
30
+ return "t1-read";
31
+ return "t2-reversible";
32
+ }
33
+ function tierOf(task, labels = []) {
34
+ const projection = "metadata" in task && "id" in task && typeof task.id === "string" ? toTaskDependencyProjection(task) : task;
35
+ return configuredTier(projection, labels.length > 0 ? labels : labelsFor(projection)) ?? "t2-reversible";
36
+ }
37
+ function baseClassification(task, blockerClass, source, rationale, labels) {
38
+ return {
39
+ taskId: task.id,
40
+ blockerClass,
41
+ actionRiskTier: tierOf(task, labels),
42
+ rationale,
43
+ source,
44
+ autoApplied: source === "llm"
45
+ };
46
+ }
47
+ function normalizeRunStatus(status) {
48
+ if (status === "waiting-approval" || status === "waiting-user-input" || status === "needs-attention" || status === "completed" || status === "failed" || status === "stopped" || status === "running")
49
+ return status;
50
+ return null;
51
+ }
52
+ function isHumanBlockerClass(blockerClass) {
53
+ return HUMAN_BLOCKERS.has(blockerClass);
54
+ }
55
+ function classifyBlocker(input) {
56
+ const labels = input.labels ?? labelsFor(input.task);
57
+ const runStatus = normalizeRunStatus(input.run?.status);
58
+ if (runStatus === "waiting-approval")
59
+ return baseClassification(input.task, "human-approval", "status", "run awaiting approval", labels);
60
+ if (runStatus === "waiting-user-input")
61
+ return baseClassification(input.task, "external-input", "status", "run awaiting user input", labels);
62
+ if (input.task.status !== "blocked")
63
+ return baseClassification(input.task, "not-blocked", "elimination", "task is not blocked", labels);
64
+ const incomplete = (input.badges.get(input.task.id)?.blockedBy ?? []).filter((dependencyId) => {
65
+ const dependency = input.tasksById.get(dependencyId);
66
+ return dependency ? !isTaskTerminalStatus(dependency.status) : false;
67
+ });
68
+ if (incomplete.length > 0)
69
+ return baseClassification(input.task, "task-blocked", "elimination", `blocked by ${incomplete.length} incomplete task(s)`, labels);
70
+ if (labels.includes("needs-decision"))
71
+ return baseClassification(input.task, "human-decision", "label", "needs-decision label", labels);
72
+ if (labels.some((label) => /^waiting-on-/.test(label)))
73
+ return baseClassification(input.task, "external-input", "label", "waiting-on-* label", labels);
74
+ if (input.config?.llm)
75
+ return baseClassification(input.task, "human-decision", "llm", "LLM residue classifier marked this as human-gated", labels);
76
+ return baseClassification(input.task, "human-decision", "elimination", "blocked, all task dependencies terminal; non-task gate remains", labels);
77
+ }
78
+ function classifyTasks(tasks, runs = [], options = {}) {
79
+ const projected = tasks.map(toTaskDependencyProjection);
80
+ const badges = computeTaskDependencyBadges(projected);
81
+ const tasksById = new Map(projected.map((task) => [task.id, task]));
82
+ const runByTask = latestRunByTaskId(runs);
83
+ const classifier = options.classifier ?? classifyBlocker;
84
+ 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));
85
+ return {
86
+ classifications,
87
+ byTaskId: new Map(classifications.map((classification) => [classification.taskId, classification])),
88
+ human: classifications.filter((classification) => isHumanBlockerClass(classification.blockerClass)),
89
+ machine: classifications.filter((classification) => !isHumanBlockerClass(classification.blockerClass)),
90
+ generatedAt: options.generatedAt ?? new Date().toISOString()
91
+ };
92
+ }
93
+ async function classifyWorkspaceBlockers(projectRoot, deps) {
94
+ const [tasks, runs] = await Promise.all([
95
+ deps.listTasks(projectRoot),
96
+ deps.listRuns ? deps.listRuns(projectRoot) : Promise.resolve([])
97
+ ]);
98
+ return classifyTasks(tasks, runs, deps);
99
+ }
100
+
101
+ // packages/blocker-classifier-plugin/src/plugin.ts
6
102
  var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
7
103
  var BLOCKERS_CLI_ID = "blocker-classifier.blockers";
8
104
  var DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
@@ -13,8 +109,10 @@ function isRecord(value) {
13
109
  function panelProjectRoot(context) {
14
110
  return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
15
111
  }
16
- async function loadBlockerClient() {
17
- return await import("@rig/client");
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 };
18
116
  }
19
117
  function taskEpicKey(task) {
20
118
  const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
@@ -27,15 +125,15 @@ async function produceHumanBlockersPanel(context) {
27
125
  const projectRoot = panelProjectRoot(context);
28
126
  if (!projectRoot)
29
127
  return;
30
- const { classifyWorkspaceBlockers, listRuns, listTasks } = await loadBlockerClient();
31
- const [tasks, blockers] = await Promise.all([
128
+ const { listRuns, listTasks } = await loadBlockerClientIo();
129
+ const [tasks, blockers2] = await Promise.all([
32
130
  listTasks(projectRoot),
33
131
  classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
34
132
  ]);
35
133
  const tasksById = new Map(tasks.map((task) => [task.id, task]));
36
134
  return {
37
135
  humanOnly: true,
38
- items: blockers.classifications.map((classification) => {
136
+ items: blockers2.classifications.map((classification) => {
39
137
  const task = tasksById.get(classification.taskId);
40
138
  return {
41
139
  taskId: classification.taskId,
@@ -72,7 +170,7 @@ async function executeBlockers(context, args) {
72
170
  const json = takeFlag(args, "--json");
73
171
  const humanOnly = takeFlag(json.rest, "--human-only");
74
172
  requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
75
- const { classifyWorkspaceBlockers, listRuns, listTasks } = await loadBlockerClient();
173
+ const { listRuns, listTasks } = await loadBlockerClientIo();
76
174
  const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
77
175
  const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
78
176
  if (context.outputMode === "text") {
@@ -103,43 +201,36 @@ var blockerClassifierPlugin = definePlugin({
103
201
  { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
104
202
  ],
105
203
  blockerClassifiers: [
106
- { id: DEFAULT_BLOCKER_CLASSIFIER_ID, description: "Default deterministic Rig blocker classifier.", priority: 0 }
204
+ {
205
+ id: DEFAULT_BLOCKER_CLASSIFIER_ID,
206
+ description: "Default deterministic Rig blocker classifier.",
207
+ priority: 0,
208
+ async classify(input) {
209
+ if (!isBlockerClassifierInput(input))
210
+ throw new Error("blocker classifier input must include task, badges, and tasksById.");
211
+ return classifyBlocker(input);
212
+ }
213
+ }
107
214
  ],
108
215
  panels: [
109
- { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers" }
216
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
110
217
  ],
111
- cliCommands: blockerClassifierCliCommands.map(({ run: _run, ...metadata }) => metadata)
218
+ cliCommands: blockerClassifierCliCommands
112
219
  }
113
- }, {
114
- featureCapabilities: [
115
- { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
116
- ],
117
- panels: [
118
- { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
119
- ],
120
- blockerClassifiers: [
121
- {
122
- id: DEFAULT_BLOCKER_CLASSIFIER_ID,
123
- description: "Default deterministic Rig blocker classifier.",
124
- priority: 0,
125
- async classify(input) {
126
- if (!isBlockerClassifierInput(input))
127
- throw new Error("blocker classifier input must include task, badges, and tasksById.");
128
- const { classifyBlocker } = await loadBlockerClient();
129
- return classifyBlocker(input);
130
- }
131
- }
132
- ],
133
- cliCommands: blockerClassifierCliCommands
134
220
  });
135
221
  function createBlockerClassifierPlugin() {
136
222
  return blockerClassifierPlugin;
137
223
  }
138
224
  var plugin_default = blockerClassifierPlugin;
139
225
  export {
226
+ tierOf,
227
+ isHumanBlockerClass,
140
228
  executeBlockers,
141
229
  plugin_default as default,
142
230
  createBlockerClassifierPlugin,
231
+ classifyWorkspaceBlockers,
232
+ classifyTasks,
233
+ classifyBlocker,
143
234
  blockerClassifierPlugin,
144
235
  blockerClassifierCliCommands,
145
236
  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.155",
3
+ "version": "0.0.6-alpha.157",
4
4
  "type": "module",
5
5
  "description": "First-party blocker classifier capability plugin for Rig.",
6
6
  "license": "UNLICENSED",
@@ -25,8 +25,8 @@
25
25
  "module": "./dist/src/index.js",
26
26
  "types": "./dist/src/index.d.ts",
27
27
  "dependencies": {
28
- "@rig/client": "npm:@h-rig/client@0.0.6-alpha.155",
29
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.155",
30
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.155"
28
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.157",
29
+ "@rig/run-worker": "npm:@h-rig/run-worker@0.0.6-alpha.157",
30
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.157"
31
31
  }
32
32
  }