gentle-pi 4.0.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 (398) hide show
  1. package/README.md +122 -8
  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 +703 -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/bundled-install.md +397 -0
  28. package/docs/delegated-verification.md +3 -5
  29. package/docs/gentle-agents-activity.md +513 -0
  30. package/docs/gentle-shell-uninstall.md +136 -0
  31. package/docs/gentle-shell.md +81 -31
  32. package/docs/install-wizard.md +2040 -0
  33. package/docs/native-authority-architecture.md +1 -1
  34. package/docs/permission-modes.md +82 -0
  35. package/docs/prompt-history.md +1 -1
  36. package/docs/readme-reference.md +267 -40
  37. package/docs/render-cache-performance.md +28 -0
  38. package/docs/sound-notifications-proposal.md +191 -0
  39. package/docs/sound-notifications.md +114 -0
  40. package/docs/yolo-mode.md +9 -9
  41. package/extensions/child-context.ts +11 -2
  42. package/extensions/child-safety.ts +4 -2
  43. package/extensions/codegraph-tools.ts +18 -5
  44. package/extensions/gentle-agents.ts +688 -88
  45. package/extensions/gentle-ai.ts +886 -238
  46. package/extensions/gentle-herdr-activity.ts +81 -0
  47. package/extensions/gentle-notifications.ts +104 -0
  48. package/extensions/gentle-shell.ts +265 -69
  49. package/extensions/gentle-todo.ts +108 -13
  50. package/extensions/history/index.ts +11 -6
  51. package/extensions/pi-pretty.ts +39 -3
  52. package/extensions/quiet-tools.ts +91 -21
  53. package/extensions/skill-registry.ts +175 -46
  54. package/extensions/startup-banner.ts +11 -2
  55. package/lib/agent-home.ts +10 -1
  56. package/lib/agent-model-resolution.ts +80 -0
  57. package/lib/agent-profiles.ts +13 -3
  58. package/lib/agents-completion-delivery.ts +11 -3
  59. package/lib/agents-messaging.ts +1 -1
  60. package/lib/agents-protocol.ts +40 -1
  61. package/lib/agents-runner.ts +90 -15
  62. package/lib/auto-permission-classifier.ts +280 -0
  63. package/lib/auto-permission-runtime.ts +268 -0
  64. package/lib/background-jobs-tools.ts +202 -0
  65. package/lib/background-jobs.ts +253 -0
  66. package/lib/background-monitor.ts +154 -0
  67. package/lib/bounded-writer-admission.ts +83 -10
  68. package/lib/builtin-codemode-optout.ts +166 -0
  69. package/lib/card-content-policy.ts +85 -0
  70. package/lib/child-package-injection.ts +62 -0
  71. package/lib/codemode-renderer.ts +14 -4
  72. package/lib/command-palette-catalog.ts +3 -2
  73. package/lib/context-threshold.ts +26 -0
  74. package/lib/gentle-ai-binary.ts +109 -4
  75. package/lib/gentle-shell-bundled.ts +517 -0
  76. package/lib/gentle-shell-launcher.ts +74 -23
  77. package/lib/gentle-shell-startup-flow.ts +54 -0
  78. package/lib/gentle-shell-startup-update.ts +92 -0
  79. package/lib/gentle-shell-uninstall.ts +840 -0
  80. package/lib/gentle-shell-update-panel.ts +231 -0
  81. package/lib/herdr-activity.ts +183 -0
  82. package/lib/inprocess-reviewer.ts +3 -0
  83. package/lib/jobs-sidebar-state.ts +20 -0
  84. package/lib/jobs-view.ts +161 -0
  85. package/lib/model-price-ratio.ts +83 -0
  86. package/lib/nan-provider.ts +33 -11
  87. package/lib/native-review-cli.ts +194 -73
  88. package/lib/notification-audio-native.ts +162 -0
  89. package/lib/notification-audio-windows.ts +399 -0
  90. package/lib/notification-audio.ts +332 -0
  91. package/lib/notification-customize.ts +241 -0
  92. package/lib/notification-events.ts +113 -0
  93. package/lib/notification-policy.ts +148 -0
  94. package/lib/notification-pulse-client.ts +367 -0
  95. package/lib/notification-pulse-protocol.ts +305 -0
  96. package/lib/notification-pulse-stream.ts +213 -0
  97. package/lib/notification-pulse-worker.ts +66 -0
  98. package/lib/notification-scheduler.ts +173 -0
  99. package/lib/notification-service.ts +202 -0
  100. package/lib/notification-ui.ts +87 -0
  101. package/lib/odd-phase.ts +1 -1
  102. package/lib/orchestrator-alias.ts +20 -0
  103. package/lib/orchestrator-catalog.ts +79 -0
  104. package/lib/orchestrator-consultation.ts +61 -0
  105. package/lib/orchestrator-discovery.ts +49 -0
  106. package/lib/orchestrator-helper-consent.ts +122 -0
  107. package/lib/orchestrator-helper.ts +167 -0
  108. package/lib/orchestrator-message-card.ts +105 -0
  109. package/lib/orchestrator-presence.ts +115 -14
  110. package/lib/orchestrator-scope.ts +80 -0
  111. package/lib/orchestrator-state.ts +107 -0
  112. package/lib/orchestrator-tool-card.ts +280 -0
  113. package/lib/orchestrator-work-search.ts +218 -0
  114. package/lib/orchestrator-work.ts +79 -0
  115. package/lib/permission-mode-policy.ts +59 -0
  116. package/lib/review-candidate-view-owner.ts +8 -6
  117. package/lib/review-candidate-view.ts +3 -2
  118. package/lib/review-compact-contract.ts +2 -2
  119. package/lib/review-host-relay.ts +7 -2
  120. package/lib/review-integration-v2.ts +100 -22
  121. package/lib/review-publication-gate.ts +1 -1
  122. package/lib/review-risk-assessment.ts +77 -51
  123. package/lib/review-sidebar-state.ts +2 -1
  124. package/lib/review-transaction.ts +72 -3
  125. package/lib/runtime-metrics-children.ts +1 -1
  126. package/lib/runtime-metrics-native.ts +3 -2
  127. package/lib/selection-engine.ts +18 -6
  128. package/lib/session-change-capture.ts +5 -4
  129. package/lib/session-changes.ts +27 -22
  130. package/lib/session-profile-binding.ts +83 -0
  131. package/lib/session-profile-freeze.ts +266 -0
  132. package/lib/session-worktree-registry.ts +74 -8
  133. package/lib/shell-bar.ts +41 -11
  134. package/lib/shell-changes-view.ts +40 -13
  135. package/lib/shell-changes.ts +86 -71
  136. package/lib/shell-sidebar-layout.ts +5 -1
  137. package/lib/shell-todo.ts +62 -19
  138. package/lib/shell-usage.ts +4 -3
  139. package/lib/status-timing-diagnostics.ts +201 -0
  140. package/lib/telemetry-trigger.ts +3 -1
  141. package/lib/vim-editor-adapter.ts +7 -3
  142. package/lib/visual-customization-policy.ts +20 -9
  143. package/lib/visual-customize-view.ts +171 -20
  144. package/lib/writer-surfaces.ts +162 -0
  145. package/lib/yolo-session-policy.ts +198 -121
  146. package/package.json +1 -1
  147. package/runtime/child-package-injection.mjs +63 -0
  148. package/runtime/gentle-ai-binary.mjs +109 -4
  149. package/runtime/gentle-shell-bundled.mjs +518 -0
  150. package/runtime/gentle-shell-launcher.mjs +73 -22
  151. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  152. package/runtime/gentle-shell-startup-update.mjs +93 -0
  153. package/runtime/gentle-shell-uninstall.mjs +841 -0
  154. package/runtime/gentle-shell-update-panel.mjs +232 -0
  155. package/runtime/native-review-cli.mjs +191 -70
  156. package/runtime/review-integration-v2.mjs +100 -22
  157. package/runtime/review-risk-assessment.mjs +77 -51
  158. package/runtime/status-timing-diagnostics.mjs +202 -0
  159. package/runtime/telemetry-trigger.mjs +3 -1
  160. package/scripts/bootstrap.cmd +255 -0
  161. package/scripts/bootstrap.sh +168 -0
  162. package/scripts/build-distribution.mjs +157 -0
  163. package/scripts/build-installer-bundles.mjs +136 -0
  164. package/scripts/build-runtime-modules.mjs +7 -0
  165. package/scripts/bundled-install.mjs +1308 -0
  166. package/scripts/bundled-wizard.mjs +399 -0
  167. package/scripts/check-types.mjs +4 -6
  168. package/scripts/gentle-ai-installer.mjs +79 -32
  169. package/scripts/install-gentle-ai.mjs +48 -1
  170. package/scripts/install-tui-mode-setting.mjs +10 -1
  171. package/scripts/install-wizard-preview.mjs +154 -0
  172. package/scripts/installer-downloads.mjs +648 -0
  173. package/scripts/installer-preflight.mjs +387 -0
  174. package/scripts/installer-probes.mjs +564 -0
  175. package/scripts/installer-runner.mjs +1102 -0
  176. package/scripts/installer-server.mjs +881 -0
  177. package/scripts/installer-windows-artifacts.json +15 -0
  178. package/scripts/installer-windows.mjs +752 -0
  179. package/scripts/main-channel.mjs +452 -0
  180. package/scripts/npm-pack-result.mjs +31 -0
  181. package/scripts/render-cache-performance-worker.mjs +205 -0
  182. package/scripts/render-cache-performance.mjs +115 -0
  183. package/scripts/run-test-suite.mjs +22 -8
  184. package/scripts/test-installer-acceptance.sh +161 -0
  185. package/scripts/test-packed-runner.mjs +23 -5
  186. package/scripts/types-baseline.json +2 -92
  187. package/scripts/verify-package-files.mjs +82 -18
  188. package/skills/gentle-ai/SKILL.md +12 -11
  189. package/tests/agent-home.test.ts +56 -3
  190. package/tests/agent-model-resolution.test.ts +113 -0
  191. package/tests/agent-profiles.test.ts +12 -0
  192. package/tests/agent-risk-escalation.test.ts +123 -0
  193. package/tests/agents-completion-delivery.test.ts +17 -0
  194. package/tests/agents-protocol.test.ts +57 -0
  195. package/tests/agents-queries.test.ts +9 -9
  196. package/tests/agents-runner-process.test.ts +31 -0
  197. package/tests/agents-runner.test.ts +89 -4
  198. package/tests/auto-mode-invariants.test.ts +258 -0
  199. package/tests/auto-mode-sdk.test.ts +57 -0
  200. package/tests/auto-mode.test.ts +49 -0
  201. package/tests/auto-permission-classifier.test.ts +283 -0
  202. package/tests/auto-permission-runtime.test.ts +196 -0
  203. package/tests/background-cache-warming.test.ts +2 -1
  204. package/tests/background-jobs.test.ts +227 -0
  205. package/tests/background-monitor.test.ts +195 -0
  206. package/tests/background-subagents-default-mode.test.ts +28 -5
  207. package/tests/bounded-stop-contract.test.ts +85 -0
  208. package/tests/bounded-writer-admission.test.ts +52 -1
  209. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  210. package/tests/bridge-wake-rendering.test.ts +56 -0
  211. package/tests/build-distribution.test.ts +269 -0
  212. package/tests/builtin-codemode-optout.test.ts +242 -0
  213. package/tests/bundled-install.test.ts +1319 -0
  214. package/tests/bundled-wizard.test.ts +505 -0
  215. package/tests/card-content-policy.test.ts +68 -0
  216. package/tests/card-style-policy.test.ts +34 -2
  217. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  218. package/tests/child-package-entrypoints.test.ts +65 -0
  219. package/tests/child-package-injection.test.ts +74 -0
  220. package/tests/child-safety.test.ts +12 -0
  221. package/tests/child-web-package.test.ts +224 -0
  222. package/tests/codegraph-tools.test.ts +35 -0
  223. package/tests/codemode-rendering.test.ts +107 -2
  224. package/tests/command-palette.test.ts +5 -5
  225. package/tests/context-threshold.test.ts +31 -0
  226. package/tests/delegate-for-reason-contract.test.ts +327 -0
  227. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  228. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  229. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  230. package/tests/engram-safety.test.ts +288 -0
  231. package/tests/generic-agent-tools.test.ts +16 -1
  232. package/tests/gentle-agents.test.ts +1722 -59
  233. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  234. package/tests/gentle-ai-binary.test.ts +19 -2
  235. package/tests/gentle-ai-child-guards.test.ts +202 -0
  236. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  237. package/tests/gentle-ai-installer.test.ts +548 -58
  238. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  239. package/tests/gentle-ai-renderer.test.ts +6 -6
  240. package/tests/gentle-ai.test.ts +999 -31
  241. package/tests/gentle-herdr-activity.test.ts +100 -0
  242. package/tests/gentle-notifications.test.ts +427 -0
  243. package/tests/gentle-shell-bin.test.ts +249 -5
  244. package/tests/gentle-shell-bundled.test.ts +1002 -0
  245. package/tests/gentle-shell-launcher.test.ts +234 -13
  246. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  247. package/tests/gentle-shell-startup-update.test.ts +85 -0
  248. package/tests/gentle-shell-uninstall.test.ts +978 -0
  249. package/tests/gentle-shell-update-panel.test.ts +158 -0
  250. package/tests/gentle-shell.test.ts +541 -96
  251. package/tests/gentle-todo.test.ts +253 -2
  252. package/tests/herdr-activity.test.ts +243 -0
  253. package/tests/history-agent-home.test.ts +81 -0
  254. package/tests/history-child-guard.test.ts +70 -0
  255. package/tests/inprocess-reviewer.test.ts +20 -0
  256. package/tests/install-gentle-ai-failure.test.ts +69 -0
  257. package/tests/install-tui-mode-setting.test.ts +15 -0
  258. package/tests/install-wizard.test.ts +1223 -0
  259. package/tests/installer-bundles.test.ts +122 -0
  260. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  261. package/tests/installer-preflight.test.ts +750 -0
  262. package/tests/installer-probes.test.ts +1106 -0
  263. package/tests/installer-runner.test.ts +2158 -0
  264. package/tests/installer-server.test.ts +1567 -0
  265. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  266. package/tests/jobs-view.test.ts +160 -0
  267. package/tests/main-channel-upgrade.test.ts +389 -0
  268. package/tests/main-channel.test.ts +344 -0
  269. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  270. package/tests/model-price-ratio.test.ts +116 -0
  271. package/tests/model-profiles-preservation.test.ts +153 -0
  272. package/tests/model-routing-package.test.ts +10 -0
  273. package/tests/nan-provider-runtime.test.ts +156 -0
  274. package/tests/nan-provider.test.ts +28 -18
  275. package/tests/native-binary-gate.test.ts +2 -0
  276. package/tests/native-review-capability-contract.test.ts +7 -1
  277. package/tests/native-review-cli.test.ts +236 -1
  278. package/tests/native-review-consent.test.ts +2 -2
  279. package/tests/native-review-parity-runtime.test.ts +1 -1
  280. package/tests/native-review-parity.test.ts +12 -48
  281. package/tests/native-risk-evidence-parity.test.ts +21 -0
  282. package/tests/no-self-pause-contract.test.ts +87 -0
  283. package/tests/notification-audio-native.test.ts +281 -0
  284. package/tests/notification-audio-windows.test.ts +581 -0
  285. package/tests/notification-audio.test.ts +548 -0
  286. package/tests/notification-customize.test.ts +655 -0
  287. package/tests/notification-events.test.ts +115 -0
  288. package/tests/notification-policy.test.ts +201 -0
  289. package/tests/notification-pulse-client.test.ts +356 -0
  290. package/tests/notification-pulse-protocol.test.ts +213 -0
  291. package/tests/notification-pulse-stream.test.ts +320 -0
  292. package/tests/notification-scheduler.test.ts +258 -0
  293. package/tests/notification-ui.test.ts +157 -0
  294. package/tests/npm-pack-result.test.ts +41 -0
  295. package/tests/odd-integration.test.ts +14 -5
  296. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  297. package/tests/odd-routing-contract.test.ts +80 -23
  298. package/tests/orchestrator-budget.test.ts +29 -6
  299. package/tests/orchestrator-catalog.test.ts +146 -0
  300. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  301. package/tests/orchestrator-consultation.test.ts +111 -0
  302. package/tests/orchestrator-discovery.test.ts +241 -0
  303. package/tests/orchestrator-helper-consent.test.ts +171 -0
  304. package/tests/orchestrator-helper.test.ts +297 -0
  305. package/tests/orchestrator-message-card.test.ts +190 -0
  306. package/tests/orchestrator-presence.test.ts +23 -0
  307. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  308. package/tests/orchestrator-scope.test.ts +107 -0
  309. package/tests/orchestrator-state.test.ts +114 -0
  310. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  311. package/tests/orchestrator-tool-card.test.ts +398 -0
  312. package/tests/orchestrator-work-search.test.ts +318 -0
  313. package/tests/orchestrator-work.test.ts +147 -0
  314. package/tests/package-manifest.test.ts +101 -17
  315. package/tests/packed-runner-owned-path.test.ts +35 -1
  316. package/tests/parallel-reason-contract.test.ts +21 -0
  317. package/tests/parallel-writer-admission.test.ts +210 -0
  318. package/tests/permission-mode-policy.test.ts +194 -0
  319. package/tests/persona-single-channel.test.ts +4 -12
  320. package/tests/pi-pretty.test.ts +56 -0
  321. package/tests/provider-defect-handoff.test.ts +4 -3
  322. package/tests/quiet-tool-rendering.test.ts +160 -3
  323. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  324. package/tests/render-cache-performance.test.ts +48 -0
  325. package/tests/review-agent-end-preflight.test.ts +62 -10
  326. package/tests/review-candidate-view.test.ts +101 -12
  327. package/tests/review-contract-prompt.test.ts +33 -4
  328. package/tests/review-controller-native-recovery.test.ts +327 -17
  329. package/tests/review-controller-native-routing.test.ts +255 -1
  330. package/tests/review-controller-workspace-root.test.ts +43 -2
  331. package/tests/review-controller.test.ts +2 -2
  332. package/tests/review-correction-lifecycle.test.ts +2 -1
  333. package/tests/review-gate.test.ts +7 -4
  334. package/tests/review-graph-schema.test.ts +2 -2
  335. package/tests/review-host-relay-routing.test.ts +5 -3
  336. package/tests/review-host-relay.test.ts +49 -2
  337. package/tests/review-integration-v2-forward.test.ts +21 -1
  338. package/tests/review-json-arguments.test.ts +167 -0
  339. package/tests/review-last-event-closure.test.ts +157 -8
  340. package/tests/review-ledger-contract.test.ts +5 -4
  341. package/tests/review-policy-judgment-day.test.ts +1 -1
  342. package/tests/review-policy-ordinary.test.ts +2 -2
  343. package/tests/review-relay-transport-agent.test.ts +66 -1
  344. package/tests/review-risk-assessment.test.ts +109 -33
  345. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  346. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  347. package/tests/review-test-fixtures.ts +7 -1
  348. package/tests/review-transaction.test.ts +18 -1
  349. package/tests/run-test-suite.test.ts +83 -5
  350. package/tests/runtime-harness.mjs +42 -15
  351. package/tests/runtime-metrics-extension.test.ts +1 -1
  352. package/tests/runtime-metrics-native.test.ts +31 -1
  353. package/tests/selection-engine.test.ts +30 -0
  354. package/tests/session-change-capture.test.ts +10 -10
  355. package/tests/session-changes-shell.test.ts +242 -48
  356. package/tests/session-changes.test.ts +17 -24
  357. package/tests/session-profile-binding.test.ts +163 -0
  358. package/tests/session-profile-freeze.test.ts +381 -0
  359. package/tests/session-subject-routing-contract.test.ts +34 -0
  360. package/tests/session-worktree-registry.test.ts +57 -2
  361. package/tests/shell-bar.test.ts +40 -0
  362. package/tests/shell-changes-view.test.ts +85 -34
  363. package/tests/shell-changes.test.ts +61 -62
  364. package/tests/shell-render-cache-runtime.test.ts +69 -0
  365. package/tests/shell-render-cache.test.ts +129 -0
  366. package/tests/shell-sidebar-layout.test.ts +27 -2
  367. package/tests/shell-todo.test.ts +120 -2
  368. package/tests/shell-usage-view.test.ts +1 -1
  369. package/tests/shell-usage.test.ts +35 -0
  370. package/tests/short-check-routing-contract.test.ts +49 -0
  371. package/tests/skill-registry-watcher.test.ts +119 -0
  372. package/tests/skill-registry.test.ts +311 -5
  373. package/tests/startup-banner.test.ts +117 -3
  374. package/tests/startup-home-repo.test.ts +170 -0
  375. package/tests/status-timing-diagnostics.test.ts +283 -0
  376. package/tests/support/orchestrator-modules.ts +19 -0
  377. package/tests/task-size-routing-contract.test.ts +241 -0
  378. package/tests/telemetry-trigger.test.ts +27 -1
  379. package/tests/test-strength-contract.test.ts +124 -0
  380. package/tests/verify-agent-contract.test.ts +90 -0
  381. package/tests/verify-first-launch-contract.test.ts +139 -0
  382. package/tests/verify-package-files.test.ts +56 -0
  383. package/tests/vim-editor-adapter.test.ts +7 -7
  384. package/tests/visual-customization-policy.test.ts +32 -3
  385. package/tests/visual-customize-view.test.ts +288 -10
  386. package/tests/writer-edit-surface-scope.test.ts +59 -2
  387. package/tests/writer-surfaces.test.ts +199 -0
  388. package/tests/yolo-customize.test.ts +18 -3
  389. package/tests/yolo-mode-runtime.test.ts +37 -10
  390. package/tests/yolo-mode.test.ts +32 -4
  391. package/tests/yolo-session-policy.test.ts +1 -1
  392. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  393. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  394. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  395. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  396. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  397. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  398. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,8 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import test from "node:test";
