dsh-taskboard 0.1.0
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/LICENSE +201 -0
- package/README.md +169 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +2085 -0
- package/lib/host/execution.js +189 -0
- package/lib/host/execution.js.map +1 -0
- package/lib/host/protocol-text.js +37 -0
- package/lib/host/protocol-text.js.map +1 -0
- package/lib/host/routes.js +369 -0
- package/lib/host/routes.js.map +1 -0
- package/lib/host/scheduler.js +91 -0
- package/lib/host/scheduler.js.map +1 -0
- package/lib/host/sdk.js +145 -0
- package/lib/host/sdk.js.map +1 -0
- package/lib/host/store.js +112 -0
- package/lib/host/store.js.map +1 -0
- package/lib/host/tools.js +620 -0
- package/lib/host/tools.js.map +1 -0
- package/lib/index.js +91 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.js +22 -0
- package/lib/invariant.js.map +1 -0
- package/lib/shared/api.js +9 -0
- package/lib/shared/api.js.map +1 -0
- package/lib/shared/protocol.js +279 -0
- package/lib/shared/protocol.js.map +1 -0
- package/package.json +74 -0
- package/src/client/api.ts +90 -0
- package/src/client/board/NewTaskModal.tsx +8 -0
- package/src/client/board/TaskBoard.tsx +184 -0
- package/src/client/board/TaskCard.tsx +61 -0
- package/src/client/board/TaskDetail.tsx +210 -0
- package/src/client/board/TaskFormModal.tsx +257 -0
- package/src/client/board-mount.tsx +92 -0
- package/src/client/controller.ts +241 -0
- package/src/client/index.ts +87 -0
- package/src/client/sidebar-entry.ts +165 -0
- package/src/client/styles.ts +391 -0
- package/src/host/execution.ts +244 -0
- package/src/host/protocol-text.ts +37 -0
- package/src/host/routes.ts +387 -0
- package/src/host/scheduler.ts +107 -0
- package/src/host/sdk.ts +200 -0
- package/src/host/store.ts +139 -0
- package/src/host/tools.ts +631 -0
- package/src/index.ts +124 -0
- package/src/invariant.ts +22 -0
- package/src/shared/api.ts +98 -0
- package/src/shared/protocol.ts +475 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { asStatus, asUrgency, canTransition, effectivePrompt, isClaim, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizePrompt, normalizeTitle, summarize } from "../shared/protocol.js";
|
|
2
|
+
import { defineTool } from "./sdk.js";
|
|
3
|
+
//#region src/host/tools.ts
|
|
4
|
+
/** Render side: one compact task line (id/status/version are load-bearing). */
|
|
5
|
+
function taskLine(t) {
|
|
6
|
+
const parts = [`- ${t.id} [${t.status}] v${t.version} · ${t.urgency} · 项目 ${t.workspaceId}`, `「${t.title}」`];
|
|
7
|
+
if (t.blocked) parts.push("·受阻");
|
|
8
|
+
if (t.executionMode === "scheduled") parts.push("·定时");
|
|
9
|
+
if (t.commentCount !== void 0 && t.commentCount > 0) parts.push(`·评论${t.commentCount}`);
|
|
10
|
+
if (t.lastExecutionOutcome !== void 0) parts.push(`·上次执行${t.lastExecutionOutcome}`);
|
|
11
|
+
if (t.trashed === true) parts.push("·已删");
|
|
12
|
+
return parts.join(" ");
|
|
13
|
+
}
|
|
14
|
+
/** Render side: the full task detail block (everything an executor needs). */
|
|
15
|
+
function taskDetail(t) {
|
|
16
|
+
const lines = [
|
|
17
|
+
`任务 ${t.id} 「${t.title}」`,
|
|
18
|
+
`状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? " · 受阻" : ""}`,
|
|
19
|
+
`执行方式: ${t.execution.mode}${t.execution.cron !== void 0 ? ` cron=${t.execution.cron}` : ""}`
|
|
20
|
+
];
|
|
21
|
+
if (t.execution.nextRunAt !== void 0) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`);
|
|
22
|
+
if (t.model !== void 0) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`);
|
|
23
|
+
lines.push(`描述: ${t.description.length > 0 ? t.description : "(无)"}`);
|
|
24
|
+
lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`);
|
|
25
|
+
if (t.comments.length > 0) {
|
|
26
|
+
lines.push(`评论 (${t.comments.length}):`);
|
|
27
|
+
for (const c of t.comments) {
|
|
28
|
+
const who = c.threadId !== void 0 ? `agent ${String(c.threadId).slice(0, 24)}` : "user";
|
|
29
|
+
lines.push(` - [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`);
|
|
30
|
+
}
|
|
31
|
+
} else lines.push("评论: 无");
|
|
32
|
+
if (t.executions.length > 0) {
|
|
33
|
+
lines.push(`执行记录 (${t.executions.length}):`);
|
|
34
|
+
for (const e of t.executions) {
|
|
35
|
+
const at = e.startedAt !== void 0 ? new Date(e.startedAt).toISOString() : "?";
|
|
36
|
+
const err = e.error !== void 0 ? ` 错误: ${e.error}` : "";
|
|
37
|
+
lines.push(` - [${e.trigger} ${at}] ${e.outcome}${err}`);
|
|
38
|
+
}
|
|
39
|
+
} else lines.push("执行记录: 无");
|
|
40
|
+
const updatedBy = t.updatedBy.kind === "agent" ? `agent ${String(t.updatedBy.sessionId).slice(0, 24)}` : "user";
|
|
41
|
+
lines.push(`更新: ${new Date(t.updatedAt).toISOString()} 由 ${updatedBy}`);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** Stable error codes surfaced at the head of tool error messages. */
|
|
45
|
+
const ERR = {
|
|
46
|
+
notFound: "not_found",
|
|
47
|
+
versionConflict: "version_conflict",
|
|
48
|
+
workspaceMismatch: "workspace_mismatch",
|
|
49
|
+
invalidTransition: "invalid_transition",
|
|
50
|
+
forbidden: "forbidden",
|
|
51
|
+
requiresAgent: "unauthorized_actor",
|
|
52
|
+
invalidInput: "invalid_input"
|
|
53
|
+
};
|
|
54
|
+
/** Tool failure: an Error whose message starts with a stable code. */
|
|
55
|
+
var ToolError = class extends Error {
|
|
56
|
+
code;
|
|
57
|
+
constructor(code, detail) {
|
|
58
|
+
super(`Error: ${code}: ${detail}`);
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** Adapt the real registry to the narrow face. */
|
|
63
|
+
function workspaceFace(registry) {
|
|
64
|
+
return {
|
|
65
|
+
resolveByPath: async (path) => {
|
|
66
|
+
const ws = await registry.resolveByPath(path);
|
|
67
|
+
return ws === void 0 ? void 0 : { id: ws.id };
|
|
68
|
+
},
|
|
69
|
+
get: (id) => {
|
|
70
|
+
const ws = registry.get(id);
|
|
71
|
+
return ws === void 0 ? void 0 : {
|
|
72
|
+
id: ws.id,
|
|
73
|
+
path: ws.path,
|
|
74
|
+
title: ws.title
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
list: () => registry.list().map((ws) => ({
|
|
78
|
+
id: ws.id,
|
|
79
|
+
path: ws.path,
|
|
80
|
+
title: ws.title
|
|
81
|
+
}))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Resolve the calling agent's actor and session id. */
|
|
85
|
+
function caller(exec) {
|
|
86
|
+
if (!exec.agent) throw new ToolError(ERR.requiresAgent, "taskboard tools require a calling agent session");
|
|
87
|
+
const sessionId = exec.agent.id;
|
|
88
|
+
return {
|
|
89
|
+
actor: {
|
|
90
|
+
kind: "agent",
|
|
91
|
+
sessionId
|
|
92
|
+
},
|
|
93
|
+
sessionId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** The calling session's workspace id (undefined when unaffiliated). */
|
|
97
|
+
async function callerWorkspace(deps, exec) {
|
|
98
|
+
const cwd = exec.agent?.session.header.cwd;
|
|
99
|
+
if (typeof cwd !== "string" || cwd.length === 0) return void 0;
|
|
100
|
+
return (await deps.workspaces.resolveByPath(cwd))?.id;
|
|
101
|
+
}
|
|
102
|
+
/** Guard: version match. */
|
|
103
|
+
function versionGuard(task, ifVersion) {
|
|
104
|
+
if (ifVersion === void 0) throw new ToolError(ERR.versionConflict, "this write requires ifVersion; read the task first");
|
|
105
|
+
if (ifVersion !== task.version) throw new ToolError(ERR.versionConflict, `stale version ${ifVersion} (current ${task.version}); re-read the task and retry once`);
|
|
106
|
+
}
|
|
107
|
+
/** Re-throw with a stable code; non-ToolErrors become invalid_input. */
|
|
108
|
+
function fail(error) {
|
|
109
|
+
if (error instanceof ToolError) throw error;
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
throw new ToolError(ERR.invalidInput, message);
|
|
112
|
+
}
|
|
113
|
+
/** Loose json output schema shared by every taskboard tool. */
|
|
114
|
+
const JSON_OUT = { type: "json" };
|
|
115
|
+
/** Deep-JSON a value for a json-rooted tool output (spread results lose implicit index signatures). */
|
|
116
|
+
function json(value) {
|
|
117
|
+
return JSON.parse(JSON.stringify(value));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Register all eight tools.
|
|
121
|
+
* @param ctx - a context exposing `tools.register`.
|
|
122
|
+
* @param deps - store + workspaces + clock.
|
|
123
|
+
* @returns dispose functions, one per tool.
|
|
124
|
+
*/
|
|
125
|
+
function registerTaskboardTools(ctx, deps) {
|
|
126
|
+
const disposers = [];
|
|
127
|
+
const { store, workspaces } = deps;
|
|
128
|
+
const register = (tool) => {
|
|
129
|
+
if (process.env.ATB_TRACE === "1" && typeof tool.execute === "function") {
|
|
130
|
+
const orig = tool.execute;
|
|
131
|
+
tool.execute = async (args, exec) => {
|
|
132
|
+
console.error(`[atb ▶] ${tool.name}`, JSON.stringify(args).slice(0, 300));
|
|
133
|
+
try {
|
|
134
|
+
const result = await orig(args, exec);
|
|
135
|
+
console.error(`[atb ✓] ${tool.name}`, JSON.stringify(result).slice(0, 300));
|
|
136
|
+
return result;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`[atb ✗] ${tool.name}`, String(error).slice(0, 400));
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return ctx.tools.register(tool);
|
|
144
|
+
};
|
|
145
|
+
disposers.push(register(defineTool({
|
|
146
|
+
name: "taskboard_list",
|
|
147
|
+
description: "List task-board tasks. Filter by project (workspaceId), status, or urgency. Returns compact summaries (id, title, status, urgency, version, claim owner). Check this before starting work to find claimable todo tasks in your project.",
|
|
148
|
+
parameters: {
|
|
149
|
+
workspaceId: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "Filter by project (DSH workspace id)."
|
|
152
|
+
},
|
|
153
|
+
status: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Filter by exact status (backlog/todo/in_progress/in_review/done/canceled/archived)."
|
|
156
|
+
},
|
|
157
|
+
urgency: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Filter by urgency (urgent/normal/relaxed)."
|
|
160
|
+
},
|
|
161
|
+
includeTrashed: {
|
|
162
|
+
type: "boolean",
|
|
163
|
+
description: "Include soft-deleted tasks (default false)."
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
output: {
|
|
167
|
+
schema: JSON_OUT,
|
|
168
|
+
render: (_args, value) => {
|
|
169
|
+
const v = value;
|
|
170
|
+
const tasks = v.tasks ?? [];
|
|
171
|
+
const head = `任务 ${tasks.length} 条(台账 rev ${v.revision ?? "?"})`;
|
|
172
|
+
if (tasks.length === 0) return [{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `${head}:无匹配任务。`
|
|
175
|
+
}];
|
|
176
|
+
return [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: [head, ...tasks.map((t) => taskLine(t))].join("\n")
|
|
179
|
+
}];
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
async execute(args) {
|
|
183
|
+
try {
|
|
184
|
+
const a = args;
|
|
185
|
+
const tasks = store.snapshot().tasks.filter((t) => (a.workspaceId === void 0 || t.workspaceId === a.workspaceId) && (a.status === void 0 || t.status === a.status) && (a.urgency === void 0 || t.urgency === a.urgency) && (a.includeTrashed === true || t.trashedAt === void 0));
|
|
186
|
+
return json({
|
|
187
|
+
revision: store.snapshot().revision,
|
|
188
|
+
tasks: tasks.map(summarize)
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
fail(error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})));
|
|
195
|
+
disposers.push(register(defineTool({
|
|
196
|
+
name: "taskboard_get",
|
|
197
|
+
description: "Read one task in full: description, prompt, project, urgency, status, comments, executions, version. Read this (and the comments) BEFORE claiming or starting work on a task.",
|
|
198
|
+
parameters: { id: {
|
|
199
|
+
type: "string",
|
|
200
|
+
required: true,
|
|
201
|
+
description: "Task id from the board."
|
|
202
|
+
} },
|
|
203
|
+
output: {
|
|
204
|
+
schema: JSON_OUT,
|
|
205
|
+
render: (_args, value) => {
|
|
206
|
+
const v = value;
|
|
207
|
+
return [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: v.task === void 0 ? "任务不存在。" : taskDetail(v.task)
|
|
210
|
+
}];
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
async execute(args) {
|
|
214
|
+
try {
|
|
215
|
+
const { id } = args;
|
|
216
|
+
const task = store.get(id);
|
|
217
|
+
if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${id}`);
|
|
218
|
+
return json({ task: {
|
|
219
|
+
...task,
|
|
220
|
+
effectivePrompt: effectivePrompt(task)
|
|
221
|
+
} });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
fail(error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})));
|
|
227
|
+
disposers.push(register(defineTool({
|
|
228
|
+
name: "taskboard_create",
|
|
229
|
+
description: "Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). Optional: description, prompt (sent to a fresh session on execution), status (default todo), execution mode (claim|scheduled + cron), model {provider, model} to pin executions to a model. Do not track trivial requests as tasks.",
|
|
230
|
+
parameters: {
|
|
231
|
+
title: {
|
|
232
|
+
type: "string",
|
|
233
|
+
required: true,
|
|
234
|
+
description: "Short imperative line (1..200 chars)."
|
|
235
|
+
},
|
|
236
|
+
workspaceId: {
|
|
237
|
+
type: "string",
|
|
238
|
+
required: true,
|
|
239
|
+
description: "Project (DSH workspace id) this task belongs to."
|
|
240
|
+
},
|
|
241
|
+
urgency: {
|
|
242
|
+
type: "string",
|
|
243
|
+
required: true,
|
|
244
|
+
description: "urgent (red) | normal (purple) | relaxed (blue)."
|
|
245
|
+
},
|
|
246
|
+
description: {
|
|
247
|
+
type: "string",
|
|
248
|
+
description: "What the task involves (plain text)."
|
|
249
|
+
},
|
|
250
|
+
prompt: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Prompt sent to a fresh session when executed; default = title+description."
|
|
253
|
+
},
|
|
254
|
+
status: {
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Initial status; default todo. backlog = not approved for execution."
|
|
257
|
+
},
|
|
258
|
+
execution: {
|
|
259
|
+
type: "object",
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
description: "Execution config: { mode: \"claim\" } (default) or { mode: \"scheduled\", cron: \"m h dom mon dow\" }.",
|
|
262
|
+
properties: {
|
|
263
|
+
mode: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "claim | scheduled."
|
|
266
|
+
},
|
|
267
|
+
cron: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Five-field cron expression (scheduled only)."
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
model: {
|
|
274
|
+
type: "object",
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
description: "Pin executions to one configured model: { provider, model }. Omit to use the default model.",
|
|
277
|
+
properties: {
|
|
278
|
+
provider: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Provider route id."
|
|
281
|
+
},
|
|
282
|
+
model: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Provider-owned model id."
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
output: {
|
|
290
|
+
schema: JSON_OUT,
|
|
291
|
+
render: (_args, value) => {
|
|
292
|
+
const t = value.task;
|
|
293
|
+
return [{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: t === void 0 ? "创建失败。" : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。`
|
|
296
|
+
}];
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
async execute(args, exec) {
|
|
300
|
+
try {
|
|
301
|
+
const { actor } = caller(exec);
|
|
302
|
+
const title = normalizeTitle(args.title);
|
|
303
|
+
if (workspaces.get(args.workspaceId) === void 0) throw new ToolError(ERR.notFound, `unknown workspaceId ${args.workspaceId}`);
|
|
304
|
+
const urgency = asUrgency(args.urgency);
|
|
305
|
+
const status = args.status === void 0 ? "todo" : asStatus(args.status);
|
|
306
|
+
if (status === "done" || status === "archived") throw new ToolError(ERR.invalidTransition, "a new task cannot start as done/archived");
|
|
307
|
+
const execution = normalizeExecution(args.execution ?? {}, deps.now());
|
|
308
|
+
if (args.model !== void 0 && (typeof args.model.provider !== "string" || typeof args.model.model !== "string")) throw new ToolError(ERR.invalidInput, "model must be { provider: string, model: string }");
|
|
309
|
+
const now = deps.now();
|
|
310
|
+
const task = {
|
|
311
|
+
id: newTaskId(),
|
|
312
|
+
title,
|
|
313
|
+
description: (args.description ?? "").trim(),
|
|
314
|
+
prompt: normalizePrompt(args.prompt),
|
|
315
|
+
workspaceId: args.workspaceId,
|
|
316
|
+
urgency,
|
|
317
|
+
status,
|
|
318
|
+
blocked: false,
|
|
319
|
+
execution,
|
|
320
|
+
model: args.model,
|
|
321
|
+
version: 1,
|
|
322
|
+
createdAt: now,
|
|
323
|
+
updatedAt: now,
|
|
324
|
+
createdBy: actor,
|
|
325
|
+
updatedBy: actor,
|
|
326
|
+
comments: [],
|
|
327
|
+
executions: []
|
|
328
|
+
};
|
|
329
|
+
await store.mutate("task-created", (ledger) => {
|
|
330
|
+
ledger.tasks.push(task);
|
|
331
|
+
return [task];
|
|
332
|
+
});
|
|
333
|
+
return json({ task: summarize(task) });
|
|
334
|
+
} catch (error) {
|
|
335
|
+
fail(error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
})));
|
|
339
|
+
disposers.push(register(defineTool({
|
|
340
|
+
name: "taskboard_update",
|
|
341
|
+
description: "Update a task's title/description/prompt/urgency/blocked. Requires ifVersion (read first). The model and execution config are read-only through this tool (they belong to the task owner/user).",
|
|
342
|
+
parameters: {
|
|
343
|
+
id: {
|
|
344
|
+
type: "string",
|
|
345
|
+
required: true,
|
|
346
|
+
description: "Task id."
|
|
347
|
+
},
|
|
348
|
+
ifVersion: {
|
|
349
|
+
type: "number",
|
|
350
|
+
required: true,
|
|
351
|
+
description: "The task version you read; the write fails on mismatch."
|
|
352
|
+
},
|
|
353
|
+
title: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "New title."
|
|
356
|
+
},
|
|
357
|
+
description: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "New description."
|
|
360
|
+
},
|
|
361
|
+
prompt: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "New execution prompt."
|
|
364
|
+
},
|
|
365
|
+
urgency: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "urgent | normal | relaxed."
|
|
368
|
+
},
|
|
369
|
+
blocked: {
|
|
370
|
+
type: "boolean",
|
|
371
|
+
description: "Blocked marker (work cannot continue right now)."
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
output: {
|
|
375
|
+
schema: JSON_OUT,
|
|
376
|
+
render: (_args, value) => {
|
|
377
|
+
const t = value.task;
|
|
378
|
+
return [{
|
|
379
|
+
type: "text",
|
|
380
|
+
text: t === void 0 ? "更新失败。" : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。`
|
|
381
|
+
}];
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
async execute(args, exec) {
|
|
385
|
+
try {
|
|
386
|
+
const { actor } = caller(exec);
|
|
387
|
+
const task = store.get(args.id);
|
|
388
|
+
if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
|
|
389
|
+
versionGuard(task, args.ifVersion);
|
|
390
|
+
if (task.status === "archived") throw new ToolError(ERR.invalidTransition, "archived tasks are immutable");
|
|
391
|
+
const next = structuredClone(task);
|
|
392
|
+
if (args.title !== void 0) next.title = normalizeTitle(args.title);
|
|
393
|
+
if (args.description !== void 0) next.description = args.description.trim();
|
|
394
|
+
if (args.prompt !== void 0) next.prompt = normalizePrompt(args.prompt);
|
|
395
|
+
if (args.urgency !== void 0) next.urgency = asUrgency(args.urgency);
|
|
396
|
+
if (args.blocked !== void 0) next.blocked = args.blocked;
|
|
397
|
+
next.version = task.version + 1;
|
|
398
|
+
next.updatedAt = deps.now();
|
|
399
|
+
next.updatedBy = actor;
|
|
400
|
+
await store.mutate("task-updated", (ledger) => {
|
|
401
|
+
const i = ledger.tasks.findIndex((t) => t.id === args.id);
|
|
402
|
+
ledger.tasks[i] = next;
|
|
403
|
+
return [next];
|
|
404
|
+
});
|
|
405
|
+
return json({ task: summarize(next) });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
fail(error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
})));
|
|
411
|
+
disposers.push(register(defineTool({
|
|
412
|
+
name: "taskboard_move",
|
|
413
|
+
description: "Move a task between statuses (requires ifVersion). Claim = todo→in_progress (only a session inside the task's project may claim; never take over a task held by another session). After implementing and self-verifying: comment, then in_progress→in_review. You can NEVER move a task to done — that requires explicit user confirmation.",
|
|
414
|
+
parameters: {
|
|
415
|
+
id: {
|
|
416
|
+
type: "string",
|
|
417
|
+
required: true,
|
|
418
|
+
description: "Task id."
|
|
419
|
+
},
|
|
420
|
+
status: {
|
|
421
|
+
type: "string",
|
|
422
|
+
required: true,
|
|
423
|
+
description: "Target status."
|
|
424
|
+
},
|
|
425
|
+
ifVersion: {
|
|
426
|
+
type: "number",
|
|
427
|
+
required: true,
|
|
428
|
+
description: "Task version you read; fails on mismatch."
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
output: {
|
|
432
|
+
schema: JSON_OUT,
|
|
433
|
+
render: (_args, value) => {
|
|
434
|
+
const t = value.task;
|
|
435
|
+
return [{
|
|
436
|
+
type: "text",
|
|
437
|
+
text: t === void 0 ? "移动失败。" : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。`
|
|
438
|
+
}];
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
async execute(args, exec) {
|
|
442
|
+
try {
|
|
443
|
+
const { actor } = caller(exec);
|
|
444
|
+
const to = asStatus(args.status);
|
|
445
|
+
const task = store.get(args.id);
|
|
446
|
+
if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
|
|
447
|
+
versionGuard(task, args.ifVersion);
|
|
448
|
+
if (to === "done") throw new ToolError(ERR.forbidden, "moving a task to done requires explicit user confirmation (GUI); agents cannot do it");
|
|
449
|
+
if (!canTransition(task.status, to)) throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`);
|
|
450
|
+
if (task.status === "in_progress" && task.updatedBy.kind === "agent" && task.updatedBy.sessionId !== actor.sessionId) throw new ToolError(ERR.forbidden, `task is held by session ${task.updatedBy.sessionId}; never take over another session's claim`);
|
|
451
|
+
if (isClaim(task.status, to)) {
|
|
452
|
+
if (await callerWorkspace(deps, exec) !== task.workspaceId) throw new ToolError(ERR.workspaceMismatch, "only a session inside this task's project may claim it");
|
|
453
|
+
}
|
|
454
|
+
const next = structuredClone(task);
|
|
455
|
+
next.status = to;
|
|
456
|
+
next.version = task.version + 1;
|
|
457
|
+
next.updatedAt = deps.now();
|
|
458
|
+
next.updatedBy = actor;
|
|
459
|
+
if (isClaim(task.status, to)) next.blocked = false;
|
|
460
|
+
await store.mutate("task-moved", (ledger) => {
|
|
461
|
+
const i = ledger.tasks.findIndex((t) => t.id === args.id);
|
|
462
|
+
ledger.tasks[i] = next;
|
|
463
|
+
return [next];
|
|
464
|
+
});
|
|
465
|
+
return json({ task: summarize(next) });
|
|
466
|
+
} catch (error) {
|
|
467
|
+
fail(error);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
})));
|
|
471
|
+
disposers.push(register(defineTool({
|
|
472
|
+
name: "taskboard_comment_add",
|
|
473
|
+
description: "Append a progress/report comment to a task. When handing off to review, the comment should cover: what changed, how it was verified, outcome, and remaining risks.",
|
|
474
|
+
parameters: {
|
|
475
|
+
id: {
|
|
476
|
+
type: "string",
|
|
477
|
+
required: true,
|
|
478
|
+
description: "Task id."
|
|
479
|
+
},
|
|
480
|
+
body: {
|
|
481
|
+
type: "string",
|
|
482
|
+
required: true,
|
|
483
|
+
description: "Comment text (1..4000 chars)."
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
output: {
|
|
487
|
+
schema: JSON_OUT,
|
|
488
|
+
render: (_args, value) => {
|
|
489
|
+
const v = value;
|
|
490
|
+
const c = v.comment;
|
|
491
|
+
const t = v.task;
|
|
492
|
+
if (c === void 0 || t === void 0) return [{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: "评论失败。"
|
|
495
|
+
}];
|
|
496
|
+
return [{
|
|
497
|
+
type: "text",
|
|
498
|
+
text: `评论 ${c.id} 已添加;任务 ${t.id} 当前 v${t.version} [${t.status}](后续写操作用此版本号).`
|
|
499
|
+
}];
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
async execute(args, exec) {
|
|
503
|
+
try {
|
|
504
|
+
const { sessionId } = caller(exec);
|
|
505
|
+
const task = store.get(args.id);
|
|
506
|
+
if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
|
|
507
|
+
const comment = {
|
|
508
|
+
id: newCommentId(),
|
|
509
|
+
body: normalizeBody(args.body),
|
|
510
|
+
version: 1,
|
|
511
|
+
createdAt: deps.now(),
|
|
512
|
+
threadId: sessionId
|
|
513
|
+
};
|
|
514
|
+
const next = structuredClone(task);
|
|
515
|
+
next.comments.push(comment);
|
|
516
|
+
next.version = task.version + 1;
|
|
517
|
+
next.updatedAt = deps.now();
|
|
518
|
+
await store.mutate("comment-added", (ledger) => {
|
|
519
|
+
const i = ledger.tasks.findIndex((t) => t.id === args.id);
|
|
520
|
+
ledger.tasks[i] = next;
|
|
521
|
+
return [next];
|
|
522
|
+
});
|
|
523
|
+
return json({
|
|
524
|
+
comment,
|
|
525
|
+
task: {
|
|
526
|
+
id: next.id,
|
|
527
|
+
version: next.version,
|
|
528
|
+
status: next.status
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
fail(error);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
})));
|
|
536
|
+
disposers.push(register(defineTool({
|
|
537
|
+
name: "taskboard_comments",
|
|
538
|
+
description: "List a task's comments, oldest first. Read them before deciding to start work.",
|
|
539
|
+
parameters: { id: {
|
|
540
|
+
type: "string",
|
|
541
|
+
required: true,
|
|
542
|
+
description: "Task id."
|
|
543
|
+
} },
|
|
544
|
+
output: {
|
|
545
|
+
schema: JSON_OUT,
|
|
546
|
+
render: (_args, value) => {
|
|
547
|
+
const list = value.comments;
|
|
548
|
+
if (list === void 0 || list.length === 0) return [{
|
|
549
|
+
type: "text",
|
|
550
|
+
text: "无评论。"
|
|
551
|
+
}];
|
|
552
|
+
const lines = list.map((c) => {
|
|
553
|
+
return `- [${c.threadId !== void 0 ? `agent ${String(c.threadId).slice(0, 24)}` : "user"} ${new Date(c.createdAt).toISOString()}] ${c.body}`;
|
|
554
|
+
});
|
|
555
|
+
return [{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: `评论 ${list.length} 条:\n${lines.join("\n")}`
|
|
558
|
+
}];
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
async execute(args) {
|
|
562
|
+
try {
|
|
563
|
+
const task = store.get(args.id);
|
|
564
|
+
if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
|
|
565
|
+
return json({ comments: task.comments });
|
|
566
|
+
} catch (error) {
|
|
567
|
+
fail(error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
})));
|
|
571
|
+
disposers.push(register(defineTool({
|
|
572
|
+
name: "taskboard_delete",
|
|
573
|
+
description: "Soft-delete a task (marks it trashed; the user confirms the purge in the GUI). Requires ifVersion. Prefer canceled/archived over delete unless the task was a mistake.",
|
|
574
|
+
parameters: {
|
|
575
|
+
id: {
|
|
576
|
+
type: "string",
|
|
577
|
+
required: true,
|
|
578
|
+
description: "Task id."
|
|
579
|
+
},
|
|
580
|
+
ifVersion: {
|
|
581
|
+
type: "number",
|
|
582
|
+
required: true,
|
|
583
|
+
description: "Task version you read."
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
output: {
|
|
587
|
+
schema: JSON_OUT,
|
|
588
|
+
render: (_args, value) => {
|
|
589
|
+
return [{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: value.trashed === true ? "任务已标记删除(等待用户在 GUI 清除)。" : "删除失败。"
|
|
592
|
+
}];
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
async execute(args, exec) {
|
|
596
|
+
try {
|
|
597
|
+
caller(exec);
|
|
598
|
+
const task = store.get(args.id);
|
|
599
|
+
if (task === void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
|
|
600
|
+
versionGuard(task, args.ifVersion);
|
|
601
|
+
const next = structuredClone(task);
|
|
602
|
+
next.trashedAt = deps.now();
|
|
603
|
+
next.version = task.version + 1;
|
|
604
|
+
await store.mutate("task-deleted", (ledger) => {
|
|
605
|
+
const i = ledger.tasks.findIndex((t) => t.id === args.id);
|
|
606
|
+
ledger.tasks[i] = next;
|
|
607
|
+
return [next];
|
|
608
|
+
});
|
|
609
|
+
return { trashed: true };
|
|
610
|
+
} catch (error) {
|
|
611
|
+
fail(error);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
})));
|
|
615
|
+
return disposers;
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
export { ERR, registerTaskboardTools, workspaceFace };
|
|
619
|
+
|
|
620
|
+
//# sourceMappingURL=tools.js.map
|