@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * It presents the saved plan for approval, then seeds the approved steps into
5
5
  * the todo list and finalizes the session goal: a `planning` goal becomes the
6
- * plan's own `## Goal`, and a session with no goal is offered the same. The
6
+ * plan's own `## Goal`, and a session with no goal is given the same. The
7
7
  * review loop and the browser lifecycle belong to `review.ts`; the nested writes
8
8
  * belong to `todo-seed.ts` and the goal tool.
9
9
  */
@@ -12,17 +12,21 @@ import { basename } from "node:path";
12
12
  import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
13
13
  import { Type, type Static } from "typebox";
14
14
  import { sanitizeLine } from "../../../lib/format.ts";
15
+ import { messageOf } from "../../../lib/errors.ts";
15
16
  import { lastGoal } from "../../../lib/goal-state.ts";
16
17
  import { GOAL_TOOL } from "../../../lib/tool-names.ts";
17
18
  import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
18
19
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
19
20
  import { paint } from "../../../lib/tui/tokens.ts";
20
21
  import { callLine, detailsOrLine, moreHint } from "../../../lib/tui/tool.ts";
22
+ import { failedResult } from "../../../lib/tool-result.ts";
21
23
  import { feedbackSummary } from "../feedback.ts";
22
24
  import { mergeSummary, overCap, preview, verificationNote } from "../format.ts";
23
25
  import { sanitizePlanText } from "../plan-text.ts";
24
26
  import { planTitle, type StoredPlan } from "../plans.ts";
25
27
  import { reviewPlan } from "../review.ts";
