@gavin-hu/my-pi-agent 0.14.0 → 0.15.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.
Files changed (222) hide show
  1. package/README.md +55 -33
  2. package/README.zh-CN.md +44 -27
  3. package/extensions/bytefly/README.md +193 -140
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/register.ts +24 -5
  7. package/extensions/bytefly/doctor/tools.ts +4 -0
  8. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  9. package/extensions/bytefly/footer/config.ts +10 -6
  10. package/extensions/bytefly/footer/price/price.ts +5 -3
  11. package/extensions/bytefly/footer/price/rates.ts +63 -26
  12. package/extensions/bytefly/footer/render/status.ts +3 -3
  13. package/extensions/bytefly/header/component.ts +3 -2
  14. package/extensions/bytefly/index.ts +3 -3
  15. package/extensions/bytefly/notice/component.ts +8 -3
  16. package/extensions/bytefly/notify/config.ts +6 -11
  17. package/extensions/bytefly/notify/dispatch.ts +13 -5
  18. package/extensions/bytefly/panel/commands.ts +21 -1
  19. package/extensions/bytefly/panel/format.ts +5 -2
  20. package/extensions/bytefly/panel/settings-runtime.ts +26 -6
  21. package/extensions/bytefly/panel/settings.ts +41 -4
  22. package/extensions/bytefly/panel/themes.ts +32 -0
  23. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  24. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  25. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  26. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  27. package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
  28. package/extensions/bytefly/panel/tui.ts +94 -152
  29. package/extensions/bytefly/picker/discovery.ts +1 -0
  30. package/extensions/bytefly/picker/types.ts +2 -0
  31. package/extensions/bytefly/separator/component.ts +14 -4
  32. package/extensions/bytefly/update/cache.ts +58 -19
  33. package/extensions/bytefly/update/check.ts +35 -0
  34. package/extensions/bytefly/update/format.ts +76 -35
  35. package/extensions/bytefly/update/register.ts +144 -55
  36. package/extensions/bytefly/update/state.ts +4 -2
  37. package/extensions/bytefly/update/suppress.ts +66 -0
  38. package/extensions/bytefly/update/types.ts +15 -0
  39. package/extensions/check/README.md +253 -0
  40. package/extensions/check/commands.ts +128 -0
  41. package/extensions/check/config.ts +39 -0
  42. package/extensions/check/detect.ts +269 -0
  43. package/extensions/check/failures.ts +230 -0
  44. package/extensions/check/format.ts +135 -0
  45. package/extensions/check/index.ts +26 -0
  46. package/extensions/check/render.ts +185 -0
  47. package/extensions/check/runner.ts +176 -0
  48. package/extensions/check/runtime.ts +244 -0
  49. package/extensions/check/source.ts +80 -0
  50. package/extensions/check/tools.ts +173 -0
  51. package/extensions/check/tui.ts +195 -0
  52. package/extensions/check/types.ts +99 -0
  53. package/extensions/document/README.md +1 -1
  54. package/extensions/document/extract/pdf.ts +8 -16
  55. package/extensions/document/formats.ts +2 -2
  56. package/extensions/file-browse/README.md +66 -32
  57. package/extensions/file-browse/client.ts +95 -32
  58. package/extensions/file-browse/config.ts +9 -9
  59. package/extensions/file-browse/git.ts +37 -47
  60. package/extensions/file-browse/html.ts +39 -58
  61. package/extensions/file-browse/markdown.ts +110 -92
  62. package/extensions/file-browse/raw-html.ts +478 -0
  63. package/extensions/file-browse/router.ts +32 -70
  64. package/extensions/file-browse/server.ts +18 -56
  65. package/extensions/file-browse/urls.ts +80 -0
  66. package/extensions/goal/README.md +103 -47
  67. package/extensions/goal/clarify-parse.ts +4 -9
  68. package/extensions/goal/clarify.ts +27 -4
  69. package/extensions/goal/commands.ts +24 -11
  70. package/extensions/goal/format.ts +41 -10
  71. package/extensions/goal/index.ts +39 -16
  72. package/extensions/goal/plan-route.ts +51 -0
  73. package/extensions/goal/schema.ts +6 -23
  74. package/extensions/goal/state.ts +39 -57
  75. package/extensions/goal/tools.ts +26 -20
  76. package/extensions/goal/types.ts +11 -1
  77. package/extensions/job/README.md +20 -6
  78. package/extensions/job/format.ts +18 -68
  79. package/extensions/job/index.ts +26 -41
  80. package/extensions/job/log-writer.ts +208 -0
  81. package/extensions/job/logs.ts +3 -2
  82. package/extensions/job/process.ts +6 -99
  83. package/extensions/job/runtime.ts +42 -181
  84. package/extensions/job/schema.ts +1 -4
  85. package/extensions/job/tools.ts +1 -15
  86. package/extensions/job/tui.ts +117 -307
  87. package/extensions/job/ui.ts +7 -33
  88. package/extensions/memory/README.md +1 -1
  89. package/extensions/memory/errors.ts +5 -6
  90. package/extensions/memory/tui.ts +14 -24
  91. package/extensions/plan/README.md +94 -18
  92. package/extensions/plan/commands.ts +9 -5
  93. package/extensions/plan/format.ts +87 -0
  94. package/extensions/plan/index.ts +31 -15
  95. package/extensions/plan/plans.ts +2 -3
  96. package/extensions/plan/policy.ts +0 -6
  97. package/extensions/plan/runtime.ts +4 -2
  98. package/extensions/plan/steps.ts +37 -8
  99. package/extensions/plan/todos.ts +44 -3
  100. package/extensions/plan/tools/enter.ts +94 -0
  101. package/extensions/plan/tools/exit.ts +296 -0
  102. package/extensions/plan/tools/nested.ts +21 -0
  103. package/extensions/plan/tools/steps.ts +136 -0
  104. package/extensions/plan/tools/todo-seed.ts +76 -0
  105. package/extensions/plan/tools/write.ts +127 -0
  106. package/extensions/plan/tools.ts +18 -703
  107. package/extensions/plan/web-client.ts +6 -22
  108. package/extensions/plan/web-page.ts +19 -30
  109. package/extensions/plan/web-router.ts +34 -71
  110. package/extensions/plan/web.ts +13 -40
  111. package/extensions/question/answers.ts +3 -8
  112. package/extensions/question/tui.ts +6 -6
  113. package/extensions/rewind/README.md +95 -22
  114. package/extensions/rewind/checklist.ts +185 -0
  115. package/extensions/rewind/commands.ts +8 -0
  116. package/extensions/rewind/config.ts +3 -11
  117. package/extensions/rewind/format.ts +220 -2
  118. package/extensions/rewind/git.ts +74 -7
  119. package/extensions/rewind/hunk-picker.ts +114 -0
  120. package/extensions/rewind/patch.ts +88 -0
  121. package/extensions/rewind/path-picker.ts +67 -100
  122. package/extensions/rewind/paths.ts +93 -19
  123. package/extensions/rewind/restore.ts +53 -10
  124. package/extensions/rewind/review-tui.ts +320 -0
  125. package/extensions/rewind/review.ts +397 -0
  126. package/extensions/rewind/rewind-tui.ts +4 -3
  127. package/extensions/rewind/runtime.ts +94 -17
  128. package/extensions/subagent/README.md +61 -6
  129. package/extensions/subagent/background-tui.ts +208 -0
  130. package/extensions/subagent/background.ts +470 -0
  131. package/extensions/subagent/commands.ts +80 -0
  132. package/extensions/subagent/config.ts +4 -0
  133. package/extensions/subagent/format.ts +24 -1
  134. package/extensions/subagent/index.ts +91 -2
  135. package/extensions/subagent/render.ts +13 -0
  136. package/extensions/subagent/run.ts +17 -16
  137. package/extensions/subagent/schema.ts +12 -0
  138. package/extensions/subagent/types.ts +11 -0
  139. package/extensions/subagent/ui.ts +50 -0
  140. package/extensions/todo/README.md +18 -13
  141. package/extensions/todo/format.ts +3 -2
  142. package/extensions/todo/runtime.ts +3 -2
  143. package/extensions/todo/schema.ts +8 -84
  144. package/extensions/todo/state.ts +5 -46
  145. package/extensions/todo/tools.ts +2 -1
  146. package/extensions/todo/types.ts +6 -15
  147. package/extensions/watch/README.md +14 -6
  148. package/extensions/watch/child.ts +150 -0
  149. package/extensions/watch/format.ts +8 -48
  150. package/extensions/watch/index.ts +42 -58
  151. package/extensions/watch/runtime.ts +49 -128
  152. package/extensions/watch/schema.ts +1 -4
  153. package/extensions/watch/tools.ts +1 -11
  154. package/extensions/watch/tui.ts +103 -273
  155. package/extensions/watch/ui.ts +5 -32
  156. package/extensions/web-access/README.md +1 -1
  157. package/extensions/web-access/fetch/page.ts +1 -1
  158. package/extensions/web-access/fetch/tool.ts +6 -2
  159. package/extensions/web-access/search/search.ts +2 -5
  160. package/extensions/wechat/README.md +1 -1
  161. package/extensions/wechat/client.ts +23 -0
  162. package/extensions/wechat/commands.ts +57 -42
  163. package/extensions/wechat/deliver.ts +5 -9
  164. package/extensions/wechat/errors.ts +3 -6
  165. package/extensions/wechat/inbound.ts +2 -5
  166. package/extensions/wechat/index.ts +2 -8
  167. package/extensions/wechat/outbound.ts +2 -4
  168. package/extensions/wechat/peer-worker.ts +3 -8
  169. package/extensions/wechat/poll.ts +10 -9
  170. package/extensions/wechat/presence.ts +35 -62
  171. package/extensions/wechat/reply-pipeline.ts +3 -1
  172. package/extensions/wechat/status-screen.ts +1 -1
  173. package/extensions/wechat/timer.ts +57 -0
  174. package/extensions/worktree/config.ts +8 -20
  175. package/extensions/worktree/enter.ts +7 -12
  176. package/extensions/worktree/git.ts +38 -69
  177. package/extensions/worktree/index.ts +5 -4
  178. package/extensions/worktree/registry.ts +7 -7
  179. package/extensions/worktree/root-tools.ts +2 -2
  180. package/extensions/worktree/status.ts +3 -2
  181. package/lib/README.md +22 -8
  182. package/lib/config.ts +50 -3
  183. package/lib/errors.ts +12 -0
  184. package/lib/format.ts +50 -0
  185. package/lib/git/read.ts +8 -0
  186. package/lib/goal-state.ts +23 -8
  187. package/lib/http/headers.ts +54 -0
  188. package/lib/http/index.ts +13 -0
  189. package/lib/http/response.ts +46 -0
  190. package/lib/http/router.ts +52 -0
  191. package/lib/http/server.ts +91 -0
  192. package/lib/lines.ts +77 -0
  193. package/lib/pdf.ts +82 -0
  194. package/lib/plan-entry.ts +71 -0
  195. package/lib/plan-state.ts +34 -0
  196. package/lib/report.ts +99 -0
  197. package/lib/requirements.ts +25 -19
  198. package/lib/spawn.ts +170 -0
  199. package/lib/todo-state.ts +110 -23
  200. package/lib/tool-names.ts +7 -0
  201. package/lib/tool-result.ts +33 -0
  202. package/lib/tui/README.md +68 -3
  203. package/lib/tui/chrome.ts +28 -4
  204. package/lib/tui/columns.ts +6 -1
  205. package/lib/tui/confirm.ts +65 -0
  206. package/lib/tui/dock-screen.ts +410 -0
  207. package/lib/tui/glyphs.ts +2 -1
  208. package/lib/tui/indent.ts +1 -1
  209. package/lib/tui/index.ts +2 -0
  210. package/lib/tui/rows.ts +21 -0
  211. package/lib/tui/status-controller.ts +35 -1
  212. package/lib/tui/tool.ts +59 -1
  213. package/lib/web/components.ts +20 -1
  214. package/lib/web/markup.ts +32 -0
  215. package/lib/web/runtime.ts +50 -0
  216. package/package.json +22 -5
  217. package/skills/init/SKILL.md +165 -0
  218. package/skills/init/references/agents-md-quality.md +103 -0
  219. package/skills/init/references/interview-questions.md +81 -0
  220. package/skills/refactor/SKILL.md +141 -0
  221. package/extensions/watch/process.ts +0 -63
  222. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -7,719 +7,34 @@
