@zhushanwen/pi-todo 0.8.9 → 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 +2 -0
- package/package.json +3 -3
- package/src/__tests__/todo.test.ts +110 -1
- package/src/__tests__/tool-prompt.test.ts +13 -0
- package/src/model.ts +28 -4
- package/src/tool.ts +11 -2
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.
|
|
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",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"vitest": "^4.1.8"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@xyz-agent/extension-protocol": "0.
|
|
33
|
-
"@zhushanwen/pi-extension-logger": "0.
|
|
32
|
+
"@xyz-agent/extension-protocol": "0.9.0",
|
|
33
|
+
"@zhushanwen/pi-extension-logger": "0.6.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
@@ -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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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",
|