@zhushanwen/pi-todo 0.5.2 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "AI-driven todo list for Pi — stateful task management with session persistence and /todos command.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -25,7 +25,7 @@
25
25
  "vitest": "^4.1.8"
26
26
  },
27
27
  "dependencies": {
28
- "@xyz-agent/extension-protocol": "^0.3.1"
28
+ "@xyz-agent/extension-protocol": "^0.4.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "*",
@@ -31,9 +31,4 @@ describe("buildGui", () => {
31
31
  expect(gui.component.props.items).toEqual([]);
32
32
  });
33
33
 
34
- it("isVerification todo still maps correctly", () => {
35
- const todos: Todo[] = [{ id: 1, text: "verify", status: "pending", isVerification: true }];
36
- const gui = buildGui(todos);
37
- expect(gui.component.props.items[0]).toMatchObject({ icon: "dot", label: "#1: verify" });
38
- });
39
34
  });
@@ -42,6 +42,8 @@ describe("handleCompletionSteer", () => {
42
42
  expect(handleCompletionSteer(s)).toBe(true);
43
43
  expect(s.completionSteered).toBe(true);
44
44
  expect(s.pendingSteerMessage).toContain("交付质量");
45
+ expect(s.pendingSteerMessage).toContain("检查实际产出");
46
+ expect(s.pendingSteerMessage).toContain("不要凭印象");
45
47
  });
46
48
 