7
7
  * approval (a scrollable review screen in the TUI, a select dialog elsewhere),
8
8
  * and on approval seeds the `todo` tool with the plan's steps (through
9
9
  * `ctx.executeTool`, so the todo widget and validation run normally).
10
- * `set_plan_steps` then keeps the file current: after approval the model ticks
11
- * each step off in place as it finishes it. The file is the source of truth:
12
- * the model executes from it.
10
+ * `set_plan_steps` then keeps the plan current: after approval the model ticks
11
+ * each step off in place as it finishes it, and the todo item approval seeded
12
+ * from that step is ticked with it. The file is the source of truth: the model
13
+ * executes from it.
14
+ *
15
+ * Each registration lives in `tools/`: its schema, execute handler, and
16
+ * transcripts stay together, and the approval-time helpers they share live next
17
+ * to them. This module only composes them, in the order the session sees them.
13
18
  *
14
19
  * Without an interactive UI the approval tools fail with an actionable message
15
20
  * instead of deciding for the user.
16
21
  */
17
22
 
18
- import { StringEnum } from "@earendil-works/pi-ai";
19
- import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
20
- import { Type, type Static } from "typebox";
21
- import { sanitizeLine } from "../../lib/format.ts";
22
- import { lastGoal } from "../../lib/goal-state.ts";
23
- import { GOAL_TOOL, TODO_TOOL } from "../../lib/tool-names.ts";
24
- import { lastTodos } from "../../lib/todo-state.ts";
25
- import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
26
- import { rejectionLine, reuseCallText, toolCallTitle } from "../../lib/tui/tool.ts";
27
- import { paint } from "../../lib/tui/tokens.ts";
28
- import { expandHint, GLYPHS } from "../../lib/tui/glyphs.ts";
29
- import { READ_ONLY_SUMMARY } from "./policy.ts";
30
- import { feedbackSummary } from "./feedback.ts";
31
- import { ENTER_TOOL, EXIT_TOOL, SET_PLAN_STEPS_TOOL, WRITE_PLAN_TOOL, type PlanRuntime } from "./runtime.ts";
32
- import {
33
- MAX_STEP_LENGTH,
34
- MAX_STEPS,
35
- PLAN_OUTLINE,
36
- extractPlanSteps,
37
- planStepSummary,
38
- verificationItem,
39
- type PlanStep,
40
- type StepUpdate,
41
- } from "./steps.ts";
42
- import { basename } from "node:path";
43
- import { planTitle, type StoredPlan } from "./plans.ts";
44
- import { DEFAULT_TODO_MAX, mergeTodos, type TodoMerge } from "./todos.ts";
45
- import { reviewPlan } from "./review.ts";
46
- import { sanitizePlanText } from "./plan-text.ts";
47
- import type { EnterPlanModeDetails, ExitPlanModeDetails, SetPlanStepsDetails, WritePlanDetails } from "./types.ts";
23
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
+ import type { PlanRuntime } from "./runtime.ts";
25
+ import { registerEnterTool } from "./tools/enter.ts";
26
+ import { registerExitTool } from "./tools/exit.ts";
27
+ import { registerSetPlanStepsTool } from "./tools/steps.ts";
28
+ import { registerWriteTool } from "./tools/write.ts";
48
29
  import { createPlanWebReview, type PlanWebReview } from "./web.ts";
