forked from laurentenhoor/devclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtask-set-level.ts
More file actions
134 lines (120 loc) · 6.29 KB
/
task-set-level.ts
File metadata and controls
134 lines (120 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* task_set_level — Set the developer level hint on a HOLD-state issue.
*
* Restricted to HOLD states only (Planning, Refining). The level hint is
* applied as a role:level label and respected by the heartbeat when the
* issue is later advanced via task_start.
*/
import { jsonResult } from "../../json-result.js";
import type { PluginContext } from "../../context.js";
import type { ToolContext } from "../../types.js";
import { log as auditLog } from "../../audit.js";
import { StateType, findStateByLabel, getCurrentStateLabel, getRoleLabelColor } from "../../workflow/index.js";
import { loadConfig } from "../../config/index.js";
import { requireWorkspaceDir, resolveChannelId, resolveProject, resolveProvider, autoAssignOwnerLabel, applyNotifyLabel } from "../helpers.js";
export function createTaskSetLevelTool(ctx: PluginContext) {
return (toolCtx: ToolContext) => ({
name: "task_set_level",
label: "Task Set Level",
description: `Set the developer level hint on a HOLD-state issue (Planning, Refining). The level is applied as a role:level label and respected by the heartbeat when the issue is advanced via task_start.
Examples:
- { issueId: 42, level: "senior" }
- { issueId: 42, level: "junior", reason: "Simple typo fix" }`,
parameters: {
type: "object",
required: ["channelId", "issueId"],
properties: {
channelId: {
type: "string",
description: "YOUR chat/group ID — the numeric ID of the chat you are in right now (e.g. '-1003844794417'). Do NOT guess; use the ID of the conversation this message came from.",
},
messageThreadId: {
type: "number",
description: "Optional Telegram forum topic ID for this project (message_thread_id). When provided, resolves the topic-bound project within the chat.",
},
issueId: {
type: "number",
description: "Issue ID to update",
},
level: {
type: "string",
description: "Override the role:level hint (e.g., 'senior', 'junior'). Applied so the heartbeat dispatches with this level when the issue is advanced via task_start.",
},
reason: {
type: "string",
description: "Optional audit log reason for the change",
},
},
},
async execute(_id: string, params: Record<string, unknown>) {
const channelId = resolveChannelId(toolCtx, params.channelId as string | undefined);
const issueId = params.issueId as number;
const newLevel = (params.level as string) ?? undefined;
const reason = (params.reason as string) ?? undefined;
const workspaceDir = requireWorkspaceDir(toolCtx);
if (!newLevel) {
throw new Error("'level' is required.");
}
const messageThreadId = params.messageThreadId as number | undefined;
const channelType = (toolCtx.messageChannel as string | undefined) ?? "telegram";
const accountId = toolCtx.agentAccountId as string | undefined;
const { project } = await resolveProject(workspaceDir, channelId, {
channel: channelType,
accountId,
messageThreadId,
});
const { provider, type: providerType } = await resolveProvider(project, ctx.runCommand);
const resolvedConfig = await loadConfig(workspaceDir, project.name);
const issue = await provider.getIssue(issueId);
const currentState = getCurrentStateLabel(issue.labels, resolvedConfig.workflow);
if (!currentState) {
throw new Error(`Issue #${issueId} has no recognized state label. Cannot perform update.`);
}
// Restrict to HOLD states only — use task_start for queue/active transitions
const currentStateConfig = findStateByLabel(resolvedConfig.workflow, currentState);
if (currentStateConfig?.type !== StateType.HOLD) {
throw new Error(`task_set_level only works on HOLD states (Planning, Refining). Issue #${issueId} is in "${currentState}". Use task_start to advance issues.`);
}
// Apply level hint label (will be respected by heartbeat when task_start advances the issue)
// Level is applied as a role:level label. Since HOLD states have no role, we look at the
// APPROVE transition target to determine which role will handle this issue.
const approveTarget = currentStateConfig.on?.["APPROVE"];
const targetKey = typeof approveTarget === "string" ? approveTarget : approveTarget?.target;
const targetState = targetKey ? resolvedConfig.workflow.states[targetKey] : undefined;
const role = targetState?.role;
if (!role) {
throw new Error(`Cannot determine target role from "${currentState}". No APPROVE transition found.`);
}
const roleConfig = resolvedConfig.roles[role];
if (!roleConfig || !roleConfig.levels.includes(newLevel)) {
throw new Error(`Invalid level "${newLevel}" for role "${role}". Valid: ${roleConfig?.levels.join(", ") ?? "none"}`);
}
const oldRoleLabels = issue.labels.filter((l) => l.startsWith(`${role}:`));
const fromLevel = oldRoleLabels[0]?.split(":")[1];
if (oldRoleLabels.length > 0) {
await provider.removeLabels(issueId, oldRoleLabels);
}
const newRoleLabel = `${role}:${newLevel}`;
await provider.ensureLabel(newRoleLabel, getRoleLabelColor(role));
await provider.addLabel(issueId, newRoleLabel);
const levelChanged = fromLevel !== newLevel;
// Apply notify label for channel routing (best-effort).
applyNotifyLabel(provider, issueId, project, channelId, issue.labels);
// Auto-assign owner label to this instance (best-effort).
autoAssignOwnerLabel(workspaceDir, provider, issueId, project).catch(() => {});
await auditLog(workspaceDir, "task_set_level", {
project: project.name, issueId,
...(levelChanged ? { fromLevel: fromLevel ?? null, toLevel: newLevel } : {}),
reason: reason ?? null, provider: providerType,
});
return jsonResult({
success: true, issueId, issueTitle: issue.title,
level: newLevel, changed: levelChanged,
project: project.name, provider: providerType,
announcement: levelChanged
? `🔄 Updated #${issueId}: level ${fromLevel ?? "none"} → ${newLevel}${reason ? ` (${reason})` : ""}`
: `Issue #${issueId} already has level "${newLevel}".`,
});
},
});
}