47
49
  it("does not steer twice (single-shot lock)", () => {
@@ -148,13 +150,40 @@ describe("handleReminder", () => {
148
150
 
149
151
  // ── reminder / context builders ─────────────────────
150
152
 
151
- describe("buildMinimalReminder", () => {
152
- it("mentions the next pending task", () => {
153
+ describe("buildMinimalReminder", () => {
154
+ it("mentions the next pending task with action directive", () => {
153
155
  const s = makeState([
154
156
  { id: 1, text: "done", status: "completed" },
155
157
  { id: 2, text: "next", status: "pending" },
156
158
  ]);
157
- expect(buildMinimalReminder(s)).toContain("#2 next");
159
+ const out = buildMinimalReminder(s);
160
+ expect(out).toContain("#2 next");
161
+ expect(out).toContain("必须处理");
162
+ expect(out).toContain("todo update");
163
+ });
164
+
165
+ it("includes in_progress todos as pending", () => {
166
+ const s = makeState([
167
+ { id: 1, text: "working", status: "in_progress" },
168
+ { id: 2, text: "next", status: "pending" },
169
+ ]);
170
+ expect(buildMinimalReminder(s)).toContain("#1 working");
171
+ });
172
+
173
+ it("excludes cancelled todos from reminder", () => {
174
+ const s = makeState([
175
+ { id: 1, text: "cancelled task", status: "cancelled" },
176
+ { id: 2, text: "active", status: "in_progress" },
177
+ ]);
178
+ const out = buildMinimalReminder(s);
179
+ expect(out).toContain("#2 active");
180
+ expect(out).not.toContain("#1");
181
+ });
182
+
183
+ it("returns empty string when only cancelled remain", () => {
184
+ expect(
185
+ buildMinimalReminder(makeState([{ id: 1, text: "x", status: "cancelled" }])),
186
+ ).toBe("");
158
187
  });
159
188
 
160
189
  it("returns empty string when no pending", () => {
@@ -176,6 +205,31 @@ describe("buildBeforeAgentStartMessage", () => {
176
205
  expect(m!.message.content).not.toContain("#2");
177
206
  });
178
207
 
208
+ it("injects action directives (process first / mark completed / stalled != done)", () => {
209
+ const s = makeState([{ id: 1, text: "a", status: "in_progress" }]);
210
+ const m = buildBeforeAgentStartMessage(s);
211
+ expect(m!.message.content).toContain("开始工作前先推进 pending 任务");
212
+ expect(m!.message.content).toContain("todo update 标记 completed");
213
+ expect(m!.message.content).toContain("搁置不等于完成");
214
+ });
215
+
216
+ it("excludes cancelled todos from context injection", () => {
217
+ const s = makeState([
218
+ { id: 1, text: "cancelled", status: "cancelled" },
219
+ { id: 2, text: "active", status: "pending" },
220
+ ]);
221
+ const m = buildBeforeAgentStartMessage(s);
222
+ expect(m).toBeDefined();
223
+ expect(m!.message.content).toContain("#2: active");
224
+ expect(m!.message.content).not.toContain("#1");
225
+ });
226
+
227
+ it("returns undefined when only cancelled remain", () => {
228
+ expect(
229
+ buildBeforeAgentStartMessage(makeState([{ id: 1, text: "x", status: "cancelled" }])),
230
+ ).toBeUndefined();
231
+ });
232
+
179
233
  it("returns undefined when list empty", () => {
180
234
  expect(buildBeforeAgentStartMessage(makeState([]))).toBeUndefined();
181
235
  });
@@ -225,6 +279,29 @@ describe("reconstructState", () => {
225
279
  reconstructState(s, makeCtx(entries));
226
280
  expect(s.todos[0].status).toBe("pending");
227
281
  });
282
+
283
+ it("skips dirty (null/primitive) elements without throwing", () => {
284
+ const entries = [
285
+ todoEntry([null, { id: 1, text: "ok", status: "pending" }] as unknown as Todo[], 3),
286
+ ];
287
+ const s = createTodoSessionState();
288
+ expect(() => reconstructState(s, makeCtx(entries))).not.toThrow();
289
+ expect(s.todos).toHaveLength(1);
290
+ expect(s.todos[0].id).toBe(1);
291
+ expect(s.nextId).toBe(3);
292
+ });
293
+
294
+ it("ignores snapshot when all elements are dirty (replay continues)", () => {
295
+ const entries = [
296
+ todoEntry([null, "garbage"] as unknown as Todo[], 5),
297
+ todoEntry([{ id: 9, text: "valid", status: "pending" }], 10),
298
+ ];
299
+ const s = createTodoSessionState();
300
+ expect(() => reconstructState(s, makeCtx(entries))).not.toThrow();
301
+ expect(s.todos).toHaveLength(1);
302
+ expect(s.todos[0].id).toBe(9);
303
+ expect(s.nextId).toBe(10);
304
+ });
228
305
  });
229
306
 
230
307
  // ── agent_end integration (短路顺序) ────────────────
@@ -53,17 +53,17 @@ describe("Todo data model", () => {
53
53
  expect(migrated.status).toBe("pending");
54
54
  });
55
55
 
56
- it("should preserve isVerification flag (FR-6)", () => {
57
- const todo = { id: 1, text: "run tests", status: "pending", isVerification: true } as unknown as Todo;
58
- const migrated = migrateTodo(todo);
59
- expect(migrated.isVerification).toBe(true);
60
- });
61
-
62
56
  it("should preserve cancelled status (FR-1 four-state)", () => {
63
57
  const todo = { id: 1, text: "dropped", status: "cancelled" } as unknown as Todo;
64
58
  const migrated = migrateTodo(todo);
65
59
  expect(migrated.status).toBe("cancelled");
66
60
  });
61
+
62
+ it("should throw on null/primitive input (dirty data guard)", () => {
63
+ expect(() => migrateTodo(null)).toThrow(TypeError);
64
+ expect(() => migrateTodo(undefined)).toThrow(TypeError);
65
+ expect(() => migrateTodo("garbage")).toThrow(TypeError);
66
+ });
67
67
  });
68
68
 
69
69
  // ── todo add ────────────────────────────────────────
@@ -103,25 +103,6 @@ describe("todo add", () => {
103
103
  expect(result.error).toBeUndefined();
104
104
  expect(result.newTodos[0].text).toBe("new task");
105
105
  });
106
-
107
- it("should mark todos as verification when isVerification=true (FR-6)", () => {
108
- const result = addTodos([], 1, ["run tests", "typecheck"], true);
109
- expect(result.error).toBeUndefined();
110
- expect(result.newTodos[0].isVerification).toBe(true);
111
- expect(result.newTodos[1].isVerification).toBe(true);
112
- });
113
-
114
- it("should not set isVerification when omitted", () => {
115
- const result = addTodos([], 1, ["regular task"]);
116
- expect(result.error).toBeUndefined();
117
- expect(result.newTodos[0].isVerification).toBeUndefined();
118
- });
119
-
120
- it("should not set isVerification when isVerification=false", () => {
121
- const result = addTodos([], 1, ["regular task"], false);
122
- expect(result.error).toBeUndefined();
123
- expect(result.newTodos[0].isVerification).toBeUndefined();
124
- });
125
106
  });
126
107
 
127
108
  // ── todo update batch ───────────────────────────────
@@ -177,39 +158,24 @@ describe("todo update batch", () => {
177
158
  expect(result.error).toContain("invalid status");
178
159
  });
179
160
 
180
- it("FR-6: cancelled todo 不可恢复(status 更新拒绝)", () => {
161
+ it("cancelled todo 不可恢复(status 更新拒绝)", () => {
181
162
  const todos: Todo[] = [{ id: 1, text: "dropped", status: "cancelled" }];
182
163
  const result = updateTodos(todos, [{ id: 1, status: "pending" }]);
183
164
  expect(result.error).toBe("id 1 is cancelled");
184
165
  expect(result.resultText).toContain("cannot be restored");
185
166
  expect(result.updatedTodos).toEqual(todos);
186
167
  });
187
-
188
- it("FR-6: 验证任务不可 cancelled", () => {
189
- const todos: Todo[] = [{ id: 2, text: "run tests", status: "in_progress", isVerification: true }];
190
- const result = updateTodos(todos, [{ id: 2, status: "cancelled" }]);
191
- expect(result.error).toBe("id 2 is verification todo");
192
- expect(result.resultText).toContain("cannot be cancelled");
193
- expect(result.updatedTodos).toEqual(todos);
194
- });
195
168
  });
196
169
 
197
- // ── handleSingleUpdate FR-6 守卫(tool 单条路径)────
170
+ // ── handleSingleUpdate 守卫(tool 单条路径)────
198
171
 
199
- describe("handleSingleUpdate FR-6 guards (tool single path)", () => {
200
- it("FR-6: cancelled todo + status → cannot restore", () => {
172
+ describe("handleSingleUpdate guards (tool single path)", () => {
173
+ it("cancelled todo + status → cannot restore", () => {
201
174
  const state = createTodoSessionState();
202
175
  state.todos = [{ id: 1, text: "dropped", status: "cancelled" }];
203
176
  expect(() => handleSingleUpdate(state, { action: "update", id: 1, status: "pending" }))
204
177
  .toThrow("#1 is cancelled (cannot restore)");
205
178
  });
206
-
207
- it("FR-6: verification todo + status=cancelled → cannot cancel", () => {
208
- const state = createTodoSessionState();
209
- state.todos = [{ id: 2, text: "run tests", status: "in_progress", isVerification: true }];
210
- expect(() => handleSingleUpdate(state, { action: "update", id: 2, status: "cancelled" }))
211
- .toThrow("#2 is verification todo (cannot cancel)");
212
- });
213
179
  });
214
180
 
215
181
  // ── completed 无拦截 ────────────────────────────────
@@ -42,10 +42,6 @@ describe("todo description — 给完整 JSON 正例", () => {
42
42
  expect(DESCRIPTION_REGION).toContain('{"action":"add","texts"');
43
43
  });
44
44
 
45
- it("add+verification 正例:含 isVerification", () => {
46
- expect(DESCRIPTION_REGION).toContain('"isVerification":true');
47
- });
48
-
49
45
  it("update single 正例:含 {\"action\":\"update\",\"id\"", () => {
50
46
  expect(DESCRIPTION_REGION).toContain('{"action":"update","id"');
51
47
  });
@@ -103,3 +99,27 @@ describe("todo runtime — throw 含 Correct 纠错正例", () => {
103
99
  expect(TOOL_SRC).toContain('"id" — that field is for update');
104
100
  });
105
101
  });
102
+
103
+ // ── promptSnippet / promptGuidelines 验证引导(verification guidance)──
104
+ // DESCRIPTION_REGION 截取止于 promptSnippet:,不含这两个字段,故对完整 TOOL_SRC 断言。
105
+
106
+ describe("todo tool prompt — verification guidance", () => {
107
+ it("promptSnippet 引导验证步骤建 todo", () => {
108
+ expect(TOOL_SRC).toContain(
109
+ "Consider adding a separate todo for verification checks like running tests or typecheck.",
110
+ );
111
+ });
112
+
113
+ it("promptSnippet 声明多步骤工作场景", () => {
114
+ expect(TOOL_SRC).toContain("Use todo when breaking multi-step work into trackable items.");
115
+ });
116
+
117
+ it("promptGuidelines 含 [验证任务] 条目(完成前确保验证通过)", () => {
118
+ expect(TOOL_SRC).toContain("[验证任务] 为测试 / 类型检查等验证步骤单独建 todo,完成前确保验证通过");
119
+ });
120
+
121
+ it("promptGuidelines 含 [自动闭合] / [批量优先] 核心条目", () => {
122
+ expect(TOOL_SRC).toContain("[自动闭合] 全部完成后工具自动清理,无需手动 clear");
123
+ expect(TOOL_SRC).toContain("[批量优先] 完成多项任务时使用 updates[] 批量更新");
124
+ });
125
+ });
@@ -20,7 +20,7 @@ interface ExecuteResult {
20
20
  content: Array<{ type: "text"; text: string }>;
21
21
  details: {
22
22
  action: string;
23
- todos: Array<{ id: number; text: string; status: string; isVerification?: boolean }>;
23
+ todos: Array<{ id: number; text: string; status: string }>;
24
24
  nextId: number;
25
25
  __gui__?: {
26
26
  v: number;
package/src/handlers.ts CHANGED
@@ -24,24 +24,29 @@ const REMINDER_INTERVAL = 2;
24
24
 
25
25
  export type RefreshDisplayFn = (ctx: ExtensionContext) => void;
26
26
 
27
- /** 构建极简提醒:只含下一个推荐任务 */
27
+ /** 未完成任务判定:pending / in_progress(cancelled 不可恢复,从提醒排除) */
28
+ function isPending(t: TodoDetails["todos"][number]): boolean {
29
+ return t.status === "pending" || t.status === "in_progress";
30
+ }
31
+
32
+ /** 构建极简提醒:只含下一个推荐任务 + 行动指令 */
28
33
  export function buildMinimalReminder(state: TodoSessionState): string {
29
- const pendingTodos = state.todos.filter((t) => t.status !== "completed");
34
+ const pendingTodos = state.todos.filter(isPending);
30
35
  if (pendingTodos.length === 0) return "";
31
36
 
32
37
  const next = pendingTodos[0];
33
- return `<todo_context>\n[TODO] 你有 ${pendingTodos.length} 个未完成任务。下一个应处理:#${next.id} ${next.text}\n</todo_context>`;
38
+ return `<todo_context>\n[TODO] 你有 ${pendingTodos.length} 个未完成任务已搁置。下一个必须处理:#${next.id} ${next.text}。完成后用 todo update 标记 completed,不要继续搁置。\n</todo_context>`;
34
39
  }
35
40
 
36
41
  export function buildBeforeAgentStartMessage(state: TodoSessionState): { message: { customType: string; content: string; display: boolean } } | undefined {
37
42
  if (state.todos.length === 0) return undefined;
38
43
 
39
- const pendingTodos = state.todos.filter((t) => t.status !== "completed");
44
+ const pendingTodos = state.todos.filter(isPending);
40
45
  if (pendingTodos.length === 0) return undefined;
41
46
 
42
47
  const lines = pendingTodos.map((t) => `#${t.id}: ${t.text}`);
43
48
  const contextStr =
44
- `<todo_context>\n[TODO] ${pendingTodos.length} tasks pending\n${lines.join("\n")}\n</todo_context>`;
49
+ `<todo_context>\n[TODO] ${pendingTodos.length} 个未完成任务待处理:\n${lines.join("\n")}\n处理规则:开始工作前先推进 pending 任务;任务做完后立即用 todo update 标记 completed,不要搁置 pending 状态(搁置不等于完成)。\n</todo_context>`;
45
50
 
46
51
  return {
47
52
  message: {
@@ -75,9 +80,21 @@ export function reconstructState(state: TodoSessionState, ctx: ExtensionContext)
75
80
 
76
81
  const details = msg.details as TodoDetails | undefined;
77
82
  if (details?.todos && Array.isArray(details.todos)) {
78
- state.todos = details.todos.map((t) => migrateTodo(t));
79
- state.nextId = details.nextId ?? (state.todos.length > 0 ? Math.max(...state.todos.map((t) => t.id)) + 1 : 1);
80
- latestIdx = i;
83
+ // 脏数据降级:单条迁移失败(null/primitive)跳过该条,全部失败则忽略整个快照,不中断回放
84
+ const migrated: TodoDetails["todos"] = [];
85
+ for (const t of details.todos) {
86
+ try {
87
+ migrated.push(migrateTodo(t));
88
+ } catch (e) {
89
+ // best-effort 降级:脏数据(null/primitive)跳过该条,不中断会话回放
90
+ console.debug("[todo] reconstructState: skipping dirty todo entry:", e);
91
+ }
92
+ }
93
+ if (migrated.length > 0) {
94
+ state.todos = migrated;
95
+ state.nextId = details.nextId ?? Math.max(...migrated.map((t) => t.id)) + 1;
96
+ latestIdx = i;
97
+ }
81
98
  }
82
99
  }
83
100
 
@@ -124,7 +141,7 @@ export function handleCompletionSteer(state: TodoSessionState): boolean {
124
141
  if (!allCompleted) return false;
125
142
 
126
143
  state.completionSteered = true;
127
- state.pendingSteerMessage = `<todo_context>\n[TODO] 所有任务已完成。请快速检查每项任务的交付质量。\n</todo_context>`;
144
+ state.pendingSteerMessage = `<todo_context>\n[TODO] 所有任务已标记完成。请逐项核对交付质量(不要凭印象,检查实际产出),确认无误后向用户汇报结果。\n</todo_context>`;
128
145
  return true;
129
146
  }
130
147
 
@@ -176,7 +193,7 @@ export function registerTodoEventHandlers(
176
193
 
177
194
  pi.on("before_agent_start", async (_event: unknown, ctx: ExtensionContext) => {
178
195
  try {
179
- const pendingTodos = state.todos.filter((t) => t.status !== "completed");
196
+ const pendingTodos = state.todos.filter(isPending);
180
197
  if (pendingTodos.length > 0) {
181
198
  ctx.ui.setStatus("todo", `📋 ${pendingTodos.length} pending`);
182
199
  }
@@ -208,6 +225,7 @@ export function registerTodoEventHandlers(
208
225
  if (handleStallDetection(state)) return;
209
226
  handleReminder(state);
210
227
  } catch (e) {
228
+ // best-effort:agent_end 事件处理器出错不阻断会话主流程,仅记录调试日志
211
229
  console.debug("[todo] agent_end error:", e);
212
230
  }
213
231
  });
package/src/model.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Todo 数据模型 — 纯函数,不依赖 Pi 运行时。
3
3
  * 四态: pending → in_progress → completed;任一状态 → cancelled
4
- * (cancelled 不可恢复;isVerification 标记验证任务,FR-6 completion audit 用)
4
+ * (cancelled 不可恢复)
5
5
  */
6
6
 
7
7
  import { guiComponent, type GuiRenderResult, guiResult, type TreeItem } from "@xyz-agent/extension-protocol";
@@ -12,8 +12,6 @@ export interface Todo {
12
12
  id: number;
13
13
  text: string;
14
14
  status: "pending" | "in_progress" | "completed" | "cancelled";
15
- /** 验证任务标记(FR-6 completion audit)。验证任务必须 completed,不可 cancelled。 */
16
- isVerification?: boolean;
17
15
  }
18
16
 
19
17
  export interface TodoDetails {
@@ -31,8 +29,15 @@ export type ValidStatus = (typeof VALID_STATUSES)[number];
31
29
  // ── 迁移/兼容 ───────────────────────────────────────
32
30
 
33
31
  /** 旧格式迁移:verifying → in_progress,failed → pending,done:boolean → status */
34
- export function migrateTodo(raw: Todo): Todo {
35
- const record = raw as unknown as Record<string, unknown>;
32
+ export function migrateTodo(raw: unknown): Todo {
33
+ // raw 是任意旧格式数据(兼容 done:boolean 等历史结构),以 Record 方式安全访问字段
34
+ // 守卫:null/原始类型(typeof null === 'object',必须显式排除 null)→ 明确报错而非混淆的 TypeError
35
+ if (raw === null || typeof raw !== "object") {
36
+ throw new TypeError(
37
+ `migrateTodo: expected object, got ${raw === null ? "null" : typeof raw}`,
38
+ );
39
+ }
40
+ const record = raw as Record<string, unknown>;
36
41
  const hasValidStatus =
37
42
  typeof record.status === "string" &&
38
43
  VALID_STATUSES.includes(record.status as ValidStatus);
@@ -42,7 +47,7 @@ export function migrateTodo(raw: Todo): Todo {
42
47
  status = record.status as ValidStatus;
43
48
  } else {
44
49
  // 极旧格式 done: boolean
45
- const { done } = record as { done?: boolean };
50
+ const done = typeof record.done === "boolean" ? record.done : undefined;
46
51
  status = done === true ? "completed" : "pending";
47
52
  }
48
53
 
@@ -55,8 +60,6 @@ export function migrateTodo(raw: Todo): Todo {
55
60
  id: record.id as number,
56
61
  text: record.text as string,
57
62
  status,
58
- // FR-6: 保留 isVerification 标记(可选字段,旧数据可能缺失)
59
- isVerification: record.isVerification === true ? true : undefined,
60
63
  };
61
64
  }
62
65
 
@@ -115,7 +118,6 @@ export function addTodos(
115
118
  currentTodos: Todo[],
116
119
  currentNextId: number,
117
120
  texts: string[],
118
- isVerification?: boolean,
119
121
  ): AddResult {
120
122
  if (!texts || texts.length === 0) {
121
123
  return {
@@ -144,8 +146,6 @@ export function addTodos(
144
146
  id: nextId++,
145
147
  text: trimmed[i],
146
148
  status: "pending" as const,
147
- // FR-6: isVerification 标记验证任务(可选,仅 add 时可设)
148
- isVerification: isVerification === true ? true : undefined,
149
149
  });
150
150
  }
151
151
  const endId = nextId - 1;
@@ -200,7 +200,7 @@ export function updateTodos(
200
200
  resultText: `Error: invalid status '${u.status}' for update item id ${u.id}`,
201
201
  };
202
202
  }
203
- // FR-6 不变量守卫:(a) cancelled 不可恢复;(b) 验证任务不可 cancelled
203
+ // cancelled 不可恢复
204
204
  if (todo.status === "cancelled" && u.status !== undefined) {
205
205
  return {
206
206
  updatedTodos: currentTodos,
@@ -208,13 +208,6 @@ export function updateTodos(
208
208
  resultText: `Error: Todo #${u.id} is cancelled and cannot be restored`,
209
209
  };
210
210
  }
211
- if (todo.isVerification && u.status === "cancelled") {
212
- return {
213
- updatedTodos: currentTodos,
214
- error: `id ${u.id} is verification todo`,
215
- resultText: `Error: Todo #${u.id} is a verification todo and cannot be cancelled`,
216
- };
217
- }
218
211
  }
219
212
 
220
213
  const updated = currentTodos.map((t) => {
package/src/tool.ts CHANGED
@@ -28,7 +28,6 @@ export interface TodoActionParams {
28
28
  texts?: string[];
29
29
  ids?: number[];
30
30
  status?: string;
31
- isVerification?: boolean;
32
31
  updates?: Array<{ id: number; status?: string; text?: string }>;
33
32
  }
34
33
 
@@ -43,11 +42,6 @@ const TodoParams = Type.Object({
43
42
  status: Type.Optional(
44
43
  StringEnum(VALID_STATUSES, { description: "Target status (for update action)" }),
45
44
  ),
46
- isVerification: Type.Optional(
47
- Type.Boolean({
48
- description: "Mark added todos as verification tasks (for add action). Verification todos must be completed (not cancelled) before goal completion.",
49
- }),
50
- ),
51
45
  updates: Type.Optional(
52
46
  Type.Array(
53
47
  Type.Object({
@@ -88,7 +82,7 @@ export function handleAdd(state: TodoSessionState, params: TodoActionParams): st
88
82
  'add requires texts parameter (non-empty array). Correct: {"action":"add","texts":["..."]}',
89
83
  );
90
84
  }
91
- const r = addTodos(state.todos, state.nextId, params.texts, params.isVerification);
85
+ const r = addTodos(state.todos, state.nextId, params.texts);
92
86
  if (r.error) throw new Error(r.resultText);
93
87
  state.todos = r.newTodos;
94
88
  state.nextId = r.newNextId;
@@ -124,13 +118,10 @@ export function handleSingleUpdate(state: TodoSessionState, params: TodoActionPa
124
118
  const todo = state.todos.find((t) => t.id === params.id);
125
119
  if (!todo) throw new Error(`Todo #${params.id} not found`);
126
120
 
127
- // FR-6 不变量守卫(失败抛错):(a) cancelled 不可恢复;(b) 验证任务不可 cancelled
121
+ // cancelled 不可恢复(失败抛错)
128
122
  if (todo.status === "cancelled" && params.status !== undefined) {
129
123
  throw new Error(`#${params.id} is cancelled (cannot restore)`);
130
124
  }
131
- if (todo.isVerification && params.status === "cancelled") {
132
- throw new Error(`#${params.id} is verification todo (cannot cancel)`);
133
- }
134
125
 
135
126
  if (params.status !== undefined) todo.status = params.status as Todo["status"];
136
127
  if (params.text !== undefined) todo.text = params.text;
@@ -260,13 +251,12 @@ export function registerTodoTool(
260
251
  "Manage a todo list." +
261
252
  "\n\nAvailable actions:" +
262
253
  "\n- list: View all todos" +
263
- "\n- add: Batch add todos (requires texts array; optional isVerification marks verification tasks)" +
254
+ "\n- add: Batch add todos (requires texts array)" +
264
255
  "\n- update: Update todo(s) — single (id + optional status/text) or batch (updates[], takes priority)" +
265
256
  "\n- delete: Batch delete todos (requires ids array)" +
266
257
  "\n- clear: Clear all todos and reset IDs" +
267
258
  "\n\nExamples:" +
268
259
  '\n{"action":"add","texts":["write spec","implement"]}' +
269
- '\n{"action":"add","texts":["run tests"],"isVerification":true}' +
270
260
  '\n{"action":"update","id":1,"status":"in_progress"}' +
271
261
  '\n{"action":"update","updates":[{"id":1,"status":"completed"},{"id":2,"status":"in_progress"}]}' +
272
262
  '\n{"action":"delete","ids":[3]}' +
@@ -274,10 +264,10 @@ export function registerTodoTool(
274
264
  '\n{"action":"add","text":"x"} ← text is for update; add uses texts:[...]' +
275
265
  '\n{"action":"delete","id":3} ← id is for update; delete uses ids:[...]' +
276
266
  '\n{"action":"update","status":"x"} ← missing id',
277
- promptSnippet: "Use todo when breaking multi-step work into trackable items. Add verification todos (isVerification=true) for checks like running tests.",
267
+ promptSnippet: "Use todo when breaking multi-step work into trackable items. Consider adding a separate todo for verification checks like running tests or typecheck.",
278
268
  promptGuidelines: [
279
269
  "[Usage] 多步骤工作(3+步)时使用。AI 自发创建,无需用户触发",
280
- "[验证任务] 执行任务 + 验证任务(isVerification=true,如 run tests / typecheck)一起建",
270
+ "[验证任务] 为测试 / 类型检查等验证步骤单独建 todo,完成前确保验证通过",
281
271
  "[批量优先] 完成多项任务时使用 updates[] 批量更新,减少工具调用次数",
282
272
  "[自动闭合] 全部完成后工具自动清理,无需手动 clear",
283
273
  "[Not for] 单步操作、简单对话",