@zhushanwen/pi-todo 0.8.6 → 0.8.7

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
@@ -1,14 +1,14 @@
1
1
  # todo
2
2
 
3
- 轻量三态任务清单 — `pending` / `in_progress` / `completed`。支持 session 持久化、状态栏、双列 widget、`/todos` TUI 视图,以及延迟 steer 驱动任务推进。
3
+ 轻量三态任务清单 — `pending` / `in_progress` / `completed`。支持 session 持久化、状态栏、单双列自适应 widget、`/todos` TUI 视图,以及延迟 steer 驱动任务推进。
4
4
 
5
5
  ## 设计定位
6
6
 
7
7
  | 维度 | todo | goal |
8
8
  |------|------|------|
9
- | 状态机 | **刻意无约束**,任意状态自由流转(含反向) | 7 态状态机 + 强制任务分解 |
9
+ | 状态机 | **刻意无约束**,任意状态自由流转(含反向) | 6 态状态机(active/paused/blocked/complete/budget_limited/cancelled) |
10
10
  | 持久化 | 复用 Pi 的 toolResult entry(不调用 appendEntry) | appendEntry 主动写入 |
11
- | 定位 | 多步骤工作的临时进度追踪 | 持久化目标驱动循环 |
11
+ | 定位 | 多步骤工作的临时进度追踪 | 持久化目标驱动循环(完成需逐条对照 successCriteria) |
12
12
 
13
13
  `in_progress` 非强制,`pending → completed` 直接跳转合法。
14
14
 
@@ -28,10 +28,10 @@ pi install npm:@zhushanwen/pi-todo
28
28
  | `add` | `texts: string[]` | 是 | 批量追加,自动分配连续 ID,初始 `status=pending` |
29
29
  | `update` | `id` + `status` 或 `text`;**或** `updates: Array<{id, status?, text?}>` | `id` 必填 | `updates[]` **优先于** single 的 `id/status/text` |
30
30
  | `delete` | `ids: number[]` | 是 | 批量删除;**部分 id 缺失则整体拒绝**(原子性) |
31
- | `clear` | — | — | 清空全部,重置 `nextId=1` 和完成态标记 |
32
31
 
33
32
  - `status` 枚举:`pending` / `in_progress` / `completed`
34
- - `add` 不接受 `status`(恒为 pending),不存在 `verifyTexts`(那是 goal 的概念)
33
+ - `add` 不接受 `status`(恒为 pending),不存在 `verifyTexts`(goal 侧的对应概念是 `successCriteria`)
34
+ - 全部 completed 后由 **auto-clear 机制**延迟 2 轮自动清空并重置 `nextId=1`(无手动 clear action)
35
35
 
36
36
  ### 错误处理约定
37
37
 
