@h-rig/blocker-classifier-plugin 0.0.6-alpha.134 → 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.
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +125 -143
- package/dist/src/plugin.d.ts +21 -3
- package/dist/src/plugin.js +131 -17
- package/package.json +5 -9
- package/dist/src/classifier.d.ts +0 -19
- package/dist/src/classifier.js +0 -135
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -1,163 +1,145 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/blocker-classifier-plugin/src/
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
46
|
-
return
|
|
17
|
+
function panelProjectRoot(context) {
|
|
18
|
+
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
47
19
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
65
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
63
|
+
function requireNoExtraArgs(args, usage) {
|
|
64
|
+
if (args.length > 0)
|
|
65
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
66
|
+
Usage: ${usage}`);
|
|
93
67
|
}
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
classifyBlockerSync,
|
|
158
|
-
classifyBlocker,
|
|
159
|
-
blockerReclassifyStageMutation,
|
|
137
|
+
executeBlockers,
|
|
138
|
+
createBlockerClassifierPlugin,
|
|
160
139
|
blockerClassifierPlugin,
|
|
161
|
-
|
|
162
|
-
|
|
140
|
+
blockerClassifierCliCommands,
|
|
141
|
+
HUMAN_BLOCKERS_PANEL_ID,
|
|
142
|
+
DEFAULT_BLOCKER_CLASSIFIER_ID,
|
|
143
|
+
BLOCKER_CLASSIFIER_PLUGIN_NAME,
|
|
144
|
+
BLOCKERS_CLI_ID
|
|
163
145
|
};
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RuntimeCliContext } from "@rig/core";
|
|
2
2
|
export declare const BLOCKER_CLASSIFIER_PLUGIN_NAME = "@rig/blocker-classifier-plugin";
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
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;
|
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
createBlockerClassifierPlugin,
|
|
30
141
|
blockerClassifierPlugin,
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.6-alpha.137",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "First-party
|
|
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/
|
|
33
|
-
"@rig/
|
|
34
|
-
"
|
|
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
|
}
|
package/dist/src/classifier.d.ts
DELETED
|
@@ -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;
|
package/dist/src/classifier.js
DELETED
|
@@ -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
|
-
};
|