@zhushanwen/pi-goal 0.1.0 → 0.1.2

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,151 +1,49 @@
1
- # /goal — Pi 目标驱动模式
1
+ # goal
2
2
 
3
- Codex 风格的 `/goal` 命令,让 Pi 进入自主循环,持续工作直到目标达成。支持任务追踪、证据验证、Token/时间预算、阻塞检测。
3
+ Codex 风格的 `/goal` 命令 持久目标驱动自主循环,支持任务追踪、证据验证、Token/时间预算、阻塞检测。
4
+
5
+ ## 功能
6
+
7
+ - **自主循环**:`/goal <目标>` 启动后,AI 自动拆分任务并持续执行
8
+ - **证据验证**:完成任务必须提供具体证据,不能空口完成
9
+ - **预算控制**:Token 和时间双预算,70% 预警、90% 收尾、100% 终止
10
+ - **阻塞检测**:连续无进展自动阻塞,用户可手动 resume
11
+ - **持久化**:状态通过 session entries 保存,重启后自动恢复
4
12
 
5
13
  ## 安装
6
14
 
7
15
  ```bash
8
- # 全局安装(所有项目生效)
9
- git clone https://github.com/zhushanwen321/xyz-pi-extensions.git
10
- ln -s $(pwd)/xyz-pi-extensions/goal ~/.pi/agent/extensions/goal
16
+ # symlink 方式(开发推荐)
17
+ ln -s /path/to/xyz-pi-extensions-workspace/main/packages/goal \
18
+ ~/.pi/agent/extensions/goal
11
19
 
12
- # 项目级安装
13
- mkdir -p .pi/extensions
14
- ln -s /path/to/xyz-pi-extensions/goal .pi/extensions/goal
20
+ # npm 方式(正式)
21
+ pi install npm:@zhushanwen/pi-goal
15
22
  ```
16
23
 
17
- 安装后重启 Pi session 生效。
18
-
19
- ## 快速开始
24
+ ## 使用
20
25
 
21
26
  ```
22
27
  /goal 修复项目中所有失败的测试
23
28
  /goal 实现用户认证功能 --tokens 500000 --timeout 30
29
+ /goal status # 查看进度
30
+ /goal pause # 暂停
31
+ /goal resume # 恢复
32
+ /goal clear # 清除
24
33
  ```
25
34
 
