@gavin-hu/my-pi-agent 0.11.4 → 0.13.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 (197) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +33 -23
  3. package/extensions/bytefly/README.md +381 -225
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +52 -6
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  9. package/extensions/bytefly/footer/config.ts +7 -1
  10. package/extensions/bytefly/footer/delegation.ts +100 -0
  11. package/extensions/bytefly/footer/price/config.ts +54 -15
  12. package/extensions/bytefly/footer/price/price.ts +20 -7
  13. package/extensions/bytefly/footer/price/types.ts +13 -7
  14. package/extensions/bytefly/footer/register.ts +13 -2
  15. package/extensions/bytefly/footer/render/format.ts +3 -2
  16. package/extensions/bytefly/footer/render/lines.ts +2 -0
  17. package/extensions/bytefly/footer/render/segments.ts +30 -2
  18. package/extensions/bytefly/footer/types.ts +12 -0
  19. package/extensions/bytefly/header/component.ts +21 -6
  20. package/extensions/bytefly/header/config.ts +3 -0
  21. package/extensions/bytefly/header/register.ts +34 -3
  22. package/extensions/bytefly/index.ts +15 -4
  23. package/extensions/bytefly/notify/config.ts +2 -0
  24. package/extensions/bytefly/notify/format.ts +19 -1
  25. package/extensions/bytefly/notify/register.ts +37 -13
  26. package/extensions/bytefly/notify/types.ts +2 -0
  27. package/extensions/bytefly/panel/commands.ts +34 -13
  28. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  29. package/extensions/bytefly/panel/settings.ts +293 -48
  30. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  31. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  32. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  33. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  34. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  35. package/extensions/bytefly/panel/tui.ts +205 -319
  36. package/extensions/bytefly/picker/runtime.ts +61 -2
  37. package/extensions/bytefly/picker/toggle.ts +56 -0
  38. package/extensions/bytefly/update/cache.ts +73 -0
  39. package/extensions/bytefly/update/check.ts +59 -0
  40. package/extensions/bytefly/update/format.ts +87 -0
  41. package/extensions/bytefly/update/register.ts +158 -0
  42. package/extensions/bytefly/update/state.ts +40 -0
  43. package/extensions/bytefly/update/version.ts +83 -0
  44. package/extensions/document/README.md +24 -14
  45. package/extensions/document/config.ts +4 -0
  46. package/extensions/document/deadline.ts +61 -0
  47. package/extensions/document/tool.ts +14 -5
  48. package/extensions/file-browse/README.md +12 -8
  49. package/extensions/file-browse/client.ts +6 -7
  50. package/extensions/file-browse/files.ts +0 -4
  51. package/extensions/file-browse/git.ts +9 -10
  52. package/extensions/file-browse/html.ts +17 -7
  53. package/extensions/file-browse/router.ts +24 -15
  54. package/extensions/file-open/README.md +30 -15
  55. package/extensions/file-open/index.ts +4 -3
  56. package/extensions/file-open/open.ts +52 -21
  57. package/extensions/goal/README.md +45 -13
  58. package/extensions/goal/clarify-prompt.ts +5 -36
  59. package/extensions/goal/clarify.ts +55 -86
  60. package/extensions/goal/commands.ts +41 -35
  61. package/extensions/goal/format.ts +12 -4
  62. package/extensions/goal/schema.ts +6 -2
  63. package/extensions/goal/tools.ts +28 -8
  64. package/extensions/goal/tui.ts +1 -1
  65. package/extensions/goal/types.ts +6 -2
  66. package/extensions/job/README.md +16 -12
  67. package/extensions/job/runtime.ts +23 -6
  68. package/extensions/job/status.ts +4 -2
  69. package/extensions/memory/README.md +56 -14
  70. package/extensions/memory/commands.ts +20 -2
  71. package/extensions/memory/doctor.ts +137 -0
  72. package/extensions/memory/format.ts +24 -7
  73. package/extensions/memory/runtime.ts +49 -0
  74. package/extensions/memory/store.ts +31 -0
  75. package/extensions/memory/tui.ts +136 -12
  76. package/extensions/plan/README.md +83 -36
  77. package/extensions/plan/blocks.ts +1 -35
  78. package/extensions/plan/commands.ts +84 -19
  79. package/extensions/plan/index.ts +35 -11
  80. package/extensions/plan/plan-index.ts +234 -0
  81. package/extensions/plan/plan-text.ts +95 -0
  82. package/extensions/plan/plans.ts +91 -2
  83. package/extensions/plan/policy.ts +4 -3
  84. package/extensions/plan/review.ts +246 -0
  85. package/extensions/plan/runtime.ts +73 -24
  86. package/extensions/plan/steps.ts +60 -39
  87. package/extensions/plan/todos.ts +29 -60
  88. package/extensions/plan/tools.ts +188 -244
  89. package/extensions/plan/tui.ts +18 -31
  90. package/extensions/plan/types.ts +12 -0
  91. package/extensions/plan/web-client.ts +51 -18
  92. package/extensions/plan/web-page.ts +7 -2
  93. package/extensions/plan/web-router.ts +11 -3
  94. package/extensions/plan/web.ts +106 -41
  95. package/extensions/question/README.md +1 -1
  96. package/extensions/question/tools.ts +1 -1
  97. package/extensions/rewind/README.md +28 -8
  98. package/extensions/rewind/commands.ts +1 -1
  99. package/extensions/rewind/format.ts +4 -4
  100. package/extensions/rewind/git.ts +56 -18
  101. package/extensions/rewind/index.ts +3 -5
  102. package/extensions/rewind/path-picker.ts +198 -0
  103. package/extensions/rewind/paths.ts +128 -0
  104. package/extensions/rewind/restore.ts +69 -28
  105. package/extensions/rewind/rewind.ts +80 -3
  106. package/extensions/rewind/runtime.ts +13 -5
  107. package/extensions/rewind/snapshot-run.ts +5 -2
  108. package/extensions/rewind/types.ts +2 -0
  109. package/extensions/subagent/README.md +7 -1
  110. package/extensions/subagent/accounting.ts +34 -0
  111. package/extensions/subagent/index.ts +23 -3
  112. package/extensions/subagent/stream.ts +2 -0
  113. package/extensions/subagent/types.ts +2 -0
  114. package/extensions/todo/README.md +19 -11
  115. package/extensions/todo/index.ts +5 -3
  116. package/extensions/todo/nudge.ts +58 -9
  117. package/extensions/todo/runtime.ts +28 -17
  118. package/extensions/watch/README.md +8 -3
  119. package/extensions/watch/index.ts +19 -0
  120. package/extensions/web-access/README.md +46 -16
  121. package/extensions/web-access/charset.ts +58 -0
  122. package/extensions/web-access/fetch/find.ts +11 -7
  123. package/extensions/web-access/fetch/format.ts +64 -18
  124. package/extensions/web-access/fetch/page.ts +5 -1
  125. package/extensions/web-access/fetch/tool.ts +20 -21
  126. package/extensions/web-access/http.ts +49 -8
  127. package/extensions/web-access/search/facts.ts +38 -0
  128. package/extensions/web-access/search/tool.ts +14 -3
  129. package/extensions/web-access/text.ts +50 -11
  130. package/extensions/wechat/README.md +5 -4
  131. package/extensions/wechat/tools.ts +0 -2
  132. package/extensions/worktree/README.md +74 -15
  133. package/extensions/worktree/config.ts +11 -0
  134. package/extensions/worktree/exit.ts +35 -3
  135. package/extensions/worktree/gaps.ts +74 -0
  136. package/extensions/worktree/guard.ts +50 -4
  137. package/extensions/worktree/handoff.ts +55 -0
  138. package/extensions/worktree/index.ts +12 -2
  139. package/extensions/worktree/runtime.ts +20 -0
  140. package/extensions/worktree/status.ts +15 -2
  141. package/extensions/worktree/tools.ts +3 -2
  142. package/lib/README.md +12 -9
  143. package/lib/config.ts +43 -1
  144. package/lib/env.ts +12 -0
  145. package/lib/events.ts +86 -1
  146. package/lib/goal-state.ts +95 -0
  147. package/lib/requirements.ts +340 -46
  148. package/lib/todo-state.ts +62 -0
  149. package/lib/tui/README.md +91 -0
  150. package/lib/tui/glyphs.ts +2 -0
  151. package/lib/web/layout.ts +12 -2
  152. package/package.json +5 -3
  153. package/themes/bytefly-dark.json +1 -1
  154. package/assets/bytefly.schema.json +0 -785
  155. package/assets/chrome.png +0 -0
  156. package/assets/file-browse.png +0 -0
  157. package/assets/job.png +0 -0
  158. package/assets/plan-review.png +0 -0
  159. package/assets/question.png +0 -0
  160. package/assets/rewind.png +0 -0
  161. package/assets/todo.png +0 -0
  162. package/assets/tool-renderings.png +0 -0
  163. package/assets/tools/ask_user_question-cancelled.png +0 -0
  164. package/assets/tools/ask_user_question.png +0 -0
  165. package/assets/tools/bash.png +0 -0
  166. package/assets/tools/edit.png +0 -0
  167. package/assets/tools/enter_plan_mode.png +0 -0
  168. package/assets/tools/enter_worktree.png +0 -0
  169. package/assets/tools/exit_plan_mode.png +0 -0
  170. package/assets/tools/exit_worktree.png +0 -0
  171. package/assets/tools/find.png +0 -0
  172. package/assets/tools/goal.png +0 -0
  173. package/assets/tools/grep.png +0 -0
  174. package/assets/tools/job.png +0 -0
  175. package/assets/tools/list_worktrees.png +0 -0
  176. package/assets/tools/ls.png +0 -0
  177. package/assets/tools/memory-long-subject.png +0 -0
  178. package/assets/tools/memory.png +0 -0
  179. package/assets/tools/notice.png +0 -0
  180. package/assets/tools/open_path.png +0 -0
  181. package/assets/tools/prune_worktrees.png +0 -0
  182. package/assets/tools/read.png +0 -0
  183. package/assets/tools/read_doc-running.png +0 -0
  184. package/assets/tools/read_doc.png +0 -0
  185. package/assets/tools/run_doctor.png +0 -0
  186. package/assets/tools/send_wechat.png +0 -0
  187. package/assets/tools/set_plan_steps.png +0 -0
  188. package/assets/tools/subagent.png +0 -0
  189. package/assets/tools/todo.png +0 -0
  190. package/assets/tools/turn-rule.png +0 -0
  191. package/assets/tools/user-message.png +0 -0
  192. package/assets/tools/watch.png +0 -0
  193. package/assets/tools/web_fetch-rejected.png +0 -0
  194. package/assets/tools/web_fetch.png +0 -0
  195. package/assets/tools/web_search.png +0 -0
  196. package/assets/tools/write.png +0 -0
  197. package/assets/tools/write_plan.png +0 -0
