gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,12 +1,17 @@
1
1
  import { isSessionChangeEvidence, type SessionChangeEvidence } from "./session-changes.ts";
2
- import type { Duplex, Readable, Writable } from "node:stream";
2
+ import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { tmpdir, userInfo } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { Duplex, type Readable, type Writable } from "node:stream";
3
7
  import { stripVTControlCharacters } from "node:util";
4
- import { RESEARCH_SELECTION_ENV } from "./sdd-research-capabilities.ts";
5
8
  import { withoutInteractiveHost } from "./rpc-host.ts";
9
+ import { childPackageExtensionArgs } from "./child-package-injection.ts";
6
10
  import { AGENT_MODE, formatModelRef, type AgentDefinition, type AgentMode, type ModelRef } from "./agents-config.ts";
7
11
  import { CHILD_QUERY_MAX_INFLIGHT, CHILD_QUERY_TIMEOUT_MS, parseChildFrame, validChildMessage, validChildQueryId } from "./agents-messaging.ts";
8
12
  import { ParentStandingReviewPermissionBroker } from "./review-session-standing-permission-ipc.ts";
9
- import { isFinished, normalizeRpcEvent, TASK_EVENT, TASK_STATUS, taskLabel, type AskRequest, type ChildResponseObservation, type TaskRecord, type TaskStore } from "./agents-protocol.ts";
13
+ import { isFinished, normalizeRpcEvent, ToolArgumentProgress, TASK_EVENT, TASK_STATUS, taskLabel, type AskRequest, type ChildResponseObservation, type TaskRecord, type TaskStore } from "./agents-protocol.ts";
14
+ import { WriterSurfaceRegistry, writerSurfaceConflictMessage } from "./writer-surfaces.ts";
10
15
 
11
16
  // Gentle Agents runner. Every subagent is its own `pi --mode rpc` process:
12
17
  // the host never runs subagent work on the TUI thread. It writes JSON
