@selesai/code 0.8.5 → 0.8.7

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 (231) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.7] - 2026-08-17
8
+
9
+ ### Added
10
+ - **Pi Subagents updated through upstream 0.50.0.** The bundled extension advances through upstream 0.49/0.50 (single-child `{ agent, task }` runs, per-tool-call `toolTimeoutMs` with config/env precedence, `debug.run` inspection, builtin role overrides with `tools: "inherit"`, resumable retained-child listings, indexed async result inboxes) with the Selesai fork layers on top: branding, `.selesai` config routing, `SELESAI_SUBAGENT*` env vars, declarative `chain`/`tasks`/`clarify` modes, workflow auto-relaunch wiring, and same-model retry for transient provider errors.
11
+ - **`model-prompt-injector` extension.** Bundled and registered in the extensions manifest, it injects model-specific prompts into the system prompt when the active model matches a configured rule (provider/model globs, bare id/name patterns, first-match-wins, enabled toggle). Modes: `prepend`, `append`, or `replace`.
12
+ - **Wire-level tool schema pruning.** `pruneToolDescriptions` (default `true`) strips every per-field `description` from tool input schemas before they reach the model, keeping one-line tool descriptions. Stripped schemas are memoized and never mutated; UI/docs definitions keep full descriptions; `keepParameterDescriptions` opts a tool out.
13
+ - **Compact subagent tool description by default.** `toolDescriptionMode` now defaults to `"compact"` (~3.2 KB vs ~6.3 KB per model turn), and a missing/invalid custom description file falls back to compact.
14
+
15
+ ### Changed
16
+ - **Slimmer default system prompt.** The always-on Pi-docs block shrank from 8 lines to 2 (paths only); docs are still read on demand when the user asks about pi.
17
+ - **Bundled defaults** now set `pruneToolDescriptions: true`.
18
+ - **Test coverage.** `src/extensions` (pi-* excluded) now enforces 100% stmt/branch/func/line coverage via vitest thresholds; `safeMaxTokens()` clamps missing/oversized context-window metadata to a sane default.
19
+
20
+ ### Fixed
21
+ - **Subagent prompt-runtime suite passes inside subagent child environments.** The watchdog-lifecycle test now scrubs the steer env vars (`SELESAI_SUBAGENT_STEER_*`) before registering, so the steering inbox's extra `agent_end` handler cannot skew the handler count.
22
+
23
+ ## [0.8.6] - 2026-08-15
24
+
25
+ ### Added
26
+ - **Auto-relaunching workflows.** The four `/workflow-*` modes (task, prototype, quicktype, loop) now run fully unattended: when a scripted workflow exhausts its per-run fan-out budget before the goal is clean, the async executor mints a fresh budget and re-runs the same script with the same mission and progress file, up to `config.maxWorkflowAutoRelaunches` (default 12; `0` = unlimited). Only `budget` results relaunch — real child failures still surface immediately. `AsyncStatus` gained `workflowRelaunchCount`.
27
+ - **Packaged prompts now load.** The pi-subagents extension registers `resources_discover`, so the bundled prompts (`review-loop.md`, `parallel-*.md`, `gather-context-and-clarify.md`) are actually discoverable. The `workflow-drive.md` prompt was removed as superseded by auto-relaunch.
28
+ - **Goal visibility on workflow launches.** Each `/workflow-*` launch emits its goal at start (visible in `subagent status` workflow emits) and names the auto-created mission after the goal (`mission.list` / `mission.show`).
29
+
30
+ ### Changed
31
+ - **Bounded build→review→fix rounds.** The workflow auto-loop now instructs the builder to implement one small, self-contained slice per round ("do not attempt the whole plan") instead of the entire plan at once, with per-run timeouts (build/fix 45m, review 15m).
32
+ - **Progress-file scoped reviews.** Builder appends a `## Round N` ledger entry (files, summary, validation) to `.pi-subagents/progress/<mode>.md`; the commentator scopes its review to the latest round entry plus the immediately preceding fix entry, falling back to the full uncommitted diff when the file is missing or empty. Blocking reviews must list a `Remaining work:` section; the next build round picks up from it.
33
+ - **Previous review feedback now reaches the next builder round** (previously it was dropped).
34
+
7
35
  ## [0.8.5] - 2026-08-14
