@maxiaochao/pi-toolkit 0.10.5 → 0.10.6
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/CHANGELOG.md +7 -0
- package/README.md +26 -4
- package/package.json +3 -5
- package/packages/herdr-subagents/CHANGELOG.md +6 -0
- package/packages/herdr-subagents/README.md +12 -1
- package/packages/herdr-subagents/TESTING.md +4 -3
- package/packages/herdr-subagents/agents/analyst.md +8 -0
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +1 -1
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/status.ts +95 -0
- package/packages/herdr-subagents/src/tool.ts +13 -2
- package/packages/herdr-subagents/src/validation.ts +2 -1
- package/packages/herdr-subagents/tests/extension-contract.ts +4 -0
- package/packages/herdr-subagents/tests/run-tests.mjs +40 -2
- package/skills/agent-team/SKILL.md +33 -57
- package/skills/agent-team/models.json +19 -12
- package/packages/todo/CHANGELOG.md +0 -9
- package/packages/todo/README.md +0 -73
- package/packages/todo/TESTING.md +0 -22
- package/packages/todo/docs/demo.gif +0 -0
- package/packages/todo/docs/screenshot.png +0 -0
- package/packages/todo/extensions/todo.ts +0 -279
- package/packages/todo/package.json +0 -25
- package/packages/todo/src/todo-state.ts +0 -159
- package/packages/todo/tests/todo-extension.mjs +0 -134
- package/packages/todo/tests/todo-state.mjs +0 -108
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
"defaults": {
|
|
3
|
-
"kind": "pi",
|
|
4
|
-
"thinking": "high"
|
|
5
|
-
},
|
|
6
2
|
"members": [
|
|
7
3
|
{
|
|
8
4
|
"name": "sol",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"charter": "只接受能逐行引用的源码、规范、commit,输出必须带 文件:行号。负责把「机制上可能」变成「代码里确实如此」;查不到就说查不到,不要用常识补。"
|
|
5
|
+
"vendor": "OpenAI",
|
|
6
|
+
"model": "pudu/gpt-5.6-sol"
|
|
12
7
|
},
|
|
13
8
|
{
|
|
14
9
|
"name": "grok",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"charter": "拿原始数据和口径,不看管理者的脚本,独立重算同一个指标,只报自己的数和差异。数对不上就是一次发现,先查口径再查代码。"
|
|
10
|
+
"vendor": "xAI",
|
|
11
|
+
"model": "pudu/grok-4.6"
|
|
18
12
|
},
|
|
19
13
|
{
|
|
20
14
|
"name": "opus",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
15
|
+
"vendor": "Anthropic",
|
|
16
|
+
"model": "pudu/claude-opus-5"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"roles": [
|
|
20
|
+
{
|
|
21
|
+
"name": "auditor",
|
|
22
|
+
"charter": "只接受能逐行引用的源码、规范、commit,输出必须带文件和行号。负责把“机制上可能”变成“代码里确实如此”;查不到就说查不到,不要用常识补。"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "replicator",
|
|
26
|
+
"charter": "拿原始数据和口径,不看管理者的脚本,独立重算同一个指标,只报自己的数和差异。数对不上就是一次发现,先查口径再查代码。"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "adversary",
|
|
23
30
|
"charter": "攻前提和方法学,不攻结论。指出指标对样本组成的敏感性、计数的一对多、缺失的阴性对照。附议没有价值,找不到问题就说明理由。"
|
|
24
31
|
}
|
|
25
32
|
]
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## Unreleased
|
|
4
|
-
|
|
5
|
-
- replace generic Todo CRUD with serialized execution-plan lifecycle commands, guarded single-step advancement, preserved replan history, canonical current-plan results, branch reconstruction tests, and a live rerendering current-plan widget
|
|
6
|
-
|
|
7
|
-
## 2026-09-30 - v0.1.0
|
|
8
|
-
|
|
9
|
-
- 从 `@maxiaochao/pi-toolkit` 提取 todo 权威源码,支持主包捆绑和独立安装。
|
package/packages/todo/README.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# @maxiaochao/pi-todo
|
|
2
|
-
|
|
3
|
-
Pi 的分支本地执行计划扩展,提供 `todo` 生命周期工具、只显示当前计划的 widget 和 `/todos` 完整历史视图。
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## 为什么有这个包
|
|
8
|
-
|
|
9
|
-
这是 pi-toolkit 自研的计划执行模块,用明确状态替代只存在于对话文本里的计划。Todo 是用户可见的后续工作顺序,不是隐藏推理、完成报告或通用任务管理器。它解决改向后历史丢失、并发调用误完成、多个步骤同时 doing、完成后不推进,以及 UI 序号与持久 ID 混用的问题。
|
|
10
|
-
|
|
11
|
-
## 演示
|
|
12
|
-
|
|
13
|
-

|
|
14
|
-
|
|
15
|
-
```text
|
|
16
|
-
todo({ action: "plan", items: ["Inspect callers", "Implement", "Verify"] })
|
|
17
|
-
todo({ action: "complete_current", expected_current_id: 1 })
|
|
18
|
-
/todos
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
第一项实际完成后,下一项自动进入 `doing`。工具结果和 widget 只展示 `CURRENT plan`;`/todos` 展示当前分支的全部完成、取消和未完成历史。
|
|
22
|
-
|
|
23
|
-
## 安装
|
|
24
|
-
|
|
25
|
-
完整工具包已经包含本扩展:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pi install npm:@maxiaochao/pi-toolkit
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
只需要 todo 时单独安装:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pi install npm:@maxiaochao/pi-todo
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
两种方式二选一,避免重复注册。
|
|
38
|
-
|
|
39
|
-
## 更新
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pi update npm:@maxiaochao/pi-toolkit # 更新主包内置版本
|
|
43
|
-
pi update npm:@maxiaochao/pi-todo # 更新独立子包
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
两个 npm 包版本独立。Todo 源码变更只有在发布新主包后才会到达主包用户,发布子包本身不会更新主包。
|
|
47
|
-
|
|
48
|
-
## 行为
|
|
49
|
-
|
|
50
|
-
- `plan(items)` 创建或重新规划。非空计划取消并保留全部旧 `doing`/`todo` 历史,保留已关闭历史,使用新的单调递增 ID,并启动第一步。
|
|
51
|
-
- `plan([])` 取消全部未完成工作,不留下当前计划;ID 不重置。
|
|
52
|
-
- `add_step(text)` 只追加一步。`complete_current` 和 `cancel_current` 必须携带匹配唯一 `doing` 步骤的 `expected_current_id`,否则原子失败;每次调用只推进一步。
|
|
53
|
-
- 调用共享可变状态并串行执行。同轮多个 sibling completion 不是支持的顺序机制。
|
|
54
|
-
- `list` 与每个变更都向模型返回简洁、规范的 `CURRENT plan`,包含稳定内部 ID 和状态;只有 `/todos` 显示完整历史。
|
|
55
|
-
- widget 从实时状态读取当前计划,使用连续显示位置、隐藏内部 ID 和关闭历史,并在计划结束时消失。
|
|
56
|
-
|
|
57
|
-
## 使用准则
|
|
58
|
-
|
|
59
|
-
- 只为明确的多步工作、确实分阶段的工作或跨模型交接创建计划;不要为简单单步任务创建计划。
|
|
60
|
-
- 仅在工作实际完成后调用 `complete_current`。仍然需要但被阻塞的步骤保持当前状态。
|
|
61
|
-
- `cancel_current` 表示当前步骤不再需要且后续计划仍有效;方向改变时使用 `plan`。
|
|
62
|
-
- 保留用户给出的步骤意图和顺序,只在可执行性要求下拆分步骤。
|
|
63
|
-
- 协议不提供任意更新、删除、清空、撤销、批量完成或 marker 文本解析。
|
|
64
|
-
|
|
65
|
-
## 开发与测试
|
|
66
|
-
|
|
67
|
-
权威源码位于 `extensions/` 和 `src/`。测试流程、用例和最近验证结果见 [TESTING.md](TESTING.md)。
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npm --prefix packages/todo test
|
|
71
|
-
npm --prefix packages/todo run load-test
|
|
72
|
-
npm pack ./packages/todo --dry-run
|
|
73
|
-
```
|
package/packages/todo/TESTING.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Todo 测试方案
|
|
2
|
-
|
|
3
|
-
## 流程
|
|
4
|
-
|
|
5
|
-
1. `npm --prefix packages/todo test`:运行状态机确定性测试和扩展/widget 集成测试。
|
|
6
|
-
2. `npm --prefix packages/todo run load-test`:让 Pi 加载真实扩展入口。
|
|
7
|
-
3. `npm pack ./packages/todo --dry-run`:确认发布文件完整。
|
|
8
|
-
4. 确认由真实状态机输出生成的 `docs/screenshot.png` 和 `docs/demo.gif` 可打开且进入 tarball。
|
|
9
|
-
|
|
10
|
-
## 用例数据
|
|
11
|
-
|
|
12
|
-
- 恢复旧格式记录、修复非法生命周期,并只从当前分支快照重建。
|
|
13
|
-
- 非空 `plan` 保留关闭历史、取消旧未完成步骤并使用单调新 ID;`plan([])` 留下空当前计划。
|
|
14
|
-
- `expected_current_id` 的 stale、重复和缺失调用原子失败;顺序完成与取消每次只推进一步。
|
|
15
|
-
- `add_step` 只追加,步骤文本不作为 marker 或唯一键解释。
|
|
16
|
-
- 并发发起的调用按提交顺序串行化,同轮重复 completion 不会跨越两个步骤。
|
|
17
|
-
- 已挂载 widget 从实时状态重渲染、主动请求 TUI render、隐藏内部 ID 和关闭历史,并在计划结束时消失。
|
|
18
|
-
- `/todos` 展示完整当前分支历史,而所有工具结果只返回规范 `CURRENT plan`。
|
|
19
|
-
|
|
20
|
-
## 最近验证结果
|
|
21
|
-
|
|
22
|
-
迁移基线:状态机和扩展集成测试全部通过,扩展可由 Pi 独立加载。发布前必须重新运行上述三步;跳过的检查不算通过。
|
|
Binary file
|
|
Binary file
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Branch-local execution-plan extension with tool-result snapshot persistence.
|
|
3
|
-
*
|
|
4
|
-
* The model sees only the current plan. The widget mirrors that plan, while
|
|
5
|
-
* `/todos` shows the complete history for the active conversation branch.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
|
-
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
|
-
import { Type } from "typebox";
|
|
12
|
-
import {
|
|
13
|
-
activeTodoRows,
|
|
14
|
-
applyTodoAction,
|
|
15
|
-
countTodoStatuses,
|
|
16
|
-
formatCurrentPlan,
|
|
17
|
-
normalizeTodo,
|
|
18
|
-
restoreTodoState,
|
|
19
|
-
type StoredTodo,
|
|
20
|
-
type Todo,
|
|
21
|
-
type TodoAction,
|
|
22
|
-
type TodoActionInput,
|
|
23
|
-
type TodoState,
|
|
24
|
-
type TodoStatus,
|
|
25
|
-
} from "../src/todo-state.ts";
|
|
26
|
-
|
|
27
|
-
interface TodoDetails {
|
|
28
|
-
action: TodoAction;
|
|
29
|
-
todos: StoredTodo[];
|
|
30
|
-
nextId: number;
|
|
31
|
-
error?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const TodoParams = Type.Object({
|
|
35
|
-
action: StringEnum(["plan", "add_step", "complete_current", "cancel_current", "list"] as const, {
|
|
36
|
-
description: "plan creates or replaces the current plan; add_step appends; complete_current or cancel_current advances one expected current step; list reads the current plan",
|
|
37
|
-
}),
|
|
38
|
-
items: Type.Optional(Type.Array(Type.String(), {
|
|
39
|
-
description: "Ordered executable steps for plan; an empty array cancels all unfinished work",
|
|
40
|
-
})),
|
|
41
|
-
text: Type.Optional(Type.String({ description: "Step text for add_step" })),
|
|
42
|
-
expected_current_id: Type.Optional(Type.Integer({
|
|
43
|
-
minimum: 1,
|
|
44
|
-
description: "Stable ID of the sole current doing step; required by complete_current and cancel_current",
|
|
45
|
-
})),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const WIDGET_KEY = "todo-list";
|
|
49
|
-
|
|
50
|
-
function sortedTodos(todos: Todo[]): Todo[] {
|
|
51
|
-
return [...todos].sort((a, b) => a.id - b.id);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function statusMarker(status: TodoStatus): string {
|
|
55
|
-
if (status === "doing") return "◼";
|
|
56
|
-
if (status === "done") return "✔";
|
|
57
|
-
if (status === "cancelled") return "✖";
|
|
58
|
-
return "◻";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function themedStatus(theme: Theme, status: TodoStatus, text: string): string {
|
|
62
|
-
if (status === "doing") return theme.fg("warning", text);
|
|
63
|
-
if (status === "done") return theme.fg("success", text);
|
|
64
|
-
if (status === "cancelled") return theme.fg("error", text);
|
|
65
|
-
return theme.fg("dim", text);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function historySummary(todos: Todo[]): string {
|
|
69
|
-
const counts = countTodoStatuses(todos);
|
|
70
|
-
return `${todos.length} steps · ${counts.done} done · ${counts.cancelled} cancelled · ${counts.doing + counts.todo} current`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function currentSummary(todos: Todo[]): string {
|
|
74
|
-
const rows = activeTodoRows(todos);
|
|
75
|
-
return `${rows.length} current step${rows.length === 1 ? "" : "s"}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function renderHistoryLine(theme: Theme, todo: Todo): string {
|
|
79
|
-
const icon = themedStatus(theme, todo.status, statusMarker(todo.status));
|
|
80
|
-
if (todo.status === "done" || todo.status === "cancelled") {
|
|
81
|
-
return `${icon} \x1b[9m${theme.fg("dim", `#${todo.id} ${todo.text}`)}\x1b[29m`;
|
|
82
|
-
}
|
|
83
|
-
return `${icon} ${theme.fg("accent", `#${todo.id}`)} ${theme.fg("text", todo.text)}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderCurrentLine(theme: Theme, todo: Todo, position: number): string {
|
|
87
|
-
const icon = themedStatus(theme, todo.status, statusMarker(todo.status));
|
|
88
|
-
return `${icon} ${theme.fg("accent", `${position}.`)} ${theme.fg("text", todo.text)}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export class CurrentPlanWidget {
|
|
92
|
-
private cachedWidth?: number;
|
|
93
|
-
private cachedLines?: string[];
|
|
94
|
-
private readonly getTodos: () => Todo[];
|
|
95
|
-
private readonly theme: Theme;
|
|
96
|
-
|
|
97
|
-
constructor(getTodos: () => Todo[], theme: Theme) {
|
|
98
|
-
this.getTodos = getTodos;
|
|
99
|
-
this.theme = theme;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
render(width: number): string[] {
|
|
103
|
-
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
104
|
-
const todos = this.getTodos();
|
|
105
|
-
const rows = activeTodoRows(todos);
|
|
106
|
-
this.cachedLines = [
|
|
107
|
-
truncateToWidth(`${this.theme.fg("accent", "●")} ${this.theme.fg("muted", currentSummary(todos))}`, width),
|
|
108
|
-
...rows.map(({ position, todo }) => truncateToWidth(` ${renderCurrentLine(this.theme, todo, position)}`, width)),
|
|
109
|
-
];
|
|
110
|
-
this.cachedWidth = width;
|
|
111
|
-
return this.cachedLines;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
invalidate(): void {
|
|
115
|
-
this.cachedWidth = undefined;
|
|
116
|
-
this.cachedLines = undefined;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class TodoListComponent {
|
|
121
|
-
private cachedWidth?: number;
|
|
122
|
-
private cachedLines?: string[];
|
|
123
|
-
private readonly todos: Todo[];
|
|
124
|
-
private readonly theme: Theme;
|
|
125
|
-
private readonly onClose: () => void;
|
|
126
|
-
|
|
127
|
-
constructor(todos: Todo[], theme: Theme, onClose: () => void) {
|
|
128
|
-
this.todos = todos;
|
|
129
|
-
this.theme = theme;
|
|
130
|
-
this.onClose = onClose;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
handleInput(data: string): void {
|
|
134
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) this.onClose();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
render(width: number): string[] {
|
|
138
|
-
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
139
|
-
|
|
140
|
-
const border = this.theme.fg("borderMuted", "-".repeat(Math.max(0, width - 10)));
|
|
141
|
-
const lines = ["", truncateToWidth(`${this.theme.fg("borderMuted", "---")}${this.theme.fg("accent", " Todos ")}${border}`, width), ""];
|
|
142
|
-
if (this.todos.length === 0) {
|
|
143
|
-
lines.push(truncateToWidth(` ${this.theme.fg("dim", "No todo history on this branch")}`, width));
|
|
144
|
-
} else {
|
|
145
|
-
lines.push(truncateToWidth(`${this.theme.fg("accent", "●")} ${this.theme.fg("muted", historySummary(this.todos))}`, width), "");
|
|
146
|
-
for (const todo of sortedTodos(this.todos)) lines.push(truncateToWidth(` ${renderHistoryLine(this.theme, todo)}`, width));
|
|
147
|
-
}
|
|
148
|
-
lines.push("", truncateToWidth(` ${this.theme.fg("dim", "Press Escape to close")}`, width), "");
|
|
149
|
-
this.cachedWidth = width;
|
|
150
|
-
this.cachedLines = lines;
|
|
151
|
-
return lines;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
invalidate(): void {
|
|
155
|
-
this.cachedWidth = undefined;
|
|
156
|
-
this.cachedLines = undefined;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
interface MountedWidget {
|
|
161
|
-
component: CurrentPlanWidget;
|
|
162
|
-
tui: { requestRender(): void };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export default function (pi: ExtensionAPI) {
|
|
166
|
-
if (process.env.PI_HERDR_SUBAGENTS_CHILD === "1") return;
|
|
167
|
-
|
|
168
|
-
let state: TodoState = { todos: [], nextId: 1 };
|
|
169
|
-
let mountedWidget: MountedWidget | undefined;
|
|
170
|
-
let actionQueue = Promise.resolve();
|
|
171
|
-
const snapshot = (): StoredTodo[] => state.todos.map((todo) => ({ ...todo }));
|
|
172
|
-
|
|
173
|
-
const refreshUI = (ctx: ExtensionContext) => {
|
|
174
|
-
if (!ctx.hasUI) return;
|
|
175
|
-
const hasCurrentPlan = activeTodoRows(state.todos).length > 0;
|
|
176
|
-
if (!hasCurrentPlan) {
|
|
177
|
-
if (mountedWidget) {
|
|
178
|
-
mountedWidget.component.invalidate();
|
|
179
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
180
|
-
mountedWidget.tui.requestRender();
|
|
181
|
-
mountedWidget = undefined;
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (mountedWidget) {
|
|
187
|
-
mountedWidget.component.invalidate();
|
|
188
|
-
mountedWidget.tui.requestRender();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
ctx.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
193
|
-
const component = new CurrentPlanWidget(() => state.todos, theme);
|
|
194
|
-
mountedWidget = { component, tui };
|
|
195
|
-
component.invalidate();
|
|
196
|
-
tui.requestRender();
|
|
197
|
-
return component;
|
|
198
|
-
});
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const reconstructState = (ctx: ExtensionContext) => {
|
|
202
|
-
state = { todos: [], nextId: 1 };
|
|
203
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
204
|
-
if (entry.type !== "message") continue;
|
|
205
|
-
const message = entry.message;
|
|
206
|
-
if (message.role !== "toolResult" || message.toolName !== "todo") continue;
|
|
207
|
-
const details = message.details as TodoDetails | undefined;
|
|
208
|
-
if (!details || !Array.isArray(details.todos) || typeof details.nextId !== "number") continue;
|
|
209
|
-
state = restoreTodoState(details.todos, details.nextId);
|
|
210
|
-
}
|
|
211
|
-
refreshUI(ctx);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
|
|
215
|
-
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
|
|
216
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
217
|
-
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
218
|
-
mountedWidget = undefined;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
pi.registerTool({
|
|
222
|
-
name: "todo",
|
|
223
|
-
label: "Todo",
|
|
224
|
-
description: "Maintain a branch-local, user-visible CURRENT execution plan. Use plan only for explicit multi-step work, genuinely multi-stage work, or cross-model handoff—not simple one-step tasks. Preserve user intent and order. Complete only work actually done; blocked-but-needed work stays current. cancel_current means the current step is no longer needed while later steps remain valid; changed direction uses plan. Each completion/cancellation must name expected_current_id and advances exactly one step.",
|
|
225
|
-
parameters: TodoParams,
|
|
226
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
227
|
-
|
|
228
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
229
|
-
const run = actionQueue.then(() => {
|
|
230
|
-
const result = applyTodoAction(state, params as TodoActionInput);
|
|
231
|
-
state = { todos: result.todos, nextId: result.nextId };
|
|
232
|
-
refreshUI(ctx);
|
|
233
|
-
return {
|
|
234
|
-
content: [{
|
|
235
|
-
type: "text" as const,
|
|
236
|
-
text: result.error ? `Error: ${result.error}\n\n${formatCurrentPlan(state.todos)}` : formatCurrentPlan(state.todos),
|
|
237
|
-
}],
|
|
238
|
-
details: {
|
|
239
|
-
action: params.action,
|
|
240
|
-
todos: snapshot(),
|
|
241
|
-
nextId: state.nextId,
|
|
242
|
-
...(result.error ? { error: result.error } : {}),
|
|
243
|
-
} as TodoDetails,
|
|
244
|
-
...(result.error ? { isError: true } : {}),
|
|
245
|
-
};
|
|
246
|
-
});
|
|
247
|
-
actionQueue = run.then(() => undefined, () => undefined);
|
|
248
|
-
return run;
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
renderCall(args, theme) {
|
|
252
|
-
let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
|
|
253
|
-
if (args.items) text += ` ${theme.fg("dim", `${args.items.length} steps`)}`;
|
|
254
|
-
if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
|
|
255
|
-
if (args.expected_current_id !== undefined) text += ` ${theme.fg("accent", `#${args.expected_current_id}`)}`;
|
|
256
|
-
return new Text(text, 0, 0);
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
renderResult(result, _options, theme) {
|
|
260
|
-
const item = result.content[0];
|
|
261
|
-
const text = item?.type === "text" ? item.text : "";
|
|
262
|
-
const details = result.details as TodoDetails | undefined;
|
|
263
|
-
return new Text(details?.error ? theme.fg("error", text) : theme.fg("muted", text), 0, 0);
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
pi.registerCommand("todos", {
|
|
268
|
-
description: "Show full Todo history on the current branch",
|
|
269
|
-
handler: async (_args, ctx) => {
|
|
270
|
-
if (ctx.mode !== "tui") {
|
|
271
|
-
ctx.ui.notify("/todos requires interactive mode", "error");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
await ctx.ui.custom<void>((_tui, theme, _keybindings, done) =>
|
|
275
|
-
new TodoListComponent(snapshot().map(normalizeTodo), theme, () => done()),
|
|
276
|
-
);
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@maxiaochao/pi-todo",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "Branch-local execution-plan lifecycle and history extension for Pi.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"keywords": ["pi-package", "pi-extension", "todo"],
|
|
7
|
-
"files": ["extensions", "src", "docs", "README.md", "TESTING.md", "CHANGELOG.md"],
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/chaochaoxiaochao/pi-toolkit.git",
|
|
11
|
-
"directory": "packages/todo"
|
|
12
|
-
},
|
|
13
|
-
"pi": { "extensions": ["./extensions/todo.ts"] },
|
|
14
|
-
"peerDependencies": {
|
|
15
|
-
"@earendil-works/pi-ai": "*",
|
|
16
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
17
|
-
"@earendil-works/pi-tui": "*",
|
|
18
|
-
"typebox": "*"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "node --experimental-strip-types tests/todo-state.mjs && node --experimental-strip-types tests/todo-extension.mjs",
|
|
22
|
-
"load-test": "pi -ne -e ./extensions/todo.ts"
|
|
23
|
-
},
|
|
24
|
-
"publishConfig": { "access": "public" }
|
|
25
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
export type TodoStatus = "todo" | "doing" | "done" | "cancelled";
|
|
2
|
-
export type TodoAction = "plan" | "add_step" | "complete_current" | "cancel_current" | "list";
|
|
3
|
-
|
|
4
|
-
export interface Todo {
|
|
5
|
-
id: number;
|
|
6
|
-
text: string;
|
|
7
|
-
status: TodoStatus;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface StoredTodo {
|
|
11
|
-
id: number;
|
|
12
|
-
text: string;
|
|
13
|
-
status?: TodoStatus;
|
|
14
|
-
done?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TodoState {
|
|
18
|
-
todos: Todo[];
|
|
19
|
-
nextId: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ActiveTodoRow {
|
|
23
|
-
position: number;
|
|
24
|
-
todo: Todo;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface TodoActionInput {
|
|
28
|
-
action: TodoAction;
|
|
29
|
-
items?: string[];
|
|
30
|
-
text?: string;
|
|
31
|
-
expected_current_id?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface TodoActionResult extends TodoState {
|
|
35
|
-
message: string;
|
|
36
|
-
error?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type TodoCounts = Record<TodoStatus, number>;
|
|
40
|
-
|
|
41
|
-
export function normalizeTodo(todo: StoredTodo): Todo {
|
|
42
|
-
const status = todo.status === "todo" || todo.status === "doing" || todo.status === "done" || todo.status === "cancelled"
|
|
43
|
-
? todo.status
|
|
44
|
-
: todo.done
|
|
45
|
-
? "done"
|
|
46
|
-
: "todo";
|
|
47
|
-
return { id: todo.id, text: todo.text, status };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function normalizeTodoFlow(todos: Todo[]): void {
|
|
51
|
-
const active = todos.filter((todo) => todo.status === "doing");
|
|
52
|
-
for (const todo of active.slice(1)) todo.status = "todo";
|
|
53
|
-
if (active.length > 0) return;
|
|
54
|
-
|
|
55
|
-
const next = todos.find((todo) => todo.status === "todo");
|
|
56
|
-
if (next) next.status = "doing";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function restoreTodoState(stored: StoredTodo[], nextId?: number): TodoState {
|
|
60
|
-
const todos = stored.map(normalizeTodo).sort((a, b) => a.id - b.id);
|
|
61
|
-
normalizeTodoFlow(todos);
|
|
62
|
-
const minimumNextId = todos.reduce((max, todo) => Math.max(max, todo.id + 1), 1);
|
|
63
|
-
return { todos, nextId: Math.max(nextId ?? 1, minimumNextId) };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function countTodoStatuses(todos: Todo[]): TodoCounts {
|
|
67
|
-
return todos.reduce<TodoCounts>(
|
|
68
|
-
(counts, todo) => {
|
|
69
|
-
counts[todo.status] += 1;
|
|
70
|
-
return counts;
|
|
71
|
-
},
|
|
72
|
-
{ todo: 0, doing: 0, done: 0, cancelled: 0 },
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function activeTodos(todos: Todo[]): Todo[] {
|
|
77
|
-
return [
|
|
78
|
-
...todos.filter((todo) => todo.status === "doing"),
|
|
79
|
-
...todos.filter((todo) => todo.status === "todo"),
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function activeTodoRows(todos: Todo[]): ActiveTodoRow[] {
|
|
84
|
-
return activeTodos(todos).map((todo, index) => ({ position: index + 1, todo }));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function currentTodo(todos: Todo[]): Todo | undefined {
|
|
88
|
-
return todos.find((todo) => todo.status === "doing");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function formatCurrentPlan(todos: Todo[]): string {
|
|
92
|
-
const current = activeTodos(todos);
|
|
93
|
-
if (current.length === 0) return "CURRENT plan: none";
|
|
94
|
-
return ["CURRENT plan", ...current.map((todo) => `#${todo.id} [${todo.status}] ${todo.text}`)].join("\n");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function cloneState(state: TodoState): TodoState {
|
|
98
|
-
return { todos: state.todos.map((todo) => ({ ...todo })), nextId: state.nextId };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function fail(state: TodoState, error: string): TodoActionResult {
|
|
102
|
-
return { ...cloneState(state), message: `Error: ${error}`, error };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function cleanText(text: string | undefined): string | undefined {
|
|
106
|
-
const cleaned = text?.trim();
|
|
107
|
-
return cleaned ? cleaned : undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function closeCurrentAndAdvance(next: TodoState, expectedId: number | undefined, status: "done" | "cancelled"): TodoActionResult {
|
|
111
|
-
const current = next.todos.filter((todo) => todo.status === "doing");
|
|
112
|
-
if (expectedId === undefined) return fail(next, "expected_current_id is required");
|
|
113
|
-
if (current.length !== 1 || current[0].id !== expectedId) {
|
|
114
|
-
const actual = current.length === 1 ? `#${current[0].id}` : "none";
|
|
115
|
-
return fail(next, `expected current #${expectedId}, but current is ${actual}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
current[0].status = status;
|
|
119
|
-
const following = next.todos.find((todo) => todo.status === "todo");
|
|
120
|
-
if (following) following.status = "doing";
|
|
121
|
-
return { ...next, message: `${status === "done" ? "Completed" : "Cancelled"} current step #${expectedId}` };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function applyTodoAction(state: TodoState, input: TodoActionInput): TodoActionResult {
|
|
125
|
-
const next = cloneState(state);
|
|
126
|
-
|
|
127
|
-
switch (input.action) {
|
|
128
|
-
case "list":
|
|
129
|
-
return { ...next, message: "Listed current plan" };
|
|
130
|
-
|
|
131
|
-
case "plan": {
|
|
132
|
-
if (!input.items) return fail(state, "items is required for plan");
|
|
133
|
-
const items = input.items.map(cleanText);
|
|
134
|
-
if (items.some((item) => item === undefined)) return fail(state, "plan items must not be empty");
|
|
135
|
-
for (const todo of next.todos) {
|
|
136
|
-
if (todo.status === "doing" || todo.status === "todo") todo.status = "cancelled";
|
|
137
|
-
}
|
|
138
|
-
const texts = items as string[];
|
|
139
|
-
for (const [index, text] of texts.entries()) {
|
|
140
|
-
next.todos.push({ id: next.nextId++, text, status: index === 0 ? "doing" : "todo" });
|
|
141
|
-
}
|
|
142
|
-
return { ...next, message: texts.length === 0 ? "Cancelled all unfinished work" : `Planned ${texts.length} steps` };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
case "add_step": {
|
|
146
|
-
const text = cleanText(input.text);
|
|
147
|
-
if (!text) return fail(state, "text is required for add_step");
|
|
148
|
-
const todo: Todo = { id: next.nextId++, text, status: currentTodo(next.todos) ? "todo" : "doing" };
|
|
149
|
-
next.todos.push(todo);
|
|
150
|
-
return { ...next, message: `Added step #${todo.id}` };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
case "complete_current":
|
|
154
|
-
return closeCurrentAndAdvance(next, input.expected_current_id, "done");
|
|
155
|
-
|
|
156
|
-
case "cancel_current":
|
|
157
|
-
return closeCurrentAndAdvance(next, input.expected_current_id, "cancelled");
|
|
158
|
-
}
|
|
159
|
-
}
|