@h-rig/blocker-classifier-plugin 0.0.6-alpha.135 → 0.0.6-alpha.137

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,2 +1 @@
1
- export * from "./classifier";
2
1
  export * from "./plugin";
package/dist/src/index.js CHANGED
@@ -1,163 +1,145 @@
1
1
  // @bun
2
- // packages/blocker-classifier-plugin/src/classifier.ts
3
- import { Schema } from "effect";
4
- import { BlockerClassification } from "@rig/contracts";
5
- import { isTaskTerminalStatus, readTaskMetadataStringList } from "@rig/core/task-graph";
6
- var HUMAN_DECISION_LABEL = {
7
- "human-decision": true,
8
- "needs-decision": true,
9
- decision: true,
10
- "blocked:decision": true
11
- };
12
- var HUMAN_APPROVAL_LABEL = {
13
- "human-approval": true,
14
- "needs-approval": true,
15
- approval: true,
16
- "blocked:approval": true,
17
- "review-blocked": true
18
- };
19
- var EXTERNAL_INPUT_LABEL = {
20
- "external-input": true,
21
- external: true,
22
- vendor: true,
23
- customer: true,
24
- "blocked:external": true
25
- };
26
- var MACHINE_BLOCKED_LABEL = {
27
- "task-blocked": true,
28
- "blocked:task": true,
29
- "dependency-blocked": true
30
- };
31
- function tierOf(blockerClass) {
32
- if (blockerClass === "human-decision")
33
- return "t2-reversible";
34
- if (blockerClass === "human-approval")
35
- return "t3-external";
36
- if (blockerClass === "external-input")
37
- return "t3-external";
38
- if (blockerClass === "unknown")
39
- return "t4-irreversible";
40
- return null;
41
- }
42
- function classification(task, blockerClass, rationale, source, autoApplied) {
43
- return { taskId: task.id, blockerClass, actionRiskTier: tierOf(blockerClass), rationale, source, autoApplied };
2
+ // packages/blocker-classifier-plugin/src/plugin.ts
3
+ import { definePlugin } from "@rig/core";
4
+ import {
5
+ classifyBlocker,
6
+ classifyWorkspaceBlockers,
7
+ listRuns,
8
+ listTasks
9
+ } from "@rig/client";
10
+ var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
11
+ var BLOCKERS_CLI_ID = "blocker-classifier.blockers";
12
+ var DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
13
+ var HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
16
  }