8
36
 
9
37
  ### Added
@@ -35,7 +35,7 @@ import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-man
35
35
  import { createSyntheticSourceInfo } from "./source-info.js";
36
36
  import { buildSystemPrompt } from "./system-prompt.js";
37
37
  import { createLocalBashOperations } from "./tools/bash.js";
38
- import { createAllToolDefinitions } from "./tools/index.js";
38
+ import { createAllToolDefinitions, stripToolParameterDescriptions } from "./tools/index.js";
39
39
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
40
40
  import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
41
41
  /** Parse consecutive skill blocks at the start of a user message. */
@@ -2056,6 +2056,18 @@ export class AgentSession {
2056
2056
  toolRegistry.set(tool.name, tool);
2057
2057
  }
2058
2058
  this._toolRegistry = toolRegistry;
2059
+ if (this.settingsManager.getPruneToolDescriptions()) {
2060
+ const keepDescriptions = new Set();
2061
+ for (const [name, entry] of definitionRegistry) {
2062
+ if (entry.definition.keepParameterDescriptions)
2063
+ keepDescriptions.add(name);
2064
+ }
2065
+ for (const [name, tool] of toolRegistry) {
2066
+ if (!keepDescriptions.has(name)) {
2067
+ toolRegistry.set(name, stripToolParameterDescriptions(tool));
2068
+ }
2069
+ }
2070
+ }
2059
2071
  const nextActiveToolNames = (options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]).filter((name) => isAllowedTool(name));
