@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.
- package/dist/src/blockers.d.ts +36 -0
- package/dist/src/blockers.js +101 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +118 -30
- package/dist/src/plugin.d.ts +3 -2
- package/dist/src/plugin.js +121 -30
- package/package.json +4 -4
|
@@ -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
|
+
};
|
package/dist/src/index.d.ts
CHANGED
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
|
|
17
|
-
|
|
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 {
|
|
31
|
-
const [tasks,
|
|
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:
|
|
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 {
|
|
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
|
-
{
|
|
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
|
|
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,
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -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").
|
|
23
|
-
export declare function createBlockerClassifierPlugin(): import("@rig/core").
|
|
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;
|
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
|
17
|
-
|
|
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 {
|
|
31
|
-
const [tasks,
|
|
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:
|
|
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 {
|
|
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
|
-
{
|
|
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
|
|
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.
|
|
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/
|
|
29
|
-
"@rig/
|
|
30
|
-
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.
|
|
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
|
}
|