dsh-taskboard 0.3.3 → 0.4.1

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.
Files changed (44) hide show
  1. package/README.md +27 -6
  2. package/lib/client.js +1201 -42
  3. package/lib/host/execution.js +6 -1
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +95 -2
  6. package/lib/host/git.js.map +1 -1
  7. package/lib/host/protocol-text.js +5 -3
  8. package/lib/host/protocol-text.js.map +1 -1
  9. package/lib/host/routes.js +184 -2
  10. package/lib/host/routes.js.map +1 -1
  11. package/lib/host/store.js +12 -0
  12. package/lib/host/store.js.map +1 -1
  13. package/lib/host/templates.js +166 -0
  14. package/lib/host/templates.js.map +1 -0
  15. package/lib/host/tools.js +202 -2
  16. package/lib/host/tools.js.map +1 -1
  17. package/lib/index.js +7 -2
  18. package/lib/index.js.map +1 -1
  19. package/lib/shared/api.js.map +1 -1
  20. package/lib/shared/protocol.js +277 -1
  21. package/lib/shared/protocol.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/client/api.ts +28 -0
  24. package/src/client/board/ImportModal.tsx +182 -0
  25. package/src/client/board/TaskBoard.tsx +45 -3
  26. package/src/client/board/TaskCard.tsx +9 -0
  27. package/src/client/board/TaskDetail.tsx +192 -8
  28. package/src/client/board/TaskFormModal.tsx +100 -18
  29. package/src/client/board/TemplateManager.tsx +121 -0
  30. package/src/client/board-mount.tsx +8 -0
  31. package/src/client/controller.ts +152 -8
  32. package/src/client/sidebar-entry.ts +6 -3
  33. package/src/client/styles.ts +153 -0
  34. package/src/host/execution.ts +10 -2
  35. package/src/host/git.ts +77 -0
  36. package/src/host/protocol-text.ts +5 -3
  37. package/src/host/routes.ts +215 -0
  38. package/src/host/store.ts +13 -0
  39. package/src/host/templates.ts +143 -0
  40. package/src/host/tools.ts +198 -2
  41. package/src/index.ts +6 -0
  42. package/src/shared/api.ts +54 -0
  43. package/src/shared/protocol.ts +344 -0
  44. package/src/shared/version.ts +1 -1
