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,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 }),
@@ -622,9 +683,10 @@ test("checkPeerVersionPin passes for a matching pin", () => {
622
683
  assert.deepEqual(result, { ok: true, pinned: ">=0.85.1" });
623
684
  });
624
685
 
625
- test("Pi 0.99.1 baseline and host peers follow the open development range policy", () => {
686
+ test("Pi 1.2.0 baseline and host peers follow the open development range policy", () => {
626
687
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
627
- assert.equal(MIN_PI_VERSION, "0.99.1");
688
+ assert.equal(MIN_PI_VERSION, "1.2.0");
689
+ assert.equal(pkg.peerDependencies["@earendil-works/pi-coding-agent"], ">=1.2.0");
628
690
  assert.equal(pkg.engines.node, ">=22.19.0");
629
691
  for (const name of ["@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
630
692
  assert.equal(pkg.peerDependencies[name], "*");
@@ -634,34 +696,38 @@ test("Pi 0.99.1 baseline and host peers follow the open development range policy
634
696
  // Development ranges are policy specifiers; their floor never drops below
635
697
  // the runtime minimum the launcher enforces.
636
698
  for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
637
- assert.equal(pkg.devDependencies[name], ">=1.0.0", name);
699
+ assert.equal(pkg.devDependencies[name], ">=1.2.0", name);
638
700
  assert.equal(checkPiVersion(pkg.devDependencies[name].slice(2)).ok, true, name);
639
701
  }
640
702
  });
641
703
 
642
704
  test("checkPiVersion accepts a version equal to the minimum", () => {
643
- assert.deepEqual(checkPiVersion("0.99.1"), { ok: true, version: "0.99.1" });
705
+ assert.deepEqual(checkPiVersion("1.2.0"), { ok: true, version: "1.2.0" });
644
706
  });
645
707
 
646
708
  test("checkPiVersion accepts a version above the minimum", () => {
647
- assert.deepEqual(checkPiVersion("0.100.0"), { ok: true, version: "0.100.0" });
709
+ for (const version of ["1.2.1", "1.3.0", "2.0.0"]) {
710
+ assert.deepEqual(checkPiVersion(version), { ok: true, version });
711
+ }
648
712
  });
649
713
 
650
714
  test("checkPiVersion accepts a v-prefixed version", () => {
651
- assert.deepEqual(checkPiVersion("v0.99.1"), { ok: true, version: "0.99.1" });
715
+ assert.deepEqual(checkPiVersion("v1.2.0"), { ok: true, version: "1.2.0" });
652
716
  });
653
717
 
654
718
  test("checkPiVersion accepts a prerelease suffix at the minimum", () => {
655
- assert.deepEqual(checkPiVersion("pi version 0.99.1-rc.2"), { ok: true, version: "0.99.1" });
719
+ assert.deepEqual(checkPiVersion("pi version 1.2.0-rc.2"), { ok: true, version: "1.2.0" });
656
720
  });
657
721
 
658
722
  test("checkPiVersion rejects a version below the minimum and names both versions", () => {
659
- const result = checkPiVersion("0.85.0");
723
+ const result = checkPiVersion("1.1.0");
660
724
  assert.equal(result.ok, false);
661
725
  if (result.ok) throw new Error("expected a failing result");
662
- assert.equal(result.version, "0.85.0");
663
- assert.match(result.message, /0\.85\.0/);
664
- assert.match(result.message, /0\.99\.1/);
726
+ assert.equal(result.version, "1.1.0");
727
+ assert.match(result.message, /1\.1\.0/);
728
+ assert.match(result.message, /1\.2\.0/);
729
+ assert.equal(checkPiVersion("1.0.0").ok, false);
730
+ assert.equal(checkPiVersion("0.99.1").ok, false);
665
731
  });
666
732
 
667
733
  test("checkPiVersion rejects a prerelease below the minimum", () => {
@@ -675,7 +741,7 @@ test("checkPiVersion reports unparsable output with the raw text and the minimum
675
741
  if (result.ok) throw new Error("expected a failing result");
676
742
  assert.equal(result.version, undefined);
677
743
  assert.match(result.message, /not a version/);
678
- assert.match(result.message, /0\.99\.1/);
744
+ assert.match(result.message, /1\.2\.0/);
679
745
  });
680
746
 
681
747
  test("checkPiVersion accepts a custom minimum", () => {
@@ -852,7 +918,7 @@ test("findGentlePiDeclaration detects a bare npm:gentle-pi object source entry",
852
918
  });
853
919
 
854
920
  test("findGentlePiDeclaration recognises a relative path entry whose package.json name is gentle-pi", () => {
855
- const resolvedDir = join("/agent", "..", "..", "work", "gentle-pi");
921
+ const resolvedDir = resolve("/agent", "..", "..", "work", "gentle-pi");
856
922
  const result = findGentlePiDeclaration('{"packages":["../../work/gentle-pi"]}', {
857
923
  agentDir: "/agent",
858
924
  readPackageName: readPackageNameStub({ [resolvedDir]: "gentle-pi" }),
@@ -863,17 +929,17 @@ test("findGentlePiDeclaration recognises a relative path entry whose package.jso
863
929
  test("findGentlePiDeclaration recognises an absolute path entry whose package.json name is gentle-pi", () => {
864
930
  const result = findGentlePiDeclaration('{"packages":["/checkouts/gentle-pi"]}', {
865
931
  agentDir: "/agent",
866
- readPackageName: readPackageNameStub({ "/checkouts/gentle-pi": "gentle-pi" }),
932
+ readPackageName: readPackageNameStub({ [resolve("/checkouts/gentle-pi")]: "gentle-pi" }),
867
933
  });
868
- assert.deepEqual(result, { kind: "path", dir: "/checkouts/gentle-pi" });
934
+ assert.deepEqual(result, { kind: "path", dir: resolve("/checkouts/gentle-pi") });
869
935
  });
870
936
 
871
937
  test("findGentlePiDeclaration recognises a path object source entry whose package.json name is gentle-pi", () => {
872
938
  const result = findGentlePiDeclaration('{"packages":[{"source":"../gentle-pi","extensions":[]}]}', {
873
939
  agentDir: "/agent",
874
- readPackageName: readPackageNameStub({ [join("/agent", "..", "gentle-pi")]: "gentle-pi" }),
940
+ readPackageName: readPackageNameStub({ [resolve("/agent", "..", "gentle-pi")]: "gentle-pi" }),
875
941
  });
876
- assert.deepEqual(result, { kind: "path", dir: join("/agent", "..", "gentle-pi") });
942
+ assert.deepEqual(result, { kind: "path", dir: resolve("/agent", "..", "gentle-pi") });
877
943
  });
878
944
 
879
945
  test("findGentlePiDeclaration is undefined for a path entry whose package.json name is not gentle-pi", () => {
@@ -906,9 +972,9 @@ test("findGentlePiDeclaration is undefined when packages lists unrelated entries
906
972
  test("findGentlePiDeclaration returns the first matching declaration, path or npm, in list order", () => {
907
973
  const result = findGentlePiDeclaration('{"packages":["../gentle-pi","npm:gentle-pi"]}', {
908
974
  agentDir: "/agent",
909
- readPackageName: readPackageNameStub({ [join("/agent", "..", "gentle-pi")]: "gentle-pi" }),
975
+ readPackageName: readPackageNameStub({ [resolve("/agent", "..", "gentle-pi")]: "gentle-pi" }),
910
976
  });
911
- assert.deepEqual(result, { kind: "path", dir: join("/agent", "..", "gentle-pi") });
977
+ assert.deepEqual(result, { kind: "path", dir: resolve("/agent", "..", "gentle-pi") });
912
978
  });
913
979
 
914
980
  // --- decideTakeOver -----------------------------------------------------------
@@ -992,7 +1058,7 @@ test("otherPackageInjections resolves a path entry relative to agentDir", () =>
992
1058
  agentDir: "/agent",
993
1059
  skip: { kind: "npm" },
994
1060
  });
995
- assert.deepEqual(result.paths, [join("/agent", "..", "..", "work", "pi-qwen-ambassador")]);
1061
+ assert.deepEqual(result.paths, [resolve("/agent", "..", "..", "work", "pi-qwen-ambassador")]);
996
1062
  });
997
1063
 
998
1064
  test("otherPackageInjections skips a git entry and warns", () => {
@@ -1018,7 +1084,7 @@ test("otherPackageInjections warns for an object entry with extensions or autolo
1018
1084
  });
1019
1085
 
1020
1086
  test("otherPackageInjections skips the entry matching a path declaration being taken over", () => {
1021
- const declaredDir = join("/agent", "..", "..", "work", "gentle-pi");
1087
+ const declaredDir = resolve("/agent", "..", "..", "work", "gentle-pi");
1022
1088
  const result = otherPackageInjections({
1023
1089
  settingsText: '{"packages":["npm:some-other","../../work/gentle-pi"]}',
1024
1090
  agentDir: "/agent",
@@ -1033,7 +1099,7 @@ test("otherPackageInjections tolerates undefined or malformed settings text", ()
1033
1099
  });
1034
1100
 
1035
1101
  test("otherPackageInjections skips every gentle-pi entry, not only the one matching the declaration kind", () => {
1036
- const declaredDir = join("/agent", "..", "..", "work", "gentle-pi");
1102
+ const declaredDir = resolve("/agent", "..", "..", "work", "gentle-pi");
1037
1103
  const result = otherPackageInjections({
1038
1104
  // The declaration being taken over is the path entry, but settings
1039
1105
  // also carries a second, unrelated npm:gentle-pi entry: both must be
@@ -1088,7 +1154,7 @@ test("otherPackageInjections keeps a declared package directory that isDirectory
1088
1154
  });
1089
1155
 
1090
1156
  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");
1157
+ const linkOtherDir = resolve("/agent", "link-other");
1092
1158
  const result = otherPackageInjections({
1093
1159
  settingsText: '{"packages":["link-other","npm:some-other"]}',
1094
1160
  agentDir: "/agent",
@@ -1107,7 +1173,7 @@ test("otherPackageInjections keeps comparing raw strings when no realpath resolv
1107
1173
  agentDir: "/agent",
1108
1174
  skip: { kind: "path", dir: "/real/other" },
1109
1175
  });
1110
- assert.deepEqual(result.paths, [join("/agent", "link-other"), join("/agent", "npm", "node_modules", "some-other")]);
1176
+ assert.deepEqual(result.paths, [resolve("/agent", "link-other"), join("/agent", "npm", "node_modules", "some-other")]);
1111
1177
  });
1112
1178
 
1113
1179
  test("otherPackageInjections defaults to including every declared package when isDirectory is not provided (existing callers keep pure string resolution)", () => {
@@ -1134,6 +1200,7 @@ test("package injection relies on Pi's -e resource discovery in isolated and tak
1134
1200
  passthrough: [],
1135
1201
  baseEnv: {},
1136
1202
  homedir: "/home/u",
1203
+ cwd: "/work",
1137
1204
  });
1138
1205
  assert.deepEqual(built.args, takeOver ? ["--no-extensions", "-e", "/pkg"] : ["-e", "/pkg"]);
1139
1206
  }
@@ -1153,6 +1220,7 @@ test("buildPiInvocation injects the launcher env into baseEnv", () => {
1153
1220
  passthrough: [],
1154
1221
  baseEnv: { PATH: "/usr/bin" },
1155
1222
  homedir: "/home/u",
1223
+ cwd: "/work",
1156
1224
  });
1157
1225
  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
1226
  });
@@ -1168,6 +1236,7 @@ test("buildPiInvocation skips injection when there is a declaration and no takeo
1168
1236
  passthrough: ["--mode", "rpc"],
1169
1237
  baseEnv: {},
1170
1238
  homedir: "/home/u",
1239
+ cwd: "/work",
1171
1240
  });
1172
1241
  assert.deepEqual(built.command, "/usr/bin/pi");
1173
1242
  assert.deepEqual(built.args, ["--mode", "rpc"]);
@@ -1184,6 +1253,7 @@ test("buildPiInvocation adds the gentle-pi injection flags when there is no decl
1184
1253
  passthrough: ["--mode", "rpc"],
1185
1254
  baseEnv: {},
1186
1255
  homedir: "/home/u",
1256
+ cwd: "/work",
1187
1257
  });
1188
1258
  assert.deepEqual(built.args, [
1189
1259
  "-e",
@@ -1205,6 +1275,7 @@ test("buildPiInvocation emits only runtime args and passthrough when a pi subcom
1205
1275
  piSubcommand: "install",
1206
1276
  baseEnv: {},
1207
1277
  homedir: "/home/u",
1278
+ cwd: "/work",
1208
1279
  });
1209
1280
  assert.deepEqual(built.args, ["install", "npm:x"]);
1210
1281
  assert.equal(built.command, "/usr/bin/pi");
@@ -1222,6 +1293,7 @@ test("buildPiInvocation still injects the launcher env for a pi subcommand", ()
1222
1293
  piSubcommand: "list",
1223
1294
  baseEnv: { PATH: "/usr/bin" },
1224
1295
  homedir: "/home/u",
1296
+ cwd: "/work",
1225
1297
  });
1226
1298
  assert.deepEqual(built.env, {
1227
1299
  PATH: "/usr/bin",
@@ -1243,6 +1315,7 @@ test("buildPiInvocation in link mode with a pi subcommand is exactly pi <subcomm
1243
1315
  piSubcommand: "auth",
1244
1316
  baseEnv: { PI_CODING_AGENT_DIR: "/pi/agent" },
1245
1317
  homedir: "/home/u",
1318
+ cwd: "/work",
1246
1319
  });
1247
1320
  assert.equal(built.command, "/usr/bin/pi");
1248
1321
  assert.deepEqual(built.args, ["auth", "status"]);
@@ -1263,6 +1336,7 @@ test("buildPiInvocation carries the user's original Pi home without weakening is
1263
1336
  passthrough: [],
1264
1337
  baseEnv,
1265
1338
  homedir: "/home/u",
1339
+ cwd: "/work",
1266
1340
  }).env;
1267
1341
  const conventional = build({});
1268
1342
  assert.equal(conventional.PI_CODING_AGENT_DIR, "/gentle-shell/agent");
@@ -1297,6 +1371,7 @@ test("buildPiInvocation keeps the runtime's own args ahead of the injection and
1297
1371
  passthrough: [],
1298
1372
  baseEnv: {},
1299
1373
  homedir: "/home/u",
1374
+ cwd: "/work",
1300
1375
  });
1301
1376
  assert.equal(built.args[0], "/bundled/cli.js");
1302
1377
  assert.equal(built.command, "/usr/bin/node");
@@ -1313,6 +1388,7 @@ test("buildPiInvocation takes over a conflicting path declaration: --no-extensio
1313
1388
  passthrough: ["--mode", "rpc"],
1314
1389
  baseEnv: {},
1315
1390
  homedir: "/home/u",
1391
+ cwd: "/work",
1316
1392
  });
1317
1393
  assert.deepEqual(built.args, [
1318
1394
  "--no-extensions",
@@ -1336,6 +1412,7 @@ test("buildPiInvocation takes over with --package-root even for a matching npm d
1336
1412
  passthrough: [],
1337
1413
  baseEnv: {},
1338
1414
  homedir: "/home/u",
1415
+ cwd: "/work",
1339
1416
  });
1340
1417
  assert.deepEqual(built.args, [
1341
1418
  "--no-extensions",
@@ -1355,6 +1432,7 @@ test("buildPiInvocation takes over with --package-root even when there is no dec
1355
1432
  passthrough: [],
1356
1433
  baseEnv: {},
1357
1434
  homedir: "/home/u",
1435
+ cwd: "/work",
1358
1436
  });
1359
1437
  assert.deepEqual(built.args, [
1360
1438
  "--no-extensions",
@@ -1377,6 +1455,7 @@ test("buildPiInvocation injects loose extension entries during a takeover, after
1377
1455
  passthrough: ["--mode", "rpc"],
1378
1456
  baseEnv: {},
1379
1457
  homedir: "/home/u",
1458
+ cwd: "/work",
1380
1459
  });
1381
1460
  assert.deepEqual(built.args, [
1382
1461
  "--no-extensions",
@@ -1404,6 +1483,7 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
1404
1483
  passthrough: [],
1405
1484
  baseEnv: {},
1406
1485
  homedir: "/home/u",
1486
+ cwd: "/work",
1407
1487
  });
1408
1488
  assert.deepEqual(withoutField.args, ["--no-extensions", "-e", "/pkg"]);
1409
1489
 
@@ -1418,6 +1498,7 @@ test("buildPiInvocation omits loose extension entry flags when the list is empty
1418
1498
  passthrough: [],
1419
1499
  baseEnv: {},
1420
1500
  homedir: "/home/u",
1501
+ cwd: "/work",
1421
1502
  });
1422
1503
  assert.deepEqual(withEmptyField.args, withoutField.args);
1423
1504
  });
@@ -1439,6 +1520,7 @@ test("buildPiInvocation dedupes loose extension entries against other-package pa
1439
1520
  passthrough: [],
1440
1521
  baseEnv: {},
1441
1522
  homedir: "/home/u",
1523
+ cwd: "/work",
1442
1524
  });
1443
1525
  assert.deepEqual(built.args, [
1444
1526
  "--no-extensions",
@@ -1468,6 +1550,7 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
1468
1550
  passthrough: [],
1469
1551
  baseEnv: {},
1470
1552
  homedir: "/home/u",
1553
+ cwd: "/work",
1471
1554
  });
1472
1555
  const eFlags = built.args.filter((arg, index) => built.args[index - 1] === "-e");
1473
1556
  assert.deepEqual(eFlags, [join("/agent", "npm", "node_modules", "some-other"), "/pkg"]);
@@ -1480,6 +1563,127 @@ test("buildPiInvocation dedupes the launcher's own package root against an other
1480
1563
  ]);
1481
1564
  });
1482
1565
 
1566
+ // --- child package injection signal (#1690) ----------------------------------
1567
+ //
1568
+ // The launcher exports exactly the -e set it computed itself, so the subagent
1569
+ // runner can forward it to delegated children. Passthrough -e flags are not
1570
+ // part of the signal.
1571
+
1572
+ const injectionInput = {
1573
+ runtime: { kind: "path" as const, command: "/usr/bin/pi", args: [] },
1574
+ home: isolatedHomeResolved,
1575
+ packageRoot: "/pkg",
1576
+ otherPackagePaths: [],
1577
+ passthrough: ["-e", "/user/ext.ts", "--mode", "rpc"],
1578
+ homedir: "/home/u",
1579
+ cwd: "/work",
1580
+ };
1581
+
1582
+ test("buildPiInvocation signals the packageRoot injection when there is no declaration", () => {
1583
+ const built = buildPiInvocation({ ...injectionInput, declaration: undefined, takeOver: false, baseEnv: {} });
1584
+ assert.deepEqual(parseChildPackageInjection(built.env), { noExtensions: false, extensionPaths: ["/pkg"] });
1585
+ });
1586
+
1587
+ test("buildPiInvocation signals the exact deduped takeover -e set in order, without passthrough -e flags", () => {
1588
+ const built = buildPiInvocation({
1589
+ ...injectionInput,
1590
+ home: linkHome,
1591
+ declaration: { kind: "path", dir: "/other/checkout" },
1592
+ takeOver: true,
1593
+ otherPackagePaths: ["/agent/npm/node_modules/some-other", "/shared/dup.ts", "/pkg"],
1594
+ looseExtensionEntries: ["/shared/dup.ts", "/agent/extensions/a.ts", "/agent/extensions/a.ts"],
1595
+ baseEnv: {},
1596
+ });
1597
+ const launcherPaths = built.args.slice(0, built.args.indexOf("--mode") - 2).filter((_, index, all) => all[index - 1] === "-e");
1598
+ const signal = parseChildPackageInjection(built.env);
1599
+ assert.deepEqual(signal, {
1600
+ noExtensions: true,
1601
+ // /pkg keeps its first (other-package) position, exactly as in argv.
1602
+ extensionPaths: ["/agent/npm/node_modules/some-other", "/shared/dup.ts", "/pkg", "/agent/extensions/a.ts"],
1603
+ });
1604
+ assert.deepEqual(signal?.extensionPaths, launcherPaths);
1605
+ });
1606
+
1607
+ test("buildPiInvocation signals absolute paths resolved against the given cwd when a takeover receives a relative loose entry", () => {
1608
+ const cwd = resolve("/elsewhere", "launch-dir");
1609
+ assert.notEqual(cwd, process.cwd());
1610
+ const built = buildPiInvocation({
1611
+ ...injectionInput,
1612
+ cwd,
1613
+ home: linkHome,
1614
+ declaration: { kind: "npm" },
1615
+ takeOver: true,
1616
+ looseExtensionEntries: [join("relative-home", "extensions", "a.ts")],
1617
+ baseEnv: {},
1618
+ });
1619
+ assert.ok(built.args.includes(join("relative-home", "extensions", "a.ts")), "the -e argv itself is unchanged");
1620
+ assert.deepEqual(parseChildPackageInjection(built.env)?.extensionPaths, [resolve(cwd, "relative-home", "extensions", "a.ts"), "/pkg"]);
1621
+ });
1622
+
1623
+ test("buildPiInvocation signals one entry for a file spelled both relative and absolute, while argv keeps both spellings", () => {
1624
+ const cwd = resolve("/elsewhere", "launch-dir");
1625
+ const relative = join("relative-home", "extensions", "a.ts");
1626
+ const absolute = resolve(cwd, relative);
1627
+ const built = buildPiInvocation({
1628
+ ...injectionInput,
1629
+ cwd,
1630
+ home: linkHome,
1631
+ declaration: { kind: "npm" },
1632
+ takeOver: true,
1633
+ otherPackagePaths: ["/agent/npm/node_modules/some-other", absolute],
1634
+ looseExtensionEntries: [relative, "/agent/extensions/b.ts"],
1635
+ baseEnv: {},
1636
+ });
1637
+ assert.deepEqual(built.args.slice(0, built.args.indexOf("--mode") - 2), [
1638
+ "--no-extensions",
1639
+ "-e",
1640
+ "/agent/npm/node_modules/some-other",
1641
+ "-e",
1642
+ absolute,
1643
+ "-e",
1644
+ relative,
1645
+ "-e",
1646
+ "/agent/extensions/b.ts",
1647
+ "-e",
1648
+ "/pkg",
1649
+ ]);
1650
+ assert.deepEqual(parseChildPackageInjection(built.env)?.extensionPaths, [
1651
+ "/agent/npm/node_modules/some-other",
1652
+ absolute,
1653
+ "/agent/extensions/b.ts",
1654
+ "/pkg",
1655
+ ]);
1656
+ });
1657
+
1658
+ test("buildPiInvocation does not signal when settings already declare gentle-pi, and drops an inherited signal", () => {
1659
+ const stale = encodeChildPackageInjection({ noExtensions: false, extensionPaths: ["/outer/pkg"] });
1660
+ for (const baseEnv of [{}, { [CHILD_PACKAGE_INJECTION_ENV]: stale }]) {
1661
+ const built = buildPiInvocation({ ...injectionInput, home: linkHome, declaration: { kind: "npm" }, takeOver: false, baseEnv });
1662
+ assert.equal(Object.hasOwn(built.env, CHILD_PACKAGE_INJECTION_ENV), false);
1663
+ }
1664
+ });
1665
+
1666
+ test("buildPiInvocation does not signal for a pi subcommand, and drops an inherited signal", () => {
1667
+ const stale = encodeChildPackageInjection({ noExtensions: false, extensionPaths: ["/outer/pkg"] });
1668
+ for (const takeOver of [false, true]) {
1669
+ const built = buildPiInvocation({
1670
+ ...injectionInput,
1671
+ declaration: undefined,
1672
+ takeOver,
1673
+ passthrough: ["list"],
1674
+ piSubcommand: "list",
1675
+ baseEnv: { [CHILD_PACKAGE_INJECTION_ENV]: stale },
1676
+ });
1677
+ assert.equal(Object.hasOwn(built.env, CHILD_PACKAGE_INJECTION_ENV), false);
1678
+ }
1679
+ });
1680
+
1681
+ test("buildPiInvocation replaces an inherited signal with its own injection", () => {
1682
+ const stale = encodeChildPackageInjection({ noExtensions: true, extensionPaths: ["/outer/pkg"] });
1683
+ const built = buildPiInvocation({ ...injectionInput, declaration: undefined, takeOver: false, baseEnv: { [CHILD_PACKAGE_INJECTION_ENV]: stale } });
1684
+ assert.deepEqual(parseChildPackageInjection(built.env), { noExtensions: false, extensionPaths: ["/pkg"] });
1685
+ });
1686
+
1483
1687
  // --- discoverLooseExtensionEntries -------------------------------------------
1484
1688
  //
1485
1689
  // Pure mirror of pi's own discoverExtensionsInDir (packages/coding-agent/src/
@@ -1806,3 +2010,25 @@ test("helpText documents pi's own package-management subcommands", () => {
1806
2010
  assert.match(text, /\bconfig\b/);
1807
2011
  assert.match(text, /\bauth\b/);
1808
2012
  });
2013
+
2014
+ // T3 B3: the isolated-home bootstrap creates ~/.gentle-shell private (0700) even under
2015
+ // umask 002, so the bundled prefix claim (which refuses group/other-writable folders) accepts it.
2016
+ test("the isolated-home bootstrap creates ~/.gentle-shell as 0700 under umask 002", { skip: process.platform === "win32" ? "POSIX modes" : false }, async () => {
2017
+ const { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } = await import("node:fs");
2018
+ const { spawnSync } = await import("node:child_process");
2019
+ const { tmpdir } = await import("node:os");
2020
+ const root = mkdtempSync(join(tmpdir(), "gentle-shell-umask-"));
2021
+ try {
2022
+ const piScript = join(root, "fake-pi.mjs");
2023
+ writeFileSync(piScript, "#!/usr/bin/env node\nif (process.argv.includes('--version')) console.log('1.2.0');\n");
2024
+ chmodSync(piScript, 0o755);
2025
+ const binPath = fileURLToPath(new URL("../bin/gentle-shell.mjs", import.meta.url));
2026
+ const env = { HOME: root, USERPROFILE: root, PATH: process.env.PATH ?? "/usr/bin:/bin", GENTLE_SHELL_PI: piScript, GENTLE_SHELL_NO_AUTO_SETUP: "1" };
2027
+ const result = spawnSync("/bin/sh", ["-c", 'umask 002; exec "$0" "$@"', process.execPath, binPath, "hello"], { env, encoding: "utf8", input: "" });
2028
+ assert.equal(result.status, 0, result.stderr);
2029
+ assert.equal(statSync(join(root, ".gentle-shell")).mode & 0o777, 0o700);
2030
+ assert.equal(statSync(join(root, ".gentle-shell", "agent")).mode & 0o777, 0o700);
2031
+ } finally {
2032
+ rmSync(root, { recursive: true, force: true });
2033
+ }
2034
+ });
@@ -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
+ });