28
+ import { decideSplit, rosterCard, SPLIT_OFF_NOTE } from "../split.ts";
29
+ import { writeTeam } from "../../../lib/plans/team.ts";
26
30
  import { EXIT_TOOL, SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
27
31
  import { goalSection, MAX_STEPS, planStepSummary } from "../steps.ts";
28
32
  import type { ExitPlanModeDetails } from "../types.ts";
@@ -30,11 +34,27 @@ import type { PlanWebReview } from "../web.ts";
30
34
  import { firstTextLine } from "./nested.ts";
31
35
  import { seedTodos } from "./todo-seed.ts";
32
36
 
37
+ const PlanUnitParams = Type.Object({
38
+ name: Type.String({ description: "Lowercase slug; the owner session takes it as its label." }),
39
+ paths: Type.Array(Type.String(), { description: "Repository-relative path prefixes this unit owns." }),
40
+ test: Type.String({ description: "The command that proves the unit is done." }),
41
+ dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Units that must land first." })),
42
+ lead: Type.Optional(Type.Boolean({ description: "True for the one unit this session keeps." })),
43
+ });
44
+
33
45
  const ExitPlanModeParams = Type.Object({
34
46
  plan_path: Type.String({
35
47
  description:
36
- "Path to the plan file written by write_plan (absolute, or relative to the working directory). The user reviews this file.",
48
+ "Path to the plan file written by write_plan: the absolute path it returned, or the `~/…` form it displayed. A bare name resolves inside the plans directory. The user reviews this file.",
37
49
  }),
50
+ units: Type.Optional(
51
+ Type.Array(PlanUnitParams, {
52
+ description:
53
+ "The plan's slices, when it has two or more independent ones the user will run in separate sessions. " +
54
+ "Only used when the plan.split setting is on; validation refuses an overlapping, looping, or oversized " +
55
+ "split before the review opens. Omit it to approve as one flow.",
56
+ }),
57
+ ),
38
58
  });
39
59
 
40
60
  type ExitPlanModeArgs = Static<typeof ExitPlanModeParams>;
@@ -50,21 +70,16 @@ interface GoalOutcome {
50
70
  /**
51
71
  * Finalise the session goal from the approved plan.
52
72
  *
53
- * A `planning` goal needs no dialog — approving the plan *is* the decision — and a
54
- * session with no goal at all gets one confirm. A final goal (`active` or
55
- * `achieved`) is the user's own: approving ordinary work never rewrites it.
56
- * Recording runs through the goal tool, so validation and the widget stay goal's,
57
- * and no model turn is spent.
73
+ * Approving the plan *is* the decision, so neither a `planning` goal nor a
74
+ * session with none is asked again. A final goal (`active` or `achieved`) is the
75
+ * user's own: approving ordinary work never rewrites it. Recording runs through
76
+ * the goal tool, so validation and the widget stay goal's, and no model turn is
77
+ * spent.
58
78
  */
59
79
  async function finalizeGoal(ctx: ExtensionToolContext, plan: StoredPlan): Promise<GoalOutcome> {
60
80
  const objective = planObjective(plan);
61
81
  const current = lastGoal(ctx.sessionManager.getBranch());
62
82
  if (current !== null && current.status !== "planning") return { objective };
63
- if (current === null) {
64
- if (!ctx.hasUI) return { objective };
65
- const approved = await ctx.ui.confirm("Record this plan as the session goal?", objective);
66
- if (!approved) return { objective };
67
- }
68
83
  const outcome = await ctx.executeTool(GOAL_TOOL, { objective, status: "active" });
69
84
  const action = current === null ? "recorded" : "promoted";
70
85
  return outcome.isError ? { objective, action, error: firstTextLine(outcome) } : { objective, action };
@@ -96,68 +111,71 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
96
111
  annotations: { readOnlyHint: true, openWorldHint: false },
97
112
 
98
113
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
99
- const planPath = (params as ExitPlanModeArgs).plan_path.trim();
114
+ const args = params as ExitPlanModeArgs;
115
+ const planPath = args.plan_path.trim();
100
116
 
101
117
  if (!runtime.isEnabled()) {
102
- return {
103
- content: [{ type: "text", text: "Not in plan mode; there is no plan to approve." }],
104
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
105
- isError: true,
106
- };
118
+ return failedResult("Not in plan mode; there is no plan to approve.", {
119
+ approved: false,
120
+ plan: "",
121
+ planPath,
122
+ } satisfies ExitPlanModeDetails);
107
123
  }
108
124
 
109
125
  const file = await runtime.plans.read(ctx.cwd, planPath);
110
126
  if (!file) {
111
- return {
112
- content: [
113
- {
114
- type: "text",
115
- text: `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
116
- },
117
- ],
118
- details: { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
119
- isError: true,
120
- };
127
+ return failedResult(
128
+ `No plan file at "${planPath}". Save the plan with write_plan first, then pass the returned path.`,
129
+ { approved: false, plan: "", planPath } satisfies ExitPlanModeDetails,
130
+ );
121
131
  }
122
132
 
123
133
  const total = planStepSummary(file.content).total;
124
134
  if (total > MAX_STEPS) {
125
135
  // A hand-edited or older plan file can still be over the cap; refuse it here
126
136
  // too, so approval never seeds a list it cannot hold in full.
127
- return {
128
- content: [
129
- {
130
- type: "text",
131
- text: `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
132
- },
133
- ],
134
- details: {
137
+ return failedResult(
138
+ `${overCap(total)} It was not presented for approval; call write_plan with plan_path to group the steps, then exit_plan_mode again.`,
139
+ {
135
140
  approved: false,
136
141
  plan: file.content,
137
142
  planPath: file.path,
138
143
  relativePath: file.relativePath,
139
144
  } satisfies ExitPlanModeDetails,
140
- isError: true,
141
- };
145
+ );
146
+ }
147
+
148
+ // The split is decided before the review opens, so a refused or rejected split
149
+ // never spends the user's review — and the screen can show what it approves.
150
+ const units = args.units?.map((unit) => ({
151
+ name: unit.name,
152
+ paths: [...unit.paths],
153
+ test: unit.test,
154
+ dependsOn: [...(unit.dependsOn ?? [])],
155
+ ...(unit.lead ? { lead: true } : {}),
156
+ }));
157
+ const split = decideSplit(ctx.cwd, await runtime.plans.rootFor(ctx.cwd), units);
158
+ if (split.kind === "refused") {
159
+ return failedResult(`Refused: ${split.reason}`, {
160
+ approved: false,
161
+ plan: file.content,
162
+ planPath: file.path,
163
+ relativePath: file.relativePath,
164
+ } satisfies ExitPlanModeDetails);
142
165
  }
166
+ const pending = split.kind === "team" ? split : undefined;
143
167
 
144
168
  if (!ctx.hasUI) {
145
- return {
146
- content: [
147
- {
148
- type: "text",
149
- 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.`,
150
- },
151
- ],
152
- details: {
169
+ return failedResult(
170
+ `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.`,
171
+ {
153
172
  approved: false,
154
173
  plan: file.content,
155
174
  planPath: file.path,
156
175
  relativePath: file.relativePath,
157
176
  unavailable: true,
158
177
  } satisfies ExitPlanModeDetails,
159
- isError: true,
160
- };
178
+ );
161
179
  }
162
180
 
163
181
  // One decision, however the session is attached; `review.ts` owns the loop,
@@ -169,6 +187,7 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
169
187
  baselines: runtime,
170
188
  plan: file,
171
189
  signal,
190
+ ...(pending === undefined ? {} : { split: { ...pending.team, verdict: pending.verdict } }),
172
191
  });
173
192
 
174
193
  if (decision.action === "refine") {
@@ -208,10 +227,56 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
208
227
  } satisfies ExitPlanModeDetails,
209
228
  };
210
229
  }
230
+ if (decision.action === "cancel") {
231
+ runtime.disable(ctx);
232
+ const shown = decision.plan.relativePath;
233
+ ctx.ui.notify("Plan mode off — full access restored. The plan was not approved.", "info");
234
+ return {
235
+ content: [
236
+ {
237
+ type: "text",
238
+ text:
239
+ "Plan mode is off and write access is restored. The plan was not approved and no steps were recorded. " +
240
+ `The plan file ${shown} remains if the user wants to review it later.`,
241
+ },
242
+ ],
243
+ details: {
244
+ approved: false,
245
+ cancelled: true,
246
+ plan: decision.plan.content,
247
+ planPath: decision.plan.path,
248
+ relativePath: decision.plan.relativePath,
249
+ } satisfies ExitPlanModeDetails,
250
+ };
251
+ }
211
252
 
253
+ // The team becomes real here, never before: the section is what a joining
254
+ // session reads, so it appears only after the user approved the split. The
255
+ // write happens while plan mode is still on, so a failure leaves the session
256
+ // able to retry instead of approved-but-empty.
257
+ let approved = decision.plan;
258
+ if (pending !== undefined) {
259
+ try {
260
+ approved = await runtime.plans.write(ctx.cwd, {
261
+ title: planTitle(basename(decision.plan.path)),
262
+ content: writeTeam(decision.plan.content, { lead: pending.team.lead, units: pending.team.units }),
263
+ planPath: decision.plan.path,
264
+ });
265
+ } catch (writeError) {
266
+ return failedResult(
267
+ `The plan was approved but its team could not be written: ${messageOf(writeError)}. Plan mode is still on and no steps were seeded; fix the plan file and call exit_plan_mode again.`,
268
+ {
269
+ approved: false,
270
+ plan: decision.plan.content,
271
+ planPath: decision.plan.path,
272
+ relativePath: decision.plan.relativePath,
273
+ } satisfies ExitPlanModeDetails,
274
+ );
275
+ }
276
+ }
212
277
  runtime.disable(ctx);
213
- const { steps, merged, error } = await seedTodos(pi, ctx, decision.plan.content);
214
- const goal = await finalizeGoal(ctx, decision.plan);
278
+ const { steps, merged, error } = await seedTodos(pi, ctx, approved.content);
279
+ const goal = await finalizeGoal(ctx, approved);
215
280
  const listing =
216
281
  steps.length > 0
217
282
  ? `\n\n${steps
@@ -238,19 +303,41 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
238
303
  const browserNote = decision.comments.length
239
304
  ? `\n\nThe user approved from the browser review with these comments:\n${feedbackSummary(decision.comments)}`
240
305
  : "";
306
+ const splitNote =
307
+ pending === undefined
308
+ ? split.kind === "off"
309
+ ? `\n\n${SPLIT_OFF_NOTE}`
310
+ : ""
311
+ : `\n\n${rosterCard(pending.team, approved.path, pi.getSessionName())}\n\nWork only your own unit (${pending.team.lead}); the other units belong to the sessions the user starts.`;
241
312
  return {
242
313
  content: [
243
314
  {
244
315
  type: "text",
245
- 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}`,
316
+ text: `Plan approved. Plan mode is off and write access is restored. The plan file ${approved.relativePath} is the source of truth; follow its steps.${browserNote}${splitNote}${tail}${verification}${goalNote}${listing}`,
246
317
  },
247
318
  ],
248
319
  details: {
249
320
  approved: true,
250
- plan: decision.plan.content,
251
- planPath: decision.plan.path,
252
- relativePath: decision.plan.relativePath,
321
+ plan: approved.content,
322
+ planPath: approved.path,
323
+ relativePath: approved.relativePath,
253
324
  annotations: decision.comments.length > 0 ? decision.comments : undefined,
325
+ ...(pending === undefined
326
+ ? split.kind === "off"
327
+ ? { splitOff: true }
328
+ : {}
329
+ : {
330
+ team: {
331
+ lead: pending.team.lead,
332
+ verdict: pending.verdict,
333
+ units: pending.team.units.map((unit) => ({
334
+ name: unit.name,
335
+ paths: unit.paths,
336
+ test: unit.test,
337
+ dependsOn: unit.dependsOn,
338
+ })),
339
+ },
340
+ }),
254
341
  } satisfies ExitPlanModeDetails,
255
342
  };
256
343
  },
@@ -269,11 +356,13 @@ export function registerExitTool(pi: ExtensionAPI, runtime: PlanRuntime, web: Pl
269
356
  const details = outcome.details;
270
357
  const heading = details.unavailable
271
358
  ? paint(theme, "warn", "No UI to approve the plan")
272
- : details.approved
273
- ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
274
- : details.refined
275
- ? paint(theme, "accent", "Refining the plan")
276
- : paint(theme, "warn", "Plan not approved");
359
+ : details.cancelled
360
+ ? paint(theme, "warn", "Plan mode off — plan not approved")
361
+ : details.approved
362
+ ? paint(theme, "ok", `${GLYPHS.check} Plan approved`)
363
+ : details.refined
364
+ ? paint(theme, "accent", "Refining the plan")
365
+ : paint(theme, "warn", "Plan not approved");
277
366
  const extra =
278
367
  details.refined && details.refinement
279
368
  ? `\n${paint(theme, "meta", `↳ ${sanitizePlanText(details.refinement)}`)}`
@@ -13,6 +13,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
13
13
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
14
14
  import { paint } from "../../../lib/tui/tokens.ts";
15
15
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
16
+ import { failedResult, textResult } from "../../../lib/tool-result.ts";
16
17
  import { describeUpdates, stepLabel } from "../format.ts";
17
18
  import { SET_PLAN_STEPS_TOOL, type PlanRuntime } from "../runtime.ts";
18
19
  import { extractPlanSteps } from "../steps.ts";
@@ -66,39 +67,26 @@ export function registerSetPlanStepsTool(pi: ExtensionAPI, runtime: PlanRuntime)
66
67
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
67
68
  const args = params as SetPlanStepsArgs;
68
69
  if (runtime.isEnabled()) {
69
- return {
70
- content: [{ type: "text", text: "Not while planning; refine the plan with write_plan instead." }],
71
- details: undefined,
72
- isError: true,
73
- };
70
+ return failedResult("Not while planning; refine the plan with write_plan instead.", undefined);
74
71
  }
75
72
  try {
76
73
  const plan = await runtime.plans.setSteps(ctx.cwd, args.plan_path, args.updates);
77
74
  const steps = extractPlanSteps(plan.content);
78
75
  const done = steps.filter((step) => step.status === "completed").length;
79
76
  const todoNote = await mirrorTodos(ctx, args.updates);
80
- return {
81
- content: [
82
- {
83
- type: "text",
84
- text: `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
85
- },
86
- ],
87
- details: {
77
+ return textResult(
78
+ `${describeUpdates(args.updates)} (${done}/${steps.length} done) in ${plan.relativePath}${todoNote}.`,
79
+ {
88
80
  path: plan.path,
89
81
  relativePath: plan.relativePath,
90
82
  updates: args.updates,
91
83
  done,
92
84
  total: steps.length,
93
85
  } satisfies SetPlanStepsDetails,
94
- };
86
+ );
95
87
  } catch (error) {
96
88
  const message = error instanceof Error ? error.message : String(error);
97
- return {
98
- content: [{ type: "text", text: `Could not update the plan: ${message}` }],
99
- details: undefined,
100
- isError: true,
101
- };
89
+ return failedResult(`Could not update the plan: ${message}`, undefined);
102
90
  }
103
91
  },
104
92
 
@@ -12,6 +12,7 @@ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
12
12
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
13
13
  import { paint } from "../../../lib/tui/tokens.ts";
14
14
  import { callLine, detailsOrLine } from "../../../lib/tui/tool.ts";
15
+ import { failedResult, textResult } from "../../../lib/tool-result.ts";
15
16
  import { overCap, stepElisionNote } from "../format.ts";
16
17
  import { WRITE_PLAN_TOOL, type PlanRuntime } from "../runtime.ts";
17
18
  import { MAX_STEPS, PLAN_OUTLINE, planStepSummary } from "../steps.ts";
@@ -39,14 +40,26 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
39
40
  name: WRITE_PLAN_TOOL,
40
41
  label: "Write plan",
41
42
  description:
42
- "Save the plan you designed as a markdown file under .pi/plans (project root, or the agent directory outside a " +
43
+ "Save the plan you designed as a markdown file in the plans directory (the agent directory, keyed by " +
43
44
  "repository). Write the complete plan, not a summary, in this outline so it is consistent and scannable:\n\n" +
44
45
  `${PLAN_OUTLINE}\n\n` +
46
+ "Each section has one job: lead it with its outcome, keep a prose section to a few bullets or two or three " +
47
+ "sentences, give each step one verifiable outcome with the detail in a nested note, and put the sketch, files, " +
48
+ "and commands in inline code or a fence rather than pasted prose. " +
49
+ "When the change alters a surface that already exists, Visual shows it before and after in one fence each; " +
50
+ "tag a fence only when every line in it is code. " +
51
+ "The outline is a guide, not a form: drop Context, Approach, Visual, or Risks when there is nothing to say. " +
52
+ "Keep Steps and Verification — the steps are read from that section (without it, the shallowest list " +
53
+ "anywhere becomes the steps), and Verification becomes the last todo item. " +
54
+ "In Steps, each top-level bullet is one outcome in the order you will do them; " +
55
+ "an indented line continues that outcome, and a deeper bullet is a note. " +
45
56
  "Returns the path to pass to exit_plan_mode; plan_path overwrites an existing file when refining. " +
46
57
  `A plan with more than ${MAX_STEPS} top-level steps in its Steps section is refused, so approval never drops steps it cannot track.`,
47
- promptSnippet: "Save the designed plan as a markdown file under .pi/plans.",
58
+ promptSnippet: "Save the designed plan as a markdown file in the plans directory.",
48
59
  promptGuidelines: [
49
60
  "While plan mode is active, write the full plan with write_plan, then call exit_plan_mode with the returned path.",
61
+ "When refining with plan_path, keep every section heading as it was: the review marks changed sections by heading, " +
62
+ "so a rename reads as an edit.",
50
63
  ],
51
64
  parameters: WritePlanParams,
52
65
  defaultActive: false,
@@ -57,27 +70,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
57
70
 
58
71
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
59
72
  if (!runtime.isEnabled()) {
60
- return {
61
- content: [{ type: "text", text: "Not in plan mode; call enter_plan_mode first." }],
62
- details: undefined,
63
- isError: true,
64
- };
73
+ return failedResult("Not in plan mode; call enter_plan_mode first.", undefined);
65
74
  }
66
75
  const args = params as WritePlanArgs;
67
76
  const summary = planStepSummary(args.content);
68
77
  if (summary.total > MAX_STEPS) {
69
78
  // Refuse before writing: a note after the fact arrives once the file exists and
70
79
  // the review has listed every step, so the overflow is already lost.
71
- return {
72
- content: [
73
- {
74
- type: "text",
75
- text: `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
76
- },
77
- ],
78
- details: undefined,
79
- isError: true,
80
- };
80
+ return failedResult(
81
+ `${overCap(summary.total)} Nothing was written; group the steps (one outcome each, sub-bullets for detail) and call write_plan again.`,
82
+ undefined,
83
+ );
81
84
  }
82
85
  try {
83
86
  const file = await runtime.plans.write(ctx.cwd, {
@@ -86,22 +89,17 @@ export function registerWriteTool(pi: ExtensionAPI, runtime: PlanRuntime): void
86
89
  planPath: args.plan_path,
87
90
  });
88
91
  runtime.setLastPlan(ctx, file.path);
89
- return {
90
- content: [
91
- {
92
- type: "text",
93
- text: `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
94
- },
95
- ],
96
- details: { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
97
- };
92
+ // A store an older version kept in the checkout was moved in by this
93
+ // write; say so once, where the user is looking.
94
+ const moved = runtime.plans.takeNotice();
95
+ if (moved) ctx.ui.notify(moved, "info");
96
+ return textResult(
97
+ `Wrote the plan to ${file.relativePath}. Call exit_plan_mode with plan_path: ${file.path}${stepElisionNote(summary.elided)}`,
98
+ { path: file.path, relativePath: file.relativePath, bytes: file.bytes } satisfies WritePlanDetails,
99
+ );
98
100
  } catch (error) {
99
101
  const message = error instanceof Error ? error.message : String(error);
100
- return {
101
- content: [{ type: "text", text: `Could not write the plan: ${message}` }],
102
- details: undefined,
103
- isError: true,
104
- };
102
+ return failedResult(`Could not write the plan: ${message}`, undefined);
105
103
  }
106
104
  },
107
105
 
@@ -2,9 +2,10 @@
2
2
  * Model-facing registration for the plan-mode tools.
3
3
  *
4
4
  * `enter_plan_mode` lets the model ask to plan before touching code; it needs
5
- * user confirmation. `write_plan` saves the plan artifact under `.pi/plans` and
6
- * returns its path. `exit_plan_mode` reads that file back, presents it for
7
- * approval (a scrollable review screen in the TUI, a select dialog elsewhere),
5
+ * user confirmation. `write_plan` saves the plan artifact in the plans directory and
6
+ * returns its path. `cancel_plan_mode` leaves planning without approving, so a
7
+ * phone user is never trapped in read-only mode. `exit_plan_mode` reads that file
8
+ * back, presents it for approval (a scrollable review screen in the TUI, a select dialog elsewhere),
8
9
  * and on approval seeds the `todo` tool with the plan's steps (through
9
10
  * `ctx.executeTool`, so the todo widget and validation run normally).
10
11
  * `set_plan_steps` then keeps the plan current: after approval the model ticks
@@ -22,6 +23,7 @@
22
23
 
23
24
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
25
  import type { PlanRuntime } from "./runtime.ts";
26
+ import { registerCancelTool } from "./tools/cancel.ts";
25
27
  import { registerEnterTool } from "./tools/enter.ts";
26
28
  import { registerExitTool } from "./tools/exit.ts";
27
29
  import { registerSetPlanStepsTool } from "./tools/steps.ts";
@@ -34,6 +36,7 @@ export function registerTools(
34
36
  web: PlanWebReview = createPlanWebReview(runtime.plans),
35
37
  ): void {
36
38
  registerEnterTool(pi, runtime);
39
+ registerCancelTool(pi, runtime);
37
40
  registerWriteTool(pi, runtime);
38
41
  registerExitTool(pi, runtime, web);
39
42
  registerSetPlanStepsTool(pi, runtime);