@@ -28,12 +28,14 @@ pi install ./ # install the package for a user
28
28
  ## What it does
29
29
 
30
30
  - Opens a file or directory with the OS-native application: `open` on macOS,
31
- `cmd /c start` on Windows, `xdg-open` on Linux.
31
+ `explorer` on Windows, `xdg-open` on Linux.
32
32
  - Reveals it in the file manager instead with `-r` (or `reveal: true`): `open -R`
33
33
  on macOS, `explorer /select,` on Windows, and the containing directory on
34
34
  Linux, which has no "select" verb.
35
35
  - Unwraps the editor's `@` file-mention prefix and surrounding quotes, so
36
36
  `/open @src/app.ts` and `/open @"my file.txt"` both work.
37
+ - Reports a launcher that cannot be started, and (on macOS and Linux) a launcher
38
+ that exits non-zero because no handler accepted the target.
37
39
  - Resolves relative paths against the effective working directory, so under the
38
40
  `worktree` extension `/open .` opens the isolated worktree.
39
41
  - Launches the handler detached, so it outlives the Pi session.
@@ -47,14 +49,17 @@ pi install ./ # install the package for a user
47
49
 
48
50
  The `-r` flag must come first. A missing path is reported through a notification
49
51
  and nothing is launched. An empty argument means the working directory. The
