@zhushanwen/pi-todo 0.8.10 → 0.9.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/README.md CHANGED
@@ -32,6 +32,8 @@ pi install npm:@zhushanwen/pi-todo
32
32
  - `status` 枚举:`pending` / `in_progress` / `completed`
33
33
  - `add` 不接受 `status`(恒为 pending),不存在 `verifyTexts`(goal 侧的对应概念是 `successCriteria`)
34
34
  - 全部 completed 后由 **auto-clear 机制**延迟 2 轮自动清空并重置 `nextId=1`(无手动 clear action)
35
+ - **add 时 auto-GC**:旧列表全部 completed 时,add 自动清空旧列表再新增(`nextId` 重置 1),开启新任务无需先手动清理
36
+ - **规模软上限**:建议 todo 总数不超过 10 个;add 后总数超过 10 时在结果中附加提醒(不拒绝,合并细粒度步骤或 delete 瘦身由模型自决)
35
37
 
36
38
  ### 错误处理约定
37
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
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",
@@ -12,7 +12,7 @@ import {
12
12
  } from "../model";
13
13
  import { renderWidgetLines } from "../render";
14
14
  import { createTodoSessionState } from "../state";
15
- import { handleSingleUpdate } from "../tool";
15
+ import { handleAdd, handleSingleUpdate } from "../tool";
16
16
 
17
17
  // ── 数据模型 + 向后兼容 ──────────────────────────────
18
18
 
@@ -105,6 +105,84 @@ describe("todo add", () => {
105
105
  });
106
106
  });
107
107
 
108
+ // ── todo add — auto-GC(全部 completed 后 add 自动清理旧列表)──
109
+
110
+ describe("todo add auto-GC", () => {
111
+ it("旧列表全部 completed → 清空旧列表,新 id 从 1 开始,nextId 重置", () => {
112
+ const existing: Todo[] = [
113
+ { id: 1, text: "old A", status: "completed" },
114
+ { id: 2, text: "old B", status: "completed" },
115
+ ];
116
+ const result = addTodos(existing, 3, ["new task"]);
117
+
118
+ expect(result.autoCleared).toBe(true);
119
+ expect(result.newTodos).toHaveLength(1);
120
+ expect(result.newTodos[0]).toEqual({ id: 1, text: "new task", status: "pending" });
121
+ expect(result.newNextId).toBe(2);
122
+ });
123
+
124
+ it("存在未完成项(pending/in_progress)→ 不清理,正常追加", () => {
125
+ const existing: Todo[] = [
126
+ { id: 1, text: "done", status: "completed" },
127
+ { id: 2, text: "wip", status: "in_progress" },
128
+ { id: 3, text: "waiting", status: "pending" },
129
+ ];
130
+ const result = addTodos(existing, 4, ["more"]);
131
+
132
+ expect(result.autoCleared).toBe(false);
133
+ expect(result.newTodos).toHaveLength(4);
134
+ expect(result.newTodos[3].id).toBe(4);
135
+ });
136
+
137
+ it("空列表 → 不触发 auto-GC(autoCleared=false)", () => {
138
+ const result = addTodos([], 1, ["first"]);
139
+ expect(result.autoCleared).toBe(false);
140
+ expect(result.newTodos[0].id).toBe(1);
141
+ });
142
+
143
+ it("resultText 说明自动清理了旧列表", () => {
144
+ const existing: Todo[] = [{ id: 1, text: "old", status: "completed" }];
145
+ const result = addTodos(existing, 2, ["new task"]);
146
+ expect(result.resultText).toContain("Auto-cleared 1 completed todo(s)");
147
+ });
148
+ });
149
+
150
+ // ── todo add — 超限软提醒(> RECOMMENDED_MAX_TODOS)──
151
+
152
+ describe("todo add over-limit reminder", () => {
153
+ function makeTodos(n: number): Todo[] {
154
+ return Array.from({ length: n }, (_, i) => ({
155
+ id: i + 1,
156
+ text: `task ${i + 1}`,
157
+ status: "pending" as const,
158
+ }));
159
+ }
160
+
161
+ it("总数超过 10 → resultText 附加提醒(含建议上限值)", () => {
162
+ const result = addTodos(makeTodos(9), 10, ["x", "y"]);
163
+ expect(result.newTodos).toHaveLength(11);
164
+ expect(result.resultText).toContain("recommended max of 10");
165
+ });
166
+
167
+ it("总数恰为 10 → 不提醒", () => {
168
+ const result = addTodos(makeTodos(9), 10, ["x"]);
169
+ expect(result.newTodos).toHaveLength(10);
170
+ expect(result.resultText).not.toContain("recommended max");
171
+ });
172
+
173
+ it("旧列表全部 completed 的 auto-GC 场景,新列表 ≤10 → 不误报提醒", () => {
174
+ const completed = Array.from({ length: 12 }, (_, i) => ({
175
+ id: i + 1,
176
+ text: `old ${i + 1}`,
177
+ status: "completed" as const,
178
+ }));
179
+ const result = addTodos(completed, 13, ["fresh task"]);
180
+ expect(result.autoCleared).toBe(true);
181
+ expect(result.newTodos).toHaveLength(1);
182
+ expect(result.resultText).not.toContain("recommended max");
183
+ });
184
+ });
185
+
108
186
  // ── todo update batch ───────────────────────────────