@@ -42,11 +42,13 @@ handler 失败**直接 `throw new Error()`**,不返回错误成功模式(见
42
42
  | `add` 缺 `texts` | `add requires texts parameter (non-empty array)` |
43
43
  | `update` 缺 `id` | `update requires id parameter` |
44
44
  | `update` 缺 `status` 和 `text` | `update requires at least status or text parameter` |
45
- | `update` `text` 空串 | `text cannot be empty string` |
45
+ | `update` `text` 空串 | `text cannot be empty or whitespace-only` |
46
46
  | `update` `status` 非法 | `status only accepts pending / in_progress / completed` |
47
47
  | `update`/`delete` id 不存在 | `Todo #N not found` |
48
48
  | `delete` 缺 `ids` | `delete requires ids parameter (non-empty array)` |
49
49
 
50
+ schema 为扁平 `Type.Object`(OpenAI 兼容):字段全 Optional,缺失必填与双形陷阱(`text`/`texts`、`id`/`ids`)由 handler 运行时校验兜底。
51
+
50
52
  ## Steer 机制(延迟注入)
51
53
 
52
54
  todo 的核心驱动力是「延迟一拍」的 steer:
@@ -56,25 +58,22 @@ agent_end 设置 pendingSteerMessage
56
58
  → 下一 turn 的 before_agent_start 消费(用户不可见,display:false)
57
59
  ```
58
60
 
59
- 四个子机制(handlers.ts,阈值常量硬编码):
61
+ 两个子机制(handlers.ts):
60
62
 
61
63
  | 机制 | 触发 | 行为 |
62
64
  |------|------|------|
63
- | **auto-clear** | 全部 completed 后再过 2 轮 | 自动清空 todos + 重置标记 |
64
65
  | **completion-steer** | 首次全部 completed | 注入「检查交付质量」steer(一次性,`completionSteered` 防重) |
65
- | **stall 检测** | todo 活动达 5 轮 | 注入极简 reminder(仅下一个任务),整个 session 只触发一次 |
66
- | **reminder** | 无 todo 活动达 2 轮 | 温和 reminder |
66
+ | **auto-clear** | 全部 completed 后再过 2 轮 | 自动清空 todos + 重置标记 |
67
67
 
68
- `agent_end` 内短路顺序:completion-steer **不短路**(继续往下),auto-clear / stall / reminder 各自短路 return。详见 `ARCHITECTURE.md`。
68
+ `agent_end` 内:completion-steer **不短路**(继续往下),auto-clear 命中(`handled`)则短路 return。详见 `ARCHITECTURE.md`。
69
69
 
70
70
  ## 持久化机制
71
71
 
72
72
  todo 扩展**自己不调用 `appendEntry`**。状态快照随 Pi 框架自动记录的 toolResult entry 落盘:
73
73
 
74
74
  1. 每次 todo tool 调用,`execute` 返回的 `details.todos` / `details.nextId` 被 Pi 自动序列化为一条 `toolResult` entry
75
- 2. `session_start` / `session_tree` 时,`reconstructState` 回放**最后一条** todo toolResult 重建状态
76
- 3. 回放后 splice 掉更早的 todo toolResult(entry GC,从后往前删避免索引漂移)
77
- 4. 向后兼容:`migrateTodo` 把旧五态(`verifying→in_progress`、`failed→pending`)和极旧的 `done:boolean` 降级映射到三态
75
+ 2. `session_start` / `session_tree` 时,`reconstructState` 回放**最后一条** todo toolResult 重建状态(纯读——Pi 的 getEntries 返回 filter-copy,splice 无效,不做 entry GC)
76
+ 3. 向后兼容:`migrateTodo` 把旧五态(`verifying→in_progress`、`failed→pending`)和极旧的 `done:boolean` 降级映射到三态
78
77
 
79
78
  ## 三层渲染
80
79
 
@@ -93,16 +92,15 @@ todo 扩展**自己不调用 `appendEntry`**。状态快照随 Pi 框架自动
93
92
  ```
94
93
  todo/
95
94
  ├── index.ts # 工厂入口(re-export src/index.ts)
96
- ├── PLAN.md # [SUPERSEDED] v2 历史计划,保留作决策记录
97
95
  ├── ARCHITECTURE.md # 架构详图(文件依赖 + steer 时序 + 事件流)
98
96
  └── src/
99
- ├── index.ts # 工厂入口(创建 state + 注册 tool/command/event)
97
+ ├── index.ts # 工厂入口(创建 state + 注册 tool/command/event + makeRefreshDisplay
100
98
  ├── state.ts # TodoSessionState 会话状态接口 + 工厂
101
99
  ├── model.ts # 纯函数数据层(类型/迁移/addTodos/updateTodos/format/buildGui)
102
- ├── tool.ts # todo tool 注册 — 5 action + execute dispatcher
103
- ├── handlers.ts # 5 事件处理器 + reconstructState + steer 四机制
100
+ ├── tool.ts # todo tool 注册 — 4 action + execute dispatcher
101
+ ├── handlers.ts # 5 事件处理器 + reconstructState + steer 双机制(autoClear/completion)
104
102
  ├── render.ts # status line / widget / tool result 三层渲染
105
103
  ├── component.ts # /todos 的 TodoListComponent TUI 组件
106
104
  ├── commands.ts # /todos 命令注册
107
- └── __tests__/ # 单测(model 纯函数 + widget 布局 + agent_end 数据条件)
105
+ └── __tests__/ # 单测(model 纯函数 + widget 布局 + steer/回放 + schema/detector/prompt 回归)
108
106
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
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.7.0",
33
- "@zhushanwen/pi-extension-logger": "0.3.1"
32
+ "@xyz-agent/extension-protocol": "0.8.0",
33
+ "@zhushanwen/pi-extension-logger": "0.4.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.84.4",
@@ -4,10 +4,10 @@
4
4
  // verify the Correct/error STRINGS exist; these exercise the actual throw logic of
5
5
  // handleAdd/handleDelete, so a refactor cannot silently drop the dual-form detection.
6
6
  //
7
- // Note: the schema layer (TodoParams, additionalProperties:false) already rejects
8
- // dual-form payloads before they reach the handler in production. These handler-level
9
- // tests are defense-in-depth they ensure the handler ALSO throws clearly if called
10
- // directly (e.g. by another extension bypassing schema validation).
7
+ // Note: since the schema was flattened to a single Type.Object (OpenAI compat), all
8
+ // fields are Optional at the schema layer — dual-form payloads now REACH the handler
9
+ // in production, and these handler-level checks are the actual enforcement point
10
+ // (schema.test.ts pins that Value.Check passes for dual-form payloads).
11
11
  //
12
12
  // handleAdd/handleDelete were exported specifically to enable these tests.
13
13
 
@@ -1,5 +1,6 @@
1
1
  // 提示词质量回归:todo tool 的 description 与 runtime 纠错文案必须是
2
- // "弱模型友好"的——条件必填字段在 schema 层强约束(见 schema.test.ts),双形陷阱
2
+ // "弱模型友好"的——条件必填字段(add texts、delete 缺 ids)在 handler 运行时校验
3
+ // (schema 扁平化后字段全 Optional,见 tool.ts 头注释),双形陷阱
3
4
  // (text/texts、id/ids)的 throw 要带 Correct 纠错正例让模型自我纠正。
4
5
  //
5
6
  // 本测试用源码文本断言锁定这些约束,防止后续重构把纠错文案删掉或弱化。读源码而非
package/src/commands.ts CHANGED
@@ -15,7 +15,7 @@ import { TodoListComponent } from "./component";
15
15
  /** 注册 /todos 命令到 pi */
16
16
  export function registerTodosCommand(pi: ExtensionAPI, state: TodoSessionState): void {
17
17
  pi.registerCommand("todos", {
18
- description: "View all todos for the current branch",
18
+ description: "View all todos for the current session",
19
19
  handler: async (_args: string | undefined, ctx: ExtensionCommandContext) => {
20
20
  if (!ctx.hasUI) {
21
21
  ctx.ui.notify("/todos requires interactive mode", "error");
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Todo Extension — 轻量三态任务清单(pending / in_progress / completed)。
3
3
  *
4
- * 设计定位:刻意不做状态机约束(与 goal 扩展的 7 态对立),状态自由流转;
4
+ * 设计定位:刻意不做状态机约束(与 goal 扩展的 6 态状态机对立),状态自由流转;
5
5
  * 状态持久化复用 Pi 自动记录的 toolResult entry(非 appendEntry);
6
6
  * 通过 agent_end → before_agent_start 的延迟 steer 驱动任务推进。
7
7
  *
package/src/model.ts CHANGED
@@ -113,10 +113,6 @@ export function buildGui(todos: Todo[]): GuiRenderResult {
113
113
  );
114
114
  }
115
115
 
116
- export function getDisplayStatus(t: Todo): string {
117
- return migrateTodo(t).status;
118
- }
119
-
120
116
  // ── Add 逻辑 ─────────────────────────────────────────
121
117
 
122
118
  export interface AddResult {
package/src/render.ts CHANGED
@@ -6,7 +6,6 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
7
 
8
8
  import {
9
- getDisplayStatus,
10
9
  type Todo,
11
10
  type TodoDetails,
12
11
  } from "./model";
@@ -42,7 +41,7 @@ function fixedWidth(text: string, width: number): string {
42
41
  export function renderStatusText(todoList: Todo[], th: Theme): string {
43
42
  if (todoList.length === 0) return "";
44
43
 
45
- const completed = todoList.filter((t) => getDisplayStatus(t) === "completed").length;
44
+ const completed = todoList.filter((t) => t.status === "completed").length;
46
45
  const total = todoList.length;
47
46
 
48
47
  if (completed === total) {
@@ -113,7 +112,7 @@ export function renderWidgetLines(
113
112
 
114
113
  const width = termWidth ?? (process.stdout.columns || FALLBACK_TERM_WIDTH);
115
114
  const lines: string[] = [];
116
- const completed = todoList.filter((t) => getDisplayStatus(t) === "completed").length;
115
+ const completed = todoList.filter((t) => t.status === "completed").length;
117
116
  const total = todoList.length;
118
117
 
119
118
  lines.push(th.fg("accent", "\u2611") + th.fg("muted", ` ${completed}/${total}`));
@@ -142,7 +141,7 @@ function buildTodoListText(todoList: Todo[], options: { expanded: boolean }, the
142
141
  let listText = theme.fg("muted", `${todoList.length} todos:`);
143
142
  const display = options.expanded ? todoList : todoList.slice(0, MAX_COLLAPSED_ITEMS);
144
143
  for (const t of display) {
145
- const status = getDisplayStatus(t);
144
+ const status = t.status;
146
145
  const mark =
147
146
  status === "completed"
148
147
  ? theme.fg("success", "\u2713")