50
- command needs an interactive terminal UI.
52
+ command first reports `Opening <path>` (or `Revealing <path>`); a launcher that
53
+ starts but then fails adds a `Could not open` error notification. The command
54
+ needs an interactive terminal UI.
51
55
 
52
56
  ## Tool
53
57
 
54
58
  `open_path` — `exposure: "codemode"`, inactive by default, not read-only,
55
- open-world (`openWorldHint: true` because it starts an OS process). It is
56
- listed by the `codemode` tool and reachable through `ctx.executeTool`; it is not
57
- declared to the model unless activated.
59
+ open-world (`openWorldHint: true` because it starts an OS process). It is listed
60
+ by the `codemode` tool for a script to call as `tools.open_path(...)`, and an
61
+ extension can reach it through `ctx.executeTool`; it is not declared to the
62
+ model unless activated.
58
63
 
59
64
  | Parameter | Type | Meaning |
60
65
  |---|---|---|
@@ -68,10 +73,11 @@ Result (`details` / `structuredContent`):
68
73
  | `target` | The absolute path that was handled. |
69
74
  | `reveal` | Whether it was revealed rather than opened. |
70
75
 
71
- The model-facing `content` is `Opened <target>` or `Revealed <target>`. A path
72
- that does not exist is thrown as an error naming the resolved path; a launch
73
- failure is reported through `ctx.ui.notify` because the handler runs detached,
74
- and the tool result still names the target.
76
+ The model-facing `content` is `Opened <target>` or `Revealed <target>` — the
77
+ handler was started, not proof that it succeeded. A path that does not exist is
78
+ thrown as an error naming the resolved path; a launcher that cannot be started,
79
+ or (on macOS and Linux) exits non-zero, is reported through `ctx.ui.notify`
80
+ because the handler runs detached, and the tool result still names the target.
75
81
 
76
82
  ## Behaviour by mode
77
83
 
@@ -94,12 +100,18 @@ JSON, and print modes; a codemode script can call it in any of them. Only the
94
100
  - **Not confined.** Unlike `read_doc`, the tool opens any path on the machine:
95
101
  confinement would defeat its purpose of showing the user a file wherever it
96
102
  lives. Relative paths resolve against the effective worktree root.