@@ -14,12 +19,12 @@ import { isFinished, normalizeRpcEvent, TASK_EVENT, TASK_STATUS, taskLabel, type
14
19
  // and enforces an inactivity watchdog per task.
15
20
 
16
21
  export interface ChildLike {
17
- pid: number | undefined;
22
+ pid?: number;
18
23
  connected?: boolean;
19
- stdin: Writable;
20
- stdout: Readable;
24
+ stdin: Writable | null;
25
+ stdout: Readable | null;
21
26
  stderr: Readable | null | undefined;
22
- stdio?: Array<Duplex | null | undefined>;
27
+ stdio?: Array<Readable | Writable | null | undefined>;
23
28
  kill(signal?: NodeJS.Signals): boolean;
24
29
  send?(message: Record<string, unknown>, callback?: (error: Error | null) => void): boolean;
25
30
  disconnect?(): void;
@@ -33,6 +38,7 @@ export interface SpawnOptions {
33
38
  cwd: string;
34
39
  env: NodeJS.ProcessEnv;
35
40
  detached?: boolean;
41
+ windowsHide?: boolean;
36
42
  stdio?: Array<"pipe" | "ignore" | "inherit" | "ipc" | "overlapped">;
37
43
  }
38
44
 
@@ -53,6 +59,7 @@ export interface RunnerDeps {
53
59
  now(): number;
54
60
  schedule(fn: () => void, ms: number): () => void;
55
61
  pi: PiCommand;
62
+ resolvePi?(): PiCommand;
56
63
  process?: ProcessControl;
57
64
  }
58
65
 
@@ -73,6 +80,7 @@ export interface AskAnswer {
73
80
  export interface TaskQuery {
74
81
  taskId: string;
75
82
  requestId: string;
83
+ message: string;
76
84
  }
77
85
 
78
86
  export const MAX_CHILD_RESPONSE_OBSERVATIONS = 128;
@@ -104,51 +112,12 @@ export interface RunnerHooks {
104
112
  // Accepts a child notification only while the originating parent session is active.
105
113
  onNotification?(task: TaskRecord, message: string): boolean | void;
106
114
  onQuery?(task: TaskRecord, requestId: string, message: string): boolean | void;
115
+ onQuerySettled?(taskId: string, requestId: string, outcome: "replied" | "expired"): void;
107
116
  // Parent-only observation of a paired successful filesystem tool, not prose.
108
117
  onSuccessfulMutation?(task: TaskRecord, tool: { toolName: "write" | "edit"; toolCallId: string; path: string; evidence?: SessionChangeEvidence }): void | Promise<void>;
109
118
  }
110
119
 
111
- export interface RemediationHarnessPlan { command?: string; naReason?: string }
112
- export interface RemediationRollbackPlan { boundary: string; command: string }
113
- export interface RemediationScope { cwd: string; editPaths: string[]; commands: string[]; allowedEditRoots: string[] }
114
- export interface RemediationPlan {
115
- editPaths?: string[];
116
- cwd: string;
117
- commands: string[];
118
- runtimeHarness: RemediationHarnessPlan;
119
- rollback: RemediationRollbackPlan;
120
- }
121
- const concrete = (value: unknown): value is string => typeof value === "string" && value.trim() === value && value.length > 3 && value.length <= 4096 && !/[\0\r\n]/.test(value);
122
- export function parseRemediationPlan(value: unknown, cwd: string): RemediationPlan {
123
- const plan = value as RemediationPlan;
124
- if (!plan || plan.cwd !== cwd || !Array.isArray(plan.commands) || plan.commands.length < 1 || plan.commands.length > 16 || !plan.commands.every(concrete) ||
125
- !concrete(plan.rollback?.boundary) || !concrete(plan.rollback?.command) || !plan.runtimeHarness ||
126
- !(concrete(plan.runtimeHarness.command) && plan.runtimeHarness.naReason === undefined || plan.runtimeHarness.command === undefined && concrete(plan.runtimeHarness.naReason) && plan.runtimeHarness.naReason.length >= 20 && /because/i.test(plan.runtimeHarness.naReason))) throw new TypeError("Invalid remediation evidence plan");
127
- return structuredClone(plan);
128
- }
129
- export const plannedCommands = (plan: RemediationPlan) => [...plan.commands, ...(plan.runtimeHarness.command ? [plan.runtimeHarness.command] : []), plan.rollback.command];
130
- // Launch-local scope only; task history is not an attempt authority.
131
- export interface RemediationContext {
132
- failedEvidenceRevision: string;
133
- plan: RemediationPlan;
134
- scope: RemediationScope;
135
- }
136
-
137
- export interface SddChangeSelection {
138
- changeName: string;
139
- workspaceRoot: string;
140
- phase: "apply" | "verify" | "archive" | "remediate";
141
- failedEvidenceRevision?: string;
142
- }
143
-
144
- export const SDD_CHANGE_FLAG = "--gentle-sdd-change";
145
-
146
- export const REMEDIATION_PLAN_ENV = "GENTLE_PI_SDD_REMEDIATION_PLAN";
147
-
148
120
  export interface TaskRequest {
149
- remediationIntent?: unknown;
150
- sddRemediation?: RemediationContext;
151
- sddPreflightContext?: string;
152
121
  agent: AgentDefinition;
153
122
  prompt: string;
154
123
  label: string | undefined;
@@ -161,11 +130,18 @@ export interface TaskRequest {
161
130
  sessionDir: string;
162
131
  resumeSessionPath: string | undefined;
163
132
  env: NodeJS.ProcessEnv;
164
- // A launch-local SDD identity. It is never prompt text or shared state.
165
- sddChange?: SddChangeSelection;
166
- // Untrusted narrowing intent; paths come only from matching host provenance.
167
- researchSelection?: unknown;
133
+ // Host-provided only: the launcher's package injection signal (#1690) or
134
+ // the curated fallback. Never derived from tool input or agent definitions.
168
135
  extensionPaths?: string[];
136
+ // Same host-only provenance as extensionPaths (#1690).
137
+ noExtensions?: boolean;
138
+ // Parsed `## Allowed edit surfaces` of a bounded writer. While the task is
139
+ // queued or running it claims them in `cwd`; run() rejects an overlapping
140
+ // claim (gentle-shell#1731). Read-only agents leave this unset.
141
+ writerSurfaces?: readonly string[];
142
+ // Canonical worktree root the claim is keyed to; the parent computes it before
143
+ // run() so admission stays synchronous. Defaults to `cwd`.
144
+ writerRoot?: string;
169
145
  // Synchronous admission recheck at dequeue, before any OS spawn. Throws fail
170
146
  // only this task; unlike onLaunch, it must never persist Changes evidence.
171
147
  beforeSpawn?: () => void;
@@ -224,6 +200,8 @@ interface LiveTask {
224
200
  processGroup: number | undefined;
225
201
  terminal: { status: TaskRecord["status"]; error: string | null } | undefined;
226
202
  childExit: number | null | undefined;
203
+ childExitSignal?: string | null;
204
+ instructionsTransportDir?: string;
227
205
  cleanupDeadlineAt: number | undefined;
228
206
  quarantined: boolean;
229
207
  nextId: number;
@@ -236,6 +214,7 @@ interface LiveTask {
236
214
  // live work, so the watchdog gives it the tool ceiling instead of the idle
237
215
  // silence budget. Keyed by call id, holding the announced tool name.
238
216
  inFlightTools: Map<string, string>;
217
+ argumentProgress: ToolArgumentProgress;
239
218
  // Bounded ring buffer of the child's raw stderr output, capped to the last
240
219
  // STDERR_TAIL_MAX characters. Only surfaced on the stall and pre-settle exit
241
220
  // terminal paths, never on completed, cancelled, or other failure reasons.
@@ -243,9 +222,16 @@ interface LiveTask {
243
222
  }
244
223
 
245
224
  const STDERR_TAIL_MAX = 512;
225
+ const DIR_MODE = 0o700;
226
+ const FILE_MODE = 0o600;
227
+ // UTF-8 bytes, not characters: argv size is what kills the child on macOS.
228
+ export const MAX_INLINE_INSTRUCTIONS_BYTES = 1000;
229
+ const MAX_TRANSPORT_PREFIX_CHARS = 64;
246
230
  const CHILD_MARKER = "GENTLE_PI_AGENTS_CHILD";
247
231
  const IPC_MARKER = "GENTLE_PI_AGENTS_OWNED_IPC";
248
232
  const PARENT_NOTIFICATION_TOOL = "subagent_parent_message";
233
+ /** The exact --tools list, so the child can report requested names Pi dropped silently (#1690). */
234
+ export const REQUESTED_TOOLS_ENV = "GENTLE_PI_AGENTS_REQUESTED_TOOLS";
249
235
  const DEFAULT_TOOLS: readonly string[] = [];
250
236
  const TERMINATION_GRACE_MS = 250;
251
237
  const GROUP_CONFIRM_MS = 25;
@@ -277,31 +263,79 @@ function queryRejection(error: unknown): string {
277
263
  return "parent rejected query";
278
264
  }
279
265
 
266
+ export function formatChildExit(code: number | null | undefined, signal?: string | null): string {
267
+ if (typeof code === "number") return `code ${code}`;
268
+ if (signal) return `signal ${signal}`;
269
+ return `code ${code ?? "unknown"}`;
270
+ }
271
+
280
272
  const hostProcess: ProcessControl = { platform: process.platform, kill: (pid, signal) => process.kill(pid, signal) };
281
273
 
282
- export function childArguments(request: TaskRequest): string[] {
274
+ // The --tools value, or undefined when Pi keeps its default tools.
275
+ function requestedTools(request: TaskRequest): string | undefined {
276
+ const tools = request.agent.tools.length > 0 ? [...new Set([...request.agent.tools, PARENT_NOTIFICATION_TOOL])] : DEFAULT_TOOLS;
277
+ return tools.length > 0 ? tools.join(",") : undefined;
278
+ }
279
+
280
+ const WEB_TOOL_NAMES = ["web_enable", "web_search", "source_check", "fetch_content", "get_search_content"];
281
+
282
+ /** Resolve one already installed, explicitly requested package. Pi's own loader
283
+ * follows its manifest and supplies SDK aliases; no install or ambient discovery. */
284
+ function requestedWebPackagePaths(request: TaskRequest): string[] {
285
+ if (!WEB_TOOL_NAMES.every(name => request.agent.tools.includes(name))) return [];
286
+ try {
287
+ let agentDir = request.env.PI_CODING_AGENT_DIR;
288
+ if (!agentDir || /^~(?:$|[/\\])/.test(agentDir)) {
289
+ const home = (process.platform === "win32" ? request.env.USERPROFILE : request.env.HOME) || userInfo().homedir;
290
+ agentDir = agentDir ? agentDir.replace(/^~/, home) : join(home, ".pi", "agent");
291
+ }
292
+ const manager = new DefaultPackageManager({
293
+ cwd: request.cwd,
294
+ agentDir,
295
+ settingsManager: SettingsManager.inMemory(),
296
+ });
297
+ const installed = manager.getInstalledPath("npm:pi-web-access", "user");
298
+ return installed ? [realpathSync(installed)] : [];
299
+ } catch {
300
+ // Missing/incompatible package remains an observable child capability gap.
301
+ return [];
302
+ }
303
+ }
304
+
305
+ export function childArguments(request: TaskRequest, instructionsPath?: string): string[] {
283
306
  const args = ["--mode", "rpc", "--session-dir", request.sessionDir];
284
- for (const path of request.extensionPaths ?? []) args.push("--extension", path);
285
- if (request.sddChange) args.push(SDD_CHANGE_FLAG, JSON.stringify(request.sddChange));
307
+ const extensionPaths = [...new Set(request.extensionPaths ?? [])];
308
+ for (const packagePath of requestedWebPackagePaths(request)) {
309
+ const injected = extensionPaths.some(path => {
310
+ try { return realpathSync(resolve(request.cwd, path)) === packagePath; }
311
+ catch { return false; }
312
+ });
313
+ if (!injected) extensionPaths.push(packagePath);
314
+ }
315
+ args.push(...childPackageExtensionArgs({ noExtensions: request.noExtensions === true, extensionPaths }));
286
316
  if (request.resumeSessionPath) args.push("--session", request.resumeSessionPath);
287
317
  if (request.model) args.push("--model", request.thinking ? `${formatModelRef(request.model)}:${request.thinking}` : formatModelRef(request.model));
288
318
  else if (request.thinking) args.push("--thinking", request.thinking);
289
- const tools = request.agent.tools.length > 0 || request.agent.name === "sdd-research" ? [...new Set([...request.agent.tools, PARENT_NOTIFICATION_TOOL])] : DEFAULT_TOOLS;
290
- if (tools.length > 0) args.push("--tools", tools.join(","));
291
- if (request.agent.instructions.length > 0) args.push("--append-system-prompt", request.agent.instructions);
319
+ const tools = requestedTools(request);
320
+ if (tools !== undefined) args.push("--tools", tools);
321
+ if (instructionsPath) {
322
+ args.push("--append-system-prompt", instructionsPath);
323
+ } else if (request.agent.instructions.length > 0) {
324
+ args.push("--append-system-prompt", request.agent.instructions);
325
+ }
292
326
  return args;
293
327
  }
294
328
 
295
- // The child is the same pi that is running us: node plus its cli entry.
329
+ // Reuse the running pi entry while it exists; upgrades may remove it.
296
330
  // GENTLE_PI_AGENTS_PI overrides it with a command line.
297
- export function piCommand(proc: ProcessLike = process): PiCommand {
331
+ export function piCommand(proc: ProcessLike = process, exists: (path: string) => boolean = existsSync): PiCommand {
298
332
  const override = proc.env.GENTLE_PI_AGENTS_PI?.trim();
299
333
  if (override) {
300
334
  const [command, ...args] = override.split(/\s+/);
301
335
  return { command, args };
302
336
  }
303
337
  const entry = proc.argv[1];
304
- if (entry && /(^|[\\/])cli\.js$/.test(entry)) return { command: proc.execPath, args: [entry] };
338
+ if (entry && /(^|[\\/])cli\.js$/.test(entry) && exists(entry)) return { command: proc.execPath, args: [entry] };
305
339
  return { command: "pi", args: [] };
306
340
  }
307
341
 
@@ -332,8 +366,7 @@ export class JsonLines {
332
366
  }
333
367
 
334
368
  export function promptText(request: TaskRequest): string {
335
- const prompt = request.context ? `${request.prompt}\n\n## Context\n${request.context}` : request.prompt;
336
- return request.sddRemediation ? `${prompt}\n\n## Human-authorized remediation plan\nExecute only these exact commands in the selected cwd; report actual results without claiming native verification.\n${JSON.stringify(request.sddRemediation.plan)}\nFailed evidence: ${request.sddRemediation.failedEvidenceRevision}` : prompt;
369
+ return request.context ? `${request.prompt}\n\n## Context\n${request.context}` : request.prompt;
337
370
  }
338
371
 
339
372
  export class AgentRunner {
@@ -347,6 +380,7 @@ export class AgentRunner {
347
380
  private readonly waiters = new Map<string, Array<(task: TaskRecord) => void>>();
348
381
  private readonly queryWaiters = new Map<string, Array<(query: TaskQuery | undefined) => void>>();
349
382
  private readonly firstQueries = new Map<string, TaskQuery>();
383
+ private readonly writers = new WriterSurfaceRegistry();
350
384
  private counter = 0;
351
385
 
352
386
  constructor(store: TaskStore, limits: RunnerLimits, deps: RunnerDeps, hooks: RunnerHooks) {
@@ -363,7 +397,6 @@ export class AgentRunner {
363
397
  const task: TaskRecord = {
364
398
  id: `${now.toString(36)}-${this.counter.toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
365
399
  agent: request.agent.name,
366
- ...(request.sddPreflightContext ? { sddPreflightContext: request.sddPreflightContext } : {}),
367
400
  mode: request.mode,
368
401
  prompt: request.prompt,
369
402
  label: taskLabel(request.prompt, request.label),
@@ -389,24 +422,16 @@ export class AgentRunner {
389
422
  return task;
390
423
  }
391
424
 
425
+ // Check and claim happen in this one synchronous call, so two launches can
426
+ // never both pass admission for overlapping surfaces; finish() releases.
392
427
  run(request: TaskRequest): TaskRecord {
393
- // Admission already confirmed the canonical cwd and human edit scope.
394
- // Check this runner's queue/live slots before scheduling any launch: history
395
- // is not a lock, and quarantined children still own their live slot.
396
- if (request.sddRemediation) {
397
- const active = [...this.queue.map(entry => entry.task), ...[...this.live.keys()].map(id => this.store.get(id))];
398
- if (active.some(task => task?.agent === "sdd-remediate" && task.cwd === request.cwd)) {
399
- throw new Error("Remediation already queued or running in this worktree; wait for confirmed cleanup or cancel the active task before requesting fresh authorization");
400
- }
428
+ if (request.writerSurfaces) {
429
+ const conflicts = this.writers.conflicts(request.writerRoot ?? request.cwd, request.writerSurfaces);
430
+ if (conflicts.length) throw new Error(writerSurfaceConflictMessage(conflicts));
401
431
  }
402
432
  const task = this.createTask(request);
403
- // A caller can retain and mutate its request after dispatch. Preserve only
404
- // the identity selected at construction for this child launch.
405
- const launchRequest = {
406
- ...request,
407
- sddChange: request.sddChange && { ...request.sddChange },
408
- };
409
- this.queue.push({ task, request: launchRequest });
433
+ if (request.writerSurfaces) this.writers.claim(task.id, request.writerRoot ?? request.cwd, request.writerSurfaces);
434
+ this.queue.push({ task, request });
410
435
  queueMicrotask(() => this.pump());
411
436
  return task;
412
437
  }
@@ -445,6 +470,7 @@ export class AgentRunner {
445
470
  if (live.queries.get(requestId) === query) {
446
471
  query.cancel();
447
472
  live.queries.delete(requestId);
473
+ this.hooks.onQuerySettled?.(id, requestId, "replied");
448
474
  }
449
475
  return accepted;
450
476
  }
@@ -507,28 +533,55 @@ export class AgentRunner {
507
533
  // one (`lib/rpc-host.ts`).
508
534
  const env = withoutInteractiveHost({
509
535
  ...request.env,
510
- ...(request.extensionPaths ? { [RESEARCH_SELECTION_ENV]: JSON.stringify(request.researchSelection ?? null) } : {}),
511
536
  [CHILD_MARKER]: "1",
512
537
  [IPC_MARKER]: `${this.deps.now()}-${Math.random().toString(36).slice(2)}`,
513
538
  ...(hasParentPermissionChannel ? { GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3" } : {}),
514
539
  });
515
- delete env[REMEDIATION_PLAN_ENV];
516
- if (request.sddRemediation) env[REMEDIATION_PLAN_ENV] = JSON.stringify({ plan: request.sddRemediation.plan, scope: request.sddRemediation.scope, selection: request.sddChange });
540
+ // Do not forward stale legacy child selection or authorization.
541
+ delete env.GENTLE_PI_SDD_REMEDIATION_PLAN;
542
+ delete env.GENTLE_PI_RESEARCH_SELECTION;
543
+ const tools = requestedTools(request);
544
+ if (tools !== undefined) env[REQUESTED_TOOLS_ENV] = tools;
545
+ else delete env[REQUESTED_TOOLS_ENV];
546
+ let instructionsTransportDir: string | undefined;
547
+ let instructionsTransportPath: string | undefined;
548
+ if (Buffer.byteLength(request.agent.instructions, "utf8") > MAX_INLINE_INSTRUCTIONS_BYTES) {
549
+ try {
550
+ const prefix = (request.agent.name || "instructions").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, MAX_TRANSPORT_PREFIX_CHARS);
551
+ instructionsTransportDir = mkdtempSync(join(tmpdir(), `gentle-pi-subagent-${prefix}-`));
552
+ try { chmodSync(instructionsTransportDir, DIR_MODE); } catch { /* best effort */ }
553
+ instructionsTransportPath = join(instructionsTransportDir, "instructions.md");
554
+ writeFileSync(instructionsTransportPath, request.agent.instructions, { mode: FILE_MODE, encoding: "utf8" });
555
+ try { chmodSync(instructionsTransportPath, FILE_MODE); } catch { /* best effort */ }
556
+ } catch (error) {
557
+ if (instructionsTransportDir) {
558
+ try { rmSync(instructionsTransportDir, { recursive: true, force: true }); } catch { /* best effort */ }
559
+ }
560
+ this.store.update(id, { status: TASK_STATUS.RUNNING, startedAt: this.deps.now(), lastStep: "starting" });
561
+ this.finish(id, TASK_STATUS.FAILED, `could not write agent instructions: ${error instanceof Error ? error.message : String(error)}`);
562
+ return;
563
+ }
564
+ }
517
565
  let child: ChildLike;
518
566
  try {
519
- child = this.deps.spawn(this.deps.pi.command, [...this.deps.pi.args, ...childArguments(request)], {
567
+ const pi = this.deps.resolvePi?.() ?? this.deps.pi;
568
+ child = this.deps.spawn(pi.command, [...pi.args, ...childArguments(request, instructionsTransportPath)], {
520
569
  cwd: request.cwd,
521
570
  env,
522
571
  detached,
572
+ windowsHide: true,
523
573
  stdio: hasParentPermissionChannel ? ["pipe", "pipe", "pipe", permissionChannelStdio, "ipc"] : ["pipe", "pipe", "pipe", "ipc"],
524
574
  });
525
575
  } catch (error) {
576
+ if (instructionsTransportDir) {
577
+ try { rmSync(instructionsTransportDir, { recursive: true, force: true }); } catch { /* best effort */ }
578
+ }
526
579
  this.store.update(id, { status: TASK_STATUS.RUNNING, startedAt: this.deps.now(), lastStep: "starting" });
527
580
  this.finish(id, TASK_STATUS.FAILED, `could not start pi: ${error instanceof Error ? error.message : String(error)}`);
528
581
  return;
529
582
  }
530
583
  const processGroup = detached && typeof child.pid === "number" && child.pid > 0 ? child.pid : undefined;
531
- const live: LiveTask = { child, sawRunEvent: false, mutationStarts: new Map(), inFlightTools: new Map(), pending: new Map(), queries: new Map(), replies: new Map(), cancelStall: () => {}, cancelGrace: () => {}, processGroup, terminal: undefined, childExit: undefined, cleanupDeadlineAt: undefined, quarantined: false, nextId: 0, ipcClosed: false, acknowledgedIpcIds: new Set(), acknowledgedIpcOrder: [], stderrTail: "" };
584
+ const live: LiveTask = { child, sawRunEvent: false, mutationStarts: new Map(), inFlightTools: new Map(), argumentProgress: new ToolArgumentProgress(), pending: new Map(), queries: new Map(), replies: new Map(), cancelStall: () => {}, cancelGrace: () => {}, processGroup, terminal: undefined, childExit: undefined, childExitSignal: undefined, instructionsTransportDir, cleanupDeadlineAt: undefined, quarantined: false, nextId: 0, ipcClosed: false, acknowledgedIpcIds: new Set(), acknowledgedIpcOrder: [], stderrTail: "" };
532
585
  if (request.prepareResponseObservations) {
533
586
  let ready = false;
534
587
  live.observationPreparation = () => ready;
@@ -542,7 +595,7 @@ export class AgentRunner {
542
595
  }
543
596
  this.live.set(id, live);
544
597
  const permissionPipe = child.stdio?.[3];
545
- if (hasParentPermissionChannel && permissionPipe !== undefined && permissionPipe !== null) {
598
+ if (hasParentPermissionChannel && permissionPipe instanceof Duplex) {
546
599
  live.permissionBroker = new ParentStandingReviewPermissionBroker(
547
600
  { readable: permissionPipe, writable: permissionPipe },
548
601
  (repositoryIdentity) => this.live.get(id) === live && !live.terminal && request.authorizeParentStandingReviewPermission?.(repositoryIdentity) === true,
@@ -560,6 +613,11 @@ export class AgentRunner {
560
613
  try { request.onLaunch?.(); }
561
614
  catch (error) { this.requestStop(id, TASK_STATUS.FAILED, `could not register launched worktree: ${error instanceof Error ? error.message : String(error)}`); }
562
615
  });
616
+ child.on("exit", (code, signal) => this.exited(id, code, signal));
617
+ if (!child.stdin || !child.stdout) {
618
+ this.requestStop(id, TASK_STATUS.FAILED, "could not start pi: missing RPC streams");
619
+ return;
620
+ }
563
621
  child.stdin.on("error", () => {});
564
622
  this.armStall(id, live);
565
623
  const lines = new JsonLines((value) => this.receive(id, request, value));
@@ -570,11 +628,6 @@ export class AgentRunner {
570
628
  const tail = live.stderrTail + chunk;
571
629
  live.stderrTail = tail.length > STDERR_TAIL_MAX ? tail.slice(-STDERR_TAIL_MAX) : tail;
572
630
  });
573
- child.on("exit", (code) => this.exited(id, code));
574
- if (request.sddRemediation && child.pid === undefined) {
575
- this.childError(id, new Error("remediation child has no process ID"));
576
- return;
577
- }
578
631
  void this.send(id, { type: "get_state" }).then((response) => {
579
632
  const data = response.data as { sessionFile?: unknown; model?: { provider?: unknown; id?: unknown } | null; thinkingLevel?: unknown } | undefined;
580
633
  if (response.success !== true || live.terminal || this.live.get(id) !== live || !data) return;
@@ -613,8 +666,8 @@ export class AgentRunner {
613
666
 
614
667
  // An idle child is bounded by the silence budget; a child whose announced
615
668
  // tool call is still running is live work and bounded by the longer tool
616
- // ceiling. The budget is chosen from the state at arm time, and every RPC
617
- // object re-arms, so a finished tool call returns the task to idle silence.
669
+ // ceiling. These are renewable silence budgets, not total duration limits.
670
+ // A finished tool call returns the task to idle silence.
618
671
  private armStall(id: string, live: LiveTask): void {
619
672
  live.cancelStall();
620
673
  const tool = live.inFlightTools.values().next().value;
@@ -668,12 +721,12 @@ export class AgentRunner {
668
721
  try {
669
722
  if (live.queries.has(parsed.frame.id)) rejectQuery("duplicate query request");
670
723
  if (live.queries.size >= CHILD_QUERY_MAX_INFLIGHT) rejectQuery("too many pending parent queries");
671
- query = { replying: false, cancel: this.deps.schedule(() => this.expireQuery(live, parsed.frame!.id), CHILD_QUERY_TIMEOUT_MS) };
724
+ query = { replying: false, cancel: this.deps.schedule(() => this.expireQuery(id, live, parsed.frame!.id), CHILD_QUERY_TIMEOUT_MS) };
672
725
  live.queries.set(parsed.frame.id, query);
673
726
  if (!this.hooks.onQuery) rejectQuery("task parent cannot accept queries");
674
727
  if (this.hooks.onQuery(task, parsed.frame.id, parsed.frame.message) === false) rejectQuery("task parent is not the active host session");
675
728
  if (task.mode === AGENT_MODE.TASK && !this.firstQueries.has(id)) {
676
- const first = { taskId: id, requestId: parsed.frame.id };
729
+ const first = { taskId: id, requestId: parsed.frame.id, message: parsed.frame.message };
677
730
  this.firstQueries.set(id, first);
678
731
  for (const resolve of this.queryWaiters.get(id) ?? []) resolve(first);
679
732
  this.queryWaiters.delete(id);
@@ -687,10 +740,11 @@ export class AgentRunner {
687
740
  }
688
741
  }
689
742
 
690
- private expireQuery(live: LiveTask, id: string): void {
743
+ private expireQuery(taskId: string, live: LiveTask, id: string): void {
691
744
  const query = live.queries.get(id);
692
745
  if (!query) return;
693
746
  live.queries.delete(id);
747
+ this.hooks.onQuerySettled?.(taskId, id, "expired");
694
748
  if (query.replying) this.settleReply(live, id, false);
695
749
  else this.sendQueryError(live, id, "parent query timed out");
696
750
  }
@@ -743,7 +797,7 @@ export class AgentRunner {
743
797
 
744
798
  private write(live: LiveTask, payload: Record<string, unknown>): void {
745
799
  try {
746
- live.child.stdin.write(`${JSON.stringify(payload)}\n`);
800
+ live.child.stdin?.write(`${JSON.stringify(payload)}\n`);
747
801
  } catch {
748
802
  // the child is gone; the exit handler settles the task
749
803
  }
@@ -782,7 +836,12 @@ export class AgentRunner {
782
836
  // renews the watchdog here, so fire-and-forget UI traffic that normalizes
783
837
  // to nothing cannot keep a child that never started its run alive forever
784
838
  // (#1034); the pre-existing timer stays armed until real progress arrives.
785
- const progress = events.length > 0;
839
+ const argumentProgress = live.argumentProgress.observe(raw);
840
+ if (argumentProgress) {
841
+ live.sawRunEvent = true;
842
+ this.store.update(id, { lastStep: "generating tool arguments", lastActivityAt: this.deps.now() });
843
+ }
844
+ const progress = events.length > 0 || argumentProgress;
786
845
  for (const event of events) {
787
846
  if (event.type === TASK_EVENT.RESPONSE_OBSERVATION) {
788
847
  const buffer = live.observations;
@@ -906,6 +965,7 @@ export class AgentRunner {
906
965
  if (this.deps.now() >= (live.cleanupDeadlineAt ?? 0)) {
907
966
  live.cancelGrace();
908
967
  live.quarantined = true;
968
+ this.cleanupLive(live);
909
969
  this.finish(id, TASK_STATUS.FAILED, `process cleanup unconfirmed after ${GROUP_CONFIRM_DEADLINE_MS}ms; capacity quarantined`, live);
910
970
  return;
911
971
  }
@@ -930,12 +990,24 @@ export class AgentRunner {
930
990
  if (this.deps.now() >= (live.cleanupDeadlineAt ?? 0)) {
931
991
  live.cancelGrace();
932
992
  live.quarantined = true;
993
+ this.cleanupLive(live);
933
994
  this.finish(id, TASK_STATUS.FAILED, `child exit unconfirmed after ${GROUP_CONFIRM_DEADLINE_MS}ms; capacity quarantined`, live);
934
995
  return;
935
996
  }
936
997
  live.cancelGrace = this.deps.schedule(() => this.confirmGroupExit(id, live), GROUP_CONFIRM_MS);
937
998
  }
938
999
 
1000
+ private cleanupLive(live: LiveTask): void {
1001
+ live.permissionBroker?.close();
1002
+ this.closeIpc(live);
1003
+ live.cancelStall();
1004
+ live.cancelGrace();
1005
+ if (live.instructionsTransportDir) {
1006
+ try { rmSync(live.instructionsTransportDir, { recursive: true, force: true }); } catch { /* best effort */ }
1007
+ live.instructionsTransportDir = undefined;
1008
+ }
1009
+ }
1010
+
939
1011
  private childError(id: string, error: Error): void {
940
1012
  const live = this.live.get(id);
941
1013
  if (!live) return;
@@ -945,41 +1017,42 @@ export class AgentRunner {
945
1017
  this.requestStop(id, TASK_STATUS.FAILED, `pi process error: ${error.message}`);
946
1018
  return;
947
1019
  }
948
- live.permissionBroker?.close();
949
- this.closeIpc(live);
950
- live.cancelStall();
951
- live.cancelGrace();
1020
+ this.cleanupLive(live);
952
1021
  this.live.delete(id);
953
1022
  this.finish(id, TASK_STATUS.FAILED, `could not start pi: ${error.message}`, live);
954
1023
  }
955
1024
 
956
- private exited(id: string, code: number | null): void {
1025
+ private exited(id: string, code: number | null, signal?: NodeJS.Signals | string | null): void {
957
1026
  const live = this.live.get(id);
958
1027
  if (!live) return;
959
1028
  live.childExit = code;
1029
+ live.childExitSignal = signal ?? null;
1030
+ const exitDesc = formatChildExit(code, signal);
960
1031
  if (this.groupExists(live)) {
961
- if (!live.terminal) this.requestStop(id, TASK_STATUS.FAILED, `pi exited with code ${code ?? "unknown"} before agent_settled${this.stderrSuffix(live)}`);
1032
+ if (!live.terminal) this.requestStop(id, TASK_STATUS.FAILED, `pi exited with ${exitDesc} before agent_settled${this.stderrSuffix(live)}`);
962
1033
  return;
963
1034
  }
964
1035
  this.completeExit(id, live);
965
1036
  }
966
1037
 
967
1038
  private completeExit(id: string, live: LiveTask): void {
968
- live.permissionBroker?.close();
969
- this.closeIpc(live);
970
- live.cancelStall();
971
- live.cancelGrace();
1039
+ this.cleanupLive(live);
972
1040
  this.live.delete(id);
973
1041
  // Quarantine already notified completion, but its retained slot is now free.
974
1042
  if (live.quarantined) {
1043
+ this.writers.release(id);
975
1044
  queueMicrotask(() => this.pump());
976
1045
  return;
977
1046
  }
978
1047
  const terminal = live.terminal;
979
- this.finish(id, terminal ? terminal.status : TASK_STATUS.FAILED, terminal ? terminal.error : `pi exited with code ${live.childExit ?? "unknown"} before agent_settled${this.stderrSuffix(live)}`, live);
1048
+ const exitDesc = formatChildExit(live.childExit, live.childExitSignal);
1049
+ this.finish(id, terminal ? terminal.status : TASK_STATUS.FAILED, terminal ? terminal.error : `pi exited with ${exitDesc} before agent_settled${this.stderrSuffix(live)}`, live);
980
1050
  }
981
1051
 
982
1052
  private finish(id: string, status: TaskRecord["status"], error: string | null, live?: LiveTask): void {
1053
+ // A quarantined child may still be writing: its claim lives as long as its
1054
+ // reserved slot and is released in completeExit on proven exit.
1055
+ if (!live?.quarantined) this.writers.release(id);
983
1056
  const current = this.store.get(id);
984
1057
  if (!current || isFinished(current.status)) return;
985
1058
  const finished = this.store.update(id, { status, endedAt: this.deps.now(), error, lastStep: error ?? "done" });
@@ -5,7 +5,7 @@ import { emptyThread, isFinished, TASK_STATUS, type TaskRecord, type TaskStore,
5
5
  import { renderThreadItem, type AgentsThreadTheme } from "./agents-thread-view.ts";
6
6
  import { formatElapsed } from "./agents-widget.ts";
7
7
  import { createNativePointerScope, type NativePointerRegion } from "./native-pointer-region.ts";
8
- import { formatTokens } from "./shell-bar.ts";
8
+ import { formatCost, formatTokens } from "./shell-bar.ts";
9
9
  import { PresenceCursor, readActivity, type Header, type Target } from "./orchestrator-presence.ts";
10
10
 
11
11
  // Gentle Agents overlay: tasks on the left, the selected task's thread on
@@ -128,8 +128,22 @@ function fit(text: string, width: number): string {
128
128
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
129
129
  }
130
130
 
131
+ // Deep equality over presence thread items: validated JSON payloads, so only
132
+ // plain objects, arrays, strings, booleans, and null appear. Used to keep the
133
+ // prior item object (and its WeakMap render cache entry) when polled content
134
+ // is render-equivalent at the same position.
135
+ function sameItemValue(a: unknown, b: unknown): boolean {
136
+ if (a === b) return true;
137
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
138
+ const left = a as Record<string, unknown>;
139
+ const right = b as Record<string, unknown>;
140
+ const keys = Object.keys(left);
141
+ if (keys.length !== Object.keys(right).length) return false;
142
+ return keys.every((key) => Object.hasOwn(right, key) && sameItemValue(left[key], right[key]));
143
+ }
144
+
131
145
  export function taskHeader(task: TaskRecord, now: number): string {
132
- const parts = [task.agent, task.status, task.model, task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? `$${task.cost.toFixed(2)}` : "", task.startedAt === null ? "" : formatElapsed((task.endedAt ?? now) - task.startedAt)];
146
+ const parts = [task.agent, task.status, task.model, task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? formatCost(task.cost) : "", task.startedAt === null ? "" : formatElapsed((task.endedAt ?? now) - task.startedAt)];
133
147
  return parts.filter((part) => part.length > 0).join(" · ");
134
148
  }
135
149
 
@@ -197,8 +211,17 @@ export class AgentsView {
197
211
  const task: TaskRecord = { ...summary, id: `peer:${id}:${summary.id}`, parentSessionId: id,
198
212
  status: summary.status as TaskRecord["status"], mode: "background", prompt: "", cwd: "",
199
213
  thinking: undefined, sessionPath: null, error: null, result: null, lastStep: "", turns: 0, toolCalls: 0, tokens: 0, cost: 0 };
214
+ const prior = this.remoteThreads.get(task.id);
200
215
  threads.set(task.id, { ...emptyThread(), ...thread,
201
- items: thread.items.map((item) => item.kind === "tool" ? { ...item, args: {} } : item) as ThreadItem[] });
216
+ items: thread.items.map((item, index) => {
217
+ // Sanitize before equality: presence tool items carry no invocation
218
+ // args, so compare against the stored sanitized form.
219
+ const sanitized = item.kind === "tool" ? { ...item, args: {} } : item;
220
+ const previous = prior?.items[index];
221
+ // Reuse the prior object when content is render-equivalent so the
222
+ // WeakMap render cache hits; any delta keeps a fresh identity.
223
+ return previous !== undefined && sameItemValue(previous, sanitized) ? previous : sanitized;
224
+ }) as ThreadItem[] });
202
225
  return task;
203
226
  });
204
227
  groups.push({ id, sessionId: id, label: `${header.label}${unavailable ? " · unavailable" : ""}`, tasks });