26
- Pi 会自动:拆分任务 → 逐个执行 → 收集证据 → 完成目标。
27
-
28
- ## 命令参考
29
-
30
- | 命令 | 说明 |
31
- |------|------|
32
- | `/goal <目标>` | 设定新目标(会替换当前未完成的目标) |
33
- | `/goal status` | 查看当前目标状态、进度、预算 |
34
- | `/goal pause` | 暂停目标 |
35
- | `/goal resume` | 恢复暂停/阻塞的目标 |
36
- | `/goal clear` | 清除目标 |
37
- | `/goal update <新目标>` | 更新目标描述(清除旧任务,重新规划) |
38
-
39
- ## 参数
40
-
41
- | 参数 | 默认值 | 说明 |
42
- |------|--------|------|
43
- | `--tokens N` | 不限制 | Token 预算上限 |
44
- | `--timeout N` | 不限制 | 时间预算(分钟) |
45
- | `--max-turns N` | 50 | 最大轮次 |
46
- | `--max-stall N` | 5 | 连续无进展轮次,超过后自动阻塞 |
47
-
48
- 示例:
49
-
50
- ```
51
- /goal 重构 auth 模块 --tokens 200000 --timeout 15 --max-turns 30
52
- /goal 写单元测试 --tokens 100000 --max-stall 3
53
- ```
54
-
55
- ## 工作流程
56
-
57
- ```
58
- 用户: /goal 修复登录bug
59
-
60
-
61
- Pi 注入上下文 → LLM 拆分任务 (create_tasks)
62
-
63
-
64
- ┌─────────────────────────────┐
65
- │ 每个 turn: │
66
- │ 1. LLM 执行任务 │
67
- │ 2. 完成后调用 complete_task │
68
- │ (必须提供具体证据) │
69
- │ 3. 检查预算/进展/阻塞 │
70
- │ 4. 注入 continuation 继续 │
71
- └─────────┬───────────────────┘
72
- │ 全部完成
73
-
74
- LLM 调用 complete_goal (提供整体证据)
75
- ```
76
-
77
- ## 目标状态
78
-
79
- ```
80
- ┌──────────┐
81
- ┌───►│ active │◄─── resume
82
- │ └────┬─────┘
83
- │ │
84
- clear ┌──┴──┬──────────┬───────────┬──────────┐
85
- │ ▼ ▼ ▼ ▼ ▼
86
- │ paused blocked complete budget_limited time_limited
87
- │ ▲ ▲
88
- │ │ │
89
- └────┴─────┘ (resume 恢复)
90
-
91
- 可恢复: paused, blocked → active
92
- 终态: complete, budget_limited, time_limited, cancelled
93
- ```
94
-
95
- ## 内置保护机制
96
-
97
- | 机制 | 说明 |
98
- |------|------|
99
- | 证据验证 | `complete_task` / `complete_goal` 必须提供具体证据,不能空口完成 |
100
- | 预算预警 | Token/时间达 70% 提示注意,90% 注入收尾 steering |
101
- | 预算终止 | Token 100% → budget_limited 终态 |
102
- | 阻塞检测 | 连续 N 轮无进展 → blocked,需用户手动 resume |
103
- | 去抖保护 | 本轮 token 消耗为 0 时不发 continuation,防止无限循环 |
104
- | 防重入 | `before_agent_start` 注入的上下文不会在 `agent_end` 重复发送 |
105
- | Resume 预算重检 | 恢复暂停目标时,如果预算已耗尽,直接拒绝 |
106
- | 零预算拒绝 | `--tokens 0` 会被拒绝 |
107
- | 空白目标拒绝 | `/goal ""` 或 `/goal " "` 会被拒绝 |
108
- | 目标长度限制 | 超过 4000 字符会被拒绝 |
109
- | 上下文空间保护 | 上下文窗口 >85% 时暂停,防止 OOM |
110
-
111
- ## goal_manager 工具
112
-
113
- Pi 注册了 `goal_manager` 工具供 LLM 调用,用户不需要直接操作:
114
-
115
- | Action | 说明 |
116
- |--------|------|
117
- | `create_tasks` | 拆分目标为任务清单 |
118
- | `complete_task` | 标记任务完成(需 taskId + evidence) |
119
- | `list_tasks` | 查看任务进度和剩余预算 |
120
- | `complete_goal` | 标记目标完成(需 evidence,且所有任务已完成) |
121
- | `cancel_goal` | 取消目标(用户要求退出/停止时,LLM 直接调用,一步退出) |
122
- | `report_blocked` | 报告阻塞原因 |
123
-
124
- 每个工具响应都包含当前预算信息(已用/剩余),让 LLM 自我调节。
125
-
126
- ## 注意事项
127
-
128
- - 目标描述会被 XML 转义,防止 prompt 注入
129
- - `/goal update` 会清除已有任务和计数器,允许重新规划
130
- - 已暂停的目标 resume 时会重新检查预算,超限则直接终止
131
- - 状态通过 session entries 持久化,session 重启后自动恢复
132
- - Token 统计排除 cached input,避免跨 turn 双重计算
133
- - **退出方式有三种**:`/goal clear`(用户手动)、`/goal pause`(暂停稍后继续)、或直接告诉 LLM「停止/取消/退出」,LLM 会调用 `cancel_goal` 一步退出
134
-
135
35
  ## 文件结构
136
36
 
137
37
  ```
138
38
  goal/
139
- ├── index.ts # 入口 — 命令、事件、工具注册
140
- ├── src/
141
- ├── index.ts # Extension 主逻辑
142
- ├── state.ts # 状态机、类型、序列化
143
- ├── commands.ts # 命令参数解析
144
- ├── templates.ts # Steering prompt 模板
145
- │ └── widget.ts # TUI 状态栏渲染
146
- └── package.json # 元数据(仅标识用)
39
+ ├── index.ts
40
+ └── src/
41
+ ├── index.ts # 入口 — 命令、事件、工具注册
42
+ ├── state.ts # 状态机(6 态)
43
+ ├── commands.ts # 命令参数解析
44
+ ├── templates.ts # Steering prompt 模板
45
+ ├── tool-handler.ts# goal_manager 工具处理
46
+ ├── budget.ts # 预算计算
47
+ ├── constants.ts # 常量
48
+ └── widget.ts # TUI 状态栏渲染
147
49
  ```
148
-
149
- ## License
150
-
151
- MIT
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
5
+ "type": "module",
5
6
  "main": "src/index.ts",
7
+ "pi": {
8
+ "extensions": [
9
+ "./src/index.ts"
10
+ ]
11
+ },
6
12
  "keywords": [
7
- "pi",
13
+ "pi-package",
8
14
  "extension",
9
15
  "goal",
10
16
  "autonomous",
@@ -16,7 +22,18 @@
16
22
  "index.ts"
17
23
  ],