6
+ import { CHILD_PACKAGE_INJECTION_ENV, encodeChildPackageInjection, parseChildPackageInjection } from "../lib/child-package-injection.ts";
6
7
  import {
7
8
  MIN_PI_VERSION,
8
9
  MIN_SETUP_GENTLE_AI_VERSION,
@@ -154,6 +155,50 @@ test("parseLauncherArgs recognises the home subcommand as argv[0] and captures t
154
155
  assert.deepEqual(parsed.passthrough, []);
155
156
  });
156
157
 
158
+ test("parseLauncherArgs recognises the upgrade subcommand as argv[0] and captures its arguments", () => {
159
+ const parsed = parseLauncherArgs(["upgrade", "--channel", "main"]);
160
+ assert.equal(parsed.command, "upgrade");
161
+ assert.deepEqual(parsed.commandArgs, ["--channel", "main"]);
162
+ assert.deepEqual(parsed.passthrough, []);
163
+ assert.equal(parsed.piSubcommand, undefined);
164
+ });
165
+
166
+ test("parseLauncherArgs recognises the self-uninstall subcommand as argv[0] and captures its arguments", () => {
167
+ const parsed = parseLauncherArgs(["self-uninstall", "--dry-run", "--include-shared"]);
168
+ assert.equal(parsed.command, "self-uninstall");
169
+ assert.deepEqual(parsed.commandArgs, ["--dry-run", "--include-shared"]);
170
+ assert.deepEqual(parsed.passthrough, []);
171
+ assert.equal(parsed.piSubcommand, undefined);
172
+ assert.equal(parsed.error, undefined);
173
+ });
174
+
175
+ test("parseLauncherArgs keeps uninstall as pi's own alias for remove", () => {
176
+ const parsed = parseLauncherArgs(["uninstall", "npm:x"]);
177
+ assert.equal(parsed.command, undefined);
178
+ assert.equal(parsed.piSubcommand, "uninstall");
179
+ assert.deepEqual(parsed.passthrough, ["uninstall", "npm:x"]);
180
+ });
181
+
182
+ test("help text documents gentle-shell self-uninstall and its flags", () => {
183
+ const text = helpText();
184
+ assert.ok(text.includes(" gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]"));
185
+ assert.ok(text.includes(" self-uninstall Remove Gentle Shell: its own data, the isolated home and the gentle-pi"));
186
+ assert.match(text, /'gentle-shell uninstall <source>' is pi's own alias for 'remove'/);
187
+ });
188
+
189
+ test("help text documents gentle-shell upgrade and its channels", () => {
190
+ const text = helpText();
191
+ assert.ok(text.includes(" gentle-shell upgrade [--channel release|main]"));
192
+ assert.ok(text.includes(" upgrade Update Gentle Shell along its channel: the latest release, or the latest"));
193
+ });
194
+
195
+ test("help text documents the bundled upgrade, its rollback and the pinned Pi update", () => {
196
+ const text = helpText();
197
+ assert.ok(text.includes(" gentle-shell upgrade --rollback"));
198
+ assert.match(text, /bundled install.*release channel only.*--rollback switches back to the previous kept version/s);
199
+ assert.match(text, /In a bundled install Pi ships pinned: 'update' without a package\n\s+runs 'gentle-shell upgrade'/);
200
+ });
201
+
157
202
  test("parseLauncherArgs treats home as a plain passthrough token when it is not argv[0]", () => {
158
203
  const parsed = parseLauncherArgs(["--isolated", "home"]);
159
204
  assert.equal(parsed.command, undefined);
@@ -310,6 +355,22 @@ test("resolveHome falls back to <homedir>/.pi/agent for --link with no override"
310
355
  assert.deepEqual(resolved, { mode: "link", dir: join("/home/alan", ".pi", "agent"), source: "flag" });
311
356
  });
312
357
 
358
+ test("--link inside an isolated session resolves the preserved original Pi home (#2015)", () => {
359
+ const nestedEnv = { PI_CODING_AGENT_DIR: "/home/alan/.gentle-shell/agent", GENTLE_SHELL_USER_PI_HOME: "/custom/pi" };
360
+ // Nested: the inherited isolated home must not be re-selected.
361
+ assert.deepEqual(
362
+ resolveHome({ args: args({ link: true }), env: nestedEnv, homedir: "/home/alan", config: undefined }),
363
+ { mode: "link", dir: "/custom/pi", source: "flag" },
364
+ );
365
+ assert.deepEqual(
366
+ resolveHome({ args: args(), env: nestedEnv, homedir: "/home/alan", config: { mode: "link" } }),
367
+ { mode: "link", dir: "/custom/pi", source: "config" },
368
+ );
369
+ // Clean terminal: a custom override and the conventional home are unchanged.
370
+ assert.equal(resolveHome({ args: args({ link: true }), env: { PI_CODING_AGENT_DIR: "/custom/pi" }, homedir: "/home/alan", config: undefined }).dir, "/custom/pi");
371
+ assert.equal(resolveHome({ args: args({ link: true }), env: {}, homedir: "/home/alan", config: undefined }).dir, join("/home/alan", ".pi", "agent"));
372
+ });
373
+
313
374
  test("resolveHome honours --isolated and reads GENTLE_SHELL_HOME", () => {
314
375
  const resolved = resolveHome({
315
376
  args: args({ isolated: true }),
@@ -852,7 +913,7 @@ test("findGentlePiDeclaration detects a bare npm:gentle-pi object source entry",
852
913
  });
853
914
 
854
915
  test("findGentlePiDeclaration recognises a relative path entry whose package.json name is gentle-pi", () => {
855
- const resolvedDir = join("/agent", "..", "..", "work", "gentle-pi");
916
+ const resolvedDir = resolve("/agent", "..", "..", "work", "gentle-pi");
856
917
  const result = findGentlePiDeclaration('{"packages":["../../work/gentle-pi"]}', {
857
918
  agentDir: "/agent",
858
919
  readPackageName: readPackageNameStub({ [resolvedDir]: "gentle-pi" }),
@@ -863,17 +924,17 @@ test("findGentlePiDeclaration recognises a relative path entry whose package.jso
863
924
  test("findGentlePiDeclaration recognises an absolute path entry whose package.json name is gentle-pi", () => {
864
925
  const result = findGentlePiDeclaration('{"packages":["/checkouts/gentle-pi"]}', {
865
926
  agentDir: "/agent",
866
- readPackageName: readPackageNameStub({ "/checkouts/gentle-pi": "gentle-pi" }),
927
+ readPackageName: readPackageNameStub({ [resolve("/checkouts/gentle-pi")]: "gentle-pi" }),
867
928
  });
868
- assert.deepEqual(result, { kind: "path", dir: "/checkouts/gentle-pi" });
929
+ assert.deepEqual(result, { kind: "path", dir: resolve("/checkouts/gentle-pi") });
869
930
  });
870
931
 
871
932
  test("findGentlePiDeclaration recognises a path object source entry whose package.json name is gentle-pi", () => {
872
933
  const result = findGentlePiDeclaration('{"packages":[{"source":"../gentle-pi","extensions":[]}]}', {
873
934
  agentDir: "/agent",
874
- readPackageName: readPackageNameStub({ [join("/agent", "..", "gentle-pi")]: "gentle-pi" }),
935
+ readPackageName: readPackageNameStub({ [resolve("/agent", "..", "gentle-pi")]: "gentle-pi" }),
875
936
  });
876
- assert.deepEqual(result, { kind: "path", dir: join("/agent", "..", "gentle-pi") });
937
+ assert.deepEqual(result, { kind: "path", dir: resolve("/agent", "..", "gentle-pi") });
877
938
  });
878
939
 
879
940
  test("findGentlePiDeclaration is undefined for a path entry whose package.json name is not gentle-pi", () => {
@@ -906,9 +967,9 @@ test("findGentlePiDeclaration is undefined when packages lists unrelated entries
906
967
  test("findGentlePiDeclaration returns the first matching declaration, path or npm, in list order", () => {
907
968
  const result = findGentlePiDeclaration('{"packages":["../gentle-pi","npm:gentle-pi"]}', {
908
969
  agentDir: "/agent",
909
- readPackageName: readPackageNameStub({ [join("/agent", "..", "gentle-pi")]: "gentle-pi" }),
970
+ readPackageName: readPackageNameStub({ [resolve("/agent", "..", "gentle-pi")]: "gentle-pi" }),
910
971
  });
911
- assert.deepEqual(result, { kind: "path", dir: join("/agent", "..", "gentle-pi") });
972
+ assert.deepEqual(result, { kind: "path", dir: resolve("/agent", "..", "gentle-pi") });
912
973
  });
913
974
 
914
975
  // --- decideTakeOver -----------------------------------------------------------
@@ -992,7 +1053,7 @@ test("otherPackageInjections resolves a path entry relative to agentDir", () =>
992
1053
  agentDir: "/agent",
993
1054
  skip: { kind: "npm" },
994
1055
  });
995
- assert.deepEqual(result.paths, [join("/agent", "..", "..", "work", "pi-qwen-ambassador")]);
1056
+ assert.deepEqual(result.paths, [resolve("/agent", "..", "..", "work", "pi-qwen-ambassador")]);
996
1057
  });
997
1058
 
998
1059
  test("otherPackageInjections skips a git entry and warns", () => {
@@ -1018,7 +1079,7 @@ test("otherPackageInjections warns for an object entry with extensions or autolo
1018
1079
  });
1019
1080
 
1020
1081
  test("otherPackageInjections skips the entry matching a path declaration being taken over", () => {
1021
- const declaredDir = join("/agent", "..", "..", "work", "gentle-pi");
1082
+ const declaredDir = resolve("/agent", "..", "..", "work", "gentle-pi");
1022
1083
  const result = otherPackageInjections({
1023
1084
  settingsText: '{"packages":["npm:some-other","../../work/gentle-pi"]}',
1024
1085
  agentDir: "/agent",
@@ -1033,7 +1094,7 @@ test("otherPackageInjections tolerates undefined or malformed settings text", ()
1033
1094
  });
1034
1095
 
1035
1096
  test("otherPackageInjections skips every gentle-pi entry, not only the one matching the declaration kind", () => {
1036
- const declaredDir = join("/agent", "..", "..", "work", "gentle-pi");
1097
+ const declaredDir = resolve("/agent", "..", "..", "work", "gentle-pi");
1037
1098
  const result = otherPackageInjections({
1038
1099
  // The declaration being taken over is the path entry, but settings
1039
1100
  // also carries a second, unrelated npm:gentle-pi entry: both must be
@@ -1088,7 +1149,7 @@ test("otherPackageInjections keeps a declared package directory that isDirectory
1088
1149
  });
1089
1150
 
1090
1151
  test("otherPackageInjections excludes a settings path entry that resolves via realpath to the same physical directory as a realpath'd skip (R4-forced-root-symlink-double-injection)", () => {
1091
- const linkOtherDir = join("/agent", "link-other");
1152
+ const linkOtherDir = resolve("/agent", "link-other");
1092
1153
  const result = otherPackageInjections({
1093
1154
  settingsText: '{"packages":["link-other","npm:some-other"]}',
1094
1155
  agentDir: "/agent",
@@ -1107,7 +1168,7 @@ test("otherPackageInjections keeps comparing raw strings when no realpath resolv
1107
1168
  agentDir: "/agent",
1108
1169
  skip: { kind: "path", dir: "/real/other" },
1109
1170
  });
1110
- assert.deepEqual(result.paths, [join("/agent", "link-other"), join("/agent", "npm", "node_modules", "some-other")]);
1171
+ assert.deepEqual(result.paths, [resolve("/agent", "link-other"), join("/agent", "npm", "node_modules", "some-other")]);
1111
1172
  });
1112
1173
 
1113
1174
  test("otherPackageInjections defaults to including every declared package when isDirectory is not provided (existing callers keep pure string resolution)", () => {
@@ -1134,6 +1195,7 @@ test("package injection relies on Pi's -e resource discovery in isolated and tak
1134
1195
  passthrough: [],
1135
1196
  baseEnv: {},
1136
1197
  homedir: "/home/u",
1198
+ cwd: "/work",
1137
1199
  });
1138
1200
  assert.deepEqual(built.args, takeOver ? ["--no-extensions", "-e", "/pkg"] : ["-e", "/pkg"]);
1139
1201
  }
@@ -1153,6 +1215,7 @@ test("buildPiInvocation injects the launcher env into baseEnv", () => {
1153
1215
  passthrough: [],
1154
1216
  baseEnv: { PATH: "/usr/bin" },
1155
1217
  homedir: "/home/u",
1218
+ cwd: "/work",
1156
1219
  });
1157
1220
  assert.deepEqual(built.env, { PATH: "/usr/bin", PI_CODING_AGENT_DIR: "/pi/agent", GENTLE_PI_AGENT_HOME: "/pi/agent", GENTLE_SHELL_USER_PI_HOME: join("/home/u", ".pi", "agent") });
1158
1221
  });
@@ -1168,6 +1231,7 @@ test("buildPiInvocation skips injection when there is a declaration and no takeo
1168
1231
  passthrough: ["--mode", "rpc"],
1169
1232
  baseEnv: {},
1170
1233
  homedir: "/home/u",
1234
+ cwd: "/work",
1171
1235
  });
1172
1236
  assert.deepEqual(built.command, "/usr/bin/pi");
1173
1237
  assert.deepEqual(built.args, ["--mode", "rpc"]);
@@ -1184,6 +1248,7 @@ test("buildPiInvocation adds the gentle-pi injection flags when there is no decl
1184
1248
  passthrough: ["--mode", "rpc"],
1185
1249
  baseEnv: {},
1186
1250
  homedir: "/home/u",
1251
+ cwd: "/work",
1187
1252
  });
1188
1253
  assert.deepEqual(built.args, [
1189
1254
  "-e",
@@ -1205,6 +1270,7 @@ test("buildPiInvocation emits only runtime args and passthrough when a pi subcom
1205
1270
  piSubcommand: "install",
1206
1271
  baseEnv: {},
1207
1272
  homedir: "/home/u",
1273
+ cwd: "/work",
1208
1274
  });
1209
1275
  assert.deepEqual(built.args, ["install", "npm:x"]);
1210
1276
  assert.equal(built.command, "/usr/bin/pi");
@@ -1222,6 +1288,7 @@ test("buildPiInvocation still injects the launcher env for a pi subcommand", ()
1222
1288
  piSubcommand: "list",
1223
1289
  baseEnv: { PATH: "/usr/bin" },
1224
1290
  homedir: "/home/u",
1291
+ cwd: "/work",
1225
1292
  });
1226
1293
  assert.deepEqual(built.env, {
1227
1294
  PATH: "/usr/bin",
@@ -1243,6 +1310,7 @@ test("buildPiInvocation in link mode with a pi subcommand is exactly pi <subcomm
1243
1310
  piSubcommand: "auth",
1244
1311
  baseEnv: { PI_CODING_AGENT_DIR: "/pi/agent" },
1245
1312
  homedir: "/home/u",
1313
+ cwd: "/work",
1246
1314
  });
1247
1315
  assert.equal(built.command, "/usr/bin/pi");
1248
1316
  assert.deepEqual(built.args, ["auth", "status"]);
@@ -1263,6 +1331,7 @@ test("buildPiInvocation carries the user's original Pi home without weakening is
1263
1331
  passthrough: [],
1264
1332
  baseEnv,
1265
1333
  homedir: "/home/u",
1334
+ cwd: "/work",
1266
1335
  }).env;
1267
1336
  const conventional = build({});
1268
1337
  assert.equal(conventional.PI_CODING_AGENT_DIR, "/gentle-shell/agent");
@@ -1297,6 +1366,7 @@ test("buildPiInvocation keeps the runtime's own args ahead of the injection and
1297
1366
  passthrough: [],
1298
1367
  baseEnv: {},
1299
1368
  homedir: "/home/u",
1369
+ cwd: "/work",
1300
1370
  });
1301
1371
  assert.equal(built.args[0], "/bundled/cli.js");
1302
1372
  assert.equal(built.command, "/usr/bin/node");
@@ -1313,6 +1383,7 @@ test("buildPiInvocation takes over a conflicting path declaration: --no-extensio
1313
1383
  passthrough: ["--mode", "rpc"],
1314
1384
  baseEnv: {},
1315
1385
  homedir: "/home/u",
1386
+ cwd: "/work",
1316
1387
  });
1317
1388
  assert.deepEqual(built.args, [
1318
1389
  "--no-extensions",
@@ -1336,6 +1407,7 @@ test("buildPiInvocation takes over with --package-root even for a matching npm d
1336
1407
  passthrough: [],
1337
1408
  baseEnv: {},
1338
1409
  homedir: "/home/u",
1410
+ cwd: "/work",
1339
1411
  });
1340
1412
  assert.deepEqual(built.args, [
1341
1413
  "--no-extensions",
@@ -1355,6 +1427,7 @@ test("buildPiInvocation takes over with --package-root even when there is no dec
1355
1427
  passthrough: [],
1356
1428
  baseEnv: {},
1357
1429
  homedir: "/home/u",
1430
+ cwd: "/work",
1358
1431
  });
1359
1432
  assert.deepEqual(built.args, [
1360
1433
  "--no-extensions",
@@ -1377,6 +1450,7 @@ test("buildPiInvocation injects loose extension entries during a takeover, after
1377
1450
  passthrough: ["--mode", "rpc"],
1378
1451
  baseEnv: {},
1379
1452
  homedir: "/home/u",
1453
+ cwd: "/work",
1380
1454
  });
1381
1455
  assert.deepEqual(built.args, [
1382
1456
  "--no-extensions",
@@ -1404,6 +1478,7 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
1404
1478
  passthrough: [],
1405
1479
  baseEnv: {},
1406
1480
  homedir: "/home/u",
1481
+ cwd: "/work",
1407
1482
  });
1408
1483
  assert.deepEqual(withoutField.args, ["--no-extensions", "-e", "/pkg"]);
1409
1484
 
@@ -1418,6 +1493,7 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
1418
1493
  passthrough: [],
1419
1494
  baseEnv: {},
1420
1495
  homedir: "/home/u",
1496
+ cwd: "/work",
1421
1497
  });
1422
1498
  assert.deepEqual(withEmptyField.args, withoutField.args);
1423
1499
  });
@@ -1439,6 +1515,7 @@ test("buildPiInvocation dedupes loose extension entries against other-package pa
1439
1515
  passthrough: [],
1440
1516
  baseEnv: {},
1441
1517
  homedir: "/home/u",
1518
+ cwd: "/work",
1442
1519
  });
1443
1520
  assert.deepEqual(built.args, [
1444
1521
  "--no-extensions",
@@ -1468,6 +1545,7 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
1468
1545
  passthrough: [],
1469
1546
  baseEnv: {},
1470
1547
  homedir: "/home/u",
1548
+ cwd: "/work",
1471
1549
  });
1472
1550
  const eFlags = built.args.filter((arg, index) => built.args[index - 1] === "-e");
1473
1551
  assert.deepEqual(eFlags, [join("/agent", "npm", "node_modules", "some-other"), "/pkg"]);
@@ -1480,6 +1558,127 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
1480
1558
  ]);
1481
1559
  });
1482
1560
 
1561
+ // --- child package injection signal (#1690) ----------------------------------
1562
+ //
1563
+ // The launcher exports exactly the -e set it computed itself, so the subagent
1564
+ // runner can forward it to delegated children. Passthrough -e flags are not
1565
+ // part of the signal.
1566
+
1567
+ const injectionInput = {
1568
+ runtime: { kind: "path" as const, command: "/usr/bin/pi", args: [] },
1569
+ home: isolatedHomeResolved,
1570
+ packageRoot: "/pkg",
1571
+ otherPackagePaths: [],
1572
+ passthrough: ["-e", "/user/ext.ts", "--mode", "rpc"],
1573
+ homedir: "/home/u",
1574
+ cwd: "/work",
1575
+ };
1576
+
1577
+ test("buildPiInvocation signals the packageRoot injection when there is no declaration", () => {
1578
+ const built = buildPiInvocation({ ...injectionInput, declaration: undefined, takeOver: false, baseEnv: {} });
1579
+ assert.deepEqual(parseChildPackageInjection(built.env), { noExtensions: false, extensionPaths: ["/pkg"] });
1580
+ });
1581
+
1582
+ test("buildPiInvocation signals the exact deduped takeover -e set in order, without passthrough -e flags", () => {
1583
+ const built = buildPiInvocation({
1584
+ ...injectionInput,
1585
+ home: linkHome,
1586
+ declaration: { kind: "path", dir: "/other/checkout" },
1587
+ takeOver: true,
1588
+ otherPackagePaths: ["/agent/npm/node_modules/some-other", "/shared/dup.ts", "/pkg"],
1589
+ looseExtensionEntries: ["/shared/dup.ts", "/agent/extensions/a.ts", "/agent/extensions/a.ts"],
1590
+ baseEnv: {},
1591
+ });
1592
+ const launcherPaths = built.args.slice(0, built.args.indexOf("--mode") - 2).filter((_, index, all) => all[index - 1] === "-e");
1593
+ const signal = parseChildPackageInjection(built.env);
1594
+ assert.deepEqual(signal, {
1595
+ noExtensions: true,
1596
+ // /pkg keeps its first (other-package) position, exactly as in argv.
1597
+ extensionPaths: ["/agent/npm/node_modules/some-other", "/shared/dup.ts", "/pkg", "/agent/extensions/a.ts"],
1598
+ });
1599
+ assert.deepEqual(signal?.extensionPaths, launcherPaths);
1600
+ });
1601
+
1602
+ test("buildPiInvocation signals absolute paths resolved against the given cwd when a takeover receives a relative loose entry", () => {
1603
+ const cwd = resolve("/elsewhere", "launch-dir");
1604
+ assert.notEqual(cwd, process.cwd());
1605
+ const built = buildPiInvocation({
1606
+ ...injectionInput,
1607
+ cwd,
1608
+ home: linkHome,
1609
+ declaration: { kind: "npm" },
1610
+ takeOver: true,
1611
+ looseExtensionEntries: [join("relative-home", "extensions", "a.ts")],
1612
+ baseEnv: {},
1613
+ });
1614
+ assert.ok(built.args.includes(join("relative-home", "extensions", "a.ts")), "the -e argv itself is unchanged");
1615
+ assert.deepEqual(parseChildPackageInjection(built.env)?.extensionPaths, [resolve(cwd, "relative-home", "extensions", "a.ts"), "/pkg"]);
1616
+ });
1617
+
1618
+ test("buildPiInvocation signals one entry for a file spelled both relative and absolute, while argv keeps both spellings", () => {
1619
+ const cwd = resolve("/elsewhere", "launch-dir");
1620
+ const relative = join("relative-home", "extensions", "a.ts");
1621
+ const absolute = resolve(cwd, relative);
1622
+ const built = buildPiInvocation({
1623
+ ...injectionInput,
1624
+ cwd,
1625
+ home: linkHome,
1626
+ declaration: { kind: "npm" },
1627
+ takeOver: true,
1628
+ otherPackagePaths: ["/agent/npm/node_modules/some-other", absolute],
1629
+ looseExtensionEntries: [relative, "/agent/extensions/b.ts"],
1630
+ baseEnv: {},
1631
+ });
1632
+ assert.deepEqual(built.args.slice(0, built.args.indexOf("--mode") - 2), [
1633
+ "--no-extensions",
1634
+ "-e",
1635
+ "/agent/npm/node_modules/some-other",
1636
+ "-e",
1637
+ absolute,
1638
+ "-e",
1639
+ relative,
1640
+ "-e",
1641
+ "/agent/extensions/b.ts",
1642
+ "-e",
1643
+ "/pkg",
1644
+ ]);
1645
+ assert.deepEqual(parseChildPackageInjection(built.env)?.extensionPaths, [
1646
+ "/agent/npm/node_modules/some-other",
1647
+ absolute,
1648
+ "/agent/extensions/b.ts",
1649
+ "/pkg",
1650
+ ]);
1651
+ });
1652
+
1653
+ test("buildPiInvocation does not signal when settings already declare gentle-pi, and drops an inherited signal", () => {
1654
+ const stale = encodeChildPackageInjection({ noExtensions: false, extensionPaths: ["/outer/pkg"] });
1655
+ for (const baseEnv of [{}, { [CHILD_PACKAGE_INJECTION_ENV]: stale }]) {
1656
+ const built = buildPiInvocation({ ...injectionInput, home: linkHome, declaration: { kind: "npm" }, takeOver: false, baseEnv });
1657
+ assert.equal(Object.hasOwn(built.env, CHILD_PACKAGE_INJECTION_ENV), false);
1658
+ }
1659
+ });
1660
+
1661
+ test("buildPiInvocation does not signal for a pi subcommand, and drops an inherited signal", () => {
1662
+ const stale = encodeChildPackageInjection({ noExtensions: false, extensionPaths: ["/outer/pkg"] });
1663
+ for (const takeOver of [false, true]) {
1664
+ const built = buildPiInvocation({
1665
+ ...injectionInput,
1666
+ declaration: undefined,
1667
+ takeOver,
1668
+ passthrough: ["list"],
1669
+ piSubcommand: "list",
1670
+ baseEnv: { [CHILD_PACKAGE_INJECTION_ENV]: stale },
1671
+ });
1672
+ assert.equal(Object.hasOwn(built.env, CHILD_PACKAGE_INJECTION_ENV), false);
1673
+ }
1674
+ });
1675
+
1676
+ test("buildPiInvocation replaces an inherited signal with its own injection", () => {
1677
+ const stale = encodeChildPackageInjection({ noExtensions: true, extensionPaths: ["/outer/pkg"] });
1678
+ const built = buildPiInvocation({ ...injectionInput, declaration: undefined, takeOver: false, baseEnv: { [CHILD_PACKAGE_INJECTION_ENV]: stale } });
1679
+ assert.deepEqual(parseChildPackageInjection(built.env), { noExtensions: false, extensionPaths: ["/pkg"] });
1680
+ });
1681
+
1483
1682
  // --- discoverLooseExtensionEntries -------------------------------------------
1484
1683
  //
1485
1684
  // Pure mirror of pi's own discoverExtensionsInDir (packages/coding-agent/src/
@@ -1806,3 +2005,25 @@ test("helpText documents pi's own package-management subcommands", () => {
1806
2005
  assert.match(text, /\bconfig\b/);
1807
2006
  assert.match(text, /\bauth\b/);
1808
2007
  });
2008
+
2009
+ // T3 B3: the isolated-home bootstrap creates ~/.gentle-shell private (0700) even under
2010
+ // umask 002, so the bundled prefix claim (which refuses group/other-writable folders) accepts it.
2011
+ test("the isolated-home bootstrap creates ~/.gentle-shell as 0700 under umask 002", { skip: process.platform === "win32" ? "POSIX modes" : false }, async () => {
2012
+ const { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } = await import("node:fs");
2013
+ const { spawnSync } = await import("node:child_process");
2014
+ const { tmpdir } = await import("node:os");
2015
+ const root = mkdtempSync(join(tmpdir(), "gentle-shell-umask-"));
2016
+ try {
2017
+ const piScript = join(root, "fake-pi.mjs");
2018
+ writeFileSync(piScript, "#!/usr/bin/env node\nif (process.argv.includes('--version')) console.log('0.99.1');\n");
2019
+ chmodSync(piScript, 0o755);
2020
+ const binPath = fileURLToPath(new URL("../bin/gentle-shell.mjs", import.meta.url));
2021
+ const env = { HOME: root, USERPROFILE: root, PATH: process.env.PATH ?? "/usr/bin:/bin", GENTLE_SHELL_PI: piScript, GENTLE_SHELL_NO_AUTO_SETUP: "1" };
2022
+ const result = spawnSync("/bin/sh", ["-c", 'umask 002; exec "$0" "$@"', process.execPath, binPath, "hello"], { env, encoding: "utf8", input: "" });
2023
+ assert.equal(result.status, 0, result.stderr);
2024
+ assert.equal(statSync(join(root, ".gentle-shell")).mode & 0o777, 0o700);
2025
+ assert.equal(statSync(join(root, ".gentle-shell", "agent")).mode & 0o777, 0o700);
2026
+ } finally {
2027
+ rmSync(root, { recursive: true, force: true });
2028
+ }
2029
+ });
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync, realpathSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { applyStartupChoice, omitStartupVersion, resolveStartupRelaunch } from "../lib/gentle-shell-startup-flow.ts";
7
+
8
+ test("relaunch resolves the activated version's own Node and preserves arguments without shell quoting", async (t) => {
9
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "startup-relaunch-")));
10
+ t.after(() => rmSync(root, { recursive: true, force: true }));
11
+ const version = join(root, "versions", "4.2.0-1.2.0");
12
+ const pkg = join(version, "node_modules", "gentle-pi");
13
+ mkdirSync(join(pkg, "scripts"), { recursive: true });
14
+ mkdirSync(join(pkg, "bin"));
15
+ writeFileSync(join(version, ".gentle-shell-version"), "4.2.0-1.2.0\n");
16
+ writeFileSync(join(pkg, "bin", "gentle-shell.mjs"), "// verified launcher\n");
17
+ writeFileSync(join(pkg, "scripts", "bundled-install.mjs"), `export function prefixLayout({root}) { return {node: root + '/runtime/new node/bin/node'}; }`);
18
+ if (process.platform === "win32") writeFileSync(join(root, "current"), "4.2.0-1.2.0\n");
19
+ else symlinkSync("versions/4.2.0-1.2.0", join(root, "current"));
20
+ const argv = ["--home", "path with spaces", "--continue", "a;literal"];
21
+ const result = await resolveStartupRelaunch({ root, home: tmpdir(), platform: process.platform, env: {}, argv });
22
+ assert.equal(result.command, root + "/runtime/new node/bin/node");
23
+ assert.deepEqual(result.args, [join(pkg, "bin", "gentle-shell.mjs"), ...argv]);
24
+ assert.equal(result.env.GENTLE_SHELL_SKIP_STARTUP_UPDATE, "1");
25
+ });
26
+
27
+ function effects(code = 0) {
28
+ const events: string[] = [];
29
+ return { events, omit: () => { events.push("omit"); }, upgrade: async () => { events.push("upgrade"); return code; },
30
+ relaunch: async () => { events.push("relaunch"); return 17; } };
31
+ }
32
+
33
+ test("not now and cancellation never update or persist; cancellation exits 130", async () => {
34
+ const deps = effects();
35
+ assert.equal(await applyStartupChoice("skip", deps), undefined);
36
+ assert.equal(await applyStartupChoice("cancel", deps), 130);
37
+ assert.deepEqual(deps.events, []);
38
+ });
39
+
40
+ test("omit persists only the selected version and launches normally", async () => {
41
+ const deps = effects();
42
+ assert.equal(await applyStartupChoice("omit", deps), undefined);
43
+ assert.deepEqual(deps.events, ["omit"]);
44
+ let stored = '{"home":"link","provisioned":{"a":1},"other":true}';
45
+ omitStartupVersion(() => stored, (text) => { stored = text; }, "4.2.0");
46
+ assert.deepEqual(JSON.parse(stored), { home: "link", provisioned: { a: 1 }, other: true, skippedStartupRelease: "4.2.0" });
47
+ });
48
+
49
+ test("malformed settings are not overwritten by omit; normal launch remains possible", async () => {
50
+ for (const text of ["{broken", "[]", "null", '"text"']) {
51
+ let writes = 0;
52
+ assert.throws(() => omitStartupVersion(() => text, () => { writes++; }, "4.2.0"));
53
+ assert.equal(writes, 0);
54
+ }
55
+ const deps = effects();
56
+ deps.omit = () => { throw new Error("read-only config"); };
57
+ assert.equal(await applyStartupChoice("omit", deps), undefined);
58
+ assert.deepEqual(deps.events, []);
59
+ });
60
+
61
+ test("accepted update reopens active Shell only after upgrade success and preserves exit status", async () => {
62
+ const deps = effects();
63
+ assert.equal(await applyStartupChoice("update", deps), 17);
64
+ assert.deepEqual(deps.events, ["upgrade", "relaunch"]);
65
+ });
66
+
67
+ test("failed upgrade keeps ordinary launch; failed relaunch never falls through to old Shell", async () => {
68
+ const deps = effects(1);
69
+ assert.equal(await applyStartupChoice("update", deps), undefined);
70
+ assert.deepEqual(deps.events, ["upgrade"]);
71
+ const successful = effects();
72
+ successful.relaunch = async () => { throw new Error("new runtime failed"); };
73
+ await assert.rejects(applyStartupChoice("update", successful), /new runtime failed/);
74
+ assert.deepEqual(successful.events, ["upgrade"]);
75
+ });
@@ -0,0 +1,85 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { parseLauncherArgs } from "../lib/gentle-shell-launcher.ts";
4
+ import { findStartupUpdate, shouldCheckStartupUpdate } from "../lib/gentle-shell-startup-update.ts";
5
+
6
+ const eligible = {
7
+ args: parseLauncherArgs([]), env: {}, bundled: true, inputTTY: true, outputTTY: true,
8
+ };
9
+
10
+ test("ordinary bundled interactive launch and resume are eligible", () => {
11
+ for (const argv of [[], ["--resume"], ["-r"], ["--continue"], ["-c"]]) {
12
+ assert.equal(shouldCheckStartupUpdate({ ...eligible, args: parseLauncherArgs(argv) }), true);
13
+ }
14
+ });
15
+
16
+ test("commands, overrides, non-TTY runs and child/offline opt-outs are excluded", () => {
17
+ for (const argv of [["upgrade"], ["home"], ["setup"], ["install", "npm:x"], ["--help"], ["--version"], ["--package-root", "/custom"]]) {
18
+ assert.equal(shouldCheckStartupUpdate({ ...eligible, args: parseLauncherArgs(argv) }), false, argv.join(" "));
19
+ }
20
+ for (const overrides of [{ bundled: false }, { inputTTY: false }, { outputTTY: false }, { args: { ...eligible.args, error: "invalid arguments" } }]) {
21
+ assert.equal(shouldCheckStartupUpdate({ ...eligible, ...overrides }), false);
22
+ }
23
+ for (const name of ["GENTLE_PI_AGENTS_CHILD", "PI_OFFLINE", "GENTLE_SHELL_SKIP_STARTUP_UPDATE"]) {
24
+ assert.equal(shouldCheckStartupUpdate({ ...eligible, env: { [name]: "1" } }), false);
25
+ }
26
+ });
27
+
28
+ test("forwarded command modes and unknown flags never trigger a startup offer", () => {
29
+ for (const argv of [["--print", "hello"], ["-p", "hello"], ["--mode", "rpc"], ["--mode=json"], ["--export", "session.json"], ["--list-models"], ["auth"], ["mcp"], ["--unknown"], ["--resume", "--print"], ["--resume", "session", "--mode", "json"]]) {
30
+ assert.equal(shouldCheckStartupUpdate({ ...eligible, args: parseLauncherArgs(argv) }), false, argv.join(" "));
31
+ }
32
+ });
33
+
34
+ test("only a newer strict stable version produces official release notes, without numeric precision loss", async () => {
35
+ for (const [current, latest] of [["1.2.3", "1.2.4"], ["1.99.99", "2.0.0"], ["1.9007199254740992.0", "1.9007199254740993.0"]]) {
36
+ assert.deepEqual(await findStartupUpdate({ current, lookup: async () => latest }), {
37
+ current, latest, notesUrl: `https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v${latest}`,
38
+ });
39
+ }
40
+ });
41
+
42
+ test("same, older, prerelease and malformed versions produce no offer", async () => {
43
+ for (const latest of ["1.2.3", "1.2.2", "0.99.99", "2.0.0-beta.1", "2.0.0+build", "v2.0.0", "02.0.0", "2.0", "2.0.0\n", "garbage", undefined]) {
44
+ assert.equal(await findStartupUpdate({ current: "1.2.3", lookup: async () => latest }), undefined);
45
+ }
46
+ for (const current of ["v1.2.3", "1.2.3-beta", "01.2.3", "1.2", ""]) {
47
+ assert.equal(await findStartupUpdate({ current, lookup: async () => "2.0.0" }), undefined);
48
+ }
49
+ });
50
+
51
+ test("skipping suppresses exactly the skipped release, not later releases", async () => {
52
+ assert.equal(await findStartupUpdate({ current: "1.0.0", skipped: "2.0.0", lookup: async () => "2.0.0" }), undefined);
53
+ assert.equal((await findStartupUpdate({ current: "1.0.0", skipped: "2.0.0", lookup: async () => "2.0.1" }))?.latest, "2.0.1");
54
+ });
55
+
56
+ test("lookup rejection and synchronous failure silently return no offer", async () => {
57
+ assert.equal(await findStartupUpdate({ current: "1.0.0", lookup: async () => { throw new Error("offline"); } }), undefined);
58
+ assert.equal(await findStartupUpdate({ current: "1.0.0", lookup: () => { throw new Error("offline"); } }), undefined);
59
+ });
60
+
61
+ test("timeout aborts and returns even when lookup ignores its signal", async (t) => {
62
+ t.mock.timers.enable({ apis: ["setTimeout"] });
63
+ let signal: AbortSignal | undefined;
64
+ const pending = findStartupUpdate({ current: "1.0.0", lookup: (value) => { signal = value; return new Promise(() => {}); } });
65
+ await Promise.resolve();
66
+ t.mock.timers.tick(1499);
67
+ assert.equal(signal?.aborted, false);
68
+ t.mock.timers.tick(1);
69
+ assert.equal(await pending, undefined);
70
+ assert.equal(signal?.aborted, true);
71
+ });
72
+
73
+ test("custom deadline is bounded and settled lookups clear the timeout", async (t) => {
74
+ t.mock.timers.enable({ apis: ["setTimeout"] });
75
+ let timedSignal: AbortSignal | undefined;
76
+ const pending = findStartupUpdate({ current: "1.0.0", timeoutMs: 20, lookup: (signal) => { timedSignal = signal; return new Promise(() => {}); } });
77
+ await Promise.resolve();
78
+ t.mock.timers.tick(20);
79
+ assert.equal(await pending, undefined);
80
+ assert.equal(timedSignal?.aborted, true);
81
+ let settledSignal: AbortSignal | undefined;
82
+ assert.ok(await findStartupUpdate({ current: "1.0.0", timeoutMs: 20, lookup: async (signal) => { settledSignal = signal; return "2.0.0"; } }));
83
+ t.mock.timers.tick(20);
84
+ assert.equal(settledSignal?.aborted, false);
85
+ });