@gavin-hu/my-pi-agent 0.13.1 → 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 (237) hide show
  1. package/README.md +66 -41
  2. package/README.zh-CN.md +53 -36
  3. package/extensions/bytefly/README.md +321 -186
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/config.ts +4 -1
  7. package/extensions/bytefly/doctor/register.ts +24 -5
  8. package/extensions/bytefly/doctor/tools.ts +4 -0
  9. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  10. package/extensions/bytefly/footer/config.ts +10 -6
  11. package/extensions/bytefly/footer/price/config.ts +3 -2
  12. package/extensions/bytefly/footer/price/price.ts +5 -3
  13. package/extensions/bytefly/footer/price/rates.ts +63 -26
  14. package/extensions/bytefly/footer/render/status.ts +3 -3
  15. package/extensions/bytefly/header/component.ts +33 -10
  16. package/extensions/bytefly/header/config.ts +22 -0
  17. package/extensions/bytefly/header/register.ts +2 -1
  18. package/extensions/bytefly/index.ts +6 -6
  19. package/extensions/bytefly/notice/component.ts +8 -3
  20. package/extensions/bytefly/notify/config.ts +7 -12
  21. package/extensions/bytefly/notify/dispatch.ts +13 -5
  22. package/extensions/bytefly/panel/commands.ts +22 -2
  23. package/extensions/bytefly/panel/format.ts +17 -21
  24. package/extensions/bytefly/panel/settings-runtime.ts +86 -22
  25. package/extensions/bytefly/panel/settings.ts +279 -46
  26. package/extensions/bytefly/panel/themes.ts +32 -0
  27. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  28. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  29. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  30. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  31. package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
  32. package/extensions/bytefly/panel/tui.ts +176 -166
  33. package/extensions/bytefly/picker/discovery.ts +1 -0
  34. package/extensions/bytefly/picker/types.ts +2 -0
  35. package/extensions/bytefly/separator/component.ts +17 -6
  36. package/extensions/bytefly/separator/config.ts +15 -0
  37. package/extensions/bytefly/separator/format.ts +60 -6
  38. package/extensions/bytefly/separator/register.ts +68 -9
  39. package/extensions/bytefly/separator/types.ts +8 -0
  40. package/extensions/bytefly/update/cache.ts +58 -19
  41. package/extensions/bytefly/update/check.ts +35 -0
  42. package/extensions/bytefly/update/format.ts +76 -35
  43. package/extensions/bytefly/update/register.ts +144 -55
  44. package/extensions/bytefly/update/state.ts +4 -2
  45. package/extensions/bytefly/update/suppress.ts +66 -0
  46. package/extensions/bytefly/update/types.ts +15 -0
  47. package/extensions/bytefly/working/config.ts +4 -5
  48. package/extensions/bytefly/working/format.ts +24 -7
  49. package/extensions/bytefly/working/register.ts +34 -3
  50. package/extensions/check/README.md +253 -0
  51. package/extensions/check/commands.ts +128 -0
  52. package/extensions/check/config.ts +39 -0
  53. package/extensions/check/detect.ts +269 -0
  54. package/extensions/check/failures.ts +230 -0
  55. package/extensions/check/format.ts +135 -0
  56. package/extensions/check/index.ts +26 -0
  57. package/extensions/check/render.ts +185 -0
  58. package/extensions/check/runner.ts +176 -0
  59. package/extensions/check/runtime.ts +244 -0
  60. package/extensions/check/source.ts +80 -0
  61. package/extensions/check/tools.ts +173 -0
  62. package/extensions/check/tui.ts +195 -0
  63. package/extensions/check/types.ts +99 -0
  64. package/extensions/document/README.md +1 -1
  65. package/extensions/document/extract/pdf.ts +8 -16
  66. package/extensions/document/formats.ts +2 -2
  67. package/extensions/file-browse/README.md +66 -32
  68. package/extensions/file-browse/client.ts +95 -32
  69. package/extensions/file-browse/config.ts +9 -9
  70. package/extensions/file-browse/git.ts +37 -47
  71. package/extensions/file-browse/html.ts +39 -58
  72. package/extensions/file-browse/markdown.ts +110 -92
  73. package/extensions/file-browse/raw-html.ts +478 -0
  74. package/extensions/file-browse/router.ts +32 -70
  75. package/extensions/file-browse/server.ts +18 -56
  76. package/extensions/file-browse/urls.ts +80 -0
  77. package/extensions/goal/README.md +103 -47
  78. package/extensions/goal/clarify-parse.ts +4 -9
  79. package/extensions/goal/clarify.ts +27 -4
  80. package/extensions/goal/commands.ts +24 -11
  81. package/extensions/goal/format.ts +41 -10
  82. package/extensions/goal/index.ts +39 -16
  83. package/extensions/goal/plan-route.ts +51 -0
  84. package/extensions/goal/schema.ts +6 -23
  85. package/extensions/goal/state.ts +39 -57
  86. package/extensions/goal/tools.ts +26 -20
  87. package/extensions/goal/types.ts +11 -1
  88. package/extensions/job/README.md +20 -6
  89. package/extensions/job/format.ts +18 -68
  90. package/extensions/job/index.ts +26 -41
  91. package/extensions/job/log-writer.ts +208 -0
  92. package/extensions/job/logs.ts +3 -2
  93. package/extensions/job/process.ts +6 -99
  94. package/extensions/job/runtime.ts +42 -181
  95. package/extensions/job/schema.ts +1 -4
  96. package/extensions/job/tools.ts +1 -15
  97. package/extensions/job/tui.ts +117 -307
  98. package/extensions/job/ui.ts +7 -33
  99. package/extensions/memory/README.md +3 -1
  100. package/extensions/memory/errors.ts +5 -6
  101. package/extensions/memory/tui.ts +14 -24
  102. package/extensions/plan/README.md +106 -18
  103. package/extensions/plan/commands.ts +9 -5
  104. package/extensions/plan/format.ts +87 -0
  105. package/extensions/plan/index.ts +31 -15
  106. package/extensions/plan/plans.ts +2 -3
  107. package/extensions/plan/policy.ts +0 -6
  108. package/extensions/plan/runtime.ts +4 -2
  109. package/extensions/plan/steps.ts +37 -8
  110. package/extensions/plan/todos.ts +44 -3
  111. package/extensions/plan/tools/enter.ts +94 -0
  112. package/extensions/plan/tools/exit.ts +296 -0
  113. package/extensions/plan/tools/nested.ts +21 -0
  114. package/extensions/plan/tools/steps.ts +136 -0
  115. package/extensions/plan/tools/todo-seed.ts +76 -0
  116. package/extensions/plan/tools/write.ts +127 -0
  117. package/extensions/plan/tools.ts +18 -703
  118. package/extensions/plan/web-client.ts +6 -22
  119. package/extensions/plan/web-page.ts +19 -30
  120. package/extensions/plan/web-router.ts +34 -71
  121. package/extensions/plan/web.ts +13 -40
  122. package/extensions/question/answers.ts +3 -8
  123. package/extensions/question/tui.ts +6 -6
  124. package/extensions/rewind/README.md +95 -22
  125. package/extensions/rewind/checklist.ts +185 -0
  126. package/extensions/rewind/commands.ts +8 -0
  127. package/extensions/rewind/config.ts +3 -11
  128. package/extensions/rewind/format.ts +220 -2
  129. package/extensions/rewind/git.ts +74 -7
  130. package/extensions/rewind/hunk-picker.ts +114 -0
  131. package/extensions/rewind/patch.ts +88 -0
  132. package/extensions/rewind/path-picker.ts +67 -100
  133. package/extensions/rewind/paths.ts +93 -19
  134. package/extensions/rewind/restore.ts +53 -10
  135. package/extensions/rewind/review-tui.ts +320 -0
  136. package/extensions/rewind/review.ts +397 -0
  137. package/extensions/rewind/rewind-tui.ts +4 -3
  138. package/extensions/rewind/runtime.ts +94 -17
  139. package/extensions/subagent/README.md +118 -8
  140. package/extensions/subagent/background-tui.ts +208 -0
  141. package/extensions/subagent/background.ts +470 -0
  142. package/extensions/subagent/commands.ts +80 -0
  143. package/extensions/subagent/config.ts +62 -0
  144. package/extensions/subagent/format.ts +24 -1
  145. package/extensions/subagent/index.ts +116 -1
  146. package/extensions/subagent/orchestrate.ts +16 -3
  147. package/extensions/subagent/render.ts +25 -0
  148. package/extensions/subagent/run.ts +113 -49
  149. package/extensions/subagent/schema.ts +12 -0
  150. package/extensions/subagent/transcripts.ts +202 -0
  151. package/extensions/subagent/types.ts +23 -0
  152. package/extensions/subagent/ui.ts +50 -0
  153. package/extensions/todo/README.md +18 -13
  154. package/extensions/todo/format.ts +3 -2
  155. package/extensions/todo/runtime.ts +3 -2
  156. package/extensions/todo/schema.ts +8 -84
  157. package/extensions/todo/state.ts +5 -46
  158. package/extensions/todo/tools.ts +2 -1
  159. package/extensions/todo/types.ts +6 -15
  160. package/extensions/watch/README.md +16 -6
  161. package/extensions/watch/child.ts +150 -0
  162. package/extensions/watch/format.ts +8 -48
  163. package/extensions/watch/index.ts +42 -58
  164. package/extensions/watch/runtime.ts +49 -128
  165. package/extensions/watch/schema.ts +1 -4
  166. package/extensions/watch/tools.ts +1 -11
  167. package/extensions/watch/tui.ts +103 -273
  168. package/extensions/watch/ui.ts +5 -32
  169. package/extensions/web-access/README.md +1 -1
  170. package/extensions/web-access/fetch/page.ts +1 -1
  171. package/extensions/web-access/fetch/tool.ts +6 -2
  172. package/extensions/web-access/search/search.ts +2 -5
  173. package/extensions/wechat/README.md +3 -1
  174. package/extensions/wechat/client.ts +23 -0
  175. package/extensions/wechat/commands.ts +57 -42
  176. package/extensions/wechat/deliver.ts +5 -9
  177. package/extensions/wechat/errors.ts +3 -6
  178. package/extensions/wechat/inbound.ts +2 -5
  179. package/extensions/wechat/index.ts +2 -8
  180. package/extensions/wechat/outbound.ts +2 -4
  181. package/extensions/wechat/peer-worker.ts +3 -8
  182. package/extensions/wechat/poll.ts +10 -9
  183. package/extensions/wechat/presence.ts +35 -62
  184. package/extensions/wechat/reply-pipeline.ts +3 -1
  185. package/extensions/wechat/status-screen.ts +1 -1
  186. package/extensions/wechat/timer.ts +57 -0
  187. package/extensions/worktree/config.ts +8 -20
  188. package/extensions/worktree/enter.ts +7 -12
  189. package/extensions/worktree/git.ts +38 -69
  190. package/extensions/worktree/index.ts +5 -4
  191. package/extensions/worktree/registry.ts +7 -7
  192. package/extensions/worktree/root-tools.ts +2 -2
  193. package/extensions/worktree/status.ts +3 -2
  194. package/lib/README.md +22 -8
  195. package/lib/config.ts +83 -12
  196. package/lib/errors.ts +12 -0
  197. package/lib/format.ts +56 -0
  198. package/lib/git/read.ts +8 -0
  199. package/lib/goal-state.ts +23 -8
  200. package/lib/http/headers.ts +54 -0
  201. package/lib/http/index.ts +13 -0
  202. package/lib/http/response.ts +46 -0
  203. package/lib/http/router.ts +52 -0
  204. package/lib/http/server.ts +91 -0
  205. package/lib/lines.ts +77 -0
  206. package/lib/pdf.ts +82 -0
  207. package/lib/plan-entry.ts +71 -0
  208. package/lib/plan-state.ts +34 -0
  209. package/lib/report.ts +99 -0
  210. package/lib/requirements.ts +25 -19
  211. package/lib/spawn.ts +170 -0
  212. package/lib/todo-state.ts +110 -23
  213. package/lib/tool-names.ts +7 -0
  214. package/lib/tool-result.ts +33 -0
  215. package/lib/tui/README.md +68 -3
  216. package/lib/tui/chrome.ts +28 -4
  217. package/lib/tui/columns.ts +6 -1
  218. package/lib/tui/confirm.ts +65 -0
  219. package/lib/tui/dock-screen.ts +410 -0
  220. package/lib/tui/glyphs.ts +2 -1
  221. package/lib/tui/indent.ts +1 -1
  222. package/lib/tui/index.ts +2 -0
  223. package/lib/tui/rows.ts +21 -0
  224. package/lib/tui/status-controller.ts +35 -1
  225. package/lib/tui/tool.ts +59 -1
  226. package/lib/web/components.ts +20 -1
  227. package/lib/web/markup.ts +32 -0
  228. package/lib/web/runtime.ts +50 -0
  229. package/package.json +23 -5
  230. package/skills/init/SKILL.md +165 -0
  231. package/skills/init/references/agents-md-quality.md +103 -0
  232. package/skills/init/references/interview-questions.md +81 -0
  233. package/skills/refactor/SKILL.md +141 -0
  234. package/themes/bytefly-dark.json +2 -3
  235. package/themes/bytefly-light.json +2 -3
  236. package/extensions/watch/process.ts +0 -63
  237. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -26,6 +26,7 @@ import {
26
26
  type TuiMouseEventResult,
27
27
  } from "@earendil-works/pi-tui";