18
24
  "peerDependencies": {
19
- "@mariozechner/pi-coding-agent": "*"
25
+ "@mariozechner/pi-coding-agent": "*",
26
+ "@earendil-works/pi-tui": "*",
27
+ "@earendil-works/pi-ai": "*",
28
+ "@sinclair/typebox": "*"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@earendil-works/pi-tui": {
32
+ "optional": true
33
+ },
34
+ "@earendil-works/pi-ai": {
35
+ "optional": true
36
+ }
20
37
  },
21
38
  "scripts": {
22
39
  "typecheck": "npx tsc --noEmit"
package/src/index.ts CHANGED
@@ -16,8 +16,11 @@
16
16
  * - deserializeState 向后兼容旧格式
17
17
  */
18
18
 
19
- import type { ExtensionAPI, ExtensionContext, CustomEntry } from "@mariozechner/pi-coding-agent";
19
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, CustomEntry, Theme } from "@mariozechner/pi-coding-agent";
20
+
20
21
  import { Text } from "@mariozechner/pi-tui";
22
+ import { type Static } from "typebox";
23
+
21
24
 
22
25
  import {
23
26
  type GoalTask,
@@ -141,8 +144,8 @@ function reconstructGoalState(pi: ExtensionAPI, session: GoalSession, ctx: Exten
141
144
 
142
145
  // ── Command Handler ───────────────────────────────────
143
146
 
144
- async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: string, ctx: ExtensionContext): Promise<void> {
145
- const parsed = parseGoalArgs(args);
147
+ async function handleGoalCommand(pi: ExtensionAPI, session: GoalSession, args: string | undefined, ctx: ExtensionContext): Promise<void> {
148
+ const parsed = parseGoalArgs(args ?? "");
146
149
 
147
150
  switch (parsed.action) {
148
151
  case "status": {
@@ -742,17 +745,20 @@ export default function goalExtension(pi: ExtensionAPI) {
742
745
  ],
743
746
  parameters: GoalManagerParams,
744
747
 
745
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
748
+ async execute(_toolCallId: string, params: Static<typeof GoalManagerParams>, _signal: AbortSignal | undefined, _onUpdate: any, ctx: ExtensionContext) {
746
749
  try {
747
750
  return await executeGoalAction(pi, session, params, ctx);
748
751
  } catch (err) {
749
752
  const msg = err instanceof Error ? err.message : String(err);
750
753
  const inputSummary = JSON.stringify(params, null, 2);
751
- throw new Error(`${msg}\n\nInput: ${inputSummary}`);
754
+ return {
755
+ content: [{ type: "text", text: `${msg}\n\nInput: ${inputSummary}` }],
756
+ isError: true,
757
+ };
752
758
  }
753
759
  },
754
760
 
755
- renderCall(args, theme) {
761
+ renderCall(args: any, theme: Theme) {
756
762
  let text = theme.fg("toolTitle", theme.bold("goal_manager ")) + theme.fg("muted", args.action);
757
763
  if (args.tasks) text += ` ${theme.fg("dim", `(${args.tasks.length} tasks)`)}`;
758
764
  if (args.updates) text += ` ${theme.fg("dim", `(${args.updates.length} updates)`)}`;
@@ -763,7 +769,7 @@ export default function goalExtension(pi: ExtensionAPI) {
763
769
  return new Text(text, 0, 0);
764
770
  },
765
771
 
766
- renderResult(result, { expanded }, theme) {
772
+ renderResult(result: any, { expanded }: any, theme: Theme) {
767
773
  const details = result.details as GoalManagerDetails | undefined;
768
774
  if (!details || !Array.isArray(details.tasks)) {
769
775
  const text = result.content[0];
@@ -811,14 +817,14 @@ export default function goalExtension(pi: ExtensionAPI) {
811
817
  pi.registerCommand("goal", {
812
818
  description:
813
819
  "目标驱动模式: /goal <objective> [--tokens N] [--timeout N] [--max-turns N] | /goal pause | /goal resume | /goal clear | /goal update <new-objective> | /goal status | /goal history",
814
- handler: async (args, ctx) => {
820
+ handler: async (args: string | undefined, ctx: ExtensionCommandContext) => {
815
821
  await handleGoalCommand(pi, session, args, ctx);
816
822
  },
817
823
  });
818
824
 
819
825
  // ── Event: before_agent_start ──────────────────────
820
826
 
821
- pi.on("before_agent_start", async (_event, ctx) => {
827
+ pi.on("before_agent_start", async (_event: any, ctx: ExtensionContext) => {
822
828
  return handleBeforeAgentStart(pi, session, ctx);
823
829
  });
824
830
 
@@ -831,7 +837,7 @@ export default function goalExtension(pi: ExtensionAPI) {
831
837
 
832
838
  // ── Event: turn_end ────────────────────────────────
833
839
 
834
- pi.on("turn_end", async (_event, ctx) => {
840
+ pi.on("turn_end", async (_event: any, ctx: ExtensionContext) => {
835
841
  if (!session.state) return;
836
842
  session.state.currentTurnIndex++;
837
843
  updateWidget(session, ctx);
@@ -839,7 +845,7 @@ export default function goalExtension(pi: ExtensionAPI) {
839
845
 
840
846
  // ── Event: message_end (token accounting) ──────────
841
847
 
842
- pi.on("message_end", async (event, _ctx) => {
848
+ pi.on("message_end", async (event: any, _ctx: ExtensionContext) => {
843
849
  if (!session.state || !isActiveStatus(session.state.status)) return;
844
850
  if (event.message.role !== "assistant") return;
845
851
 
@@ -858,13 +864,13 @@ export default function goalExtension(pi: ExtensionAPI) {
858
864
 
859
865
  // ── Event: agent_end ───────────────────────────────
860
866
 
861
- pi.on("agent_end", async (_event, ctx) => {
867
+ pi.on("agent_end", async (_event: any, ctx: ExtensionContext) => {
862
868
  await handleAgentEnd(pi, session, ctx);
863
869
  });
864
870
 
865
871
  // ── Event: session_start (state reconstruction) ───
866
872
 
867
- pi.on("session_start", async (_event, ctx) => {
873
+ pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
868
874
  reconstructGoalState(pi, session, ctx);
869
875
  if (session.state) {
870
876
  session.tasksCompletedAtAgentStart = getCompletedCount(session.state.tasks);
@@ -881,7 +887,7 @@ export default function goalExtension(pi: ExtensionAPI) {
881
887
  ];
882
888
 
883
889
  for (const customType of goalMessageTypes) {
884
- pi.registerMessageRenderer(customType, (message, _options, theme) => {
890
+ pi.registerMessageRenderer(customType, (message: any, _options: any, theme: Theme) => {
885
891
  const prefix =
886
892
  message.customType === "goal-context-exceeded"
887
893
  ? theme.fg("error", "[GOAL 预算] ")
@@ -237,7 +237,7 @@ export async function executeGoalAction(
237
237
  `如需追加任务请用 add_tasks,如需全部重新规划请用 /goal update。`,
238
238
  );
239
239
  }
240
- state.tasks = params.tasks.map((desc, i) => ({
240
+ state.tasks = params.tasks.map((desc: string, i: number) => ({
241
241
  id: i + 1,
242
242
  description: normalizeDescription(desc),
243
243
  status: "pending" as const,
@@ -256,7 +256,7 @@ export async function executeGoalAction(
256
256
  const startId = state.tasks.length > 0
257
257
  ? Math.max(...state.tasks.map((t) => t.id)) + 1
258
258
  : 1;
259
- const newTasks: GoalTask[] = params.tasks.map((desc, i) => ({
259
+ const newTasks: GoalTask[] = params.tasks.map((desc: string, i: number) => ({
260
260
  id: startId + i,
261
261
  description: normalizeDescription(desc),
262
262
  status: "pending" as const,
@@ -273,8 +273,8 @@ export async function executeGoalAction(
273
273
  if (!params.updates || params.updates.length === 0) {
274
274
  throw new Error("update_tasks requires a non-empty updates array");
275
275
  }
276
- const taskIds = params.updates.map((u) => u.taskId);
277
- const duplicateIds = taskIds.filter((id, i) => taskIds.indexOf(id) !== i);
276
+ const taskIds = params.updates.map((u: { taskId: number; status: string; evidence?: string }) => u.taskId);
277
+ const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
278
278
  if (duplicateIds.length > 0) {
279
279
  throw new Error(`重复的 taskId: ${[...new Set(duplicateIds)].join(", ")}`);
280
280
  }
@@ -400,11 +400,11 @@ export async function executeGoalAction(
400
400
  }
401
401
  const subtasks = parentTask.subtasks ?? [];
402
402
  const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
403
- const trimmed = params.texts.map((t) => t.trim()).filter((t) => t.length > 0);
403
+ const trimmed = params.texts.map((t: string) => t.trim()).filter((t: string) => t.length > 0);
404
404
  if (trimmed.length === 0) {
405
405
  throw new Error("texts 中至少需要一个非空字符串");
406
406
  }
407
- const newSubtasks: Subtask[] = trimmed.map((text, i) => ({
407
+ const newSubtasks: Subtask[] = trimmed.map((text: string, i: number) => ({
408
408
  id: startId + i,
409
409
  text,
410
410
  status: "pending" as const,