49
30
 
50
- const ExitPlanModeParams = Type.Object({
51
- plan_path: Type.String({
52
- description:
53
- "Path to the plan file written by write_plan (absolute, or relative to the working directory). The user reviews this file.",
54
- }),
55
- });
56
-
57
- const WritePlanParams = Type.Object({
58
- title: Type.String({
59
- description: 'Short title for the plan, used as the file-name slug, e.g. "Add rate limiting".',
60
- }),
61
- content: Type.String({
62
- description: "The complete plan as markdown in the documented outline, including every step.",
63
- }),
64
- plan_path: Type.Optional(
65
- Type.String({
66
- description:
67
- "Overwrite an existing plan file (one returned by an earlier write_plan) to refine it instead of creating a new file.",
68
- }),
69
- ),
70
- });
71
-
72
- const StepUpdateParams = Type.Object({
73
- step: Type.Integer({
74
- minimum: 1,
75
- description: "1-based step number, in the order the plan was approved.",
76
- }),
77
- status: StringEnum(["pending", "completed"] as const, {
78
- description: "The step's new state.",
79
- }),
80
- expected: Type.Optional(
81
- Type.String({
82
- description:
83
- "The step's text as exit_plan_mode listed it. When given, the update is refused if the step at that number no longer matches, so a reordered plan is caught instead of mis-ticked.",
84
- }),
85
- ),
86
- });
87
-
88
- const SetPlanStepsParams = Type.Object({
89
- plan_path: Type.String({
90
- description: "Path to the approved plan file, as returned by write_plan or exit_plan_mode.",
91
- }),
92
- updates: Type.Array(StepUpdateParams, {
93
- minItems: 1,
94
- description:
95
- "The steps to set; each entry sets one step's checkbox, and the last entry wins for a duplicate step. Pass expected as well to refuse a stale step number.",
96
- }),
97
- });
98
-
99
- type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
100
- type WritePlanArgs = Static<typeof WritePlanParams>;
101
- type SetPlanStepsArgs = Static<typeof SetPlanStepsParams>;
102
-
103
- /** Lines of plan shown in an unexpanded result. */
104
- const PREVIEW_LINES = 8;
105
-
106
- const ENTER_CONFIRM =
107
- `Plan mode is read-only exploration: ${READ_ONLY_SUMMARY}. ` +
108
- "Enter plan mode to investigate and propose an approach before making changes?";
109
-
110
- const ENTERED_TEXT =
111
- "You are now in plan mode. Explore read-only, then save the full plan with write_plan (a short title and the markdown) " +
112
- "and call exit_plan_mode with the returned path so the user can read the file and choose. " +
113
- `While planning, ${READ_ONLY_SUMMARY}.`;
114
-
115
- function preview(plan: string): { body: string; more: number } {
116
- const lines = plan.split("\n");
117
- if (lines.length <= PREVIEW_LINES) return { body: plan, more: 0 };
118
- return { body: lines.slice(0, PREVIEW_LINES).join("\n"), more: lines.length - PREVIEW_LINES };
119
- }
120
-
121
- /**
122
- * One sentence when a step is longer than the display cap. The text stays in the
123
- * plan file either way; only the todo list and the approval listing shorten it.
124
- */
125
- function stepElisionNote(elided: number): string {
126
- if (elided === 0) return "";
127
- const plural = elided === 1 ? " is" : "s are";
128
- return ` Note: ${elided} step${plural} longer than ${MAX_STEP_LENGTH} characters and will be shortened in the todo list.`;
129
- }
130
-
131
- /**
132
- * The diagnosis both cap refusals share: a step list longer than approval can
133
- * track. Each caller appends the fix that is available from where it stands.
134
- */
135
- function overCap(total: number): string {
136
- return `The plan has ${total} top-level steps but approval tracks at most ${MAX_STEPS}.`;
137
- }
138
-
139
- /** "Step 2" or "Steps 1, 2" for a set of step numbers. */
140
- function stepLabel(steps: number[]): string {
141
- return `${steps.length === 1 ? "Step" : "Steps"} ${steps.join(", ")}`;
142
- }
143
-
144
- /** "Steps 1, 2 completed; step 3 reopened" for the tool's text result. */
145
- function describeUpdates(updates: StepUpdate[]): string {
146
- const completed = updates.filter((update) => update.status === "completed").map((update) => update.step);
147
- const pending = updates.filter((update) => update.status === "pending").map((update) => update.step);
148
- const parts: string[] = [];
149
- if (completed.length > 0) parts.push(`${stepLabel(completed)} completed`);
150
- if (pending.length > 0) parts.push(`${stepLabel(pending)} reopened`);
151
- return parts.join("; ");
152
- }
153
-
154
- /**
155
- * The todo tool's item cap, read from its registered schema so the merge stops
156
- * before the nested call would reject the list. Falls back to the documented
157
- * default when the tool is absent or has not declared one.
158
- */
159
- function todoMaxItems(pi: ExtensionAPI): number {
160
- const info = pi.getAllTools().find((tool) => tool.name === TODO_TOOL);
161
- const parameters = info?.parameters as { properties?: { todos?: { maxItems?: unknown } } } | undefined;
162
- const max = parameters?.properties?.todos?.maxItems;
163
- return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_TODO_MAX;
164
- }
165
-
166
- /**
167
- * Record the plan's steps in the todo list. The list is whole-list replacement,
168
- * so the steps are merged into what the branch already holds: existing items
169
- * keep their place and status, duplicates are skipped, and the cap defers the
170
- * rest. Returns what happened for the approval message.
171
- */
172
- async function seedTodos(
173
- pi: ExtensionAPI,
174
- ctx: ExtensionToolContext,
175
- plan: string,
176
- ): Promise<{ steps: PlanStep[]; merged?: TodoMerge; error?: string }> {
177
- const steps = extractPlanSteps(plan);
178
- const verification = verificationItem(plan);
179
- if (steps.length === 0 && !verification) return { steps };
180
- const merged = mergeTodos(lastTodos(ctx.sessionManager.getBranch()), steps, todoMaxItems(pi), verification);
181
- // Nothing new to write: every step (and the verification item) is already in
182
- // the list, or the list is full, so a rewrite would only churn the widget.
183
- if (merged.added === 0 && merged.trailing !== "added") return { steps, merged };
184
- const outcome = await ctx.executeTool(TODO_TOOL, { todos: merged.todos });
185
- if (!outcome.isError) return { steps, merged };
186
- // The nested call fails both when the tool is unavailable and when it rejects
187
- // the steps; surface its first line so the caller reports the real reason.
188
- const part = outcome.result?.content?.find((entry) => entry.type === "text");
189
- const firstLine = part?.type === "text" ? part.text.split("\n", 1)[0] : undefined;
190
- return { steps, merged, error: firstLine ? sanitizeLine(firstLine) : undefined };
191
- }
192
-
193
- /** What the goal offer did: `recorded` undefined when there was nothing to offer. */
194
- interface GoalOffer {
195
- objective: string;
196
- recorded?: boolean;
197
- error?: string;
198
- }
199
-
200
- /**
201
- * Offer to record the approved plan as the session goal — only when the session
202
- * has no goal at all (an active or achieved one is the user's, never replaced) and
203
- * the session can ask. The objective is the plan's own `# title`, or the file-name
204
- * slug; recording runs through the goal tool, so validation and the widget stay
205
- * goal's, and no model turn is spent.
206
- */
207
- async function offerSessionGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOffer> {
208
- const objective = planObjective(plan);
209
- if (!ctx.hasUI) return { objective };
210
- // The branch already carries a goal: leave it alone, and say nothing.
211
- if (lastGoal(ctx.sessionManager.getBranch()) !== null) return { objective };
212
- const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
213
- if (!approved) return { objective };
214
- const outcome = await ctx.executeTool(GOAL_TOOL, { objective });
215
- if (!outcome.isError) return { objective, recorded: true };
216
- const part = outcome.result?.content?.find((entry) => entry.type === "text");
217
- const firstLine = part?.type === "text" ? part.text.split("\n", 1)[0] : undefined;
218
- return { objective, recorded: false, error: firstLine ? sanitizeLine(firstLine) : undefined };
219
- }
220
-
221
- /** The plan's own title: its first `# heading`, else the file-name slug. */
222
- function planObjective(plan: StoredPlan): string {
223
- const heading = /^ {0,3}#\s+(.+)$/m.exec(plan.content)?.[1];
224
- const title = heading === undefined ? "" : sanitizeLine(heading).trim();
225
- return title || planTitle(basename(plan.path));
226
- }
227
-
228
- /**
229
- * One sentence about the plan's verification item, or "" when the plan has none
230
- * (or it was already in the list). The item is a todo, not a plan step, so it is
231
- * ticked in the list rather than in the file.
232
- */
233
- function verificationNote(merged: TodoMerge | undefined): string {
234
- if (merged?.trailing === "added") {
235
- return "\nThe plan's verification section is the last todo item; tick it in the todo list, not in the file.";
236
- }
237
- if (merged?.trailing === "deferred") {
238
- return "\nThe plan's verification section did not fit in the full todo list; keep it in mind.";
239
- }
240
- return "";
241
- }
242
-
243
- /** What the merge did, for the approval message; `steps` is the plan's step count. */
244
- function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
245
- const total = steps.length;
246
- const counted = `${merged.added + merged.skipped} of ${total} plan step${total === 1 ? "" : "s"}`;
247
- const notes = [
248
- merged.skipped > 0 ? `${merged.skipped} already present` : "",
249
- merged.kept > 0 ? `${merged.kept} existing kept` : "",
250
- merged.deferred > 0 ? `${merged.deferred} deferred, list full` : "",
251
- ].filter(Boolean);
252
- const suffix = notes.length > 0 ? ` (${notes.join(", ")})` : "";
253
- const tick = `Tick each one off in the file with ${SET_PLAN_STEPS_TOOL} as you finish it, passing the step's text as \`expected\`.`;
254
- if (merged.added === 0 && merged.skipped === 0) {
255
- return ` The todo list is full, so no plan steps were added${suffix}. Keep these steps in mind. ${tick}`;
256
- }
257
- if (merged.added === 0)
258
- return ` ${total === 1 ? "The plan's step is" : "Every plan step is"} already in the todo list${suffix}. ${tick}`;
259
- return ` Merged ${counted} into the todo list${suffix}. ${tick}`;
260
- }
261
-
262
31
  export function registerTools(
263
32
  pi: ExtensionAPI,
264
33
  runtime: PlanRuntime,
265
34
  web: PlanWebReview = createPlanWebReview(runtime.plans),
266
35
  ): void {
267
- pi.registerTool({
268
- name: ENTER_TOOL,
269
- label: "Enter plan mode",
270
- description:
271
- "Enter read-only plan mode: investigate and design, then save the plan with write_plan and present it with " +
272
- "exit_plan_mode. Entering switches the session's active tool set to read-only and requires the user to confirm.",
273
- promptSnippet: "Ask the user to enter read-only plan mode.",
274
- promptGuidelines: [
275
- "Call enter_plan_mode before a non-trivial implementation task; skip it for small, obvious changes.",
276
- ],
277
- parameters: Type.Object({}),
278
- executionMode: "sequential",
279
- // readOnlyHint stays true: entering plan mode changes only the session's
280
- // tool set, never files or system state, so it needs no approval.
281
- annotations: { readOnlyHint: true, openWorldHint: false },
282
-
283
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
284
- if (runtime.isEnabled()) {
285
- return {
286
- content: [{ type: "text", text: "Already in plan mode." }],
287
- details: { entered: true, already: true } satisfies EnterPlanModeDetails,
288
- };
289
- }
290
- if (!ctx.hasUI) {
291
- return {
292
- content: [
293
- {
294
- type: "text",
295
- text: "No interactive UI is available to confirm entering plan mode. Ask the user whether they want a plan.",
296
- },
297
- ],
298
- details: { entered: false, unavailable: true } satisfies EnterPlanModeDetails,
299
- isError: true,
300
- };
301
- }
302
-
303
- const approved = await ctx.ui.confirm("Enter plan mode?", ENTER_CONFIRM);
304
- if (!approved) {
305
- return {
306
- content: [{ type: "text", text: "Stayed in normal mode. Proceed, or ask the user how to continue." }],
307
- details: { entered: false } satisfies EnterPlanModeDetails,
308
- };
309
- }
310
- runtime.enable(ctx);
311
- return {
312
- content: [{ type: "text", text: ENTERED_TEXT }],
313
- details: { entered: true } satisfies EnterPlanModeDetails,
314
- };
315
- },
316
-
317
- renderCall(_args, theme, context) {
318
- const text = reuseCallText(context?.lastComponent);
319
- text.setText(toolCallTitle(theme, `${ENTER_TOOL} `) + paint(theme, "meta", "requested plan mode"));
320
- return text;
321
- },
322
-
323
- renderResult(result, options, theme, context) {
324
- const text = reuseText(context?.lastComponent);
325
- const details = toolDetails<EnterPlanModeDetails>(result.details);
326
- if (!details) {
327
- text.setText(rejectionLine(theme, result, options.expanded));
328
- return text;
329
- }
330
- if (details.unavailable) text.setText(paint(theme, "warn", "No UI to confirm plan mode"));
331
- else if (details.already) text.setText(paint(theme, "meta", "Already in plan mode"));
332
- else if (details.entered) text.setText(paint(theme, "ok", `${GLYPHS.check} Plan mode enabled`));
333
- else text.setText(paint(theme, "meta", "Stayed in normal mode"));
334
- return text;
335
- },
336
- });
337
-
338
- pi.registerTool({
339
- name: WRITE_PLAN_TOOL,
340
- label: "Write plan",
341
- description:
342
- "Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
343
- "repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
344
- `${PLAN_OUTLINE}\n\n` +
345
- "Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
346
- `A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
347
- promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
348
- promptGuidelines: [
349
- "While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
350
- ],
351
- parameters: WritePlanParams,
352
- defaultActive: false,
353
- executionMode: "sequential",
354
- // destructiveHint is true because plan_path overwrites an existing plan
355
- // file; a plain write creates a new one.
356
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
357
-
358
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
359
- if (!runtime.isEnabled()) {
360
- return {
361
- content: [{ type: "text", text: "Not in plan mode; call enter_plan_mode first." }],
362
- details: undefined,
363
- isError: true,
364
- };
365
- }
366
- const args = params as WritePlanArgs;
367
- const summary = planStepSummary(args.content);
368
- if (summary.total > MAX_STEPS) {
369
- // Refuse before writing: a note after the fact arrives once the file exists and
370
- // the review has listed every step, so the overflow is already lost.
371
- return {
372
- content: [
373
- {
374
- type: "text",
375
- text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
376
- },
377
- ],
378
- details: undefined,
379
- isError: true,
380
- };
381
- }
382
- try {
383
- const file = await runtime.plans.write(ctx.cwd, {
384
- title: args.title,
385
- content: args.content,
386
- planPath: args.plan_path,
387
- });
388
- runtime.setLastPlan(ctx, file.path);
389
- return {
390
- content: [
391
- {
392
- type: "text",
393
- text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
394
- },
395
- ],
396
- details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
397
- };
398
- } catch (error) {
399
- const message = error instanceof Error ? error.message : String(error);
400
- return {
401
- content: [{ type: "text", text: `Could not write the plan: ${message}` }],
402
- details: undefined,
403
- isError: true,
404
- };
405
- }
406
- },
407
-
408
- renderCall(args, theme, context) {
409
- const text = reuseCallText(context?.lastComponent);
410
- const title = (args as Partial<WritePlanArgs>).title;
411
- const suffix = title ? paint(theme, "meta", sanitizeLine(title)) : paint(theme, "meta", "plan");
412
- text.setText(toolCallTitle(theme, `${WRITE_PLAN_TOOL} `) + suffix);
413
- return text;
414
- },
415
-
416
- renderResult(result, options, theme, context) {
417
- const text = reuseText(context?.lastComponent);
418
- const details = toolDetails<WritePlanDetails>(result.details);
419
- if (!details) {
420
- text.setText(rejectionLine(theme, result, options.expanded));
421
- return text;
422
- }
423
- text.setText(paint(theme, "ok", `${GLYPHS.check} Saved plan `) + paint(theme, "meta", details.relativePath));
424
- return text;
425
- },
426
- });
427
-
428
- pi.registerTool({
429
- name: EXIT_TOOL,
430
- label: "Exit plan mode",
431
- description:
432
- "Present the saved plan and ask the user to approve it. Reads the file written by write_plan, so call it with " +
433
- "plan_path. On approval plan mode ends, write access returns, and the plan's steps go into the todo list; the " +
434
- "user may instead keep planning or request a refinement. The plan file stays the source of truth.",
435
- promptSnippet: "Present the saved plan and ask the user to approve leaving plan mode.",
436
- parameters: ExitPlanModeParams,
437
- defaultActive: false,
438
- executionMode: "sequential",
439
- // readOnlyHint stays true: it writes no files, and the review dialog inside
440
- // execute is the human gate, so an outer prompt would only double-ask.
441
- annotations: { readOnlyHint: true, openWorldHint: false },
442
-
443
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
444
- const planPath = (params as ExitPlanModeArgs).plan_path.trim();
445
-
446
- if (!runtime.isEnabled()) {
447
- return {
448
- content: [{ type: "text", text: "Not in plan mode; there is no plan to approve." }],
449
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
450
- isError: true,
451
- };
452
- }
453
-
454
- const file = await runtime.plans.read(ctx.cwd, planPath);
455
- if (!file) {
456
- return {
457
- content: [
458
- {
459
- type: "text",
460
- text: `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
461
- },
462
- ],
463
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
464
- isError: true,
465
- };
466
- }
467
-
468
- const total = planStepSummary(file.content).total;
469
- if (total > MAX_STEPS) {
470
- // A hand-edited or older plan file can still be over the cap; refuse it here
471
- // too, so approval never seeds a list it cannot hold in full.
472
- return {
473
- content: [
474
- {
475
- type: "text",
476
- text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
477
- },
478
- ],
479
- details: {
480
- approved: false,
481
- plan: file.content,
482
- planPath: file.path,
483
- relativePath: file.relativePath,
484
- } satisfies ExitPlanModeDetails,
485
- isError: true,
486
- };
487
- }
488
-
489
- if (!ctx.hasUI) {
490
- return {
491
- content: [
492
- {
493
- type: "text",
494
- text: `No interactive UI is available to approve the plan. It is saved at ${file.relativePath}; present it and ask the user to approve it in their reply.`,
495
- },
496
- ],
497
- details: {
498
- approved: false,
499
- plan: file.content,
500
- planPath: file.path,
501
- relativePath: file.relativePath,
502
- unavailable: true,
503
- } satisfies ExitPlanModeDetails,
504
- isError: true,
505
- };
506
- }
507
-
508
- // One decision, however the session is attached; `review.ts` owns the loop,
509
- // the baseline, and closing the browser review when the turn is aborted.
510
- const decision = await reviewPlan({
511
- ctx,
512
- web,
513
- plans: runtime.plans,
514
- baselines: runtime,
515
- plan: file,
516
- signal,
517
- });
518
-
519
- if (decision.action === "refine") {
520
- // A browser save already wrote its comments into the file; either way the
521
- // model revises the same plan and presents it again.
522
- const feedback = decision.fromBrowser
523
- ? `The user reviewed the plan in the browser and left feedback:\n${decision.refinement}`
524
- : `The user asked to refine the plan: ${decision.refinement}`;
525
- return {
526
- content: [
527
- {
528
- type: "text",
529
- text: `${feedback}\nStay in plan mode, revise the plan (write_plan, reusing plan_path), then call exit_plan_mode again.`,
530
- },
531
- ],
532
- details: {
533
- approved: false,
534
- plan: decision.plan.content,
535
- planPath: decision.plan.path,
536
- relativePath: decision.plan.relativePath,
537
- refined: true,
538
- refinement: decision.refinement,
539
- annotations: decision.comments.length > 0 ? decision.comments : undefined,
540
- } satisfies ExitPlanModeDetails,
541
- };
542
- }
543
- if (decision.action === "keep") {
544
- return {
545
- content: [
546
- { type: "text", text: "Plan not approved. Stay in plan mode, ask what to change, and revise the plan." },
547
- ],
548
- details: {
549
- approved: false,
550
- plan: decision.plan.content,
551
- planPath: decision.plan.path,
552
- relativePath: decision.plan.relativePath,
553
- } satisfies ExitPlanModeDetails,
554
- };
555
- }
556
-
557
- runtime.disable(ctx);
558
- const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
559
- const goal = await offerSessionGoal(ctx, decision.plan);
560
- const listing =
561
- steps.length > 0
562
- ? `\n\n${steps
563
- .map((step, index) => `${index + 1}. [${step.status === "completed" ? "x" : " "}] ${step.content}`)
564
- .join("\n")}`
565
- : "";
566
- const tail =
567
- steps.length === 0
568
- ? " The plan has no steps to record."
569
- : error
570
- ? ` Could not record the steps in the todo list: ${error}. Keep these steps in mind and tick them off with ${SET_PLAN_STEPS_TOOL} as you go.`
571
- : merged
572
- ? mergeSummary(merged, steps)
573
- : ` Could not read the todo list. Record these steps with the todo tool before you start, and tick them off with ${SET_PLAN_STEPS_TOOL}.`;
574
- const verification = verificationNote(merged);
575
- const goalNote =
576
- goal.recorded === undefined
577
- ? ""
578
- : goal.recorded
579
- ? ` Recorded the session goal: ${goal.objective}.`
580
- : ` Could not record the session goal: ${goal.error ?? "unknown error"}. Use the goal tool to set it.`;
581
- const browserNote = decision.comments.length
582
- ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
583
- : "";
584
- return {
585
- content: [
586
- {
587
- type: "text",
588
- text: `Plan approved. Plan mode is off and write access is restored. The plan file ${decision.plan.relativePath} is the source of truth; follow its steps.${browserNote}${tail}${verification}${goalNote}${listing}`,
589
- },
590
- ],
591
- details: {
592
- approved: true,
593
- plan: decision.plan.content,
594
- planPath: decision.plan.path,
595
- relativePath: decision.plan.relativePath,
596
- annotations: decision.comments.length > 0 ? decision.comments : undefined,
597
- } satisfies ExitPlanModeDetails,
598
- };
599
- },
600
-
601
- renderCall(_args, theme, context) {
602
- const text = reuseCallText(context?.lastComponent);
603
- text.setText(toolCallTitle(theme, `${EXIT_TOOL} `) + paint(theme, "meta", "submitted a plan"));
604
- return text;
605
- },
606
-
607
- renderResult(result, { expanded }, theme, context) {
608
- const text = reuseText(context?.lastComponent);
609
- const details = toolDetails<ExitPlanModeDetails>(result.details);
610
- if (!details) {
611
- text.setText(rejectionLine(theme, result, expanded));
612
- return text;
613
- }
614
- const heading = details.unavailable
615
- ? paint(theme, "warn", "No UI to approve the plan")
616
- : details.approved
617
- ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
618
- : details.refined
619
- ? paint(theme, "accent", "Refining the plan")
620
- : paint(theme, "warn", "Plan not approved");
621
- const extra =
622
- details.refined && details.refinement
623
- ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
624
- : "";
625
- const shown = details.relativePath ?? details.planPath;
626
- const path = shown ? `\n${paint(theme, "meta", shown)}` : "";
627
- const planText = sanitizePlanText(details.plan);
628
- let out = `${heading}${extra}${path}`;
629
- if (planText) {
630
- const { body, more } = expanded ? { body: planText, more: 0 } : preview(planText);
631
- // A blank line separates the header block from the plan body.
632
- out += `\n\n${theme.fg("toolOutput", body)}`;
633
- if (more > 0) out += `\n${paint(theme, "faint", `… ${more} more lines`)} ${expandHint(theme)}`;
634
- }
635
- text.setText(out);
636
- return text;
637
- },
638
- });
639
-
640
- pi.registerTool({
641
- name: SET_PLAN_STEPS_TOOL,
642
- label: "Set plan steps",
643
- description:
644
- "Keep an approved plan file current by setting the checkboxes of the given top-level steps in place. Each step " +
645
- "number matches the order the plan was approved in; a step can be marked completed or reopened. Only the " +
646
- "checkboxes change; the step text is left as written. The file is rewritten once, and nothing changes when any " +
647
- "step is out of range or an entry's expected text no longer matches the file.",
648
- promptSnippet: "Tick steps off in the approved plan file.",
649
- promptGuidelines: [
650
- "After a plan is approved, call set_plan_steps as you complete steps, passing each step's text as expected so a reordered plan is refused instead of mis-ticked.",
651
- ],
652
- parameters: SetPlanStepsParams,
653
- executionMode: "sequential",
654
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
655
-
656
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
657
- const args = params as SetPlanStepsArgs;
658
- if (runtime.isEnabled()) {
659
- return {
660
- content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
661
- details: undefined,
662
- isError: true,
663
- };
664
- }
665
- try {
666
- const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
667
- const steps = extractPlanSteps(plan.content);
668
- const done = steps.filter((step) => step.status === "completed").length;
669
- return {
670
- content: [
671
- {
672
- type: "text",
673
- text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}.`,
674
- },
675
- ],
676
- details: {
677
- path: plan.path,
678
- relativePath: plan.relativePath,
679
- updates: args.updates,
680
- done,
681
- total: steps.length,
682
- } satisfies SetPlanStepsDetails,
683
- };
684
- } catch (error) {
685
- const message = error instanceof Error ? error.message : String(error);
686
- return {
687
- content: [{ type: "text", text: `Could not update the plan: ${message}` }],
688
- details: undefined,
689
- isError: true,
690
- };
691
- }
692
- },
693
-
694
- renderCall(args, theme, context) {
695
- const text = reuseCallText(context?.lastComponent);
696
- const updates = (args as Partial<SetPlanStepsArgs>).updates;
697
- const label =
698
- updates && updates.length === 1 && updates[0]?.step !== undefined
699
- ? `step ${updates[0].step}`
700
- : updates && updates.length > 1
701
- ? `${updates.length} steps`
702
- : "steps";
703
- text.setText(toolCallTitle(theme, `${SET_PLAN_STEPS_TOOL} `) + paint(theme, "meta", label));
704
- return text;
705
- },
706
-
707
- renderResult(result, options, theme, context) {
708
- const text = reuseText(context?.lastComponent);
709
- const details = toolDetails<SetPlanStepsDetails>(result.details);
710
- if (!details) {
711
- text.setText(rejectionLine(theme, result, options.expanded));
712
- return text;
713
- }
714
- const completed = details.updates.filter((update) => update.status === "completed").map((u) => u.step);
715
- const pending = details.updates.filter((update) => update.status === "pending").map((u) => u.step);
716
- const heading =
717
- completed.length > 0
718
- ? paint(theme, "ok", `${GLYPHS.check} ${stepLabel(completed)} done`) +
719
- (pending.length > 0 ? paint(theme, "meta", ` · ${stepLabel(pending)} reopened`) : "")
720
- : paint(theme, "meta", `${stepLabel(pending)} reopened`);
721
- text.setText(heading + paint(theme, "faint", ` · ${details.done}/${details.total}`));
722
- return text;
723
- },
724
- });
36
+ registerEnterTool(pi, runtime);
37
+ registerWriteTool(pi, runtime);
38
+ registerExitTool(pi, runtime, web);
39
+ registerSetPlanStepsTool(pi, runtime);
725
40
  }