2060
2072
  if (allowedToolNames) {
2061
2073
  for (const toolName of this._toolRegistry.keys()) {
@@ -353,6 +353,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
353
353
  promptGuidelines?: string[];
354
354
  /** Parameter schema (TypeBox) */
355
355
  parameters: TParams;
356
+ /** When global pruneToolDescriptions is enabled, keep this tool's per-field parameter descriptions on the wire (already pruned by the tool itself). */
357
+ keepParameterDescriptions?: boolean;
356
358
  /** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
357
359
  constrainedSampling?: false | ConstrainedSamplingConfig;
358
360
  /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
@@ -2,6 +2,17 @@ import { lazyStream, } from "@earendil-works/pi-ai";
2
2
  import { getApiProvider } from "@earendil-works/pi-ai/compat";
3
3
  import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js";
4
4
  export const clearApiKeyCache = clearConfigValueCache;
5
+ /** Sane default when a model definition omits maxTokens (kept small so providers never reject it). */
6
+ const DEFAULT_MAX_TOKENS = 16384;
7
+ /**
8
+ * Resolve a safe max output token count. Missing or non-positive values fall back
9
+ * (to a sane default or an inherited value); explicit values are never allowed to
10
+ * exceed the model's context window.
11
+ */
12
+ function safeMaxTokens(value, contextWindow, fallback = DEFAULT_MAX_TOKENS) {
13
+ const resolved = typeof value === "number" && value > 0 ? value : fallback;
14
+ return Math.min(resolved, Math.max(contextWindow, 1));
15
+ }
5
16
  function mergeCompat(base, override) {
6
17
  if (!override)
7
18
  return base;
@@ -20,6 +31,7 @@ function mergeCompat(base, override) {
20
31
  return merged;
21
32
  }
22
33
  function applyModelOverride(model, override) {
34
+ const contextWindow = override.contextWindow ?? model.contextWindow;
23
35
  return {
24
36
  ...model,
25
37
  name: override.name ?? model.name,
@@ -37,8 +49,8 @@ function applyModelOverride(model, override) {
37
49
  tiers: override.cost.tiers ?? model.cost.tiers,
38
50
  }
39
51
  : model.cost,
40
- contextWindow: override.contextWindow ?? model.contextWindow,
41
- maxTokens: override.maxTokens ?? model.maxTokens,
52
+ contextWindow,
53
+ maxTokens: safeMaxTokens(override.maxTokens ?? model.maxTokens, contextWindow, model.maxTokens),
42
54
  compat: mergeCompat(model.compat, override.compat),
43
55
  };
44
56
  }
@@ -56,6 +68,7 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
56
68
  if (definition.maxTokens !== undefined && definition.maxTokens <= 0) {
57
69
  throw new Error(`Provider ${providerId}, model ${definition.id}: invalid maxTokens`);
58
70
  }
71
+ const contextWindow = definition.contextWindow ?? 128000;
59
72
  return {
60
73
  id: definition.id,
61
74
  name: definition.name ?? definition.id,
@@ -66,8 +79,8 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
66
79
  thinkingLevelMap: definition.thinkingLevelMap,
67
80
  input: (definition.input ?? ["text"]),
68
81
  cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
69
- contextWindow: definition.contextWindow ?? 128000,
70
- maxTokens: definition.maxTokens ?? 16384,
82
+ contextWindow,
83
+ maxTokens: safeMaxTokens(definition.maxTokens, contextWindow),
71
84
  headers: undefined,
72
85
  compat: mergeCompat(providerConfig.compat, definition.compat),
73
86
  };
@@ -77,6 +77,7 @@ export interface Settings {
77
77
  branchSummary?: BranchSummarySettings;
78
78
  retry?: RetrySettings;
79
79
  hideThinkingBlock?: boolean;
80
+ pruneToolDescriptions?: boolean;
80
81
  showCacheMissNotices?: boolean;
81
82
  externalEditor?: string;
82
83
  shellPath?: string;
@@ -236,6 +237,7 @@ export declare class SettingsManager {
236
237
  };
237
238
  getWebSocketConnectTimeoutMs(): number | undefined;
238
239
  getHideThinkingBlock(): boolean;
240
+ getPruneToolDescriptions(): boolean;
239
241
  getShowCacheMissNotices(): boolean;
240
242
  getExternalEditorCommand(): string;
241
243
  setHideThinkingBlock(hide: boolean): void;
@@ -598,6 +598,9 @@ export class SettingsManager {
598
598
  getHideThinkingBlock() {
599
599
  return this.settings.hideThinkingBlock ?? false;
600
600
  }
601
+ getPruneToolDescriptions() {
602
+ return this.settings.pruneToolDescriptions ?? true;
603
+ }
601
604
  getShowCacheMissNotices() {
602
605
  return this.settings.showCacheMissNotices ?? false;
603
606
  }
@@ -103,14 +103,8 @@ In addition to the tools above, you may have access to other custom tools depend
103
103
  Guidelines:
104
104
  ${guidelines}
105
105
 
106
- Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
107
- - Main documentation: ${readmePath}
108
- - Additional docs: ${docsPath}
109
- - Examples: ${examplesPath} (extensions, custom tools, SDK)
110
- - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
111
- - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
112
- - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
113
- - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
106
+ Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): main docs ${readmePath}, additional docs ${docsPath}, examples ${examplesPath}.
107
+ When working on pi topics, resolve docs/... and examples/... under those paths (not cwd), read the relevant .md files completely, and follow cross-references before implementing.`;
114
108
  if (appendSection) {
115
109
  prompt += appendSection;
116
110
  }
@@ -12,6 +12,12 @@ describe("buildSystemPrompt", () => {
12
12
  expect(prompt).toMatch(/Guidelines:/);
13
13
  expect(prompt).toContain("Current working directory: /tmp/project");
14
14
  });
15
+ it("keeps the collapsed Pi-docs block without the per-topic filename map", () => {
16
+ const prompt = buildSystemPrompt({ cwd: "/tmp/project" });
17
+ expect(prompt).toContain("Pi documentation");
18
+ expect(prompt).not.toContain("When asked about:");
19
+ expect(prompt).not.toContain("(docs/extensions.md");
20
+ });
15
21
  it("keeps the default tool snippets, skills, and agents XML read-gated", () => {
16
22
  const skill = {
17
23
  name: "test-skill",
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, type FindOperations, type Fin
5
5
  export { createGrepTool, createGrepToolDefinition, type GrepOperations, type GrepToolDetails, type GrepToolInput, type GrepToolOptions, } from "./grep.ts";
6
6
  export { createLsTool, createLsToolDefinition, type LsOperations, type LsToolDetails, type LsToolInput, type LsToolOptions, } from "./ls.ts";
7
7
  export { createReadTool, createReadToolDefinition, type ReadOperations, type ReadToolDetails, type ReadToolInput, type ReadToolOptions, } from "./read.ts";
8
+ export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.ts";
8
9
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationOptions, type TruncationResult, truncateHead, truncateLine, truncateTail, } from "./truncate.ts";
9
10
  export { createWriteTool, createWriteToolDefinition, type WriteOperations, type WriteToolInput, type WriteToolOptions, } from "./write.ts";
10
11
  import type { AgentTool } from "@earendil-works/pi-agent-core";
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, } from "./find.js";
5
5
  export { createGrepTool, createGrepToolDefinition, } from "./grep.js";
6
6
  export { createLsTool, createLsToolDefinition, } from "./ls.js";
7
7
  export { createReadTool, createReadToolDefinition, } from "./read.js";
8
+ export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.js";
8
9
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, } from "./truncate.js";
9
10
  export { createWriteTool, createWriteToolDefinition, } from "./write.js";
10
11
  import { createBashTool, createBashToolDefinition } from "./bash.js";
@@ -0,0 +1,16 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { TSchema } from "typebox";
3
+ /**
4
+ * Structural deep clone that omits every `description` key at any depth while
5
+ * preserving all other own keys (including TypeBox internals such as `~kind`
6
+ * and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
7
+ * property descriptors so non-enumerable metadata survives; never mutates the
8
+ * input.
9
+ */
10
+ export declare function stripSchemaDescriptions<T>(value: T): T;
11
+ /**
12
+ * Returns a shallow copy of the tool with its parameter schema descriptions
13
+ * stripped. Top-level tool fields (name, description, execute, ...) are
14
+ * untouched.
15
+ */
16
+ export declare function stripToolParameterDescriptions<T extends TSchema>(tool: AgentTool<T>): AgentTool<T>;
@@ -0,0 +1,39 @@
1
+ // Memoized stripped clones, keyed by the original schema root. The input is
2
+ // never mutated; repeat calls return the same cached clone.
3
+ const strippedSchemaCache = new WeakMap();
4
+ /**
5
+ * Structural deep clone that omits every `description` key at any depth while
6
+ * preserving all other own keys (including TypeBox internals such as `~kind`
7
+ * and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
8
+ * property descriptors so non-enumerable metadata survives; never mutates the
9
+ * input.
10
+ */
11
+ export function stripSchemaDescriptions(value) {
12
+ if (!value || typeof value !== "object")
13
+ return value;
14
+ const cached = strippedSchemaCache.get(value);
15
+ if (cached !== undefined)
16
+ return cached;
17
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
18
+ for (const key of Reflect.ownKeys(value)) {
19
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
20
+ if (!descriptor)
21
+ continue;
22
+ if (key === "description")
23
+ continue;
24
+ if ("value" in descriptor) {
25
+ descriptor.value = stripSchemaDescriptions(descriptor.value);
26
+ }
27
+ Object.defineProperty(result, key, descriptor);
28
+ }
29
+ strippedSchemaCache.set(value, result);
30
+ return result;
31
+ }
32
+ /**
33
+ * Returns a shallow copy of the tool with its parameter schema descriptions
34
+ * stripped. Top-level tool fields (name, description, execute, ...) are
35
+ * untouched.
36
+ */
37
+ export function stripToolParameterDescriptions(tool) {
38
+ return { ...tool, parameters: stripSchemaDescriptions(tool.parameters) };
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Type } from "typebox";
3
+ import { Value } from "typebox/value";
4
+ import { SettingsManager } from "../settings-manager.js";
5
+ import { stripSchemaDescriptions, stripToolParameterDescriptions } from "./schema-prune.js";
6
+ describe("stripSchemaDescriptions", () => {
7
+ it("removes description keys at all depths (properties, anyOf arrays, items)", () => {
8
+ const schema = {
9
+ type: "object",
10
+ description: "root",
11
+ properties: {
12
+ name: { type: "string", description: "the name" },
13
+ tags: {
14
+ type: "array",
15
+ description: "the tags",
16
+ items: { type: "string", description: "a tag" },
17
+ },
18
+ choice: {
19
+ anyOf: [{ type: "string", description: "string branch" }, { type: "number" }],
20
+ },
21
+ },
22
+ };
23
+ const stripped = stripSchemaDescriptions(schema);
24
+ expect("description" in stripped).toBe(false);
25
+ expect("description" in stripped.properties).toBe(false);
26
+ expect("description" in stripped.properties.tags).toBe(false);
27
+ expect("description" in stripped.properties.tags.items).toBe(false);
28
+ expect("description" in stripped.properties.choice.anyOf[0]).toBe(false);
29
+ });
30
+ it("preserves non-description keys such as type, enum, required, minimum, additionalProperties", () => {
31
+ const schema = {
32
+ type: "object",
33
+ properties: {
34
+ level: { type: "string", enum: ["a", "b"], description: "level desc" },
35
+ count: { type: "integer", minimum: 1, description: "count desc" },
36
+ },
37
+ required: ["level"],
38
+ additionalProperties: false,
39
+ };
40
+ const stripped = stripSchemaDescriptions(schema);
41
+ expect(stripped).toEqual({
42
+ type: "object",
43
+ properties: {
44
+ level: { type: "string", enum: ["a", "b"] },
45
+ count: { type: "integer", minimum: 1 },
46
+ },
47
+ required: ["level"],
48
+ additionalProperties: false,
49
+ });
50
+ });
51
+ it("never mutates the input schema", () => {
52
+ const schema = {
53
+ type: "object",
54
+ properties: { name: { type: "string", description: "the name" } },
55
+ };
56
+ const snapshot = structuredClone(schema);
57
+ stripSchemaDescriptions(schema);
58
+ expect(schema).toEqual(snapshot);
59
+ expect(schema.properties.name.description).toBe("the name");
60
+ });
61
+ it("memoizes stripped clones per original root", () => {
62
+ const schema = {
63
+ type: "object",
64
+ properties: { name: { type: "string", description: "the name" } },
65
+ };
66
+ const first = stripSchemaDescriptions(schema);
67
+ const second = stripSchemaDescriptions(schema);
68
+ expect(first).toBe(second);
69
+ });
70
+ it("keeps real TypeBox schemas valid after stripping (symbols survive)", () => {
71
+ const schema = Type.Object({
72
+ name: Type.String({ description: "the name" }),
73
+ age: Type.Optional(Type.Integer({ minimum: 0, description: "the age" })),
74
+ });
75
+ const stripped = stripSchemaDescriptions(schema);
76
+ expect(Value.Check(schema, { name: "pi", age: 3 })).toBe(true);
77
+ expect(Value.Check(stripped, { name: "pi", age: 3 })).toBe(true);
78
+ expect(Value.Check(stripped, { name: "pi", age: -1 })).toBe(false);
79
+ });
80
+ });
81
+ describe("stripToolParameterDescriptions", () => {
82
+ it("keeps the tool description and name while stripping parameter descriptions", () => {
83
+ const tool = {
84
+ name: "read",
85
+ label: "Read",
86
+ description: "Read a file",
87
+ parameters: {
88
+ type: "object",
89
+ properties: { path: { type: "string", description: "file path" } },
90
+ },
91
+ execute: () => Promise.resolve({ content: [], details: {} }),
92
+ };
93
+ const stripped = stripToolParameterDescriptions(tool);
94
+ expect(stripped).not.toBe(tool);
95
+ expect(stripped.name).toBe("read");
96
+ expect(stripped.description).toBe("Read a file");
97
+ expect("description" in stripped.parameters.properties.path).toBe(false);
98
+ // Original tool is untouched.
99
+ expect("description" in tool.parameters.properties.path).toBe(true);
100
+ });
101
+ });
102
+ describe("pruneToolDescriptions setting", () => {
103
+ it("defaults to true", () => {
104
+ const manager = SettingsManager.inMemory();
105
+ expect(manager.getPruneToolDescriptions()).toBe(true);
106
+ });
107
+ it("respects explicit true and false", () => {
108
+ expect(SettingsManager.inMemory({ pruneToolDescriptions: true }).getPruneToolDescriptions()).toBe(true);
109
+ expect(SettingsManager.inMemory({ pruneToolDescriptions: false }).getPruneToolDescriptions()).toBe(false);
110
+ });
111
+ });
@@ -16,6 +16,7 @@
16
16
  "reasoning": true,
17
17
  "input": ["text"],
18
18
  "contextWindow": 512000,
19
+ "maxTokens": 64000,
19
20
  "thinkingLevelMap": {
20
21
  "minimal": null,
21
22
  "low": null,
@@ -36,6 +37,7 @@
36
37
  "reasoning": true,
37
38
  "input": ["text"],
38
39
  "contextWindow": 512000,
40
+ "maxTokens": 64000,
39
41
  "thinkingLevelMap": {
40
42
  "minimal": null,
41
43
  "low": null,
@@ -56,6 +58,7 @@
56
58
  "reasoning": true,
57
59
  "input": ["text", "image"],
58
60
  "contextWindow": 512000,
61
+ "maxTokens": 64000,
59
62
  "thinkingLevelMap": {
60
63
  "minimal": null,
61
64
  "low": "low",
@@ -75,6 +78,7 @@
75
78
  "reasoning": true,
76
79
  "input": ["text", "image"],
77
80
  "contextWindow": 262128,
81
+ "maxTokens": 64000,
78
82
  "thinkingLevelMap": {
79
83
  "minimal": null,
80
84
  "low": "low",
@@ -94,6 +98,7 @@
94
98
  "reasoning": true,
95
99
  "input": ["text"],
96
100
  "contextWindow": 1000000,
101
+ "maxTokens": 64000,
97
102
  "thinkingLevelMap": {
98
103
  "minimal": null,
99
104
  "low": "low",
@@ -25,6 +25,7 @@
25
25
  "git:github.com/MasuRii/pi-tool-display",
26
26
  "git:github.com/omaclaren/pi-markdown-preview"
27
27
  ],
28
+ "pruneToolDescriptions": true,
28
29
  "quietStartup": true,
29
30
  "retry": {
30
31
  "enabled": true,
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
2
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import agentBrowserSetupExtension, {
6
6
  DECLINED_MARKER_NAME,
7
7
  ensureAgentBrowser,
@@ -192,4 +192,180 @@ describe("agent-browser setup extension", () => {
192
192
  it("declined marker path lives in the agent dir", () => {
193
193
  expect(getDeclinedMarkerPath()).toBe(join(tmpDir, DECLINED_MARKER_NAME));
194
194
  });
195
+
196
+ it("skips probing when the decline marker exists", async () => {
197
+ const { pi, handlers, exec } = makePi();
198
+ await agentBrowserSetupExtension(pi);
199
+ const markerPath = getDeclinedMarkerPath();
200
+ mkdirSync(dirname(markerPath), { recursive: true });
201
+ writeFileSync(markerPath, "1", "utf-8");
202
+ try {
203
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, makeCtx());
204
+ expect(exec).not.toHaveBeenCalled();
205
+ } finally {
206
+ rmSync(markerPath, { force: true });
207
+ }
208
+ });
209
+
210
+ it("isAgentBrowserInstalled returns false when exec throws", async () => {
211
+ const { pi } = makePi(async () => {
212
+ throw new Error("spawn failed");
213
+ });
214
+ expect(await isAgentBrowserInstalled(pi)).toBe(false);
215
+ });
216
+
217
+ it("isAgentBrowserInstalled returns false when the probe is killed", async () => {
218
+ const { pi } = makePi(async () => ({ code: 0, stdout: "", stderr: "", killed: true }));
219
+ expect(await isAgentBrowserInstalled(pi)).toBe(false);
220
+ });
221
+
222
+ it("reports a timed-out npm install", async () => {
223
+ const execImpl = vi
224
+ .fn()
225
+ .mockResolvedValueOnce(fail) // probe: missing
226
+ .mockResolvedValueOnce({ code: 0, stdout: "", stderr: "", killed: true }); // npm killed
227
+ const { pi, handlers, exec } = makePi(execImpl as any);
228
+ await agentBrowserSetupExtension(pi);
229
+ const ctx = makeCtx();
230
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
231
+
232
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("timed out"), "error");
233
+ expect(exec).toHaveBeenCalledTimes(2);
234
+ });
235
+
236
+ it("reports npm success but missing binary on PATH", async () => {
237
+ const execImpl = vi
238
+ .fn()
239
+ .mockResolvedValueOnce(fail) // probe: missing
240
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
241
+ .mockResolvedValueOnce(fail) // probe: still missing
242
+ .mockResolvedValueOnce(ok); // (unused)
243
+ const { pi, handlers, exec } = makePi(execImpl as any);
244
+ await agentBrowserSetupExtension(pi);
245
+ const ctx = makeCtx();
246
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
247
+
248
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("still not on PATH"), "warning");
249
+ expect(exec).toHaveBeenCalledTimes(3);
250
+ });
251
+
252
+ it("warns when browser provisioning fails after install", async () => {
253
+ const execImpl = vi
254
+ .fn()
255
+ .mockResolvedValueOnce(fail) // probe: missing
256
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
257
+ .mockResolvedValueOnce(ok) // probe: now present
258
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "browser failed", killed: false }); // agent-browser install fails
259
+ const { pi, handlers, exec } = makePi(execImpl as any);
260
+ await agentBrowserSetupExtension(pi);
261
+ const ctx = makeCtx();
262
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
263
+
264
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Run `agent-browser install`"), "info");
265
+ expect(exec).toHaveBeenCalledTimes(4);
266
+ });
267
+
268
+ it("provisionBrowser returns false when exec throws", async () => {
269
+ const execImpl = vi
270
+ .fn()
271
+ .mockResolvedValueOnce(fail) // probe: missing
272
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
273
+ .mockResolvedValueOnce(ok) // probe: now present
274
+ .mockRejectedValueOnce(new Error("spawn failed")); // agent-browser install throws
275
+ const { pi, handlers, exec } = makePi(execImpl as any);
276
+ await agentBrowserSetupExtension(pi);
277
+ const ctx = makeCtx();
278
+ await getSessionStartHandler(handlers)({ type: "session_start", reason: "startup" }, ctx);
279
+
280
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Run `agent-browser install`"), "info");
281
+ expect(exec).toHaveBeenCalledTimes(4);
282
+ });
283
+
284
+ it("ensureAgentBrowser returns false when the user declines", async () => {
285
+ const execImpl = vi.fn().mockResolvedValue(fail);
286
+ const { pi } = makePi(execImpl as any);
287
+ const ctx = makeCtx({ confirmResult: false });
288
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
289
+ });
290
+
291
+ it("ensureAgentBrowser returns true when already installed", async () => {
292
+ const { pi } = makePi(async () => ok);
293
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(true);
294
+ });
295
+
296
+ it("ensureAgentBrowser returns false without UI", async () => {
297
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
298
+ const execImpl = vi.fn().mockResolvedValue(fail);
299
+ const { pi } = makePi(execImpl as any);
300
+ expect(await ensureAgentBrowser(pi, makeCtx({ hasUI: false }))).toBe(false);
301
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(INSTALL_COMMAND));
302
+ });
303
+
304
+ it("ensureAgentBrowser returns false when npm install fails", async () => {
305
+ const execImpl = vi
306
+ .fn()
307
+ .mockResolvedValueOnce(fail) // probe
308
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "EACCES", killed: false }); // npm fails
309
+ const { pi } = makePi(execImpl as any);
310
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(false);
311
+ });
312
+
313
+ it("npm install failure surfaces stdout when stderr is empty", async () => {
314
+ const execImpl = vi
315
+ .fn()
316
+ .mockResolvedValueOnce(fail) // probe
317
+ .mockResolvedValueOnce({ code: 1, stdout: "npm error log", stderr: "", killed: false }); // npm fails with stdout only
318
+ const { pi } = makePi(execImpl as any);
319
+ const ctx = makeCtx();
320
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
321
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("npm error log"), "error");
322
+ });
323
+
324
+ it("npm install failure falls back to exit status when output is empty", async () => {
325
+ const execImpl = vi
326
+ .fn()
327
+ .mockResolvedValueOnce(fail) // probe
328
+ .mockResolvedValueOnce({ code: 1, stdout: "", stderr: "", killed: false }); // npm fails with no output
329
+ const { pi } = makePi(execImpl as any);
330
+ const ctx = makeCtx();
331
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(false);
332
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("exit status 1"), "error");
333
+ });
334
+
335
+ it("notify is a no-op without UI even when the CLI is installed", async () => {
336
+ const { pi } = makePi(async () => ok);
337
+ const ctx = makeCtx({ hasUI: false });
338
+ expect(await ensureAgentBrowser(pi, ctx)).toBe(true);
339
+ });
340
+
341
+ it("ensureAgentBrowser returns true after full install flow", async () => {
342
+ const execImpl = vi
343
+ .fn()
344
+ .mockResolvedValueOnce(fail) // probe: missing
345
+ .mockResolvedValueOnce(ok) // npm i -g agent-browser
346
+ .mockResolvedValueOnce(ok) // probe: now present
347
+ .mockResolvedValueOnce(ok); // agent-browser install
348
+ const { pi } = makePi(execImpl as any);
349
+ expect(await ensureAgentBrowser(pi, makeCtx())).toBe(true);
350
+ });
351
+
352
+ it("cliCommand wraps args in cmd.exe on win32", () => {
353
+ const original = process.platform;
354
+ Object.defineProperty(process, "platform", { value: "win32" });
355
+ try {
356
+ const { ensureAgentBrowser: _e, ...rest } = {} as any;
357
+ // Re-import to pick up the platform at module scope is not needed;
358
+ // cliCommand is module-private, so exercise it through the public flow.
359
+ const execImpl = vi.fn().mockResolvedValue(ok);
360
+ const { pi } = makePi(execImpl as any);
361
+ return ensureAgentBrowser(pi, makeCtx()).then((result) => {
362
+ expect(result).toBe(true);
363
+ const call = execImpl.mock.calls[0];
364
+ expect(call[0]).toBe("cmd");
365
+ expect(call[1]).toEqual(["/c", "agent-browser", "--version"]);
366
+ });
367
+ } finally {
368
+ Object.defineProperty(process, "platform", { value: original });
369
+ }
370
+ });
195
371
  });
@@ -75,6 +75,8 @@ async function runNpmInstall(pi: ExtensionAPI): Promise<string | null> {
75
75
  const result = await pi.exec(command, args, { timeout: INSTALL_TIMEOUT_MS });
76
76
  if (result.killed) return "npm install timed out";
77
77
  if (result.code !== 0) {
78
+ // The exit-status fallback is always non-empty, so the final || is unreachable.
79
+ /* v8 ignore next 1 */
78
80
  return (result.stderr || result.stdout || `exit status ${result.code}`).trim() || "npm install failed";
79
81
  }
80
82
  return null;