97
- - **Best-effort launch.** A missing handler or a refused launch is reported, not
98
- retried.
103
+ - **Best-effort launch.** A launcher that cannot be started is reported, and so
104
+ is a non-zero exit on macOS and Linux, where `open` and `xdg-open` return a
105
+ failure status when no handler accepts the target. Windows `explorer` can exit
106
+ non-zero after a successful open, so only a failure to start is reported
107
+ there. Nothing is retried.
99
108
 
100
109
  ## Limitations
101
110
 
102
111
  - Reveal has no native meaning on Linux, so it opens the containing directory.
112
+ - On Windows a refused target is not reported: `explorer` does not relay the
113
+ failure and its exit status is unreliable, so `/open` and `open_path` can only
114
+ report that the launcher started.
103
115
  - A launch that the OS accepts but the user never sees (a backgrounded app, a
104
116
  hidden window) is not detected: `spawn` reports only that the process started.
105
117
  - `-r` is parsed only as the first token of `/open`, so a path that genuinely
@@ -115,7 +127,7 @@ JSON, and print modes; a codemode script can call it in any of them. Only the
115
127
  | Command | `/open [path]` and `/open -r [path]`; reports through `ctx.ui.notify`. |
116
128
  | State | None persisted, and none needed: every call is independent. |
117
129
  | Lifecycle | No `session_start` / `session_shutdown` hooks; the factory holds no long-lived resources. |
118
- | Host seams | `resolveEffectiveCwd` and `isExtensionEnabled` (`lib/env.ts`), `stripControlChars` (`lib/format.ts`), `spawn` / `open` / `xdg-open` / `cmd` / `explorer` (via the injected launcher in `open.ts`). |
130
+ | Host seams | `resolveEffectiveCwd` and `isExtensionEnabled` (`lib/env.ts`), `stripControlChars` (`lib/format.ts`), `spawn` / `open` / `xdg-open` / `explorer` (via the injected launcher in `open.ts`). |
119
131
  | Config | None. |
120
132
 
121
133
  ## Design notes
@@ -123,8 +135,11 @@ JSON, and print modes; a codemode script can call it in any of them. Only the
123
135
  `buildCommand` and `normalizeTarget` are pure and `openTarget` takes an injected
124
136
  launcher, so the command for each platform is asserted without a process and a
125
137
  test launches nothing. The launch is best-effort: `spawn` reports a missing
126
- handler asynchronously, so the failure goes to `onError` (and a notification)
127
- rather than being thrown from a synchronous call the tool could catch.
138
+ launcher asynchronously and the child reports its exit, so both failures go to
139
+ `onError` (and a notification) rather than being thrown from a synchronous call
140
+ the tool could catch. `exitMeansFailure` opts Windows out because `explorer` can
141
+ exit non-zero after a successful open. Windows opens through `explorer` rather
142
+ than `cmd /c start` so cmd cannot re-parse a path containing `%`.
128
143
 
129
144
  The tool is `codemode`-exposed, matching how it was configured before the move:
130
145
  opening a native app is a side effect the user should trigger, so it stays out
@@ -135,7 +150,7 @@ of the model's active tool set unless a script explicitly reaches for it.
135
150
  | Module | Responsibility |
136
151
  |---|---|
137
152
  | `index.ts` | The extension factory: registers `open_path` and the `/open` command, resolves the target, and routes a launch failure to `ctx.ui.notify`. |
138
- | `open.ts` | `normalizeTarget` and `buildCommand` (pure), `resolveTarget` (existence check), and `openTarget` with the injectable launcher. |
153
+ | `open.ts` | `normalizeTarget` and `buildCommand` (pure), `resolveTarget` (existence check), and `openTarget` with the injectable launcher, which reports a start failure and a meaningful non-zero exit. |
139
154
  | `schema.ts` | `TOOL_NAME`, the TypeBox `parameters` / `outputSchema`, and the `OpenPathArgs` / `OpenPathResult` types. |
140
155
  | `open.test.ts` | Pure-module tests for normalization, resolution, per-platform commands, and the launcher seam. |
141
156
  | `extension.test.ts` | Extension-surface tests: registration, disabling, the tool call, and the command. |
@@ -25,8 +25,9 @@ export default function fileOpen(pi: ExtensionAPI, deps: FileOpenDeps = {}): voi
25
25
 
26
26
  /**
27
27
  * Resolve `raw` against the effective root and hand it to the native
28
- * handler. A launch failure is reported through `ctx.ui.notify` because the
29
- * handler runs detached; the target is returned for the caller's message.
28
+ * handler. A start failure, or a non-zero exit where the platform reports
29
+ * one, goes through `ctx.ui.notify` because the handler runs detached; the
30
+ * target is returned for the caller's message.
30
31
  */