109
187
 
110
188
  describe("todo update batch", () => {
@@ -197,6 +275,37 @@ describe("handleSingleUpdate guards (tool single path)", () => {
197
275
  });
198
276
  });
199
277
 
278
+ // ── handleAdd — auto-GC 重置完成周期跟踪 ────────────
279
+
280
+ describe("handleAdd auto-GC resets completion-cycle tracking", () => {
281
+ it("全部 completed 后 add → completionSteered / allCompletedAtCount 重置,新一轮完成可再次 steer", () => {
282
+ const state = createTodoSessionState();
283
+ state.todos = [{ id: 1, text: "old", status: "completed" }];
284
+ state.completionSteered = true;
285
+ state.allCompletedAtCount = 3;
286
+
287
+ handleAdd(state, { action: "add", texts: ["new task"] });
288
+
289
+ expect(state.completionSteered).toBe(false);
290
+ expect(state.allCompletedAtCount).toBeNull();
291
+ expect(state.todos).toHaveLength(1);
292
+ expect(state.todos[0].id).toBe(1);
293
+ expect(state.nextId).toBe(2);
294
+ });
295
+
296
+ it("存在未完成项时 add → 完成周期跟踪不受影响", () => {
297
+ const state = createTodoSessionState();
298
+ state.todos = [{ id: 1, text: "wip", status: "in_progress" }];
299
+ state.nextId = 2;
300
+ state.completionSteered = false;
301
+
302
+ handleAdd(state, { action: "add", texts: ["more"] });
303
+
304
+ expect(state.todos).toHaveLength(2);
305
+ expect(state.nextId).toBe(3);
306
+ });
307
+ });
308
+
200
309
  // ── completed 无拦截 ────────────────────────────────
201
310
 