@@ -0,0 +1,166 @@
1
+ import { readFile } from "node:fs/promises";
2
+ //#region src/host/templates.ts
3
+ /**
4
+ * Host-side task-template store (0.4.0): one JSON side file next to the
5
+ * ledger, seeded with the built-in templates on first load, mutated through
6
+ * the same atomic persist discipline as the ledger.
7
+ *
8
+ * Pure data, no Cordis deps — the routes layer owns it and tests drive it
9
+ * directly against a temp dir.
10
+ *
11
+ * @module dsh-taskboard/host/templates
12
+ */
13
+ /** The built-in templates seeded when the side file does not exist yet. */
14
+ const BUILTIN_TEMPLATES = [
15
+ {
16
+ id: "tpl-bugfix",
17
+ name: "Bug 修复",
18
+ task: {
19
+ title: "修复:",
20
+ prompt: [
21
+ "修复以下问题并按序交接:",
22
+ "1. 复现问题(写最小复现步骤或测试)",
23
+ "2. 定位根因,说明为什么会发生",
24
+ "3. 修复并补回归测试",
25
+ "4. 运行相关测试套件确认无回归"
26
+ ].join("\n"),
27
+ urgency: "urgent",
28
+ checklist: [
29
+ "已复现并定位根因",
30
+ "修复已提交到任务分支",
31
+ "回归测试通过"
32
+ ]
33
+ }
34
+ },
35
+ {
36
+ id: "tpl-release",
37
+ name: "发布检查",
38
+ task: {
39
+ title: "发布:",
40
+ prompt: "执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。",
41
+ urgency: "normal",
42
+ checklist: [
43
+ "版本号已更新(package.json 与版本常量同步)",
44
+ "构建通过",
45
+ "全部测试通过",
46
+ "变更记录已写"
47
+ ]
48
+ }
49
+ },
50
+ {
51
+ id: "tpl-patrol",
52
+ name: "例行巡检",
53
+ task: {
54
+ title: "巡检:",
55
+ prompt: [
56
+ "例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。",
57
+ "发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。",
58
+ "输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。"
59
+ ].join("\n"),
60
+ urgency: "relaxed",
61
+ execution: {
62
+ mode: "scheduled",
63
+ cron: "0 9 * * 1"
64
+ }
65
+ }
66
+ }
67
+ ];
68
+ /** Mint a template id. */
69
+ function newTemplateId() {
70
+ return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
71
+ }
72
+ /**
73
+ * The template store. NOT thread-synchronized like the ledger (template
74
+ * writes are rare, human-paced GUI operations; last-write-wins is fine).
75
+ */
76
+ var TemplateStore = class {
77
+ file;
78
+ templates;
79
+ loaded = false;
80
+ /** @param file - absolute side-file path (next to the ledger). */
81
+ constructor(file) {
82
+ this.file = file;
83
+ }
84
+ /** Load once; a missing file seeds the built-ins; a corrupt file resets. */
85
+ async ensure() {
86
+ if (this.loaded) return;
87
+ let parsed;
88
+ try {
89
+ const raw = await readFile(this.file, "utf8");
90
+ const value = JSON.parse(raw);
91
+ if (Array.isArray(value.templates)) parsed = value.templates.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.task === "object");
92
+ } catch {}
93
+ if (parsed === void 0) {
94
+ const now = Date.now();
95
+ parsed = BUILTIN_TEMPLATES.map((t, i) => ({
96
+ ...t,
97
+ task: { ...t.task },
98
+ builtin: true,
99
+ createdAt: now,
100
+ updatedAt: now + i
101
+ }));
102
+ try {
103
+ await this.persist(parsed);
104
+ } catch {}
105
+ }
106
+ this.templates = parsed;
107
+ this.loaded = true;
108
+ }
109
+ /** Atomic persist (temp + rename), same discipline as the ledger. */
110
+ async persist(templates) {
111
+ const { mkdir, writeFile, rename } = await import("node:fs/promises");
112
+ const { dirname, join } = await import("node:path");
113
+ await mkdir(dirname(this.file), { recursive: true });
114
+ const temp = join(dirname(this.file), `.${Math.random().toString(36).slice(2)}.tmp`);
115
+ await writeFile(temp, JSON.stringify({ templates }, null, 2), "utf8");
116
+ await rename(temp, this.file);
117
+ }
118
+ /** All templates (oldest first). */
119
+ async list() {
120
+ await this.ensure();
121
+ return (this.templates ?? []).slice();
122
+ }
123
+ /**
124
+ * Create or replace a template by id (a body without id creates).
125
+ * @returns the stored template.
126
+ */
127
+ async upsert(input) {
128
+ await this.ensure();
129
+ const templates = this.templates ?? [];
130
+ const name = input.name.trim();
131
+ if (name.length === 0 || name.length > 60) throw new Error("模板名必须 1..60 字符");
132
+ const now = Date.now();
133
+ const existing = input.id !== void 0 ? templates.find((t) => t.id === input.id) : void 0;
134
+ const stored = existing !== void 0 ? {
135
+ ...existing,
136
+ name,
137
+ task: input.task,
138
+ updatedAt: now
139
+ } : {
140
+ id: input.id ?? newTemplateId(),
141
+ name,
142
+ task: input.task,
143
+ createdAt: now,
144
+ updatedAt: now
145
+ };
146
+ const index = existing !== void 0 ? templates.indexOf(existing) : -1;
147
+ if (index >= 0) templates[index] = stored;
148
+ else templates.push(stored);
149
+ await this.persist(templates);
150
+ return stored;
151
+ }
152
+ /** Delete a template by id; returns whether it existed. */
153
+ async remove(id) {
154
+ await this.ensure();
155
+ const templates = this.templates ?? [];
156
+ const index = templates.findIndex((t) => t.id === id);
157
+ if (index < 0) return false;
158
+ templates.splice(index, 1);
159
+ await this.persist(templates);
160
+ return true;
161
+ }
162
+ };
163
+ //#endregion
164
+ export { BUILTIN_TEMPLATES, TemplateStore };
165
+
166
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","names":[],"sources":["../../src/host/templates.ts"],"sourcesContent":["/**\n * Host-side task-template store (0.4.0): one JSON side file next to the\n * ledger, seeded with the built-in templates on first load, mutated through\n * the same atomic persist discipline as the ledger.\n *\n * Pure data, no Cordis deps — the routes layer owns it and tests drive it\n * directly against a temp dir.\n *\n * @module dsh-taskboard/host/templates\n */\nimport { readFile } from 'node:fs/promises'\nimport type { TaskTemplate } from '../shared/api.ts'\n\n/** The built-in templates seeded when the side file does not exist yet. */\nexport const BUILTIN_TEMPLATES: ReadonlyArray<{ id: string; name: string; task: TaskTemplate['task'] }> = [\n {\n id: 'tpl-bugfix',\n name: 'Bug 修复',\n task: {\n title: '修复:',\n prompt: [\n '修复以下问题并按序交接:',\n '1. 复现问题(写最小复现步骤或测试)',\n '2. 定位根因,说明为什么会发生',\n '3. 修复并补回归测试',\n '4. 运行相关测试套件确认无回归',\n ].join('\\n'),\n urgency: 'urgent',\n checklist: ['已复现并定位根因', '修复已提交到任务分支', '回归测试通过'],\n },\n },\n {\n id: 'tpl-release',\n name: '发布检查',\n task: {\n title: '发布:',\n prompt: '执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。',\n urgency: 'normal',\n checklist: ['版本号已更新(package.json 与版本常量同步)', '构建通过', '全部测试通过', '变更记录已写'],\n },\n },\n {\n id: 'tpl-patrol',\n name: '例行巡检',\n task: {\n title: '巡检:',\n prompt: [\n '例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。',\n '发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。',\n '输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。',\n ].join('\\n'),\n urgency: 'relaxed',\n execution: { mode: 'scheduled', cron: '0 9 * * 1' },\n },\n },\n]\n\n/** Mint a template id. */\nfunction newTemplateId(): string {\n return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * The template store. NOT thread-synchronized like the ledger (template\n * writes are rare, human-paced GUI operations; last-write-wins is fine).\n */\nexport class TemplateStore {\n private templates: TaskTemplate[] | undefined\n private loaded = false\n\n /** @param file - absolute side-file path (next to the ledger). */\n constructor(private readonly file: string) {}\n\n /** Load once; a missing file seeds the built-ins; a corrupt file resets. */\n private async ensure(): Promise<void> {\n if (this.loaded) return\n let parsed: TaskTemplate[] | undefined\n try {\n const raw = await readFile(this.file, 'utf8')\n const value = JSON.parse(raw) as { templates?: unknown }\n if (Array.isArray(value.templates)) {\n parsed = value.templates.filter((t): t is TaskTemplate =>\n typeof t === 'object' && t !== null && typeof (t as TaskTemplate).id === 'string'\n && typeof (t as TaskTemplate).name === 'string' && typeof (t as TaskTemplate).task === 'object')\n }\n } catch { /* missing or corrupt → seed */ }\n if (parsed === undefined) {\n const now = Date.now()\n parsed = BUILTIN_TEMPLATES.map((t, i) => ({ ...t, task: { ...t.task }, builtin: true, createdAt: now, updatedAt: now + i }))\n try { await this.persist(parsed) } catch { /* best effort — the seed returns in-memory */ }\n }\n this.templates = parsed\n this.loaded = true\n }\n\n /** Atomic persist (temp + rename), same discipline as the ledger. */\n private async persist(templates: TaskTemplate[]): Promise<void> {\n const { mkdir, writeFile, rename } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n await mkdir(dirname(this.file), { recursive: true })\n const temp = join(dirname(this.file), `.${Math.random().toString(36).slice(2)}.tmp`)\n await writeFile(temp, JSON.stringify({ templates }, null, 2), 'utf8')\n await rename(temp, this.file)\n }\n\n /** All templates (oldest first). */\n async list(): Promise<TaskTemplate[]> {\n await this.ensure()\n return (this.templates ?? []).slice()\n }\n\n /**\n * Create or replace a template by id (a body without id creates).\n * @returns the stored template.\n */\n async upsert(input: { id?: string; name: string; task: TaskTemplate['task'] }): Promise<TaskTemplate> {\n await this.ensure()\n const templates = this.templates ?? []\n const name = input.name.trim()\n if (name.length === 0 || name.length > 60) throw new Error('模板名必须 1..60 字符')\n const now = Date.now()\n const existing = input.id !== undefined ? templates.find(t => t.id === input.id) : undefined\n const stored: TaskTemplate = existing !== undefined\n ? { ...existing, name, task: input.task, updatedAt: now }\n : { id: input.id ?? newTemplateId(), name, task: input.task, createdAt: now, updatedAt: now }\n const index = existing !== undefined ? templates.indexOf(existing) : -1\n if (index >= 0) templates[index] = stored\n else templates.push(stored)\n await this.persist(templates)\n return stored\n }\n\n /** Delete a template by id; returns whether it existed. */\n async remove(id: string): Promise<boolean> {\n await this.ensure()\n const templates = this.templates ?? []\n const index = templates.findIndex(t => t.id === id)\n if (index < 0) return false\n templates.splice(index, 1)\n await this.persist(templates)\n return true\n }\n}\n"],"mappings":"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"}
package/lib/host/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { asIsolation, asStatus, asUrgency, canTransition, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
1
+ import { asIsolation, asStatus, asUrgency, canTransition, checklistFromTexts, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
2
2
  import { defineTool } from "./sdk.js";
3
3
  //#region src/host/tools.ts
4
4
  /** Render side: one compact task line (id/status/version are load-bearing). */
@@ -7,6 +7,7 @@ function taskLine(t) {
7
7
  if (t.blocked) parts.push("·受阻");
8
8
  if (t.executionMode === "scheduled") parts.push("·定时");
9
9
  if (t.commentCount !== void 0 && t.commentCount > 0) parts.push(`·评论${t.commentCount}`);
10
+ if (t.checklist !== void 0 && t.checklist.total > 0) parts.push(`·清单${t.checklist.done}/${t.checklist.total}`);
10
11
  if (t.lastExecutionOutcome !== void 0) parts.push(`·上次执行${t.lastExecutionOutcome}`);
11
12
  if (t.trashed === true) parts.push("·已删");
12
13
  return parts.join(" ");
@@ -26,6 +27,16 @@ function taskDetail(t) {
26
27
  if (t.presetId !== void 0) lines.push(`执行模式: ${t.presetId}(未指定时为部署默认 preset)`);
27
28
  lines.push(`描述: ${t.description.length > 0 ? t.description : "(无)"}`);
28
29
  lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`);
30
+ if (t.checklist !== void 0 && t.checklist.length > 0) {
31
+ const done = t.checklist.filter((i) => i.checked).length;
32
+ lines.push(`验收清单 (${done}/${t.checklist.length}):`);
33
+ for (const item of t.checklist) {
34
+ const mark = item.checked ? "☑" : "☐";
35
+ const who = item.checkedBy === void 0 ? "" : item.checkedBy === "user" ? " ·用户勾选" : ` ·agent ${String(item.checkedBy).slice(0, 24)}勾选`;
36
+ const note = item.note !== void 0 ? ` ·证据: ${item.note}` : "";
37
+ lines.push(` ${mark} ${item.text}${who}${note}`);
38
+ }
39
+ }
29
40
  if (t.comments.length > 0) {
30
41
  lines.push(`评论 (${t.comments.length}):`);
31
42
  for (const c of t.comments) {
@@ -38,7 +49,8 @@ function taskDetail(t) {
38
49
  for (const e of t.executions) {
39
50
  const at = e.startedAt !== void 0 ? new Date(e.startedAt).toISOString() : "?";
40
51
  const err = e.error !== void 0 ? ` 错误: ${e.error}` : "";
41
- lines.push(` - [${e.trigger} ${at}] ${e.outcome}${err}`);
52
+ const report = e.report !== void 0 ? " [已交报告]" : "";
53
+ lines.push(` - [${e.trigger} ${at}] ${e.outcome}${report}${err}`);
42
54
  }
43
55
  } else lines.push("执行记录: 无");
44
56
  const updatedBy = t.updatedBy.kind === "agent" ? `agent ${String(t.updatedBy.sessionId).slice(0, 24)}` : "user";
@@ -303,6 +315,11 @@ function registerTaskboardTools(ctx, deps) {
303
315
  presetId: {
304
316
  type: "string",
305
317
  description: "Agent preset the execution session is composed from (its tool set / persona); default = the deployment default preset. Optional."
318
+ },
319
+ checklist: {
320
+ type: "array",
321
+ description: `Acceptance checklist (DoD) item texts (≤30 × 200 chars); agents check them off at handoff, the user reviews.`,
322
+ items: { type: "string" }
306
323
  }
307
324
  },
308
325
  output: {
@@ -327,6 +344,7 @@ function registerTaskboardTools(ctx, deps) {
327
344
  const model = args.model !== void 0 ? checkModel(deps, args.model) : void 0;
328
345
  const isolation = args.isolation === void 0 ? void 0 : asIsolation(args.isolation);
329
346
  const presetId = args.presetId?.trim() || void 0;
347
+ const checklist = args.checklist !== void 0 ? checklistFromTexts(args.checklist) : void 0;
330
348
  const now = deps.now();
331
349
  const task = {
332
350
  id: newTaskId(),
@@ -341,6 +359,7 @@ function registerTaskboardTools(ctx, deps) {
341
359
  model,
342
360
  ...isolation !== void 0 ? { isolation } : {},
343
361
  ...presetId !== void 0 ? { presetId } : {},
362
+ ...checklist !== void 0 ? { checklist } : {},
344
363
  version: 1,
345
364
  createdAt: now,
346
365
  updatedAt: now,
@@ -636,6 +655,187 @@ function registerTaskboardTools(ctx, deps) {
636
655
  }
637
656
  }
638
657
  })));
658
+ disposers.push(register(defineTool({
659
+ name: "taskboard_checklist",
660
+ description: "Manage the task's acceptance checklist (DoD). Actions: \"add\" (append item texts, ≤10 per call), \"check\" (mark an item done, with an optional evidence note), \"uncheck\" (reopen an item). Checking items NEVER completes the task — done stays a user-only action. Requires ifVersion.",
661
+ parameters: {
662
+ id: {
663
+ type: "string",
664
+ required: true,
665
+ description: "Task id."
666
+ },
667
+ action: {
668
+ type: "string",
669
+ required: true,
670
+ description: "add | check | uncheck."
671
+ },
672
+ ifVersion: {
673
+ type: "number",
674
+ required: true,
675
+ description: "Task version you read; fails on mismatch."
676
+ },
677
+ items: {
678
+ type: "array",
679
+ description: "Item texts to append (action=add only; 1..10 per call, 200 chars each).",
680
+ items: { type: "string" }
681
+ },
682
+ itemId: {
683
+ type: "string",
684
+ description: "The checklist item id (action=check/uncheck)."
685
+ },
686
+ note: {
687
+ type: "string",
688
+ description: "Evidence note recorded with the check (≤400 chars)."
689
+ }
690
+ },
691
+ output: {
692
+ schema: JSON_OUT,
693
+ render: (_args, value) => {
694
+ const v = value;
695
+ if (v.task === void 0 || v.checklist === void 0) return [{
696
+ type: "text",
697
+ text: "清单操作失败。"
698
+ }];
699
+ const lines = v.checklist.map((i, index) => `${i.checked === true ? "☑" : "☐"} [${index + 1}] ${String(i.text)}${i.note !== void 0 ? `(证据: ${String(i.note)})` : ""} id=${String(i.id)}`);
700
+ return [{
701
+ type: "text",
702
+ text: `任务 ${v.task.id} 验收清单 ${v.done ?? 0}/${v.total ?? 0} 已完成,当前 v${v.task.version}:\n${lines.join("\n")}`
703
+ }];
704
+ }
705
+ },
706
+ async execute(args, exec) {
707
+ try {
708
+ const { actor } = caller(exec);
709
+ const task = store.get(args.id);
710
+ if (task === void 0 || task.trashedAt !== void 0) throw new ToolError(ERR.notFound, `no task ${args.id}`);
711
+ versionGuard(task, args.ifVersion);
712
+ if (task.status === "archived") throw new ToolError(ERR.invalidTransition, "archived tasks are immutable");
713
+ const next = structuredClone(task);
714
+ const checklist = next.checklist === void 0 ? [] : [...next.checklist];
715
+ if (args.action === "add") {
716
+ const texts = args.items ?? [];
717
+ if (texts.length === 0 || texts.length > 10) throw new ToolError(ERR.invalidInput, "items must carry 1..10 texts per add call");
718
+ if (checklist.length + texts.length > 30) throw new ToolError(ERR.invalidInput, `checklist may hold at most 30 items (currently ${checklist.length})`);
719
+ checklist.push(...checklistFromTexts(texts));
720
+ } else if (args.action === "check") {
721
+ if (args.itemId === void 0) throw new ToolError(ERR.invalidInput, "itemId is required for check");
722
+ const item = checklist.find((i) => i.id === args.itemId);
723
+ if (item === void 0) throw new ToolError(ERR.notFound, `no checklist item ${args.itemId} (task ${args.id})`);
724
+ const note = args.note !== void 0 && args.note.trim().length > 0 ? args.note.trim().slice(0, 400) : void 0;
725
+ item.checked = true;
726
+ item.checkedBy = actor.sessionId;
727
+ item.checkedAt = deps.now();
728
+ if (note !== void 0) item.note = note;
729
+ } else if (args.action === "uncheck") {
730
+ if (args.itemId === void 0) throw new ToolError(ERR.invalidInput, "itemId is required for uncheck");
731
+ const item = checklist.find((i) => i.id === args.itemId);
732
+ if (item === void 0) throw new ToolError(ERR.notFound, `no checklist item ${args.itemId} (task ${args.id})`);
733
+ item.checked = false;
734
+ delete item.checkedBy;
735
+ delete item.checkedAt;
736
+ delete item.note;
737
+ } else throw new ToolError(ERR.invalidInput, `action must be add | check | uncheck (got "${args.action}")`);
738
+ if (checklist.length > 0) next.checklist = checklist;
739
+ else delete next.checklist;
740
+ next.version = task.version + 1;
741
+ next.updatedAt = deps.now();
742
+ next.updatedBy = actor;
743
+ await store.mutate("task-updated", (ledger) => {
744
+ const i = ledger.tasks.findIndex((t) => t.id === args.id);
745
+ ledger.tasks[i] = next;
746
+ return [next];
747
+ });
748
+ const progress = next.checklist !== void 0 ? {
749
+ done: next.checklist.filter((i) => i.checked).length,
750
+ total: next.checklist.length
751
+ } : {
752
+ done: 0,
753
+ total: 0
754
+ };
755
+ return json({
756
+ task: {
757
+ id: next.id,
758
+ version: next.version
759
+ },
760
+ checklist: next.checklist ?? [],
761
+ ...progress
762
+ });
763
+ } catch (error) {
764
+ fail(error);
765
+ }
766
+ }
767
+ })));
768
+ disposers.push(register(defineTool({
769
+ name: "taskboard_execution_report",
770
+ description: "Submit the structured execution report for the task you are currently executing (summary / changed files / how you verified / artifacts / remaining risk). Submit BEFORE moving the task to in_review; a later submission overwrites the previous report. Commits and diffs are host-collected — do not repeat them.",
771
+ parameters: {
772
+ summary: {
773
+ type: "string",
774
+ required: true,
775
+ description: "What was done (1..2000 chars)."
776
+ },
777
+ changedFiles: {
778
+ type: "array",
779
+ description: "Files you changed (paths, ≤50 × 300 chars).",
780
+ items: { type: "string" }
781
+ },
782
+ checks: {
783
+ type: "array",
784
+ description: "How the work was verified (e.g. test commands + outcomes, ≤50 entries).",
785
+ items: { type: "string" }
786
+ },
787
+ artifacts: {
788
+ type: "array",
789
+ description: "Artifacts worth reviewing (build outputs, screenshots, docs, ≤30 entries).",
790
+ items: { type: "string" }
791
+ },
792
+ risk: {
793
+ type: "string",
794
+ description: "Known remaining risks or follow-ups (≤2000 chars, optional)."
795
+ }
796
+ },
797
+ output: {
798
+ schema: JSON_OUT,
799
+ render: (_args, value) => {
800
+ const v = value;
801
+ if (v.taskId === void 0 || v.report === void 0) return [{
802
+ type: "text",
803
+ text: "报告提交失败。"
804
+ }];
805
+ return [{
806
+ type: "text",
807
+ text: `执行报告已记录到任务 ${v.taskId}(执行 ${v.executionId}):${v.report.summary?.slice(0, 120) ?? ""}\n接下来:taskboard_comment_add 留交接评论,然后 taskboard_move 移至待验收 in_review。`
808
+ }];
809
+ }
810
+ },
811
+ async execute(args, exec) {
812
+ try {
813
+ const { sessionId } = caller(exec);
814
+ const report = normalizeExecutionReport(args);
815
+ let taskId;
816
+ let executionId;
817
+ await store.mutate("execution-recorded", (ledger) => {
818
+ for (const task of ledger.tasks) {
819
+ const execution = task.executions.find((e) => e.sessionId === sessionId && e.outcome === "running");
820
+ if (execution !== void 0) {
821
+ execution.report = report;
822
+ taskId = task.id;
823
+ executionId = execution.id;
824
+ return [task];
825
+ }
826
+ }
827
+ });
828
+ if (taskId === void 0 || executionId === void 0) throw new ToolError(ERR.forbidden, "no running execution belongs to this session — the report can only be submitted while the taskboard execution session is still running");
829
+ return json({
830
+ taskId,
831
+ executionId,
832
+ report
833
+ });
834
+ } catch (error) {
835
+ fail(error);
836
+ }
837
+ }
838
+ })));
639
839
  return disposers;
640
840
  }
641
841
  //#endregion