gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  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 +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,7 +1,9 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { existsSync, type FSWatcher, watch } from "node:fs";
3
4
  import {
4
5
  access,
6
+ lstat,
5
7
  mkdir,
6
8
  readFile,
7
9
  readdir,
@@ -10,9 +12,12 @@ import {
10
12
  writeFile,
11
13
  } from "node:fs/promises";
12
14
  import { homedir } from "node:os";
13
- import { basename, join, normalize, relative, sep } from "node:path";
15
+ import { basename, delimiter, dirname, join, normalize, relative, resolve, sep } from "node:path";
14
16
  import { fileURLToPath } from "node:url";
17
+ import { promisify } from "node:util";
15
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { isHomeOrBroadUserDirectory } from "../lib/bounded-writer-admission.ts";
20
+ import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
16
21
 
17
22
  const REGISTRY_REL_PATH = ".atl/skill-registry.md";
18
23
  const CACHE_REL_PATH = ".atl/.skill-registry.cache.json";
@@ -311,11 +316,65 @@ interface RegenResult {
311
316
  regenerated: boolean;
312
317
  skillCount: number;
313
318
  reason: string;
319
+ warning?: string;
320
+ }
321
+
322
+ const execFileAsync = promisify(execFile);
323
+ // A tracking check that cannot answer (for example a repository rooted at HOME)
324
+ // fails closed through the ordinary detection-failure path instead of hanging.
325
+ let gitTimeoutMs = 5000;
326
+
327
+ // A failed Git lookup is genuinely non-Git only when no ancestor has metadata.
328
+ // lstat also detects dangling redirects; permission failures remain fail-closed.
329
+ async function hasNoGitMetadata(cwd: string): Promise<boolean> {
330
+ if (process.env.GIT_DIR || process.env.GIT_WORK_TREE) return false;
331
+ const ceilings = (process.env.GIT_CEILING_DIRECTORIES ?? "").split(delimiter).filter(Boolean).map((path) => resolve(path));
332
+ let directory = resolve(cwd);
333
+ for (;;) {
334
+ // Git does not inspect ancestors at or above its discovery ceiling.
335
+ if (directory !== resolve(cwd) && ceilings.includes(directory)) return true;
336
+ try {
337
+ await lstat(join(directory, ".git"));
338
+ return false;
339
+ } catch (error) {
340
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false;
341
+ }
342
+ const parent = dirname(directory);
343
+ if (parent === directory) return true;
344
+ directory = parent;
345
+ }
346
+ }
347
+
348
+ // Freshness (including forced self-heal) never grants automatic write permission.
349
+ async function automaticWriteWarning(
350
+ cwd: string,
351
+ targets: string[],
352
+ remedy = "Run /skill-registry:refresh to deliberately regenerate runtime files.",
353
+ ): Promise<string | undefined> {
354
+ let tracked: string[];
355
+ try {
356
+ const { stdout } = await execFileAsync("git", ["ls-files", "--cached", "-z", "--", ...targets], {
357
+ cwd,
358
+ env: { ...process.env, LC_ALL: "C" },
359
+ timeout: gitTimeoutMs,
360
+ });
361
+ tracked = stdout.split("\0").filter(Boolean);
362
+ } catch (error) {
363
+ const failure = error as { code?: number; stderr?: string };
364
+ if (failure.code === 128 && failure.stderr?.startsWith("fatal: not a git repository") && await hasNoGitMetadata(cwd)) return undefined;
365
+ throw new Error(`Cannot verify Git tracking for ${targets.join(", ")}; automatic writes skipped. Resolve Git detection, or deliberately run /skill-registry:refresh.`);
366
+ }
367
+ if (tracked.length === 0) return undefined;
368
+ return `Skill registry automatic writes skipped for tracked targets: ${tracked.join(", ")}. ${remedy}`;
314
369
  }
315
370
 
316
- async function ensureAtlIgnored(cwd: string): Promise<void> {
371
+ async function ensureAtlIgnored(cwd: string, intentional = false): Promise<string | undefined> {
317
372
  const atlDir = join(cwd, ".atl");
318
373
  const gitignorePath = join(atlDir, ".gitignore");
374
+ if (!intentional) {
375
+ const warning = await automaticWriteWarning(cwd, [".atl/.gitignore"]);
376
+ if (warning) return warning;
377
+ }
319
378
  if (await pathExists(gitignorePath)) {
320
379
  const existing = await readFile(gitignorePath, "utf8");
321
380
  const ignoreRules = existing
@@ -355,7 +414,7 @@ async function nextLegacyDisabledPath(cwd: string): Promise<string> {
355
414
  return `${base}.${Date.now()}`;
356
415
  }
357
416
 
358
- async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
417
+ async function quarantineLegacyProjectRegistry(cwd: string, notifyWarning: (message: string) => void): Promise<boolean> {
359
418
  const legacyPath = join(cwd, LEGACY_PROJECT_REGISTRY_REL_PATH);
360
419
  if (!(await pathExists(legacyPath))) return false;
361
420
  let source = "";
@@ -366,6 +425,15 @@ async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
366
425
  }
367
426
  if (!isGeneratedLegacyProjectRegistry(source)) return false;
368
427
  const disabledPath = await nextLegacyDisabledPath(cwd);
428
+ const warning = await automaticWriteWarning(
429
+ cwd,
430
+ [LEGACY_PROJECT_REGISTRY_REL_PATH, relative(cwd, disabledPath).split(sep).join("/")],
431
+ "Review and manually handle the tracked legacy extension (and any tracked quarantine destination) before retrying startup; refresh only regenerates registry runtime files.",
432
+ );
433
+ if (warning) {
434
+ notifyWarning(warning);
435
+ return false;
436
+ }
369
437
  try {
370
438
  await rename(legacyPath, disabledPath);
371
439
  return true;
@@ -377,6 +445,7 @@ async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
377
445
  async function regenerateRegistry(
378
446
  cwd: string,
379
447
  force: boolean,
448
+ intentional = false,
380
449
  ): Promise<RegenResult> {
381
450
  const existingDirs = await uniqueExistingDirs([
382
451
  ...projectSkillDirs(cwd),
@@ -401,6 +470,10 @@ async function regenerateRegistry(
401
470
  if (!force && cached === fp && (await pathExists(registryPath))) {
402
471
  return { regenerated: false, skillCount: 0, reason: "cache-hit" };
403
472
  }
473
+ if (!intentional) {
474
+ const warning = await automaticWriteWarning(cwd, [REGISTRY_REL_PATH, CACHE_REL_PATH]);
475
+ if (warning) return { regenerated: false, skillCount: 0, reason: "tracked-target", warning };
476
+ }
404
477
  const entries: SkillEntry[] = [];
405
478
  for (const file of files) {
406
479
  const entry = await loadSkill(file);
@@ -423,6 +496,17 @@ async function regenerateRegistry(
423
496
  }
424
497
 
425
498
  const watchedCwds = new Set<string>();
499
+ // Startup regeneration runs after session_start returns; watcher refreshes
500
+ // wait for it so an older startup scan never overwrites a newer refresh.
501
+ const pendingStartups = new Map<string, Promise<void>>();
502
+ let startupGeneration = 0;
503
+
504
+ // A repository rooted at HOME (or a broad user folder) makes every Git call
505
+ // scan the user's whole tree. gentle-ai owns the single user-facing warning.
506
+ function repositoryRootedAtHome(cwd: string): boolean {
507
+ const root = resolveSessionWorktree(cwd, cwd)?.root;
508
+ return root !== undefined && isHomeOrBroadUserDirectory(root);
509
+ }
426
510
 
427
511
  function isTruthyEnv(value: string | undefined): boolean {
428
512
  return value === "1" || value === "true" || value === "yes" || value === "on";
@@ -438,6 +522,9 @@ function shouldSkipSkillRegistryStartup(
438
522
  env = process.env,
439
523
  ): boolean {
440
524
  return (
525
+ // gentle-shell#1690: delegated children share the parent's cwd; the
526
+ // parent owns .atl/ writes, the legacy rename and the watcher.
527
+ env.GENTLE_PI_AGENTS_CHILD === "1" ||
441
528
  pi.getFlag(NO_SKILL_REGISTRY_FLAG) === true ||
442
529
  isTruthyEnv(env[NO_SKILL_REGISTRY_ENV]) ||
443
530
  hasCliArg(argv, "--no-skills", "-ns")
@@ -461,7 +548,7 @@ function extensionSourcePath(source: string): string | undefined {
461
548
  function shouldSkipDuplicateExtensionLoad(
462
549
  source = import.meta.url,
463
550
  cwd = process.cwd(),
464
- state = globalThis as typeof globalThis & SkillRegistryExtensionGlobal,
551
+ state: SkillRegistryExtensionGlobal = globalThis as typeof globalThis & SkillRegistryExtensionGlobal,
465
552
  ): boolean {
466
553
  const currentPath = extensionSourcePath(source);
467
554
  const projectLocalPath = comparablePath(join(cwd, "extensions", "skill-registry.ts"));
@@ -492,7 +579,7 @@ function closeSkillRegistryWatchers(): void {
492
579
 
493
580
  async function startSkillRegistryWatcher(
494
581
  cwd: string,
495
- notify: (message: string) => void,
582
+ notify: (message: string, level?: "info" | "warning") => void,
496
583
  ): Promise<void> {
497
584
  if (watchedCwds.has(cwd)) return;
498
585
  watchedCwds.add(cwd);
@@ -506,12 +593,14 @@ async function startSkillRegistryWatcher(
506
593
  timer = setTimeout(() => {
507
594
  void (async () => {
508
595
  try {
596
+ await pendingStartups.get(cwd);
509
597
  const result = await regenerateRegistry(cwd, false);
598
+ if (result.warning) notify(result.warning, "warning");
510
599
  if (result.regenerated) {
511
- notify(`Skill registry refreshed (${result.skillCount} skills)`);
600
+ notify(`Skill registry refreshed (${result.skillCount} skills)`, "info");
512
601
  }
513
- } catch {
514
- // Keep the watcher best-effort; session_start/manual refresh surfaces detailed failures.
602
+ } catch (error) {
603
+ notify(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
515
604
  }
516
605
  })();
517
606
  }, WATCH_DEBOUNCE_MS);
@@ -519,6 +608,13 @@ async function startSkillRegistryWatcher(
519
608
  for (const dir of dirs) {
520
609
  try {
521
610
  const watcher = watch(dir, { recursive: true }, refresh);
611
+ watcher.on("error", (error) => {
612
+ // Recursive rescans can fail after watch() returns (e.g. a removed
613
+ // assets directory). Without this listener Node terminates Pi.
614
+ watcher.close();
615
+ activeWatchers.delete(watcher);
616
+ notify(`Skill registry watcher stopped: ${error.message}. Run /skill-registry:refresh to update manually.`, "warning");
617
+ });
522
618
  activeWatchers.add(watcher);
523
619
  } catch {
524
620
  // Some filesystems do not support recursive watches; session_start/manual refresh still work.
@@ -545,12 +641,21 @@ export const __testing = {
545
641
  activeWatcherCount() {
546
642
  return activeWatchers.size;
547
643
  },
644
+ async startupSettled(): Promise<void> {
645
+ await Promise.all(pendingStartups.values());
646
+ },
647
+ setGitTimeoutMs(ms: number): number {
648
+ const previous = gitTimeoutMs;
649
+ gitTimeoutMs = ms;
650
+ return previous;
651
+ },
548
652
  };
549
653
 
550
654
  export default function (pi: ExtensionAPI) {
551
655
  if (shouldSkipDuplicateExtensionLoad()) return;
552
656
 
553
657
  pi.on("session_shutdown", () => {
658
+ startupGeneration += 1;
554
659
  closeSkillRegistryWatchers();
555
660
  });
556
661
 
@@ -562,47 +667,71 @@ export default function (pi: ExtensionAPI) {
562
667
 
563
668
  pi.on("session_start", async (_event, ctx) => {
564
669
  if (shouldSkipSkillRegistryStartup(pi)) return;
565
- try {
566
- await ensureAtlIgnored(ctx.cwd);
567
- const quarantinedLegacy = await quarantineLegacyProjectRegistry(ctx.cwd);
568
- const result = await regenerateRegistry(ctx.cwd, quarantinedLegacy);
569
- if (result.regenerated && ctx.hasUI) {
570
- ctx.ui.notify(
571
- `Skill registry refreshed (${result.skillCount} skills)`,
572
- "info",
573
- );
574
- }
575
- if (quarantinedLegacy && ctx.hasUI) {
576
- ctx.ui.notify(
577
- "Disabled stale project-local skill registry extension; using package registry with project skills first.",
578
- "warning",
579
- );
670
+ if (repositoryRootedAtHome(ctx.cwd)) return;
671
+ const cwd = ctx.cwd;
672
+ const generation = startupGeneration;
673
+ const notifyWarning = (message: string) => {
674
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
675
+ };
676
+ const regenerate = async () => {
677
+ if (generation !== startupGeneration) return;
678
+ try {
679
+ const ignoreWarning = await ensureAtlIgnored(cwd);
680
+ if (ignoreWarning) notifyWarning(ignoreWarning);
681
+ const quarantinedLegacy = await quarantineLegacyProjectRegistry(cwd, notifyWarning);
682
+ const result = await regenerateRegistry(cwd, quarantinedLegacy);
683
+ if (result.warning) notifyWarning(result.warning);
684
+ if (result.regenerated && ctx.hasUI) {
685
+ ctx.ui.notify(
686
+ `Skill registry refreshed (${result.skillCount} skills)`,
687
+ "info",
688
+ );
689
+ }
690
+ if (quarantinedLegacy && ctx.hasUI) {
691
+ ctx.ui.notify(
692
+ "Disabled stale project-local skill registry extension; using package registry with project skills first.",
693
+ "warning",
694
+ );
695
+ }
696
+ if (quarantinedLegacy) {
697
+ setTimeout(() => {
698
+ if (generation !== startupGeneration) return;
699
+ void (async () => {
700
+ try {
701
+ const result = await regenerateRegistry(cwd, true);
702
+ if (result.warning) notifyWarning(result.warning);
703
+ } catch (error) {
704
+ notifyWarning(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`);
705
+ }
706
+ })();
707
+ }, WATCH_DEBOUNCE_MS);
708
+ }
709
+ } catch (error) {
710
+ if (ctx.hasUI) {
711
+ const message =
712
+ error instanceof Error ? error.message : String(error);
713
+ ctx.ui.notify(
714
+ `Skill registry refresh failed: ${message}`,
715
+ "warning",
716
+ );
717
+ }
580
718
  }
719
+ };
720
+ // Pi awaits session_start handlers one after another, so the Git tracking
721
+ // checks and the skill scan run after startup instead of inside it.
722
+ const startup = new Promise<void>((resolve) => setImmediate(resolve))
723
+ .then(regenerate)
724
+ .catch(() => { /* A replaced session's stale UI must not surface here. */ })
725
+ .finally(() => { if (pendingStartups.get(cwd) === startup) pendingStartups.delete(cwd); });
726
+ pendingStartups.set(cwd, startup);
727
+ try {
581
728
  if (ctx.hasUI) {
582
- await startSkillRegistryWatcher(ctx.cwd, (message) => {
583
- ctx.ui.notify(message, "info");
729
+ await startSkillRegistryWatcher(cwd, (message, level = "info") => {
730
+ ctx.ui.notify(message, level);
584
731
  });
585
732
  }
586
- if (quarantinedLegacy) {
587
- setTimeout(() => {
588
- void (async () => {
589
- try {
590
- await regenerateRegistry(ctx.cwd, true);
591
- } catch {
592
- // Best-effort same-session self-heal in case the stale extension already ran.
593
- }
594
- })();
595
- }, WATCH_DEBOUNCE_MS);
596
- }
597
733
  } catch (error) {
598
- if (ctx.hasUI) {
599
- const message =
600
- error instanceof Error ? error.message : String(error);
601
- ctx.ui.notify(
602
- `Skill registry refresh failed: ${message}`,
603
- "warning",
604
- );
605
- }
734
+ notifyWarning(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`);
606
735
  }
607
736
  });
608
737
 
@@ -610,8 +739,8 @@ export default function (pi: ExtensionAPI) {
610
739
  description: "Regenerate .atl/skill-registry.md from local skill sources.",
611
740
  handler: async (_args, ctx) => {
612
741
  try {
613
- await ensureAtlIgnored(ctx.cwd);
614
- const result = await regenerateRegistry(ctx.cwd, true);
742
+ await ensureAtlIgnored(ctx.cwd, true);
743
+ const result = await regenerateRegistry(ctx.cwd, true, true);
615
744
  ctx.ui.notify(
616
745
  `Skill registry: ${result.skillCount} skill(s) written to ${REGISTRY_REL_PATH}`,
617
746
  "info",
@@ -7,6 +7,7 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { resolveAnimationPolicy } from "../lib/animation-policy.ts";
9
9
  import { PI_SUBCOMMANDS } from "../lib/gentle-shell-launcher.ts";
10
+ import { bundledGentlePiConfigHome } from "../lib/gentle-shell-bundled.ts";
10
11
 
11
12
 
12
13
  export type BannerColor = "pink" | "cyan" | "yellow" | "green";
@@ -67,7 +68,7 @@ function rgb(r: number, g: number, b: number, text: string): string {
67
68
  }
68
69
 
69
70
  function gentleAiConfigHome(): string {
70
- return process.env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai");
71
+ return bundledGentlePiConfigHome(process.env) ?? process.env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai");
71
72
  }
72
73
 
73
74
  function bannerConfigPath(configHome = gentleAiConfigHome()): string {
@@ -548,12 +549,17 @@ async function countPackageExtensions(packages: unknown[]): Promise<number> {
548
549
  return count;
549
550
  }
550
551
 
551
- export function readGitBranch(cwd: string, run: typeof execFile = execFile): Promise<string> {
552
+ // A repository rooted at HOME can make even this lookup crawl; a timeout reads
553
+ // as the ordinary failure instead of holding the banner stats.
554
+ const GIT_BRANCH_TIMEOUT_MS = 3000;
555
+
556
+ export function readGitBranch(cwd: string, run: typeof execFile = execFile, timeoutMs = GIT_BRANCH_TIMEOUT_MS): Promise<string> {
552
557
  return new Promise((resolve) => {
553
558
  run("git", ["-C", cwd, "branch", "--show-current"], {
554
559
  encoding: "utf8",
555
560
  shell: false,
556
561
  windowsHide: true,
562
+ timeout: timeoutMs,
557
563
  }, (error, stdout) => {
558
564
  if (error) {
559
565
  resolve("Not a git repo");
@@ -647,6 +653,9 @@ export default function (pi: ExtensionAPI) {
647
653
  pi.on("session_start", async (_event, ctx) => {
648
654
  disposeHeader();
649
655
  if (!ctx.hasUI) return;
656
+ // Delegated rpc children report hasUI=true but have no terminal to paint
657
+ // (gentle-shell#1690); do not rely on a piped stdout lacking rows/columns.
658
+ if (process.env.GENTLE_PI_AGENTS_CHILD === "1") return;
650
659
 
651
660
  // CLI subcommands such as `pi update` or `pi install` skip the animated intro.
652
661
  if (isPiCliSubcommandInvocation(process.argv)) return;
package/lib/agent-home.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { bundledGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.ts";
3
4
 
4
5
  // Pi Subagents resolves its global directory as `PI_CODING_AGENT_DIR || ~/.pi/agent`,
5
6
  // so an empty value must fall through here too or the two homes diverge again.
@@ -11,6 +12,14 @@ export function resolveGentlePiAgentHome(env: NodeJS.ProcessEnv = process.env):
11
12
  // commands own lives here. It is defined once because the launch-time profile pin
12
13
  // resolver reads the same profiles store the `/gentle:profiles` panel writes, and
13
14
  // two spellings of the override would silently read two different stores.
15
+ // A bundled Gentle Shell keeps it inside its prefix (S9, bundledGentlePiConfigHome).
14
16
  export function gentlePiConfigHome(env: NodeJS.ProcessEnv = process.env): string {
15
- return env.GENTLE_PI_CONFIG_HOME || join(homedir(), ".pi", "gentle-ai");
17
+ return bundledGentlePiConfigHome(env) ?? (env.GENTLE_PI_CONFIG_HOME || join(homedir(), ".pi", "gentle-ai"));
18
+ }
19
+
20
+ // The agent home Pi runs with, only in a bundled Gentle Shell (S9): there the
21
+ // extensions that otherwise write under ~/.pi/agent keep their files in it.
22
+ // Undefined elsewhere, where they keep their existing locations.
23
+ export function bundledGentlePiAgentHome(env: NodeJS.ProcessEnv = process.env): string | undefined {
24
+ return runningBundledPrefix(env) === undefined ? undefined : resolveGentlePiAgentHome(env);
16
25
  }
@@ -0,0 +1,80 @@
1
+ import { join, resolve } from "node:path";
2
+ import { resolveGentlePiAgentHome } from "./agent-home.ts";
3
+ import { resolveProfilePin } from "./agent-profile-pin.ts";
4
+ import {
5
+ discoverAgents,
6
+ loadAgentsConfig,
7
+ resolveAgentProfile,
8
+ withPinnedModelProfiles,
9
+ type AgentDefinition,
10
+ type DiscoveryRoots,
11
+ type ModelRef,
12
+ type ResolvedProfile,
13
+ } from "./agents-config.ts";
14
+ import type { WorktreeResolver } from "./session-worktree-registry.ts";
15
+
16
+ // gentle-shell#1731: subagent launches and the harness model-routing fact share
17
+ // one resolution of the effective subagent model, so the fact the orchestrator
18
+ // reads can never disagree with the model a launch would actually use.
19
+
20
+ export interface AgentHomeSelection {
21
+ env: NodeJS.ProcessEnv;
22
+ home: string;
23
+ /** An explicit agent home, kept literal. */
24
+ agentHome?: string;
25
+ /** An explicit home selects `<home>/.pi/agent` and ignores the environment. */
26
+ homeOverridden?: boolean;
27
+ }
28
+
29
+ /** The agent home Gentle Agents discovers definitions and `subagents.json` from. */
30
+ export function resolveAgentHomeDirectory(selection: AgentHomeSelection): string {
31
+ const selectedHome = selection.agentHome ?? (selection.homeOverridden ? join(selection.home, ".pi", "agent") : resolveGentlePiAgentHome(selection.env));
32
+ // Expand environment tildes like Pi, but leave explicit path APIs literal.
33
+ const environmentHome = selection.agentHome === undefined && !selection.homeOverridden;
34
+ const expandedHome = environmentHome && selectedHome === "~" ? selection.home
35
+ : environmentHome && (selectedHome.startsWith("~/") || (process.platform === "win32" && selectedHome.startsWith("~\\"))) ? join(selection.home, selectedHome.slice(2)) : selectedHome;
36
+ return resolve(expandedHome);
37
+ }
38
+
39
+ export interface AgentProfileScope {
40
+ roots: DiscoveryRoots;
41
+ /** The directory whose repository profile pin applies to the launch. */
42
+ pinCwd: string;
43
+ configHome: string;
44
+ resolveWorktree?: WorktreeResolver;
45
+ }
46
+
47
+ /**
48
+ * The launch profile for an agent: global and project `subagents.json`, with a
49
+ * repository profile pin replacing `modelProfiles` wholesale when one applies.
50
+ */
51
+ export function resolvePinnedAgentProfile(agent: AgentDefinition, scope: AgentProfileScope): ResolvedProfile {
52
+ const config = withPinnedModelProfiles(
53
+ loadAgentsConfig(scope.roots),
54
+ resolveProfilePin({ cwd: scope.pinCwd, configHome: scope.configHome, resolveWorktree: scope.resolveWorktree })?.modelProfiles,
55
+ );
56
+ return resolveAgentProfile(agent, config);
57
+ }
58
+
59
+ export interface EffectiveAgentModel {
60
+ agent: string;
61
+ model: ModelRef | undefined;
62
+ /** No profile, definition, or default routes the agent: it inherits the session model. */
63
+ inherited: boolean;
64
+ }
65
+
66
+ /**
67
+ * The model a launch of `agentName` would use, falling back to the session model
68
+ * when nothing routes it; `undefined` when the agent is not discovered.
69
+ */
70
+ export function resolveEffectiveAgentModel(
71
+ agentName: string,
72
+ scope: AgentProfileScope & { fallback?: ModelRef },
73
+ ): EffectiveAgentModel | undefined {
74
+ const agent = discoverAgents(scope.roots).agents.find((candidate) => candidate.name === agentName);
75
+ if (!agent) return undefined;
76
+ const profile = resolvePinnedAgentProfile(agent, scope);
77
+ return profile.model === undefined
78
+ ? { agent: agentName, model: scope.fallback, inherited: true }
79
+ : { agent: agentName, model: profile.model, inherited: false };
80
+ }
@@ -406,18 +406,28 @@ export interface ProfileListItem {
406
406
  description: string;
407
407
  }
408
408
 
409
+ /** One list item per saved profile, named with the markers that explain what a launch would use: the global `(active)` claim, plus `(pinned)` for the pin winner or `(session)` for this session's binding when either applies. */
409
410
  export function buildProfileListItems(
410
411
  file: AgentProfilesFile,
411
412
  pinned?: string,
413
+ sessionBound?: string,
412
414
  ): ProfileListItem[] {
413
415
  return Object.entries(file.profiles).map(([name, config]) => {
414
416
  const roles = profileRoleEntries(config).length;
415
417
  const active = name === file.active ? `${name} (active)` : name;
418
+ // A pinned profile is the one this repository launches with, which is not the
419
+ // same thing as the globally active profile, so both are named. A session
420
+ // binding outranks the pin in this list (slice 1 stores it; slice 2,
421
+ // gentle-shell#1558, makes launches resolve it), so its marker wins on the
422
+ // same profile.
423
+ const label = name === sessionBound
424
+ ? `${active} (session)`
425
+ : name === pinned
426
+ ? `${active} (pinned)`
427
+ : active;
416
428
  return {
417
429
  id: name,
418
- // A pinned profile is the one this repository launches with, which is not the
419
- // same thing as the globally active profile, so both are named.
420
- label: name === pinned ? `${active} (pinned)` : active,
430
+ label,
421
431
  description: `${roles} ${roles === 1 ? "role" : "roles"}`,
422
432
  };
423
433
  });
@@ -11,9 +11,10 @@
11
11
  // behavior is unit-testable against a fake clock.
12
12
 
13
13
  /**
14
- * A completion older than this at flush time is stale: the parent had many
15
- * turns to pull the result with subagent_result, so replaying it into the
16
- * model context would re-enter state the conversation may already have used.
14
+ * A completion older than this at flush time is stale: replaying its full
15
+ * report into the model context could re-enter state the conversation may
16
+ * already have used, so the parent gets a compact notice instead. The parent
17
+ * is told never to poll, so a stale completion must still announce itself.
17
18
  * Ninety seconds comfortably covers one slow orchestrator turn (model latency
18
19
  * plus a few tool calls) while staying orders of magnitude below the 50-58
19
20
  * minute followUp delays measured in issue #867.
@@ -36,6 +37,8 @@ export interface CompletionQueue<T extends { id: string }> {
36
37
  consume(taskId: string): void;
37
38
  /** Remove and return the still-pending completions in settle order, each with its staleness decision at `now`. Consumed entries are dropped and never returned. */
38
39
  takeDeliverable(now: number): Array<DeliverableCompletion<T>>;
40
+ /** Return a taken completion whose forwarding failed to the pending queue, so a later flush retries it. A consumed id stays dropped. */
41
+ requeue(entry: DeliverableCompletion<T>): void;
39
42
  /** Discard everything: pending completions and consumed/delivered bookkeeping. Used on session start and shutdown so nothing replays after a resume. */
40
43
  dropAll(): void;
41
44
  }
@@ -63,6 +66,11 @@ export function createCompletionQueue<T extends { id: string }>(): CompletionQue
63
66
  }
64
67
  return deliverable;
65
68
  },
69
+ requeue(entry) {
70
+ delivered.delete(entry.task.id);
71
+ if (consumed.has(entry.task.id) || pending.some((pendingEntry) => pendingEntry.task.id === entry.task.id)) return;
72
+ pending.push({ task: entry.task, settledAt: entry.settledAt });
73
+ },
66
74
  dropAll() {
67
75
  pending = [];
68
76
  consumed.clear();
@@ -95,7 +95,7 @@ function parseAck(value: unknown): AckFrame | undefined {
95
95
  if (!record(value) || !exact(value, ["id", "kind", "accepted", "error"]) || !validChildId(value.id) || value.kind !== "ack" || typeof value.accepted !== "boolean") return undefined;
96
96
  if (value.accepted && value.error !== undefined) return undefined;
97
97
  if (value.error !== undefined && !boundedText(value.error, CHILD_ACK_ERROR_MAX_BYTES)) return undefined;
98
- return { id: value.id, accepted: value.accepted, ...(value.error === undefined ? {} : { error: value.error }) };
98
+ return { id: value.id, accepted: value.accepted, ...(typeof value.error === "string" ? { error: value.error } : {}) };
99
99
  }
100
100
 
101
101
  function parseReply(value: unknown): ReplyFrame | undefined {
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { sanitizeTerminalText } from "./terminal-theme.ts";
3
3
 
4
4
  // Gentle Agents protocol. A child pi process streams RPC events; the host
@@ -148,10 +148,23 @@ export interface TaskSummary {
148
148
 
149
149
  export type TaskListener = (task: TaskRecord, thread: TaskThread) => void;
150
150
  export type SummaryListener = (summary: TaskSummary) => void;
151
+ export interface TaskStatusChange {
152
+ readonly producerId: string;
153
+ readonly sessionId: string;
154
+ readonly parentSessionId: string;
155
+ readonly taskId: string;
156
+ readonly runId: string;
157
+ readonly status: TaskStatus;
158
+ readonly previousStatus?: TaskStatus;
159
+ readonly sequence: number;
160
+ }
161
+ export type StatusChangeListener = (change: TaskStatusChange) => void | Promise<void>;
151
162
 
152
163
  const DEFAULT_LIMITS: ThreadLimits = { maxItems: 400, maxOutputChars: 16_000 };
153
164
  /** Child UI requests that block on an answer; everything else (notify, setStatus, setWidget) is noise here. */
154
165
  export const DIALOG_METHODS: ReadonlySet<string> = new Set(["select", "confirm", "input", "editor"]);
166
+ /** The one child notify that reaches the task thread: requested --tools the child does not have (#1690). */
167
+ export const MISSING_TOOLS_NOTE_PREFIX = "gentle-agents: requested tools missing in child:";
155
168
  const LABEL_MAX = 72;
156
169
  const TEXT_CAP = 20_000;
157
170
  const ELLIPSIS = "…";
@@ -330,6 +343,7 @@ export function normalizeRpcEvent(raw: unknown, options: { observeResponses?: bo
330
343
  case "agent_settled":
331
344
  return [{ type: TASK_EVENT.AGENT_SETTLED }];
332
345
  case "extension_ui_request":
346
+ if (event.method === "notify" && typeof event.message === "string" && event.message.startsWith(MISSING_TOOLS_NOTE_PREFIX)) return [{ type: TASK_EVENT.NOTE, text: clean(event.message) }];
333
347
  return DIALOG_METHODS.has(String(event.method)) ? [{ type: TASK_EVENT.ASK, request: askRequest(event) }] : [];
334
348
  case "auto_retry_start":
335
349
  return [{ type: TASK_EVENT.NOTE, text: `retrying (${String(event.attempt ?? "?")}/${String(event.maxAttempts ?? "?")})` }];
@@ -460,14 +474,21 @@ export class TaskStore {
460
474
  private readonly listeners = new Map<string, Set<TaskListener>>();
461
475
  private readonly summaryListeners = new Set<SummaryListener>();
462
476
  private readonly limits: Partial<ThreadLimits>;
477
+ private readonly producerId = randomUUID();
478
+ private sequence = 0;
479
+ private readonly runIds = new Map<string, string>();
480
+ private readonly statusListeners = new Set<StatusChangeListener>();
463
481
 
464
482
  constructor(limits: Partial<ThreadLimits> = {}) {
465
483
  this.limits = limits;
466
484
  }
467
485
 
468
486
  add(task: TaskRecord): void {
487
+ const previous = this.tasks.get(task.id)?.status;
488
+ if (!this.tasks.has(task.id)) this.runIds.set(task.id, randomUUID());
469
489
  this.tasks.set(task.id, task);
470
490
  this.threads.set(task.id, emptyThread(this.limits));
491
+ this.notifyStatus(task, previous);
471
492
  this.notifySummary();
472
493
  }
473
494
 
@@ -476,6 +497,7 @@ export class TaskStore {
476
497
  restore(task: TaskRecord, thread: TaskThread): boolean {
477
498
  if (this.tasks.has(task.id)) return false;
478
499
  this.tasks.set(task.id, task);
500
+ this.runIds.set(task.id, randomUUID());
479
501
  this.threads.set(task.id, { ...thread, limits: { ...DEFAULT_LIMITS, ...this.limits } });
480
502
  this.notifySummary();
481
503
  return true;
@@ -506,6 +528,7 @@ export class TaskStore {
506
528
  if (!current) return undefined;
507
529
  const next = { ...current, ...patch };
508
530
  this.tasks.set(id, next);
531
+ this.notifyStatus(next, current.status);
509
532
  this.notifyTask(next);
510
533
  if (next.status !== current.status) this.notifySummary();
511
534
  return next;
@@ -533,6 +556,22 @@ export class TaskStore {
533
556
  return () => this.summaryListeners.delete(listener);
534
557
  }
535
558
 
559
+ /** Optional, content-free observer. Restores and unchanged snapshots are silent. */
560
+ subscribeStatusChanges(listener: StatusChangeListener): () => void {
561
+ this.statusListeners.add(listener);
562
+ return () => { this.statusListeners.delete(listener); };
563
+ }
564
+
565
+ private notifyStatus(task: TaskRecord, previousStatus?: TaskStatus): void {
566
+ if (task.status === previousStatus) return;
567
+ const change: TaskStatusChange = Object.freeze({ producerId: this.producerId,
568
+ sessionId: task.parentSessionId, parentSessionId: task.parentSessionId, taskId: task.id,
569
+ runId: this.runIds.get(task.id)!, status: task.status, previousStatus, sequence: ++this.sequence });
570
+ for (const listener of [...this.statusListeners]) {
571
+ try { void Promise.resolve(listener(change)).catch(() => {}); } catch { /* Observability cannot fail a mutation. */ }
572
+ }
573
+ }
574
+
536
575
  private notifyTask(task: TaskRecord): void {
537
576
  const set = this.listeners.get(task.id);
538
577
  if (!set) return;