28
28
  import { sanitizeLine } from "../../lib/format.ts";
29
+ import { ConfirmState } from "../../lib/tui/confirm.ts";
29
30
  import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
30
31
  import { dockListFrame, editorTheme, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
31
32
  import { clip } from "../../lib/tui/columns.ts";
@@ -91,8 +92,8 @@ export class MemoryPickerComponent implements Component {
91
92
  private mergePair: { first: Note; second: Note } | undefined;
92
93
  /** The note marked with `m`, awaiting a second `m` to merge into it. */
93
94
  private mergeSource: Note | undefined;
94
- /** Note awaiting a y/N forget confirmation. */
95
- private confirm: Note | undefined;
95
+ /** The y/N forget confirmation, shared with the other destructive screens. */
96
+ private readonly confirm = new ConfirmState();
96
97
  /** Transient feedback shown in place of the hint. */
97
98
  private notice: string | undefined;
98
99
  private componentFocused = false;
@@ -355,17 +356,7 @@ export class MemoryPickerComponent implements Component {
355
356
  }
356
357
 
357
358
  private handleConfirm(data: string): void {
358
- const note = this.confirm;
359
- if (!note) return;
360
- if (data === "y" || data === "Y" || matchesKey(data, Key.enter)) {
361
- this.confirm = undefined;
362
- this.forgetNote(note);
363
- return;
364
- }
365
- if (data === "n" || data === "N" || isCloseKey(data) || data === "q") {
366
- this.confirm = undefined;
367
- this.requestRender();
368
- }
359
+ if (this.confirm.resolve(data) !== undefined) this.requestRender();
369
360
  }
370
361
 
371
362
  handleInput(data: string): void {
@@ -378,7 +369,7 @@ export class MemoryPickerComponent implements Component {
378
369
  this.requestRender();
379
370
  return;
380
371
  }
381
- if (this.confirm) {
372
+ if (this.confirm.pending) {
382
373
  this.handleConfirm(data);
383
374
  return;
384
375
  }
@@ -417,14 +408,14 @@ export class MemoryPickerComponent implements Component {
417
408
  else if (data === "p") this.moveNote(focused);
418
409
  else if (data === "d") {
419
410
  if (!focused) return;
420
- this.confirm = focused;
411
+ this.confirm.ask(this.confirmPrompt(focused), () => this.forgetNote(focused));
421
412
  this.requestRender();
422
413
  } else if (data === "r") this.reload();
423
414
  }
424
415
 
425
416
  /** Wheel scrolling moves the selection, matching the other list screens. */
426
417
  handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
427
- if (this.mode !== "list" || this.confirm) return undefined;
418
+ if (this.mode !== "list" || this.confirm.pending) return undefined;
428
419
  const notes = this.notes();
429
420
  const handled = scrollList(event, this.cursor, notes.length);
430
421
  if (handled) this.selectedKey = notes[this.cursor.selected] ? noteKey(notes[this.cursor.selected]) : undefined;
@@ -472,9 +463,7 @@ export class MemoryPickerComponent implements Component {
472
463
  });
473
464
  }
474
465
 
475
- private confirmPrompt(): string {
476
- const note = this.confirm;
477
- if (!note) return "";
466
+ private confirmPrompt(note: Note): string {
478
467
  const label = sanitizeLine(note.text);
479
468
  const clipped = label.length > 40 ? `${label.slice(0, 39)}…` : label;
480
469
  return `Forget ${scopeLabel(note.scope)} note "${clipped}"? y/N`;
@@ -519,17 +508,18 @@ export class MemoryPickerComponent implements Component {
519
508
  }
520
509
  }
521
510
 
522
- const hint = this.confirm
523
- ? clip(` ${paint(this.theme, "warn", this.confirmPrompt())}`, w)
511
+ const pending = this.confirm.pending;
512
+ const hint = pending
513
+ ? clip(` ${paint(this.theme, "warn", pending.prompt)}`, w)
524
514
  : this.notice !== undefined
525
515
  ? clip(` ${paint(this.theme, "meta", this.notice)}`, w)
526
516
  : this.mergeSource
527
517
  ? screenHint(this.theme, w, ["m merge into the focused note", "Esc cancel merge", "↑/↓"])
528
518
  : screenHint(this.theme, w, [
529
519
  "Esc close",
530
- "Enter/e edit",
531
- "a add",
532
- "d forget",
520
+ { text: "Enter/e edit", color: "accent" },
521
+ { text: "a add", color: "accent" },
522
+ { text: "d forget", color: "warning" },
533
523
  "m merge",
534
524
  "p move",
535
525
  "s/Tab scope",
@@ -38,6 +38,12 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
38
38
  and `ls` tools.
39
39
  - **Model entry point.** The model can call `enter_plan_mode` to ask for plan
40
40
  mode before a non-trivial task; the user confirms.
41
+ - **AGENTS.md bootstrap.** The packaged `init` skill (`/skill:init`) enters plan
42
+ mode, reconnoiters the repository, runs one built-in interview of at
43
+ most four questions, creates or updates `AGENTS.md`, gates the draft through a
44
+ quality checklist, and writes only after plan approval. It supports existing and
45
+ empty repositories, and falls back to evidence with recorded assumptions when
46
+ the interview is unavailable or cancelled; it never invents commands.
41
47
  - **Plan files.** The model saves the plan with `write_plan`; the file lands in
42
48
  `<repo-root>/.pi/plans/<YYYY-MM-DD-HHmm>-<slug>.md` (or `<agent-dir>/plans`
43
49
  outside a repository). The plans directory is self-ignoring, so plan files
@@ -66,8 +72,9 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
66
72
  or a `/tree` restore marks only what changed on its own branch.
67
73
  - **Kept current.** After approval, `set_plan_steps` flips the checkboxes of
68
74
  the steps you name in the file, so the plan mirrors what has actually been
69
- done. Pass the step's text as `expected` and an update whose step number moved
70
- is refused instead of ticking the wrong line.
75
+ done, and ticks the todo item approval seeded from that step with it, so the
76
+ widget and the plan do not drift apart. Pass the step's text as `expected` and
77
+ an update whose step number moved is refused instead of ticking the wrong line.
71
78
  - **Reviewable plan.** `exit_plan_mode` reads the plan file back and presents
72
79
  it for approval. The user approves, keeps planning, or asks for a refinement.
73
80
  On a remote turn (WeChat) the plan file is sent first, then a digest of its
@@ -98,11 +105,16 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
98
105
  against the 50-step cap. A plan without a `Steps` heading keeps the older rule —
99
106
  the shallowest list in the document — so prose bullets in a plan that follows the
100
107
  outline never become todos, and its verification bullets are already covered.
101
- - **Goal-aware.** While planning, the injected context restates an active
108
+ Each seeded item is matched back to its step by text, so ticking a step with
109
+ `set_plan_steps` completes the item too.
110
+ - **Goal-aware.** While planning, the injected context restates a pursued
102
111
  session goal as one bullet, so the plan is designed against the objective
103
- instead of beside it. On approval, when the session has no goal at all, the user
104
- is offered one confirm to record the plan's title as the goal — never replacing
105
- an existing goal, and with no extra model call.
112
+ instead of beside it. On approval, a goal the goal workflow left as a draft
113
+ (`planning`) is finalized from the plan's own `## Goal` section — falling back
114
+ to the title — through the `goal` tool, with no extra dialog, because approving
115
+ the plan *is* the decision. A session with no goal at all gets one confirm to
116
+ record the same objective, and an `active` or `achieved` goal is never
117
+ rewritten: a final objective is the user's. No path spends a model turn.
106
118
  - **Branch-aware.** The enabled flag and the review baseline are custom session
107
119
  entries, so `/resume` and `/tree` restore the right mode and keep the
108
120
  changed-section markers.
@@ -114,7 +126,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
114
126
  | `enter_plan_mode` | normal mode | Ask to enter plan mode; empty schema, confirmed with `ctx.ui.confirm`. |
115
127
  | `write_plan` | plan mode | Save the plan: `title` (file-name slug), `content` (full markdown in the documented outline), optional `plan_path` to overwrite a refinement. Returns the absolute path. A plan with more than 50 top-level steps in its `Steps` section is refused, with the cap and the fix named, and nothing is written. |
116
128
  | `exit_plan_mode` | plan mode | Read the plan file (`plan_path`) and present it for approval. Refuses a file whose step list is over the cap, before the review screen. |
117
- | `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and optionally `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. |
129
+ | `set_plan_steps` | normal mode | Keep an approved plan current: set the checkboxes of the given top-level steps (`updates`, each a 1-based `step` in the order `exit_plan_mode` listed, plus `status`, and `expected` — the step's text, so a reordered plan is refused) to `pending` or `completed` in one write. Only the checkboxes change, and a reopened step drops its `[DONE:n]` marker. The todo item approval seeded from a step is ticked with it: the file is written first and is authoritative, a renamed item is left alone, and a failed todo write is reported in the result. |
118
130
 
119
131
  ### Commands and flags
120
132
 
@@ -125,6 +137,7 @@ How the `write_plan` and `exit_plan_mode` calls and their results look in the tr
125
137
  | `/plan <prompt>` | the user, with the task | immediate |
126
138
  | `/plan list` | the user, browsing the plans directory | immediate |
127
139
  | `/plan review [path]` | the user, reviewing a plan in the browser | immediate |
140
+ | `/skill:init` | the user, creating or updating `AGENTS.md` | enters plan mode; write waits for plan approval |
128
141
  | `Ctrl+Alt+P` | the user | immediate |
129
142
  | `--plan` | the user, at launch | immediate |
130
143
 
@@ -139,8 +152,8 @@ already runs read-only:
139
152
  `/plan <prompt>` never disables plan mode; only the argument-less `/plan` (or
140
153
  `Ctrl+Alt+P`) toggles. Because slash commands go through Pi directly, `/plan` is
141
154
  the toggle that works in every terminal (for example Zed's integrated terminal,
142
- which does not forward `Ctrl+Alt+P`). The footer shows `⋮ plan` while plan mode
143
- is on, and `⋮ plan · <plan-file>` once the model has written a plan.
155
+ which does not forward `Ctrl+Alt+P`). The footer shows `≡ plan` while plan mode
156
+ is on, and `≡ plan · <plan-file>` once the model has written a plan.
144
157
 
145
158
  `/plan review` is the one reserved subcommand. Bare `/plan review` reviews the
146
159
  plan this session wrote, or, in a later session, the newest plan file in the
@@ -149,6 +162,45 @@ with a separator) reviews that plan. A line that merely starts with the word —
149
162
  `review the auth flow` — is still treated as a planning task, so the subcommand
150
163
  never swallows one.
151
164
 
165
+ ### `/skill:init`
166
+
167
+ `/skill:init` loads the packaged `init` skill. The skill enters plan mode itself
168
+ when it is not active, then decides what to do; nothing is written before
169
+ approval. Its workflow is:
170
+
171
+ ```text
172
+ /skill:init
173
+ └─> plan mode (read-only)
174
+ └─> recon: manifests, docs, CI, tests, existing instructions
175
+ └─> interview: built-in bank, one call of at most 4 questions
176
+ ├─ answered ───────────────> reconcile with evidence
177
+ └─ unavailable/cancelled ──> evidence + assumptions
178
+ └─> draft: new file, exact edits, or an honest no-op
179
+ └─> quality gate
180
+ └─> write_plan ──> exit_plan_mode
181
+ ├─ not approved ──> refine the same plan
182
+ └─ approved ──────> write/edit AGENTS.md
183
+ └─ read back + quality checklist
184
+ ```
185
+
186
+ The interview comes from a built-in bank in
187
+ [`skills/init/references/interview-questions.md`](../../skills/init/references/interview-questions.md):
188
+ core questions cover purpose, verification, non-obvious rules, isolation,
189
+ completion, sensitive paths, and dependency or network limits, with state-specific
190
+ entries for an existing file, an empty repository, and related instruction files.
191
+ The first call uses a deterministic four-question set for the repository state;
192
+ a second call is allowed only for a material ambiguity. If the tool is inactive,
193
+ there is no UI, or the user cancels, the skill records the gaps as assumptions
194
+ and continues from repository evidence.
195
+
196
+ The quality reference is
197
+ [`skills/init/references/agents-md-quality.md`](../../skills/init/references/agents-md-quality.md).
198
+ An existing `AGENTS.md` is reviewed and updated only after approval, an accurate
199
+ file can produce a no-op plan, and an empty repository gets only known facts.
200
+ The skill needs the plan extension for its review gate. It also ships with the
201
+ package, so load the whole package (`pi -e .`) or install it; loading only
202
+ `./extensions/plan` leaves `/skill:init` unavailable.
203
+
152
204
  ## Behaviour by mode
153
205
 
154
206
  The review screen is TUI-only. `exit_plan_mode` opens `PlanViewComponent`: a
@@ -311,6 +363,18 @@ agent file that overrides it.
311
363
  - `readOnly` is also a normal `subagent` parameter; outside plan mode a caller
312
364
  may set it to run any agent read-only on purpose.
313
365
 
366
+ ## Review screen
367
+
368
+ <img src="../../assets/plan.png" alt="The terminal plan review: a Plan Review title rule, a notice that one section changed since the last review, the plan body with its headings and ◼/◻ task checkboxes, a scroll gauge naming the current section and the visible line range, and the Esc/a/r/w, scroll, and section-jump hints" width="100%">
369
+
370
+ `exit_plan_mode` opens this screen on the plan file it just wrote. The body is
371
+ Pi's Markdown renderer with the active theme, so headings, lists, code, and
372
+ links read as they do in the transcript; the checkboxes in `Steps` use the
373
+ shared `◼`/`◻` vocabulary. A plan taller than the terminal scrolls under a gauge
374
+ that names the section at the top of the viewport and the line range it shows,
375
+ and the footer lists only the keys that apply. `Esc` keeps planning, `a` approves
376
+ and runs, `r` refines inline, and `w` opens the browser review below.
377
+
314
378
  ## Browser review
315
379
 
316
380
  <img src="../../assets/plan-review.png" alt="The browser-based plan review: a sticky header with the plan title, a status chip, and the Approve & run, Save feedback, and Back to Pi actions, a side panel with step progress and a section outline, and the plan as one measured column with a comment rail and an inline review thread" width="100%">
@@ -414,6 +478,9 @@ file and returns them to the model.
414
478
  dropped. A step longer than 200 characters is shortened in the todo list and
415
479
  the approval listing — a display rule only, because the plan file always keeps
416
480
  the full text.
481
+ - The todo item for a step is matched by its text. An item renamed after
482
+ approval no longer matches, so `set_plan_steps` leaves it alone and says so in
483
+ its result; the plan file is still updated.
417
484
  - Browser review needs a local browser: the server is loopback-only, so a remote
418
485
  turn cannot drive it.
419
486
  - A browser that never connects leaves the review waiting until the user
@@ -435,13 +502,23 @@ file and returns them to the model.
435
502
  | Integration point | How |
436
503
  |---|---|
437
504
  | Tools | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps` registered with `executionMode: "sequential"`. `enter_plan_mode` is active normally with `readOnlyHint: true`; `write_plan` (`readOnlyHint: false`) and `exit_plan_mode` (`readOnlyHint: true`) are `defaultActive: false`. `set_plan_steps` is active normally (`readOnlyHint: false`, `idempotentHint: true`); the read-only policy default-denies it in plan mode. Each returns typed `details` (`EnterPlanModeDetails`, `WritePlanDetails`, `ExitPlanModeDetails`, `SetPlanStepsDetails`), and `ExitPlanModeDetails` carries the review's `annotations` when a browser approval or save supplied comments; no `outputSchema`/`structuredContent`. |
438
- | Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
439
- | State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan never writes a goal itself: an approval offer records one through `ctx.executeTool("goal", { objective })`. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
440
- | Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with an active session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
505
+ | Commands and flags | `/plan` (toggle, or enter and send a task), `/plan list` (plans index), `/plan review [path]` (browser review), `/skill:init` (skill-backed `AGENTS.md` bootstrap), `--plan` launch flag, `Ctrl+Alt+P` shortcut. |
506
+ | State | Enabled flag and the tools it hid persisted with `pi.appendEntry("plan-mode", { enabled, removed })`, and each review's baseline persisted with `pi.appendEntry("plan-baseline", { path, sections })` as one digest per heading section; both rebuilt from `ctx.sessionManager.getBranch()`, so `/resume` and `/tree` restore the mode and the changed markers of the branch they land on. Plan writes a goal only at approval, through `ctx.executeTool("goal", { objective })`: a `planning` goal is promoted to the plan's `## Goal`, a session with none is offered one, and an `active`/`achieved` goal is left alone. A review that shows the same content appends nothing. The last plan path lives in runtime memory for the footer chip, and a bare `/plan review` falls back to the newest plan file in the directory. |
507
+ | Entry requests | `pi.events` channel `PLAN_ENTER_REQUEST` from `lib/plan-entry.ts`: the `goal` workflow asks, with the shared consent sentence, and plan enables the mode on the caller's context without a second dialog. Subscribed on `session_start`, released on `session_shutdown`. |
508
+ | Hooks | `tool_call` blocks non-read-only calls and forces `subagent` read-only; `before_agent_start` injects the hidden `[PLAN MODE ACTIVE]` context, with a pursued session goal read from the branch through `lib/goal-state.ts`; `context` keeps only the newest plan context while enabled and drops stale ones otherwise. |
441
509
  | Lifecycle | `session_start`/`session_tree` restore state; `session_shutdown` clears the footer status and closes any browser review server. |
442
510
 
443
511
  ## Design notes
444
512
 
513
+ - **Consent is one dialog, whichever extension asks.** The goal workflow offers
514
+ plan mode as a phase of the goal, but a command context cannot call
515
+ `enter_plan_mode` (`ExtensionCommandContext` has no `executeTool`). So the
516
+ consent sentence left `policy.ts` for `lib/plan-entry.ts`, and the entry itself
517
+ is a `pi.events` request: the asker shows the question, plan enables silently
518
+ with the asker's context. `enter_plan_mode` keeps its own confirm, using the
519
+ same sentence, so the model-initiated path is unchanged and the two texts
520
+ cannot drift. A request that nobody answers resolves `false` rather than
521
+ hanging, so `goal` loaded alone still works.
445
522
  - **One review loop owns the human gate.** `review.ts` asks however the session
446
523
  is attached — TUI screen, dialog plus editor, remote file plus digest, or
447
524
  browser — and returns one `ReviewDecision`, so `exit_plan_mode` and
@@ -476,6 +553,13 @@ file and returns them to the model.
476
553
  channel; no import from that extension) and the steps are merged into them.
477
554
  A rejected seed reports the nested tool's own reason in the approval message
478
555
  instead of failing silently.
556
+ - **Ticking a step ticks its item, through the same channel.** The seed's text is
557
+ the step's text (`PlanStep.content`, already elided for a long step), so the
558
+ mirror matches on that text and changes only `status`: an item the user
559
+ renamed is left alone and reported as unmatched rather than overwritten, and a
560
+ repeated call writes nothing. The file is written first and stays
561
+ authoritative, so a failed nested write is reported in the result text — once
562
+ one artifact has changed there is no previous state to hand back.
479
563
  - **A cap is enforced where it can still be fixed.** Both refusal points —
480
564
  `write_plan` and `exit_plan_mode` — sit before the write and before the review,
481
565
  because those are the moments the model can still regroup the steps. A note on
@@ -504,7 +588,7 @@ file and returns them to the model.
504
588
  (`isContainedPlan`), in a single all-or-nothing write — refusing every entry
505
589
  if any step is out of range or an entry's `expected` text no longer matches —
506
590
  and refuses while plan mode is on, so the read-only guarantee and the path
507
- backstop are unchanged. The optional `expected` is what makes an ordinal safe:
591
+ backstop are unchanged. A required `expected` is what makes an ordinal safe:
508
592
  a plan edited between approval and ticking is caught instead of silently
509
593
  marking the step that moved into that number.
510
594
  - **Direction: enforce capabilities at the tool boundary.** (1) A shared
@@ -543,24 +627,28 @@ file and returns them to the model.
543
627
  | `git-bash.ts` | `checkReadOnlyGit`: the pure allowlist that restricts `bash` to read-only git commands. |
544
628
  | `plans.ts` | Plan-file store: slug/stamp naming, directory resolution, containment, atomic `write`/`setSteps`/`saveFeedback`, and `read`/`latest`. |
545
629
  | `todos.ts` | Approval-time todo merge (pure): the last list replayed from the branch, and the merge of the plan's steps into it with the cap respected. |
546
- | `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, and the checkbox edit. |
630
+ | `steps.ts` | Plan-document structure (pure): the outline, step extraction and the `Steps`-section scope, heading sections, the plan's own goal (`goalSection`), and the checkbox edit. |
631
+ | `format.ts` | Pure result/approval text: the plan preview, the step-cap diagnosis, step labels, the merge summary, and the verification note. |
547
632
  | `plan-text.ts` | The plan text a review shows and compares: `sanitizePlanText`, the one normalization both sides use, the per-section `sectionDigests`, and `changedSections`. |
548
633
  | `runtime.ts` | Enabled state, tool gating, persistence (mode and review baseline), control-tool identity, footer status. |
549
- | `tools.ts` | `enter_plan_mode`, `write_plan`, `exit_plan_mode`, and `set_plan_steps`: schemas, results, transcripts, and the approval-time todo seed. |
634
+ | `tools.ts` | Registration entry: composes the four tool registrations in the order the session sees them. |
635
+ | `tools/` | One module per tool (`enter.ts`, `write.ts`, `exit.ts`, `steps.ts`), plus `todo-seed.ts` (the two nested `todo` writes) and `nested.ts` (reading a nested result). |
550
636
  | `review.ts` | The review gate: one decision from the TUI screen, a dialog, or the browser, the shared browser lifecycle, and the "mark what was shown" baseline rule. |
551
637
  | `commands.ts` | `/plan` (enter, optional task) and the `/plan review [path]` browser subcommand. |
552
638
  | `tui.ts` | `PlanViewComponent` (sectioned Markdown body, changed-section summary and marker, section jump, scroll gauge, approve/refine/browser/keep). |
553
639
  | `plan-index.ts` | `/plan list`: the notice text and the docked index screen (title, step progress, age, focused path; Enter reviews). |
554
640
  | `blocks.ts` | Source-line block model (fence-aware headings, paragraphs, list items, code, quotes, tables, rules, blanks). |
555
641
  | `feedback.ts` | Browser-comment codec: `stripFeedback`, `parseFeedback`, `formatFeedback`, `feedbackSummary`, and payload validation. |
556
- | `web.ts` | `createPlanWebReview`: the loopback server, the section outline (with changed-section flags) and code highlighting, and the save/approve/cancel/abort session. |
557
- | `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes. |
642
+ | `web.ts` | `createPlanWebReview`: the review session (SSE close detection, the body cap, settle-after-flush, abort) over the shared loopback bind, writer, and close. |
643
+ | `web-router.ts` | Pure request handler: token/`Host`/method checks and the review routes, over the shared page and JSON header sets. |
558
644
  | `web-page.ts` | The review page shell (header, reading progress, review facts, section outline, shortcut sheet), its data island, and inline CSS. |
559
645
  | `web-client.ts` | The `/client.js` source (block/thread rendering, the comment rail, outline and facts, inline comments, selection quotes, tables, highlighted code, keyboard nav, save/approve/back). |
560
646
  | `path-guard.ts` | Recursive path-argument detection for the read-only backstop. |
561
647
  | [`../../lib/browser.ts`](../../lib/browser.ts) | Cross-platform default-browser opener (shared with `file-browse`). |
648
+ | [`../../lib/plan-entry.ts`](../../lib/plan-entry.ts) | Plan-mode entry shared with `goal`: the read-only summary, the consent sentence, and the `pi.events` request `plan` answers by enabling the mode. |
562
649
  | [`../../lib/shell.ts`](../../lib/shell.ts) | The shell-escape rule the read-only git guard shares with the worktree isolation guard. |
563
650
  | [`../../lib/highlight.ts`](../../lib/highlight.ts) | First-party escape-first syntax tokenizer (shared with `file-browse`). |
564
- | [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, and shared markup. |
651
+ | [`../../lib/web/index.ts`](../../lib/web/index.ts) | Shared Bytefly web brand system: tokens, base rules, `tok-*` colors, layout, the bootstrap, the toggle wiring, the shortcut sheet (`keysSheet`, `keysDialogScript`), and shared markup. |
652
+ | [`../../lib/http/index.ts`](../../lib/http/index.ts) | Loopback HTTP layer shared with `file-browse`: the page policy and header sets, `ServeResponse`, the request-envelope helpers, bind/respond/close. |
565
653
  | `../../lib/policy.ts` | Shared read-only capability policy (default-deny + `readOnlyHint`). |
566
654
  | `../../lib/tui/chrome.ts` | Shared screen header, viewport-row helper, and styled key-hint footer. |
@@ -10,9 +10,9 @@
10
10
 
11
11
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { isRemoteTurn } from "../../lib/interaction.ts";
13
+ import { READ_ONLY_SUMMARY } from "../../lib/plan-entry.ts";
13
14
  import { feedbackSummary } from "./feedback.ts";
14
15
  import { PlanIndexComponent, formatPlanIndex } from "./plan-index.ts";
15
- import { READ_ONLY_SUMMARY } from "./policy.ts";
16
16
  import { runBrowserReview } from "./review.ts";
17
17
  import type { PlanRuntime } from "./runtime.ts";
18
18
  import type { PlanSummary } from "./plans.ts";
@@ -28,6 +28,13 @@ export function planModeNotice(enabled: boolean): string {
28
28
  return enabled ? ENABLED_NOTICE : DISABLED_NOTICE;
29
29
  }
30
30
 
31
+ /** Enter plan mode when needed, with the same notice `/plan <task>` shows. */
32
+ function ensurePlanMode(runtime: PlanRuntime, ctx: ExtensionContext): void {
33
+ if (runtime.isEnabled()) return;
34
+ runtime.enable(ctx);
35
+ ctx.ui.notify(ENABLED_NOTICE, "info");
36
+ }
37
+
31
38
  /** The plan path a `/plan review` line names, or undefined to review the newest plan. */
32
39
  function reviewTarget(args: string): string | undefined {
33
40
  const target = args.slice(REVIEW_PREFIX.length).trim();
@@ -189,10 +196,7 @@ export function registerCommands(
189
196
  }
190
197
 
191
198
  // A task always means "plan this": enter if needed, then send.
192
- if (!runtime.isEnabled()) {
193
- runtime.enable(ctx);
194
- ctx.ui.notify(ENABLED_NOTICE, "info");
195
- }
199
+ ensurePlanMode(runtime, ctx);
196
200
  if (ctx.isIdle()) await pi.sendUserMessage(prompt);
197
201
  else await pi.sendUserMessage(prompt, { deliverAs: "followUp" });
198
202
  },
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure text for the plan tools' results and approval message.
3
+ *
4
+ * No host calls and no theme: the registrations in `tools/` use these to keep
5
+ * the cap diagnosis, the preview, and the merge summary in one place.
6
+ */
7
+
8
+ import { SET_PLAN_STEPS_TOOL } from "./runtime.ts";
9
+ import { MAX_STEP_LENGTH, MAX_STEPS, type PlanStep, type StepUpdate } from "./steps.ts";
10
+ import type { TodoMerge } from "./todos.ts";
11
+
12
+ /** Lines of plan shown in an unexpanded result. */
13
+ const PREVIEW_LINES = 8;
14
+
15
+ /** The collapsed view of a plan body and how many lines it hid. */
16
+ export function preview(plan: string): { body: string; more: number } {
17
+ const lines = plan.split("\n");
18
+ if (lines.length <= PREVIEW_LINES) return { body: plan, more: 0 };
19
+ return { body: lines.slice(0, PREVIEW_LINES).join("\n"), more: lines.length - PREVIEW_LINES };
20
+ }
21
+
22
+ /**
23
+ * One sentence when a step is longer than the display cap. The text stays in the
24
+ * plan file either way; only the todo list and the approval listing shorten it.
25
+ */
26
+ export function stepElisionNote(elided: number): string {
27
+ if (elided === 0) return "";
28
+ const plural = elided === 1 ? " is" : "s are";
29
+ return ` Note: ${elided} step${plural} longer than ${MAX_STEP_LENGTH} characters and will be shortened in the todo list.`;
30
+ }
31
+
32
+ /**
33
+ * The diagnosis both cap refusals share: a step list longer than approval can
34
+ * track. Each caller appends the fix that is available from where it stands.
35
+ */
36
+ export function overCap(total: number): string {
37
+ return `The plan has ${total} top-level steps but approval tracks at most ${MAX_STEPS}.`;
38
+ }
39
+
40
+ /** "Step 2" or "Steps 1, 2" for a set of step numbers. */
41
+ export function stepLabel(steps: number[]): string {
42
+ return `${steps.length === 1 ? "Step" : "Steps"} ${steps.join(", ")}`;
43
+ }
44
+
45
+ /** "Steps 1, 2 completed; step 3 reopened" for the tool's text result. */
46
+ export function describeUpdates(updates: StepUpdate[]): string {
47
+ const completed = updates.filter((update) => update.status === "completed").map((update) => update.step);
48
+ const pending = updates.filter((update) => update.status === "pending").map((update) => update.step);
49
+ const parts: string[] = [];
50
+ if (completed.length > 0) parts.push(`${stepLabel(completed)} completed`);
51
+ if (pending.length > 0) parts.push(`${stepLabel(pending)} reopened`);
52
+ return parts.join("; ");
53
+ }
54
+
55
+ /**
56
+ * One sentence about the plan's verification item, or "" when the plan has none
57
+ * (or it was already in the list). The item is a todo, not a plan step, so it is
58
+ * ticked in the list rather than in the file.
59
+ */
60
+ export function verificationNote(merged: TodoMerge | undefined): string {
61
+ if (merged?.trailing === "added") {
62
+ return "\nThe plan's verification section is the last todo item; tick it in the todo list, not in the file.";
63
+ }
64
+ if (merged?.trailing === "deferred") {
65
+ return "\nThe plan's verification section did not fit in the full todo list; keep it in mind.";
66
+ }
67
+ return "";
68
+ }
69
+
70
+ /** What the merge did, for the approval message; `steps` is the plan's step count. */
71
+ export function mergeSummary(merged: TodoMerge, steps: PlanStep[]): string {
72
+ const total = steps.length;
73
+ const counted = `${merged.added + merged.skipped} of ${total} plan step${total === 1 ? "" : "s"}`;
74
+ const notes = [
75
+ merged.skipped > 0 ? `${merged.skipped} already present` : "",
76
+ merged.kept > 0 ? `${merged.kept} existing kept` : "",
77
+ merged.deferred > 0 ? `${merged.deferred} deferred, list full` : "",
78
+ ].filter(Boolean);
79
+ const suffix = notes.length > 0 ? ` (${notes.join(", ")})` : "";
80
+ 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\`.`;
81
+ if (merged.added === 0 && merged.skipped === 0) {
82
+ return ` The todo list is full, so no plan steps were added${suffix}. Keep these steps in mind. ${tick}`;
83
+ }
84
+ if (merged.added === 0)
85
+ return ` ${total === 1 ? "The plan's step is" : "Every plan step is"} already in the todo list${suffix}. ${tick}`;
86
+ return ` Merged ${counted} into the todo list${suffix}. ${tick}`;
87
+ }
@@ -22,19 +22,14 @@ import { fileURLToPath } from "node:url";
22
22
  import { hasPathInput } from "./path-guard.ts";
23
23
  import { isExtensionEnabled } from "../../lib/env.ts";
24
24
  import { sanitizeLine } from "../../lib/format.ts";
25
- import { lastGoal, type PublishedGoal } from "../../lib/goal-state.ts";
25
+ import { lastGoal, isPursuedGoal, type PublishedGoal } from "../../lib/goal-state.ts";
26
+ import { READ_ONLY_SUMMARY, PLAN_ENTER_REQUEST, type PlanEnterRequest } from "../../lib/plan-entry.ts";
26
27
  import { ASK_USER_QUESTION_TOOL, SUBAGENT_READ_ONLY_PARAM, SUBAGENT_TOOL } from "../../lib/tool-names.ts";
27
28
  import { clip } from "../../lib/tui/columns.ts";
28
29
  import { STATUS_KEYS } from "../../lib/tui/glyphs.ts";
29
30
  import { planModeNotice, registerCommands } from "./commands.ts";
30
31
  import { checkReadOnlyGit } from "./git-bash.ts";
31
- import {
32
- createPlanPolicy,
33
- BASH_BLOCKED_GUIDANCE,
34
- BLOCKED_GUIDANCE,
35
- PLAN_SAFE_TOOLS,
36
- READ_ONLY_SUMMARY,
37
- } from "./policy.ts";
32
+ import { createPlanPolicy, BASH_BLOCKED_GUIDANCE, BLOCKED_GUIDANCE, PLAN_SAFE_TOOLS } from "./policy.ts";
38
33
  import { createPlanRuntime } from "./runtime.ts";
39
34
  import { registerTools } from "./tools.ts";
40
35
  import { createPlanWebReview } from "./web.ts";
@@ -51,21 +46,20 @@ const GOAL_CONTEXT_CHARS = 64;
51
46
 
52
47
  /**
53
48
  * Injected each turn while planning: the facts and the next action, not the tool
54
- * contracts. An active session goal becomes one extra bullet, so a plan is
55
- * designed against the objective instead of beside it.
49
+ * contracts. A pursued session goal — being planned or already final — becomes one
50
+ * extra bullet, so a plan is designed against the objective instead of beside it.
56
51
  */
57
52
  export function planModeContext(goal?: PublishedGoal | null): string {
58
53
  const lines = [
59
54
  PLAN_MODE_MARKER,
60
- "You are in plan mode: read-only exploration for safe code analysis.",
61
55
  "",
62
56
  `- Do not modify anything except the plan file: while planning, ${READ_ONLY_SUMMARY}.`,
63
- "- bash is git-only: read-only commands (git status, git diff, git log) may be chained with && or ;, but pipes, substitution, and redirection are blocked and powershell is unavailable. The cwd is already the project root, so do not prefix commands with cd; use read, grep, find, and ls for files.",
57
+ "- bash is git-only: read-only git commands chain with && or ; — no pipes, redirection, or substitution, and no powershell. The cwd is already the project root, so skip cd, and read files with read, grep, find, and ls.",
64
58
  "- Investigate and design a concrete plan, then save it with write_plan and call exit_plan_mode with the returned plan_path for approval.",
65
- "- Delegate broad exploration with subagent; while planning it is forced read-only.",
59
+ "- Delegate broad exploration with subagent.",
66
60
  `- To choose between approaches, ask with ${ASK_USER_QUESTION_TOOL} before finalizing.`,
67
61
  ];
68
- if (goal?.status === "active") {
62
+ if (goal && isPursuedGoal(goal)) {
69
63
  lines.push(`- Current session goal: ${clip(sanitizeLine(goal.objective), GOAL_CONTEXT_CHARS)}`);
70
64
  }
71
65
  return lines.join("\n");
@@ -109,10 +103,32 @@ export default function planMode(pi: ExtensionAPI): void {
109
103
  },
110
104
  });
111
105
 
112
- pi.on("session_start", (_event, ctx) => runtime.restore(ctx));
106
+ // Another extension that offers plan mode (the goal workflow) asks on this
107
+ // channel instead of calling a tool it cannot reach from a command context. It
108
+ // owns the user's answer; enabling here must therefore not ask again.
109
+ const unsubscribes: Array<() => void> = [];
110
+ const listenForEntryRequests = (): void => {
111
+ for (const unsubscribe of unsubscribes) unsubscribe();
112
+ unsubscribes.length = 0;
113
+ unsubscribes.push(
114
+ pi.events.on(PLAN_ENTER_REQUEST, (data) => {
115
+ const request = data as Partial<PlanEnterRequest> | undefined;
116
+ if (!request?.ctx || typeof request.respond !== "function") return;
117
+ if (!runtime.isEnabled()) runtime.enable(request.ctx);
118
+ request.respond(true);
119
+ }),
120
+ );
121
+ };
122
+
123
+ pi.on("session_start", (_event, ctx) => {
124
+ listenForEntryRequests();
125
+ runtime.restore(ctx);
126
+ });
113
127
  pi.on("session_tree", (_event, ctx) => runtime.restore(ctx));
114
128
  pi.on("session_shutdown", async (_event, ctx) => {
115
129
  await web.closeAll();
130
+ for (const unsubscribe of unsubscribes) unsubscribe();
131
+ unsubscribes.length = 0;
116
132
  ctx.ui.setStatus(STATUS_KEYS.planMode, undefined);
117
133
  });
118
134
 
@@ -292,10 +292,9 @@ export function createPlanStore(pi: ExtensionAPI, options: { now?: () => Date }
292
292
  const range = total === 0 ? "it has no steps" : `valid steps are 1 to ${total}`;
293
293
  throw new Error(`Step ${outOfRange.step} is out of range; ${range}.`);
294
294
  }
295
- // A caller-supplied text is the guard against a reordered plan: refuse the
296
- // whole batch rather than tick the step that moved into that number.
295
+ // The expected text is the guard against a reordered plan: refuse the whole
296
+ // batch rather than tick the step that moved into that number.
297
297
  const stale = updates.find((update) => {
298
- if (update.expected === undefined) return false;
299
298
  const step = steps[update.step - 1];
300
299
  return step === undefined || !matchesStepText(step.content, update.expected);
301
300
  });
@@ -46,12 +46,6 @@ export const PLAN_ALLOWED_TOOLS = [...PLAN_SAFE_TOOLS, SUBAGENT_TOOL];
46
46
  /** Tools always blocked while planning, even when annotated read-only. */
47
47
  const ALWAYS_BLOCKED_TOOLS = ["write", "edit", ENTER_TOOL];
48
48
 
49
- /** One-clause description of the read-only guarantee, reused across prompts. */
50
- export const READ_ONLY_SUMMARY =
51
- "write and edit are disabled, bash is limited to read-only git commands, and other mutating tools are blocked; " +
52
- "subagent delegation is forced read-only, and the only file write is write_plan (under .pi/plans, or the agent " +
53
- "directory outside a repository)";
54
-
55
49
  /** Shown when a tool call is refused while planning. */
56
50
  export const BLOCKED_GUIDANCE = "Call exit_plan_mode and get approval before using it.";
57
51
 
@@ -21,7 +21,9 @@ import { realpathSync } from "node:fs";
21
21
  import { truncateToWidth } from "@earendil-works/pi-tui";
22
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
23
23
  import { sanitizeLine } from "../../lib/format.ts";
24
+ import { PLAN_MODE_ENTRY_TYPE } from "../../lib/plan-state.ts";
24
25
  import type { ReadOnlyPolicy } from "../../lib/policy.ts";
26
+ import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
25
27
  import { GLYPHS, STATUS_KEYS } from "../../lib/tui/glyphs.ts";
26
28
  import { paint } from "../../lib/tui/tokens.ts";
27
29
  import { createPlanStore, type PlanStore } from "./plans.ts";
@@ -31,13 +33,13 @@ import type { PlanModeEntry, ReviewBaselineEntry } from "./types.ts";
31
33
  /** Tool the model calls to leave plan mode after approval. */
32
34
  export const EXIT_TOOL = "exit_plan_mode";
33
35
  /** Tool the model calls to ask to enter plan mode. */
34
- export const ENTER_TOOL = "enter_plan_mode";
36
+ export const ENTER_TOOL = PLAN_ENTER_TOOL;
35
37
  /** Tool the model calls to save the plan artifact. */
36
38
  export const WRITE_PLAN_TOOL = "write_plan";
37
39
  /** Tool the model calls to keep an approved plan's steps current. */
38
40
  export const SET_PLAN_STEPS_TOOL = "set_plan_steps";
39
41
  /** Custom-entry type used for plan-mode state. */
40
- export const STATE_TYPE = "plan-mode";
42
+ export const STATE_TYPE = PLAN_MODE_ENTRY_TYPE;
41
43
  /** Custom-entry type used for a plan's review baseline. */
42
44
  export const BASELINE_TYPE = "plan-baseline";
43
45