45
- function normalizedLabels(task) {
46
- return readTaskMetadataStringList(task, "labels").map((label) => label.trim().toLowerCase()).filter((label) => label.length > 0);
17
+ function panelProjectRoot(context) {
18
+ return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
47
19
  }
48
- function classifyFromLabels(task) {
49
- const labels = normalizedLabels(task);
50
- if (labels.some((label) => HUMAN_DECISION_LABEL[label]))
51
- return classification(task, "human-decision", "Task carries a human-decision blocker label.", "label", true);
52
- if (labels.some((label) => HUMAN_APPROVAL_LABEL[label]))
53
- return classification(task, "human-approval", "Task carries a human-approval blocker label.", "label", true);
54
- if (labels.some((label) => EXTERNAL_INPUT_LABEL[label]))
55
- return classification(task, "external-input", "Task carries an external-input blocker label.", "label", true);
56
- if (labels.some((label) => MACHINE_BLOCKED_LABEL[label]))
57
- return classification(task, "task-blocked", "Task carries a task/dependency blocker label.", "label", true);
58
- return null;
20
+ function taskEpicKey(task) {
21
+ const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
22
+ if (direct)
23
+ return direct;
24
+ const metadata = isRecord(task?.metadata) ? task.metadata : {};
25
+ return typeof metadata.epicKey === "string" ? metadata.epicKey : typeof metadata.epic === "string" ? metadata.epic : null;
59
26
  }
60
- function residue(task) {
61
- return `${task.title}
62
- ${task.description}`.toLowerCase();
27
+ async function produceHumanBlockersPanel(context) {
28
+ const projectRoot = panelProjectRoot(context);
29
+ if (!projectRoot)
30
+ return;
31
+ const [tasks, blockers] = await Promise.all([
32
+ listTasks(projectRoot),
33
+ classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
34
+ ]);
35
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
36
+ return {
37
+ humanOnly: true,
38
+ items: blockers.classifications.map((classification) => {
39
+ const task = tasksById.get(classification.taskId);
40
+ return {
41
+ taskId: classification.taskId,
42
+ title: task?.title ?? classification.taskId,
43
+ blockerClass: classification.blockerClass,
44
+ rationale: classification.rationale,
45
+ riskTier: classification.actionRiskTier,
46
+ epicKey: taskEpicKey(task),
47
+ autoApplied: classification.autoApplied
48
+ };
49
+ })
50
+ };
63
51
  }
64
- function classifyFromResidue(task) {
65
- const text = residue(task);
66
- if (/\b(approval|approve|review required|sign[ -]?off)\b/u.test(text))
67
- return classification(task, "human-approval", "Task text asks for approval or sign-off.", "status", true);
68
- if (/\b(decide|decision|choose|clarify|question)\b/u.test(text))
69
- return classification(task, "human-decision", "Task text asks for a human decision or clarification.", "status", true);
70
- if (/\b(vendor|customer|external|third[- ]party|credential|account)\b/u.test(text))
71
- return classification(task, "external-input", "Task text depends on an external party or account input.", "status", true);
72
- return null;
52
+ function printJson(value) {
53
+ console.log(JSON.stringify(value, null, 2));
73
54
  }
74
- function dependencyClassification(task, badge, options) {
75
- if (options.activeRunStatus === "waiting-approval")
76
- return classification(task, "human-approval", "Active run is waiting for approval.", "status", true);
77
- if (options.activeRunStatus === "waiting-user-input" || options.activeRunStatus === "needs-attention") {
78
- return classification(task, "human-decision", "Active run is waiting for operator input or attention.", "status", true);
79
- }
80
- if (!badge?.blocked)
81
- return null;
82
- if (!options.tasksById)
83
- return classification(task, "task-blocked", "Task is blocked by incomplete dependency tasks.", "elimination", true);
84
- const dependencyTasks = badge.blockedBy.map((taskId) => options.tasksById?.get(taskId)).filter((value) => value !== undefined);
85
- if (dependencyTasks.some((dependency) => !isTaskTerminalStatus(dependency.status))) {
86
- return classification(task, "task-blocked", "Task is blocked by at least one incomplete dependency task.", "elimination", true);
87
- }
88
- return classification(task, "human-decision", "Task is marked blocked even though known dependencies are terminal.", "elimination", false);
55
+ function takeFlag(args, flag) {
56
+ const rest = [...args];
57
+ const index = rest.indexOf(flag);
58
+ if (index < 0)
59
+ return { value: false, rest };
60
+ rest.splice(index, 1);
61
+ return { value: true, rest };
89
62
  }
90
- async function classifyResidueWithLlm(task, badge, port) {
91
- const decoded = Schema.decodeUnknownSync(BlockerClassification)(await port.classify({ task, badge, residue: residue(task) }));
92
- return { ...decoded, taskId: task.id, actionRiskTier: decoded.actionRiskTier ?? tierOf(decoded.blockerClass), source: "llm", autoApplied: false };
63
+ function requireNoExtraArgs(args, usage) {
64
+ if (args.length > 0)
65
+ throw new Error(`Unexpected argument: ${args[0]}
66
+ Usage: ${usage}`);
93
67
  }
94
- async function classifyBlocker(task, badges, options = {}) {
95
- const badge = badges.get(task.id) ?? null;
96
- const dependencyBlocked = dependencyClassification(task, badge, options);
97
- if (dependencyBlocked?.blockerClass === "human-approval")
98
- return dependencyBlocked;
99
- const labelClassification = classifyFromLabels(task);
100
- if (labelClassification)
101
- return labelClassification;
102
- const residueClassification = classifyFromResidue(task);
103
- if (residueClassification)
104
- return residueClassification;
105
- if (dependencyBlocked)
106
- return dependencyBlocked;
107
- if (task.status === "blocked" && options.llm)
108
- return classifyResidueWithLlm(task, badge, options.llm);
109
- if (task.status === "blocked")
110
- return classification(task, "unknown", "Task is blocked but no deterministic blocker class matched.", "elimination", false);
111
- return classification(task, "not-blocked", "Task has no blocked status, dependency blocker, or blocker label.", "elimination", true);
68
+ function isBlockerClassifierInput(input) {
69
+ return typeof input === "object" && input !== null && "task" in input && "badges" in input && "tasksById" in input;
112
70
  }
113
- function classifyBlockerSync(task, badges, options = {}) {
114
- const badge = badges.get(task.id) ?? null;
115
- const dependencyBlocked = dependencyClassification(task, badge, options);
116
- if (dependencyBlocked?.blockerClass === "human-approval")
117
- return dependencyBlocked;
118
- const labelClassification = classifyFromLabels(task);
119
- if (labelClassification)
120
- return labelClassification;
121
- const residueClassification = classifyFromResidue(task);
122
- if (residueClassification)
123
- return residueClassification;
124
- if (dependencyBlocked)
125
- return dependencyBlocked;
126
- if (task.status === "blocked")
127
- return classification(task, "unknown", "Task is blocked but no deterministic blocker class matched.", "elimination", false);
128
- return classification(task, "not-blocked", "Task has no blocked status, dependency blocker, or blocker label.", "elimination", true);
71
+ async function executeBlockers(context, args) {
72
+ const json = takeFlag(args, "--json");
73
+ const humanOnly = takeFlag(json.rest, "--human-only");
74
+ requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
75
+ const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
76
+ const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
77
+ if (context.outputMode === "text") {
78
+ if (json.value)
79
+ printJson(details);
80
+ else
81
+ console.log(result.classifications.length === 0 ? "No blockers classified." : result.classifications.map((entry) => `${entry.taskId} ${entry.blockerClass} ${entry.actionRiskTier ?? "\u2014"} ${entry.rationale}`).join(`
82
+ `));
83
+ }
84
+ return { ok: true, group: "blockers", command: "show", details };
129
85
  }
130
- // packages/blocker-classifier-plugin/src/plugin.ts
131
- import { definePlugin } from "@rig/core";
132
- var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
133
- var BLOCKER_RECLASSIFY_STAGE_ID = "blocker-reclassify";
134
- var blockerReclassifyStageMutation = {
135
- op: "insert",
136
- contributedBy: BLOCKER_CLASSIFIER_PLUGIN_NAME,
137
- stage: {
138
- id: BLOCKER_RECLASSIFY_STAGE_ID,
139
- kind: "observe",
140
- after: ["journal-append"],
141
- priority: 0,
142
- protected: false
86
+ var blockerClassifierCliCommands = [
87
+ {
88
+ id: BLOCKERS_CLI_ID,
89
+ family: "blockers",
90
+ command: "rig blockers [--human-only] [--json]",
91
+ description: "Classify task blockers from dependency and run state.",
92
+ usage: "rig blockers [--human-only] [--json]",
93
+ projectRequired: true,
94
+ run: executeBlockers
143
95
  }
144
- };
96
+ ];
145
97
  var blockerClassifierPlugin = definePlugin({
146
98
  name: BLOCKER_CLASSIFIER_PLUGIN_NAME,
147
99
  version: "0.0.0-alpha.1",
148
- provides: [],
149
- requires: [],
150
100
  contributes: {
151
- stageMutations: [blockerReclassifyStageMutation]
101
+ capabilities: [
102
+ { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
103
+ ],
104
+ blockerClassifiers: [
105
+ { id: DEFAULT_BLOCKER_CLASSIFIER_ID, description: "Default deterministic Rig blocker classifier.", priority: 0 }
106
+ ],
107
+ panels: [
108
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers" }
109
+ ],
110
+ cliCommands: blockerClassifierCliCommands.map(({ run: _run, ...metadata }) => metadata)
152
111
  }
112
+ }, {
113
+ featureCapabilities: [
114
+ { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
115
+ ],
116
+ panels: [
117
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
118
+ ],
119
+ blockerClassifiers: [
120
+ {
121
+ id: DEFAULT_BLOCKER_CLASSIFIER_ID,
122
+ description: "Default deterministic Rig blocker classifier.",
123
+ priority: 0,
124
+ classify(input) {
125
+ if (!isBlockerClassifierInput(input))
126
+ throw new Error("blocker classifier input must include task, badges, and tasksById.");
127
+ return classifyBlocker(input);
128
+ }
129
+ }
130
+ ],
131
+ cliCommands: blockerClassifierCliCommands
153
132
  });
133
+ function createBlockerClassifierPlugin() {
134
+ return blockerClassifierPlugin;
135
+ }
154
136
  export {
155
- tierOf,
156
- classifyResidueWithLlm,
157
- classifyBlockerSync,
158
- classifyBlocker,
159
- blockerReclassifyStageMutation,
137
+ executeBlockers,
138
+ createBlockerClassifierPlugin,
160
139
  blockerClassifierPlugin,
161
- BLOCKER_RECLASSIFY_STAGE_ID,
162
- BLOCKER_CLASSIFIER_PLUGIN_NAME
140
+ blockerClassifierCliCommands,
141
+ HUMAN_BLOCKERS_PANEL_ID,
142
+ DEFAULT_BLOCKER_CLASSIFIER_ID,
143
+ BLOCKER_CLASSIFIER_PLUGIN_NAME,
144
+ BLOCKERS_CLI_ID
163
145
  };
@@ -1,6 +1,24 @@
1
- import type { StageMutation } from "@rig/contracts";
1
+ import { type RuntimeCliContext } from "@rig/core";
2
2
  export declare const BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
3
- export declare const BLOCKER_RECLASSIFY_STAGE_ID = "blocker-reclassify";
4
- export declare const blockerReclassifyStageMutation: StageMutation;
3
+ export declare const BLOCKERS_CLI_ID = "blocker-classifier.blockers";
4
+ export declare const DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
5
+ export declare const HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
6
+ type CommandOutcome = {
7
+ readonly ok: boolean;
8
+ readonly group: string;
9
+ readonly command: string;
10
+ readonly details?: Record<string, unknown>;
11
+ };
12
+ export declare function executeBlockers(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
13
+ export declare const blockerClassifierCliCommands: readonly [{
14
+ readonly id: "blocker-classifier.blockers";
15
+ readonly family: "blockers";
16
+ readonly command: "rig blockers [--human-only] [--json]";
17
+ readonly description: "Classify task blockers from dependency and run state.";
18
+ readonly usage: "rig blockers [--human-only] [--json]";
19
+ readonly projectRequired: true;
20
+ readonly run: typeof executeBlockers;
21
+ }];
5
22
  export declare const blockerClassifierPlugin: import("@rig/core").RigPluginWithRuntime;
23
+ export declare function createBlockerClassifierPlugin(): import("@rig/core").RigPluginWithRuntime;
6
24
  export default blockerClassifierPlugin;
@@ -1,33 +1,147 @@
1
1
  // @bun
2
2
  // packages/blocker-classifier-plugin/src/plugin.ts
3
3
  import { definePlugin } from "@rig/core";
4
+ import {
5
+ classifyBlocker,
6
+ classifyWorkspaceBlockers,
7
+ listRuns,
8
+ listTasks
9
+ } from "@rig/client";
4
10
  var BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
5
- var BLOCKER_RECLASSIFY_STAGE_ID = "blocker-reclassify";
6
- var blockerReclassifyStageMutation = {
7
- op: "insert",
8
- contributedBy: BLOCKER_CLASSIFIER_PLUGIN_NAME,
9
- stage: {
10
- id: BLOCKER_RECLASSIFY_STAGE_ID,
11
- kind: "observe",
12
- after: ["journal-append"],
13
- priority: 0,
14
- protected: false
11
+ var BLOCKERS_CLI_ID = "blocker-classifier.blockers";
12
+ var DEFAULT_BLOCKER_CLASSIFIER_ID = "blocker-classifier.default";
13
+ var HUMAN_BLOCKERS_PANEL_ID = "human-blockers";
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function panelProjectRoot(context) {
18
+ return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
19
+ }
20
+ function taskEpicKey(task) {
21
+ const direct = typeof task?.epicKey === "string" ? task.epicKey : null;
22
+ if (direct)
23
+ return direct;
24
+ const metadata = isRecord(task?.metadata) ? task.metadata : {};
25
+ return typeof metadata.epicKey === "string" ? metadata.epicKey : typeof metadata.epic === "string" ? metadata.epic : null;
26
+ }
27
+ async function produceHumanBlockersPanel(context) {
28
+ const projectRoot = panelProjectRoot(context);
29
+ if (!projectRoot)
30
+ return;
31
+ const [tasks, blockers] = await Promise.all([
32
+ listTasks(projectRoot),
33
+ classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns, humanOnly: true })
34
+ ]);
35
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
36
+ return {
37
+ humanOnly: true,
38
+ items: blockers.classifications.map((classification) => {
39
+ const task = tasksById.get(classification.taskId);
40
+ return {
41
+ taskId: classification.taskId,
42
+ title: task?.title ?? classification.taskId,
43
+ blockerClass: classification.blockerClass,
44
+ rationale: classification.rationale,
45
+ riskTier: classification.actionRiskTier,
46
+ epicKey: taskEpicKey(task),
47
+ autoApplied: classification.autoApplied
48
+ };
49
+ })
50
+ };
51
+ }
52
+ function printJson(value) {
53
+ console.log(JSON.stringify(value, null, 2));
54
+ }
55
+ function takeFlag(args, flag) {
56
+ const rest = [...args];
57
+ const index = rest.indexOf(flag);
58
+ if (index < 0)
59
+ return { value: false, rest };
60
+ rest.splice(index, 1);
61
+ return { value: true, rest };
62
+ }
63
+ function requireNoExtraArgs(args, usage) {
64
+ if (args.length > 0)
65
+ throw new Error(`Unexpected argument: ${args[0]}
66
+ Usage: ${usage}`);
67
+ }
68
+ function isBlockerClassifierInput(input) {
69
+ return typeof input === "object" && input !== null && "task" in input && "badges" in input && "tasksById" in input;
70
+ }
71
+ async function executeBlockers(context, args) {
72
+ const json = takeFlag(args, "--json");
73
+ const humanOnly = takeFlag(json.rest, "--human-only");
74
+ requireNoExtraArgs(humanOnly.rest, "rig blockers [--human-only] [--json]");
75
+ const result = await classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns, humanOnly: humanOnly.value });
76
+ const details = { classifications: result.classifications, human: result.human, machine: result.machine, generatedAt: result.generatedAt };
77
+ if (context.outputMode === "text") {
78
+ if (json.value)
79
+ printJson(details);
80
+ else
81
+ console.log(result.classifications.length === 0 ? "No blockers classified." : result.classifications.map((entry) => `${entry.taskId} ${entry.blockerClass} ${entry.actionRiskTier ?? "\u2014"} ${entry.rationale}`).join(`
82
+ `));
15
83
  }
16
- };
84
+ return { ok: true, group: "blockers", command: "show", details };
85
+ }
86
+ var blockerClassifierCliCommands = [
87
+ {
88
+ id: BLOCKERS_CLI_ID,
89
+ family: "blockers",
90
+ command: "rig blockers [--human-only] [--json]",
91
+ description: "Classify task blockers from dependency and run state.",
92
+ usage: "rig blockers [--human-only] [--json]",
93
+ projectRequired: true,
94
+ run: executeBlockers
95
+ }
96
+ ];
17
97
  var blockerClassifierPlugin = definePlugin({
18
98
  name: BLOCKER_CLASSIFIER_PLUGIN_NAME,
19
99
  version: "0.0.0-alpha.1",
20
- provides: [],
21
- requires: [],
22
100
  contributes: {
23
- stageMutations: [blockerReclassifyStageMutation]
101
+ capabilities: [
102
+ { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
103
+ ],
104
+ blockerClassifiers: [
105
+ { id: DEFAULT_BLOCKER_CLASSIFIER_ID, description: "Default deterministic Rig blocker classifier.", priority: 0 }
106
+ ],
107
+ panels: [
108
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers" }
109
+ ],
110
+ cliCommands: blockerClassifierCliCommands.map(({ run: _run, ...metadata }) => metadata)
24
111
  }
112
+ }, {
113
+ featureCapabilities: [
114
+ { id: "workspace.blockers", title: "Workspace blocker classification", commandId: BLOCKERS_CLI_ID, panelId: HUMAN_BLOCKERS_PANEL_ID }
115
+ ],
116
+ panels: [
117
+ { id: HUMAN_BLOCKERS_PANEL_ID, slot: "capability", title: "Human blockers", capabilityId: "workspace.blockers", produce: produceHumanBlockersPanel }
118
+ ],
119
+ blockerClassifiers: [
120
+ {
121
+ id: DEFAULT_BLOCKER_CLASSIFIER_ID,
122
+ description: "Default deterministic Rig blocker classifier.",
123
+ priority: 0,
124
+ classify(input) {
125
+ if (!isBlockerClassifierInput(input))
126
+ throw new Error("blocker classifier input must include task, badges, and tasksById.");
127
+ return classifyBlocker(input);
128
+ }
129
+ }
130
+ ],
131
+ cliCommands: blockerClassifierCliCommands
25
132
  });
133
+ function createBlockerClassifierPlugin() {
134
+ return blockerClassifierPlugin;
135
+ }
26
136
  var plugin_default = blockerClassifierPlugin;
27
137
  export {
138
+ executeBlockers,
28
139
  plugin_default as default,
29
- blockerReclassifyStageMutation,
140
+ createBlockerClassifierPlugin,
30
141
  blockerClassifierPlugin,
31
- BLOCKER_RECLASSIFY_STAGE_ID,
32
- BLOCKER_CLASSIFIER_PLUGIN_NAME
142
+ blockerClassifierCliCommands,
143
+ HUMAN_BLOCKERS_PANEL_ID,
144
+ DEFAULT_BLOCKER_CLASSIFIER_ID,
145
+ BLOCKER_CLASSIFIER_PLUGIN_NAME,
146
+ BLOCKERS_CLI_ID
33
147
  };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@h-rig/blocker-classifier-plugin",
3
- "version": "0.0.6-alpha.135",
3
+ "version": "0.0.6-alpha.137",
4
4
  "type": "module",
5
- "description": "First-party human-vs-machine blocker classifier plugin for Rig.",
5
+ "description": "First-party blocker classifier capability plugin for Rig.",
6
6
  "license": "UNLICENSED",
7
7
  "files": [
8
8
  "dist",
@@ -13,10 +13,6 @@
13
13
  "types": "./dist/src/index.d.ts",
14
14
  "import": "./dist/src/index.js"
15
15
  },
16
- "./classifier": {
17
- "types": "./dist/src/classifier.d.ts",
18
- "import": "./dist/src/classifier.js"
19
- },
20
16
  "./plugin": {
21
17
  "types": "./dist/src/plugin.d.ts",
22
18
  "import": "./dist/src/plugin.js"
@@ -29,8 +25,8 @@
29
25
  "module": "./dist/src/index.js",
30
26
  "types": "./dist/src/index.d.ts",
31
27
  "dependencies": {
32
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.135",
33
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.135",
34
- "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@8881a9b"
28
+ "@rig/client": "npm:@h-rig/client@0.0.6-alpha.137",
29
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.137",
30
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.137"
35
31
  }
36
32
  }
@@ -1,19 +0,0 @@
1
- import { BlockerClassification, type BlockerClass, type RunStatus, type TaskSummary } from "@rig/contracts";
2
- import { type TaskDependencyBadgeSummary } from "@rig/core/task-graph";
3
- export interface ResidueClassifierPrompt {
4
- readonly task: TaskSummary;
5
- readonly badge: TaskDependencyBadgeSummary | null;
6
- readonly residue: string;
7
- }
8
- export interface LlmResidueClassifierPort {
9
- classify(prompt: ResidueClassifierPrompt): Promise<unknown> | unknown;
10
- }
11
- export interface ClassifyBlockerOptions {
12
- readonly activeRunStatus?: RunStatus;
13
- readonly tasksById?: ReadonlyMap<string, TaskSummary>;
14
- readonly llm?: LlmResidueClassifierPort;
15
- }
16
- export declare function tierOf(blockerClass: BlockerClass): BlockerClassification["actionRiskTier"];
17
- export declare function classifyResidueWithLlm(task: TaskSummary, badge: TaskDependencyBadgeSummary | null, port: LlmResidueClassifierPort): Promise<BlockerClassification>;
18
- export declare function classifyBlocker(task: TaskSummary, badges: ReadonlyMap<string, TaskDependencyBadgeSummary>, options?: ClassifyBlockerOptions): Promise<BlockerClassification>;
19
- export declare function classifyBlockerSync(task: TaskSummary, badges: ReadonlyMap<string, TaskDependencyBadgeSummary>, options?: Omit<ClassifyBlockerOptions, "llm">): BlockerClassification;
@@ -1,135 +0,0 @@
1
- // @bun
2
- // packages/blocker-classifier-plugin/src/classifier.ts
3
- import { Schema } from "effect";
4
- import { BlockerClassification } from "@rig/contracts";
5
- import { isTaskTerminalStatus, readTaskMetadataStringList } from "@rig/core/task-graph";
6
- var HUMAN_DECISION_LABEL = {
7
- "human-decision": true,
8
- "needs-decision": true,
9
- decision: true,
10
- "blocked:decision": true
11
- };
12
- var HUMAN_APPROVAL_LABEL = {
13
- "human-approval": true,
14
- "needs-approval": true,
15
- approval: true,
16
- "blocked:approval": true,
17
- "review-blocked": true
18
- };
19
- var EXTERNAL_INPUT_LABEL = {
20
- "external-input": true,
21
- external: true,
22
- vendor: true,
23
- customer: true,
24
- "blocked:external": true
25
- };
26
- var MACHINE_BLOCKED_LABEL = {
27
- "task-blocked": true,
28
- "blocked:task": true,
29
- "dependency-blocked": true
30
- };
31
- function tierOf(blockerClass) {
32
- if (blockerClass === "human-decision")
33
- return "t2-reversible";
34
- if (blockerClass === "human-approval")
35
- return "t3-external";
36
- if (blockerClass === "external-input")
37
- return "t3-external";
38
- if (blockerClass === "unknown")
39
- return "t4-irreversible";
40
- return null;
41
- }
42
- function classification(task, blockerClass, rationale, source, autoApplied) {
43
- return { taskId: task.id, blockerClass, actionRiskTier: tierOf(blockerClass), rationale, source, autoApplied };
44
- }
45
- function normalizedLabels(task) {
46
- return readTaskMetadataStringList(task, "labels").map((label) => label.trim().toLowerCase()).filter((label) => label.length > 0);
47
- }
48
- function classifyFromLabels(task) {
49
- const labels = normalizedLabels(task);
50
- if (labels.some((label) => HUMAN_DECISION_LABEL[label]))
51
- return classification(task, "human-decision", "Task carries a human-decision blocker label.", "label", true);
52
- if (labels.some((label) => HUMAN_APPROVAL_LABEL[label]))
53
- return classification(task, "human-approval", "Task carries a human-approval blocker label.", "label", true);
54
- if (labels.some((label) => EXTERNAL_INPUT_LABEL[label]))
55
- return classification(task, "external-input", "Task carries an external-input blocker label.", "label", true);
56
- if (labels.some((label) => MACHINE_BLOCKED_LABEL[label]))
57
- return classification(task, "task-blocked", "Task carries a task/dependency blocker label.", "label", true);
58
- return null;
59
- }
60
- function residue(task) {
61
- return `${task.title}
62
- ${task.description}`.toLowerCase();
63
- }
64
- function classifyFromResidue(task) {
65
- const text = residue(task);
66
- if (/\b(approval|approve|review required|sign[ -]?off)\b/u.test(text))
67
- return classification(task, "human-approval", "Task text asks for approval or sign-off.", "status", true);
68
- if (/\b(decide|decision|choose|clarify|question)\b/u.test(text))
69
- return classification(task, "human-decision", "Task text asks for a human decision or clarification.", "status", true);
70
- if (/\b(vendor|customer|external|third[- ]party|credential|account)\b/u.test(text))
71
- return classification(task, "external-input", "Task text depends on an external party or account input.", "status", true);
72
- return null;
73
- }
74
- function dependencyClassification(task, badge, options) {
75
- if (options.activeRunStatus === "waiting-approval")
76
- return classification(task, "human-approval", "Active run is waiting for approval.", "status", true);
77
- if (options.activeRunStatus === "waiting-user-input" || options.activeRunStatus === "needs-attention") {
78
- return classification(task, "human-decision", "Active run is waiting for operator input or attention.", "status", true);
79
- }
80
- if (!badge?.blocked)
81
- return null;
82
- if (!options.tasksById)
83
- return classification(task, "task-blocked", "Task is blocked by incomplete dependency tasks.", "elimination", true);
84
- const dependencyTasks = badge.blockedBy.map((taskId) => options.tasksById?.get(taskId)).filter((value) => value !== undefined);
85
- if (dependencyTasks.some((dependency) => !isTaskTerminalStatus(dependency.status))) {
86
- return classification(task, "task-blocked", "Task is blocked by at least one incomplete dependency task.", "elimination", true);
87
- }
88
- return classification(task, "human-decision", "Task is marked blocked even though known dependencies are terminal.", "elimination", false);
89
- }
90
- async function classifyResidueWithLlm(task, badge, port) {
91
- const decoded = Schema.decodeUnknownSync(BlockerClassification)(await port.classify({ task, badge, residue: residue(task) }));
92
- return { ...decoded, taskId: task.id, actionRiskTier: decoded.actionRiskTier ?? tierOf(decoded.blockerClass), source: "llm", autoApplied: false };
93
- }
94
- async function classifyBlocker(task, badges, options = {}) {
95
- const badge = badges.get(task.id) ?? null;
96
- const dependencyBlocked = dependencyClassification(task, badge, options);
97
- if (dependencyBlocked?.blockerClass === "human-approval")
98
- return dependencyBlocked;
99
- const labelClassification = classifyFromLabels(task);
100
- if (labelClassification)
101
- return labelClassification;
102
- const residueClassification = classifyFromResidue(task);
103
- if (residueClassification)
104
- return residueClassification;
105
- if (dependencyBlocked)
106
- return dependencyBlocked;
107
- if (task.status === "blocked" && options.llm)
108
- return classifyResidueWithLlm(task, badge, options.llm);
109
- if (task.status === "blocked")
110
- return classification(task, "unknown", "Task is blocked but no deterministic blocker class matched.", "elimination", false);
111
- return classification(task, "not-blocked", "Task has no blocked status, dependency blocker, or blocker label.", "elimination", true);
112
- }
113
- function classifyBlockerSync(task, badges, options = {}) {
114
- const badge = badges.get(task.id) ?? null;
115
- const dependencyBlocked = dependencyClassification(task, badge, options);
116
- if (dependencyBlocked?.blockerClass === "human-approval")
117
- return dependencyBlocked;
118
- const labelClassification = classifyFromLabels(task);
119
- if (labelClassification)
120
- return labelClassification;
121
- const residueClassification = classifyFromResidue(task);
122
- if (residueClassification)
123
- return residueClassification;
124
- if (dependencyBlocked)
125
- return dependencyBlocked;
126
- if (task.status === "blocked")
127
- return classification(task, "unknown", "Task is blocked but no deterministic blocker class matched.", "elimination", false);
128
- return classification(task, "not-blocked", "Task has no blocked status, dependency blocker, or blocker label.", "elimination", true);
129
- }
130
- export {
131
- tierOf,
132
- classifyResidueWithLlm,
133
- classifyBlockerSync,
134
- classifyBlocker
135
- };