@h-rig/blocker-classifier-plugin 0.0.6-alpha.133

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/README.md ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/blocker-classifier-plugin
@@ -0,0 +1,19 @@
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;
@@ -0,0 +1,135 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./classifier";
2
+ export * from "./plugin";
@@ -0,0 +1,163 @@
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
+ // 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
143
+ }
144
+ };
145
+ var blockerClassifierPlugin = definePlugin({
146
+ name: BLOCKER_CLASSIFIER_PLUGIN_NAME,
147
+ version: "0.0.0-alpha.1",
148
+ provides: [],
149
+ requires: [],
150
+ contributes: {
151
+ stageMutations: [blockerReclassifyStageMutation]
152
+ }
153
+ });
154
+ export {
155
+ tierOf,
156
+ classifyResidueWithLlm,
157
+ classifyBlockerSync,
158
+ classifyBlocker,
159
+ blockerReclassifyStageMutation,
160
+ blockerClassifierPlugin,
161
+ BLOCKER_RECLASSIFY_STAGE_ID,
162
+ BLOCKER_CLASSIFIER_PLUGIN_NAME
163
+ };
@@ -0,0 +1,6 @@
1
+ import type { StageMutation } from "@rig/contracts";
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;
5
+ export declare const blockerClassifierPlugin: import("@rig/core").RigPluginWithRuntime;
6
+ export default blockerClassifierPlugin;
@@ -0,0 +1,33 @@
1
+ // @bun
2
+ // packages/blocker-classifier-plugin/src/plugin.ts
3
+ import { definePlugin } from "@rig/core";
4
+ 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
15
+ }
16
+ };
17
+ var blockerClassifierPlugin = definePlugin({
18
+ name: BLOCKER_CLASSIFIER_PLUGIN_NAME,
19
+ version: "0.0.0-alpha.1",
20
+ provides: [],
21
+ requires: [],
22
+ contributes: {
23
+ stageMutations: [blockerReclassifyStageMutation]
24
+ }
25
+ });
26
+ var plugin_default = blockerClassifierPlugin;
27
+ export {
28
+ plugin_default as default,
29
+ blockerReclassifyStageMutation,
30
+ blockerClassifierPlugin,
31
+ BLOCKER_RECLASSIFY_STAGE_ID,
32
+ BLOCKER_CLASSIFIER_PLUGIN_NAME
33
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@h-rig/blocker-classifier-plugin",
3
+ "version": "0.0.6-alpha.133",
4
+ "type": "module",
5
+ "description": "First-party human-vs-machine blocker classifier plugin for Rig.",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/index.d.ts",
14
+ "import": "./dist/src/index.js"
15
+ },
16
+ "./classifier": {
17
+ "types": "./dist/src/classifier.d.ts",
18
+ "import": "./dist/src/classifier.js"
19
+ },
20
+ "./plugin": {
21
+ "types": "./dist/src/plugin.d.ts",
22
+ "import": "./dist/src/plugin.js"
23
+ }
24
+ },
25
+ "engines": {
26
+ "bun": ">=1.3.11"
27
+ },
28
+ "main": "./dist/src/index.js",
29
+ "module": "./dist/src/index.js",
30
+ "types": "./dist/src/index.d.ts",
31
+ "dependencies": {
32
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.133",
33
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.133",
34
+ "effect": "4.0.0-beta.78"
35
+ }
36
+ }