202
311
  describe("completed without interception", () => {
@@ -59,6 +59,15 @@ describe("todo description — 中文版(动作 + 规则)", () => {
59
59
  expect(DESCRIPTION_REGION).toContain("未真正完成不得标记 completed");
60
60
  });
61
61
 
62
+ it("含规模控制规则(建议不超过 10 个)", () => {
63
+ expect(DESCRIPTION_REGION).toContain("建议 todo 总数不超过 10 个");
64
+ expect(DESCRIPTION_REGION).toContain("细粒度步骤优先合并");
65
+ });
66
+
67
+ it("含 auto-GC 行为说明(旧任务全部 completed 后 add 自动清理)", () => {
68
+ expect(DESCRIPTION_REGION).toContain("旧任务全部 completed 后再 add 会自动清理旧列表");
69
+ });
70
+
62
71
  it("已删除旧英文 Examples 段与 Don't 段", () => {
63
72
  expect(DESCRIPTION_REGION).not.toContain("Available actions");
64
73
  expect(DESCRIPTION_REGION).not.toContain("Don't");
@@ -103,4 +112,8 @@ describe("todo tool prompt — snippet & guidelines", () => {
103
112
  expect(TOOL_SRC).toContain("[自动闭合] 全部完成后自动清理");
104
113
  expect(TOOL_SRC).toContain("[批量优先] 完成多项任务时使用 updates[] 批量更新");
105
114
  });
115
+
116
+ it("promptGuidelines 含 [规模控制] 条目(不超过 10 个)", () => {
117
+ expect(TOOL_SRC).toContain("[规模控制] todo 总数建议不超过 10 个");
118
+ });
106
119
  });
package/src/model.ts CHANGED
@@ -115,16 +115,25 @@ export function buildGui(todos: Todo[]): GuiRenderResult {
115
115
 
116
116
  // ── Add 逻辑 ─────────────────────────────────────────
117
117
 
118
+ /** 建议的单 session todo 数上限(软约束:超限提醒,不硬拒绝) */
119
+ export const RECOMMENDED_MAX_TODOS = 10;
120
+
118
121
  export interface AddResult {
119
122
  newTodos: Todo[];
120
123
  newNextId: number;
121
124
  resultText: string;
125
+ /** 旧列表全部 completed 被自动清理时为 true(handleAdd 据此重置完成周期跟踪) */
126
+ autoCleared: boolean;
122
127
  }
123
128
 
124
129
  /**
125
130
  * 批量新增 todo。
126
131
  * texts 整体 trim;任一项 trim 后为空串则 throw(不再静默 filter 丢弃——
127
132
  * 模型应学到传有效项,C1 决策)。
133
+ *
134
+ * auto-GC:旧列表非空且全部 completed 时视为「上一任务已结束、开启新任务」,
135
+ * 先清空旧列表再新增(nextId 重置为 1,与 handlers.handleAutoClear 的清理
136
+ * 语义一致),避免已完结任务长期堆积在列表里。
128
137
  */
129
138
  export function addTodos(
130
139
  currentTodos: Todo[],
@@ -141,9 +150,13 @@ export function addTodos(
141
150
  throw new Error("texts must not contain empty or whitespace-only items");
142
151
  }
143
152
 
144
- const startId = currentNextId;
145
- const newTodos = [...currentTodos];
146
- let nextId = currentNextId;
153
+ const autoCleared =
154
+ currentTodos.length > 0 && currentTodos.every((t) => t.status === "completed");
155
+ const baseTodos = autoCleared ? [] : currentTodos;
156
+ const startId = autoCleared ? 1 : currentNextId;
157
+
158
+ const newTodos = [...baseTodos];
159
+ let nextId = startId;
147
160
  for (let i = 0; i < trimmed.length; i++) {
148
161
  newTodos.push({
149
162
  id: nextId++,
@@ -153,10 +166,21 @@ export function addTodos(
153
166
  }
154
167
  const endId = nextId - 1;
155
168
 
169
+ let resultText = `Added ${trimmed.length} todos (#${startId}-#${endId})`;
170
+ if (autoCleared) {
171
+ resultText += `\nAuto-cleared ${currentTodos.length} completed todo(s) from the previous task`;
172
+ }
173
+
174
+ // 软上限提醒:总数超过建议值时附加提醒(不拒绝,把决策留给模型)
175
+ if (newTodos.length > RECOMMENDED_MAX_TODOS) {
176
+ resultText += `\nNote: ${newTodos.length} todos exceeds the recommended max of ${RECOMMENDED_MAX_TODOS}. Prefer consolidating fine-grained steps or deleting items no longer needed.`;
177
+ }
178
+
156
179
  return {
157
180
  newTodos,
158
181
  newNextId: nextId,
159
- resultText: `Added ${trimmed.length} todos (#${startId}-#${endId})`,
182
+ resultText,
183
+ autoCleared,
160
184
  };
161
185
  }
162
186
 
package/src/tool.ts CHANGED
@@ -91,10 +91,16 @@ export function handleAdd(state: TodoSessionState, params: TodoParamsT): string
91
91
  'add requires texts parameter (non-empty array). Correct: {"action":"add","texts":["..."]}',
92
92
  );
93
93
  }
94
- // addTodos 内部对空项 trim+throw(C1)
94
+ // addTodos 内部对空项 trim+throw(C1);旧列表全部 completed 时自动清理(auto-GC
95
95
  const r = addTodos(state.todos, state.nextId, params.texts);
96
96
  state.todos = r.newTodos;
97
97
  state.nextId = r.newNextId;
98
+ if (r.autoCleared) {
99
+ // 新任务周期:重置完成跟踪,否则上一任务的 completionSteered=true
100
+ // 会屏蔽本轮全部完成时的质量检查 steer
101
+ state.allCompletedAtCount = null;
102
+ state.completionSteered = false;
103
+ }
98
104
  return r.resultText;
99
105
  }
100
106
 
@@ -249,13 +255,16 @@ export function registerTodoTool(
249
255
  "\n\n规则:" +
250
256
  "\n- 同一时间只有一个 todo 处于 in_progress" +
251
257
  "\n- 完成一个 todo 立即标记 completed,不要攒到最后批量标记" +
252
- "\n- 未真正完成不得标记 completed:被阻塞或测试失败时保持 in_progress",
258
+ "\n- 未真正完成不得标记 completed:被阻塞或测试失败时保持 in_progress" +
259
+ "\n- 列表保持聚焦:建议 todo 总数不超过 10 个,细粒度步骤优先合并" +
260
+ "\n- 旧任务全部 completed 后再 add 会自动清理旧列表,直接添加新任务即可",
253
261
  promptSnippet: "用 todo 跟踪多步骤工作;记得为验证步骤(测试、类型检查)单独建 todo。",
254
262
  promptGuidelines: [
255
263
  "[Usage] 多步骤工作(3+步)时使用,AI 自发创建,无需用户触发",
256
264
  "[验证任务] 为测试 / 类型检查等验证步骤单独建 todo,完成前确保验证通过",
257
265
  "[批量优先] 完成多项任务时使用 updates[] 批量更新,减少工具调用次数",
258
266
  "[自动闭合] 全部完成后自动清理,无需手动 delete",
267
+ "[规模控制] todo 总数建议不超过 10 个;超过时合并细粒度步骤或 delete 掉不再需要的项",
259
268
  "[Not for] 单步操作、简单对话",
260
269
  ],
261
270
  executionMode: "sequential",