31
32
  const launch = (mode: OpenMode, raw: string, ctx: ExtensionContext): string => {
32
33
  const root = resolveEffectiveCwd(ctx.cwd);
@@ -84,7 +85,7 @@ export default function fileOpen(pi: ExtensionAPI, deps: FileOpenDeps = {}): voi
84
85
  const rest = trimmed.replace(/^-r\b\s*/, "").trim();
85
86
  try {
86
87
  const target = launch(reveal ? "reveal" : "open", rest, ctx);
87
- ctx.ui.notify(`${reveal ? "Revealed" : "Opened"} ${target}`, "info");
88
+ ctx.ui.notify(`${reveal ? "Revealing" : "Opening"} ${target}`, "info");
88
89
  } catch (error) {
89
90
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
90
91
  }
@@ -5,7 +5,7 @@
5
5
  * runs is asserted without a process. `openTarget` takes an injectable spawn so
6
6
  * a test launches nothing. Opening is best-effort: the caller decides what to do
7
7
  * with a failure, which is reported through `onError` rather than thrown, since
8
- * `spawn` reports a missing handler asynchronously.
8
+ * the launcher reports a start failure and a non-zero exit asynchronously.
9
9
  */
10
10
 
11
11
  import { spawn } from "node:child_process";
@@ -19,6 +19,11 @@ export type OpenMode = "open" | "reveal";
19
19
  export interface NativeCommand {
20
20
  command: string;
21
21
  args: string[];
22
+ /**
23
+ * Whether a non-zero exit means the target was not opened. Windows
24
+ * `explorer` can exit non-zero after a successful open, so it opts out.
25
+ */
26
+ exitMeansFailure: boolean;
22
27
  }
23
28
 
24
29
  /**
@@ -51,49 +56,75 @@ export function buildCommand(
51
56
  platform: NodeJS.Platform = process.platform,
52
57
  ): NativeCommand {
53
58
  if (platform === "darwin") {
54
- return mode === "reveal" ? { command: "open", args: ["-R", target] } : { command: "open", args: [target] };
59
+ return mode === "reveal"
60
+ ? { command: "open", args: ["-R", target], exitMeansFailure: true }
61
+ : { command: "open", args: [target], exitMeansFailure: true };
55
62
  }
56
63
  if (platform === "win32") {
64
+ // `explorer` opens with the default handler without routing the path
65
+ // through cmd, which would expand `%NAME%` inside it.
57
66
  return mode === "reveal"
58
- ? { command: "explorer", args: [`/select,${target}`] }
59
- : { command: "cmd", args: ["/c", "start", "", target] };
67
+ ? { command: "explorer", args: [`/select,${target}`], exitMeansFailure: false }
68
+ : { command: "explorer", args: [target], exitMeansFailure: false };
60
69
  }
61
70
  // Linux et al. xdg-open has no "select" verb, so reveal opens the parent.
62
- return { command: "xdg-open", args: [mode === "reveal" ? dirname(target) : target] };
71
+ return {
72
+ command: "xdg-open",
73
+ args: [mode === "reveal" ? dirname(target) : target],
74
+ exitMeansFailure: true,
75
+ };
76
+ }
77
+
78
+ /** The child surface `openTarget` listens on; an injected launcher may return none. */
79
+ export interface LauncherChild {
80
+ on(event: "error", listener: (error: Error) => void): unknown;
81
+ on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
63
82
  }
64
83
 
65
84
  /** A launcher seam; the default spawns the command detached. */
66
- export type SpawnFn = (command: string, args: string[]) => unknown;
85
+ export type SpawnFn = (command: string, args: string[]) => LauncherChild | undefined;
67
86
 
68
87
  export interface OpenTargetOptions {
69
88
  /** Platform to build the command for; defaults to the running one. */
70
89
  platform?: NodeJS.Platform;
71
90
  /** Override the launcher (tests). */
72
91
  spawn?: SpawnFn;
73
- /** Called when the handler cannot be started or reports an error. */
92
+ /** Called once when the handler cannot start, or exits non-zero where that is meaningful. */
74
93
  onError?: (error: Error) => void;
75
94
  }
76
95
 
96
+ /** Spawn the handler detached so the application outlives the session. */
97
+ const spawnDetached: SpawnFn = (command, args) => {
98
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
99
+ child.unref();
100
+ return child;
101
+ };
102
+
77
103
  /** Start the native handler for `target`, detached so it outlives the session. */
78
104
  export function openTarget(mode: OpenMode, target: string, options: OpenTargetOptions = {}): void {
79
- const { command, args } = buildCommand(mode, target, options.platform);
80
- const onError = options.onError;
105
+ const { command, args, exitMeansFailure } = buildCommand(mode, target, options.platform);
106
+ const launch = options.spawn ?? spawnDetached;
81
107
 
82
- if (options.spawn) {
83
- try {
84
- options.spawn(command, args);
85
- } catch (error) {
86
- onError?.(toError(error));
87
- }
88
- return;
89
- }
108
+ // A start failure and an exit both arrive asynchronously; report at most
109
+ // one, since both can fire for the same launch.
110
+ let reported = false;
111
+ const report = (error: Error): void => {
112
+ if (reported) return;
113
+ reported = true;
114
+ options.onError?.(error);
115
+ };
90
116
 
91
117
  try {
92
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
93
- child.on("error", (error) => onError?.(error));
94
- child.unref();
118
+ const child = launch(command, args);
119
+ child?.on("error", report);
120
+ if (child && exitMeansFailure) {
121
+ child.on("exit", (code, signal) => {
122
+ if (signal) report(new Error(`the OS handler was stopped by ${signal}`));
123
+ else if (code !== 0) report(new Error(`the OS handler exited with status ${code}`));
124
+ });
125
+ }
95
126
  } catch (error) {
96
- onError?.(toError(error));
127
+ report(toError(error));
97
128
  }
98
129
  }
99
130
 
@@ -21,7 +21,11 @@ How the `goal` call and its result look in the transcript — the call line as a
21
21
  objective. Send the complete objective every call; an empty objective clears
22
22
  it.
23
23
  - Tracks `status`: `active` while the goal is being pursued, `achieved` when it
24
- is done.
24
+ is done. Achieving it while the branch's todo list still has open items names
25
+ the count in the result (`1 todo item is still open…`), so the model closes the
26
+ *how* instead of declaring the *what* done over an unfinished list. The count
27
+ is read from the branch through `lib/todo-state.ts`; the structured result is
28
+ unchanged.
25
29
  - Shows a persistent one-line rail above the editor whenever a goal exists,
26
30
  label-first to match the [`todo`](../todo/) widget, pinned at the top of the
27
31
  `Goal / Todos` stack when both are loaded:
@@ -44,8 +48,9 @@ How the `goal` call and its result look in the transcript — the call line as a
44
48
  - Understands the goal before recording it: `/goal <prompt>` asks the session
45
49
  model to restate the goal as it understands it and, when it cannot yet, to ask
46
50
  up to three questions — which the command asks directly through host dialogs
47
- (`select` / `input`). The model then folds the answers into the final
48
- restatement, and the user confirms or corrects it in the editor. `/goal set
51
+ (`select` / `input`). The answers are carried into the editor under the
52
+ restatement as `Also: <question> → <answer>` notes, and the user confirms,
53
+ folds them in, or corrects the goal there. `/goal set
49
54
  <prompt>` reuses the loop but records without starting a turn, `/goal edit`
50
55
  changes the current goal in place, and `/goal raw <prompt>` records the typed
51
56
  words verbatim. The loop is best-effort and needs a dialog UI plus a model; the
@@ -79,8 +84,19 @@ line: control characters (including `ESC`) become spaces and any whitespace run
79
84
  (newlines, tabs, repeated spaces) collapses to one space. This keeps the widget
80
85
  to one logical line and stops model text from injecting terminal sequences.
81
86
 
87
+ A successful `status: "achieved"` call also appends one line to the model-facing
88
+ text when the branch's todo list still has open items — `3 todo items are still
89
+ open; close or clear the list if that work is done.` The list is replayed from
90
+ the branch, so the note appears whether the todos were written by the model, by
91
+ `/todos`, or by an approved plan. It is text only: `details`,
92
+ `structuredContent`, and the transcript header are unaffected.
93
+
82
94
  `goal` is allowlisted in `plan-mode` (alongside `todo`), so the model can record
83
95
  or update the objective during read-only exploration without leaving plan mode.
96
+ Plan mode also reads the goal: it restates an active one in its injected context,
97
+ and when a plan is approved with no goal at all it offers to record the plan's
98
+ title as the goal (`lib/goal-state.ts` is the reader). An existing goal — active
99
+ or achieved — is never replaced by that offer.
84
100
 
85
101
  ### `/goal` command
86
102
 
@@ -90,9 +106,9 @@ entry; `reconstructGoal` replays both entry kinds in branch order.
90
106
  | Command | What it does |
91
107
  |---|---|
92
108
  | `/goal` | Show the current goal, or the command usage when none is set. |
93
- | `/goal <prompt>` | Run the understanding loop — restate, ask any questions the model needs, re-comprehend, then confirm/edit — and start the run. Replacing a live goal is confirmed first. |
109
+ | `/goal <prompt>` | Run the understanding loop — restate, ask any questions the model needs, then confirm/edit in the editor, which carries the answers as notes — and start the run. Replacing a live goal is confirmed first. |
94
110
  | `/goal raw <prompt>` | Skip the loop: record the typed words verbatim and start the run. |
95
- | `/goal set <prompt>` (or `/goal edit <prompt>`) | The same loop, but record the goal without starting a run. |
111
+ | `/goal set <prompt>` (or `/goal edit <prompt>`) | The same loop, but record the goal without starting a run. An explicit `set`/`edit <text>` replaces an active goal without asking, since the command already says so. |
96
112
  | `/goal set` (or `/goal edit`) | Open the editor prefilled with the current goal and save it as the active goal, without starting a run. |
97
113
  | `/goal done` (or `/goal achieved`) | Mark the current goal achieved (idempotent). |
98
114
  | `/goal clear` | Clear the goal. |
@@ -113,7 +129,7 @@ without one they show the usage instead. `/goal raw` never needs one.
113
129
 
114
130
  The tool works in every mode. The persistent widget requires interactive
115
131
  (`tui`) mode; the reminder and the `/goal` command work everywhere. The
116
- `/goal <prompt>` runs the loop: each model call uses a cancellable `tui` spinner
132
+ `/goal <prompt>` runs the loop: its model call uses a cancellable `tui` spinner
117
133
  in interactive mode and runs directly in RPC; the questions and the confirm step
118
134
  are host dialogs, forwarded by RPC. In print and JSON mode the loop is skipped
119
135
  and the typed text is sent. The widget is always a single line; the transcript
@@ -166,8 +182,8 @@ depends on config.
166
182
  | State storage | tool-result `details` plus a `pi.appendEntry("goal", { goal })` custom entry for command-set goals |
167
183
  | Lifecycle hooks | `session_start` (load config, reconstruct), `session_tree` (reconstruct), `session_shutdown` (clear widget); `before_agent_start` (inject `goal-context`), `context` (filter stale/duplicate reminders) |
168
184
  | UI | `ctx.ui.setWidget` one-line rail in `tui` mode; coordinated through `pi.events` so it stays the top rail |
169
- | Model calls | Up to two `ctx.modelRegistry.complete` calls on the session model (comprehend, then re-comprehend), each with a 30 s timeout and `cacheRetention: "none"`; skipped without a dialog UI or a model; their usage is not counted in session totals |
170
- | Questions | When the goal is not yet understood, the command asks the model's questions through `ctx.ui.select` / `ctx.ui.input` and passes the answers to the re-comprehension call. No cross-extension tool is used. |
185
+ | Model calls | One `ctx.modelRegistry.complete` call on the session model with a 30 s timeout and `cacheRetention: "none"`; skipped without a dialog UI or a model; its usage is not counted in session totals |
186
+ | Questions | When the goal is not yet understood, the command asks the model's questions through `ctx.ui.select` / `ctx.ui.input` and carries the answers into the editor prefill. No cross-extension tool is used. |
171
187
 
172
188
  ## Design notes
173
189
 
@@ -181,8 +197,10 @@ depends on config.
181
197
  user-driven path: one comprehension call (`clarify-prompt.ts` + `clarify.ts`)
182
198
  returns `{ understood, goal, questions }`. An understood goal opens a prefilled
183
199
  `ctx.ui.editor`, then `pi.sendUserMessage` sets and runs it. A not-understood
184
- goal has its questions asked through host dialogs and its answers folded into
185
- the goal by a second model call, then confirmed in the editor. `/goal set`
200
+ goal has its questions asked through host dialogs, and its answers ride into
201
+ the editor as `Also:` notes under the restatement: no second model call
202
+ rewrites the goal, so the user is the one who folds them into the sentence.
203
+ `/goal set`
186
204
  reuses the loop but skips the send; a bare `/goal set` or `/goal edit` opens
187
205
  the editor prefilled with the current goal; and `/goal raw` skips the loop
188
206
  entirely. The loop is best-effort (a failure keeps the typed text) and a
@@ -214,6 +232,20 @@ depends on config.
214
232
  the status header alone (`Goal · active`), because the call line above it
215
233
  already carries the objective; it does not restate it. `achieved: "show"`
216
234
  restores the dimmed widget line; config only affects presentation.
235
+ - **Completing the *what* checks the *how*.** Marking a goal achieved while the
236
+ todo list still holds open items appends one line to the model-facing result
237
+ naming the count. The list is replayed from the branch by
238
+ `lib/todo-state.ts` (shared with plan's approval-time merge, which reads the
239
+ same channel), never by importing the `todo` extension. The same reasoning
240
+ applies to the goal itself: `lib/goal-state.ts` owns the entry type and the
241
+ objective limit, and plan reads the published goal through it. Only the text is
242
+ affected: `details` and `structuredContent` stay the goal alone, and the
243
+ transcript result is still the status header.
244
+ - **One verb table.** `GOAL_VERBS` is the only list of reserved words: the
245
+ parser looks a first word up in it, the completions are generated from it, and
246
+ a test asserts every word is offered as a completion and named in `GOAL_HELP`.
247
+ Adding a verb without documenting or completing it fails that test instead of
248
+ drifting silently.
217
249
  - **Sanitize at the boundary.** `normalizeGoal` and `parseClarification` both route
218
250
  model text through `lib/format.ts`'s `sanitizeLine`, so an embedded newline
219
251
  cannot corrupt the widget and raw escapes cannot restyle the terminal.
@@ -225,16 +257,16 @@ depends on config.
225
257
  | `index.ts` | Factory: wire the runtime, tool, command, reminder, and session events. |
226
258
  | `types.ts` | `Goal`, `GoalStatus`, `GoalDetails`. |
227
259
  | `schema.ts` | TypeBox parameters and pure validation/normalization. |
228
- | `clarify-prompt.ts` | Comprehension and re-comprehension prompts plus their message builders (pure). |
260
+ | `clarify-prompt.ts` | The comprehension prompt and its message builder (pure). |
229
261
  | `clarify-parse.ts` | Parse the `{ understood, goal, questions }` reply (and the plain-text fallback), normalize questions, and expose the limits (pure). |
230
- | `clarify.ts` | The two model calls, their cancellable loaders, the in-command questionnaire, and the confirm/edit dialog (host). |
262
+ | `clarify.ts` | The model call, its cancellable loader, the in-command questionnaire, and the confirm/edit dialog (host). |
231
263
  | `state.ts` | Branch reconstruction (pure). |
232
264
  | `format.ts` | Model-facing and terminal text plus the header and rail vocabulary (pure). |
233
265
  | `config.ts` | Widget config load/validation (the `goal` section of `bytefly.json`). |
234
266
  | `tui.ts` | The persistent goal widget and the transcript status header. |
235
267
  | `runtime.ts` | Session-scoped state and widget synchronization. |
236
268
  | `tools.ts` | `goal` tool registration and rendering. |
237
- | `commands.ts` | `/goal`. |
269
+ | `commands.ts` | `/goal`: the `GOAL_VERBS` table, the argument parser, the completions, and the command handler. |
238
270
 
239
271
  ## Testing
240
272
 
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * Prompt text for the goal comprehension step (pure).
3
3
  *
4
- * Both system prompts are sent only on the nested clarifier calls, so they are
5
- * not part of Pi's always-on prompt budget. The model's job is to understand and
6
- * restate the user's goal — not to improve it — and, when it cannot yet, to ask
7
- * only what it needs from the user. The second call folds the answers into the
8
- * final restatement.
4
+ * Sent only on the nested clarifier call, so it is not part of Pi's always-on
5
+ * prompt budget. The model's job is to understand and restate the user's goal —
6
+ * not to improve it — and, when it cannot yet, to ask only what it needs from
7
+ * the user.
9
8
  */
10
9
 
11
10
  import type { UserMessage } from "@earendil-works/pi-ai";
12
- import { MAX_QUESTIONS, type ClarifyAnswer } from "./clarify-parse.ts";
11
+ import { MAX_QUESTIONS } from "./clarify-parse.ts";
13
12
 
14
13
  /** The comprehension system prompt: understand the goal, ask only what is missing. */
15
14
  export const CLARIFY_SYSTEM_PROMPT = `You are establishing a shared understanding of the user's goal for this session.
@@ -33,12 +32,6 @@ Rules:
33
32
  - Ask only what only the user can answer: purpose, scope, preference, or priority. Never ask for a fact the work itself would reveal, and never ask the user to restate the goal.
34
33
  - Give a question 2-4 concrete options when the answers are a small known set, or no options at all when free-form.`;
35
34
 
36
- /** The re-comprehension system prompt: fold the answers into one goal statement. */
37
- export const REFINE_SYSTEM_PROMPT = `The user has answered questions about their goal. Restate the goal as you now understand it.
38
-
39
- Reply with ONLY the goal as one sentence, no prose and no code fence.
40
- Use their answers and the original request; add nothing they did not state.`;
41
-
42
35
  /** The user message sent to the clarifier alongside {@link CLARIFY_SYSTEM_PROMPT}. */
43
36
  export function buildClarifyUserText(text: string): string {
44
37
  return `Understand this session goal:\n\n${text}`;
@@ -54,27 +47,3 @@ export function buildClarifyMessages(
54
47
  messages: [{ role: "user", content: [{ type: "text", text: buildClarifyUserText(text) }], timestamp: now }],
55
48
  };
56
49
  }
57
-
58
- /** The user message that carries the original request, the first restatement, and the answers. */
59
- export function buildRefineUserText(original: string, goal: string, answers: readonly ClarifyAnswer[]): string {
60
- const lines = [`Original request:\n${original}`];
61
- if (goal) lines.push(`First restatement: ${goal}`);
62
- lines.push("The user was asked, and answered:");
63
- for (const item of answers) lines.push(`- ${item.question}\n ${item.answer}`);
64
- return lines.join("\n");
65
- }
66
-
67
- /** The request sent to the clarifier after the user answers. */
68
- export function buildRefineMessages(
69
- original: string,
70
- goal: string,
71
- answers: readonly ClarifyAnswer[],
72
- now = Date.now(),
73
- ): { systemPrompt: string; messages: UserMessage[] } {
74
- return {
75
- systemPrompt: REFINE_SYSTEM_PROMPT,
76
- messages: [
77
- { role: "user", content: [{ type: "text", text: buildRefineUserText(original, goal, answers) }], timestamp: now },
78
- ],
79
- };
80
- }