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
@@ -0,0 +1,535 @@
1
+ import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { request as httpRequest } from "node:http";
3
+ import { createConnection } from "node:net";
4
+ import { dirname, posix, win32 } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // The launcher in a bundled install (S5, S8): pure decisions over an injected
8
+ // filesystem. bin/gentle-shell.mjs wires them to the real one. A bundled install
9
+ // is `<prefix>/versions/<id>/` with this gentle-pi in its node_modules, marked by
10
+ // scripts/bundled-install.mjs, and `current` naming it; anything else keeps the
11
+ // launcher's ordinary behavior unchanged.
12
+
13
+ export interface BundledFs {
14
+ // The canonical path, or undefined when it does not exist.
15
+ realpath: (path: string) => string | undefined;
16
+ // A regular file's text, or undefined.
17
+ readText: (path: string) => string | undefined;
18
+ // A symbolic link's target, or undefined.
19
+ readLink: (path: string) => string | undefined;
20
+ }
21
+
22
+ export interface BundledInstall {
23
+ root: string;
24
+ id: string;
25
+ versionDir: string;
26
+ }
27
+
28
+ type Env = Record<string, string | undefined>;
29
+
30
+ // The same rules scripts/bundled-install.mjs writes versions and markers with.
31
+ const VERSION_ID = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
32
+ const VERSION_MARKER = ".gentle-shell-version";
33
+ const PI_PACKAGE = "@earendil-works/pi-coding-agent";
34
+ const PI_BIN = "dist/bundle/cli.js";
35
+
36
+ // `gentle-shell upgrade` sets this to the version it just installed while it
37
+ // runs that version's `--version`: the check then runs in bundled mode, with
38
+ // its exact Pi, before `current` names it. Never passed on to children.
39
+ export const BUNDLED_VERIFY_ENV = "GENTLE_SHELL_VERIFY_VERSION";
40
+
41
+ const pathFor = (platform: string) => (platform === "win32" ? win32 : posix);
42
+
43
+ function inside(path: typeof posix, parent: string, child: string): boolean {
44
+ const rest = path.relative(parent, child);
45
+ return rest !== "" && !rest.startsWith("..") && !path.isAbsolute(rest);
46
+ }
47
+
48
+ function parseJson(text: string | undefined): any {
49
+ if (text === undefined) return undefined;
50
+ try {
51
+ return JSON.parse(text);
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ // The bundled install this package runs from: the nearest `versions/<id>`
58
+ // ancestor of its real root whose marker names it, whose node_modules/gentle-pi
59
+ // is this package, and which `current` names (a relative symlink on POSIX, a
60
+ // pointer file on Windows) or `verifying` names (BUNDLED_VERIFY_ENV, for a
61
+ // `--version` check only). Undefined otherwise.
62
+ // `kept` accepts any finished version of the prefix, whether `current` names it
63
+ // or not (runningBundledPrefix; never the launcher's own choice of version).
64
+ export function resolveBundledInstall(input: { packageRoot: string; platform: string; fs: BundledFs; verifying?: string; kept?: boolean }): BundledInstall | undefined {
65
+ const path = pathFor(input.platform);
66
+ const real = input.fs.realpath(input.packageRoot) ?? input.packageRoot;
67
+ for (let dir = path.dirname(real); path.dirname(dir) !== dir; dir = path.dirname(dir)) {
68
+ if (path.basename(path.dirname(dir)) !== "versions") continue;
69
+ const id = path.basename(dir);
70
+ const root = path.dirname(path.dirname(dir));
71
+ if (!VERSION_ID.test(id) || id.includes("..")) return undefined;
72
+ if (input.fs.readText(path.join(dir, VERSION_MARKER)) !== `${id}\n`) return undefined;
73
+ if (input.fs.realpath(path.join(dir, "node_modules", "gentle-pi")) !== real) return undefined;
74
+ const current = path.join(root, "current");
75
+ const active = input.platform === "win32" ? input.fs.readText(current) === `${id}\n` : input.fs.readLink(current) === `versions/${id}`;
76
+ return active || input.verifying === id || input.kept === true ? { root, id, versionDir: dir } : undefined;
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ export type BundledPi = { ok: true; cli: string; version: string } | { ok: false; message: string };
82
+
83
+ // The real filesystem behind BundledFs.
84
+ export const nodeBundledFs: BundledFs = {
85
+ realpath: (path) => {
86
+ try {
87
+ return realpathSync(path);
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ },
92
+ readText: (path) => {
93
+ try {
94
+ return lstatSync(path).isFile() ? readFileSync(path, "utf8") : undefined;
95
+ } catch {
96
+ return undefined;
97
+ }
98
+ },
99
+ readLink: (path) => {
100
+ try {
101
+ return readlinkSync(path);
102
+ } catch {
103
+ return undefined;
104
+ }
105
+ },
106
+ };
107
+
108
+ // The bundled launcher passes its prefix to Pi (bundledChildEnv), and so to
109
+ // every process Pi starts, the agent's shell included.
110
+ export const GENTLE_SHELL_PREFIX_ENV = "GENTLE_SHELL_PREFIX";
111
+
112
+ // This package's root, from lib/ or the generated runtime/ alike.
113
+ const ownPackageRoot = () => dirname(dirname(fileURLToPath(import.meta.url)));
114
+
115
+ let installOverlay: { root: string } | null | undefined;
116
+ const runningMemo = new Map<string, string | undefined>();
117
+
118
+ /** Testing only: the bundled install this package runs from (null: none), or undefined for the real one. */
119
+ export function setBundledInstallForTesting(install: { root: string } | null | undefined): void {
120
+ installOverlay = install;
121
+ runningMemo.clear();
122
+ }
123
+
124
+ /** Testing only: forget the remembered answers. */
125
+ export function resetRunningBundledPrefixForTesting(): void {
126
+ runningMemo.clear();
127
+ }
128
+
129
+ // S9: the prefix of the bundled install this gentle-pi runs from, or
130
+ // undefined. Both must hold: GENTLE_SHELL_PREFIX is absolute and names that
131
+ // install's root, and resolveBundledInstall finds a finished version of the
132
+ // prefix around the package (its version marker). `current` need not name it:
133
+ // a subagent started after `gentle-shell upgrade` moved `current` still runs
134
+ // from the previous, kept version, and stays bundled. A gentle-pi elsewhere
135
+ // (npm, pnpm, nvm), started from the agent's shell that inherits the variable,
136
+ // gets undefined. The answer is remembered per package root and prefix value,
137
+ // so a `gentle-shell upgrade` that moves `current` while Pi runs does not
138
+ // change it mid-session; an injected filesystem is never remembered.
139
+ export function runningBundledPrefix(env: Env, options: { packageRoot?: string; platform?: string; fs?: BundledFs } = {}): string | undefined {
140
+ const platform = options.platform ?? process.platform;
141
+ const prefix = env[GENTLE_SHELL_PREFIX_ENV];
142
+ if (prefix === undefined || prefix.length === 0 || !pathFor(platform).isAbsolute(prefix)) return undefined;
143
+ const overlay = options.packageRoot === undefined ? installOverlay : undefined;
144
+ const packageRoot = options.packageRoot ?? ownPackageRoot();
145
+ const fs = options.fs ?? nodeBundledFs;
146
+ const key = `${packageRoot}\0${prefix}`;
147
+ const remember = options.fs === undefined;
148
+ if (remember && runningMemo.has(key)) return runningMemo.get(key);
149
+ const install = overlay !== undefined ? overlay ?? undefined : resolveBundledInstall({ packageRoot, platform, fs, kept: true });
150
+ const real = fs.realpath(prefix);
151
+ const root = install !== undefined && real !== undefined && real === (fs.realpath(install.root) ?? install.root) ? install.root : undefined;
152
+ if (remember) runningMemo.set(key, root);
153
+ return root;
154
+ }
155
+
156
+ // The parent of gentle-pi's bundled config folder. Gentle AI 4.0.0 reads
157
+ // GENTLE_PI_CONFIG_HOME as this parent and writes <parent>/gentle-ai
158
+ // (internal/cli/pi_background.go), so only the Gentle AI processes Gentle Shell
159
+ // starts get it (gentleAiIsolationEnv).
160
+ export function bundledPiConfigParent(prefix: string, platform: string = process.platform): string {
161
+ return pathFor(platform).join(prefix, "config");
162
+ }
163
+
164
+ // gentle-pi's config folder inside a bundled prefix: <prefix>/config/gentle-ai,
165
+ // the folder a Gentle AI given that parent writes, with or without #5462.
166
+ export function bundledPiConfigDir(prefix: string, platform: string = process.platform): string {
167
+ return pathFor(platform).join(bundledPiConfigParent(prefix, platform), "gentle-ai");
168
+ }
169
+
170
+ // The config home of a bundled gentle-pi, or undefined outside one. It is
171
+ // derived, never exported as GENTLE_PI_CONFIG_HOME: Gentle AI 4.0.0 reads that
172
+ // variable as the parent of its own gentle-ai folder.
173
+ export function bundledGentlePiConfigHome(env: Env): string | undefined {
174
+ const prefix = runningBundledPrefix(env);
175
+ return prefix === undefined ? undefined : bundledPiConfigDir(prefix);
176
+ }
177
+
178
+ // The exact Pi of the version (S8): the Pi the version's package.json pins,
179
+ // installed inside the version folder with Pi's own bin entry. Never another Pi.
180
+ export function bundledPi(install: BundledInstall, input: { platform: string; fs: BundledFs }): BundledPi {
181
+ const path = pathFor(input.platform);
182
+ const pinned = parseJson(input.fs.readText(path.join(install.versionDir, "package.json")))?.dependencies?.[PI_PACKAGE];
183
+ const failure = (found?: string): BundledPi => ({
184
+ ok: false,
185
+ message: [
186
+ `gentle-shell: Gentle Shell ${install.id} needs its bundled Pi ${pinned ?? "(unknown pin)"} in ${install.versionDir}, ${found ? `but found Pi ${found}` : "but it is missing"}.`,
187
+ "Run 'gentle-shell upgrade' to install the latest release, or run the Gentle Shell installer again to repair this version.",
188
+ ].join("\n"),
189
+ });
190
+ const dir = input.fs.realpath(path.join(install.versionDir, "node_modules", ...PI_PACKAGE.split("/")));
191
+ if (typeof pinned !== "string" || dir === undefined || !inside(path, input.fs.realpath(install.versionDir) ?? install.versionDir, dir)) return failure();
192
+ const metadata = parseJson(input.fs.readText(path.join(dir, "package.json")));
193
+ if (metadata?.name !== PI_PACKAGE || metadata.bin?.pi !== PI_BIN) return failure();
194
+ if (metadata.version !== pinned) return failure(String(metadata.version));
195
+ const cli = path.join(dir, ...PI_BIN.split("/"));
196
+ if (input.fs.realpath(cli) !== cli || input.fs.readText(cli) === undefined) return failure();
197
+ return { ok: true, cli, version: pinned };
198
+ }
199
+
200
+ // The environment of a bundled Pi and its subagents: the user's own, unchanged
201
+ // (S9: Pi's tools run the user's commands in the user's projects, so its PATH,
202
+ // NODE_OPTIONS and npm settings stay the user's; Pi itself runs on our node,
203
+ // given explicitly, and its npm through the home's npmCommand setting), plus
204
+ // only no Pi override, subagents on the same node and Pi (the subagent runner
205
+ // splits GENTLE_PI_AGENTS_PI on whitespace, so a spaced path is left to its
206
+ // default, the running node and cli.js), and no Pi update check, since Pi only
207
+ // updates with Gentle Shell (S5). S9: the prefix (GENTLE_SHELL_PREFIX, from
208
+ // which gentle-pi derives its config folder and gives its own gentle-ai runs
209
+ // Gentle AI's variables, never set here because the agent's shell would pass
210
+ // them to a gentle-ai the user runs), and the published Engram as
211
+ // gentle-engram's ENGRAM_BIN unless the user chose one. With a memory decision
212
+ // (S2, S6): the startup policy, and ENGRAM_BIN is the verified core only when
213
+ // memory is allowed locally; otherwise the Shell-owned guard, so no provider,
214
+ // whatever loaded it, runs an Engram binary against an unverified store.
215
+ export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
216
+ export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe store or server reuse. Matching versions or updating alone do not prove compatibility. Reuse requires a server with verified /compatibility evidence or matching Shell-provisioned store identity. Back up existing Engram data before manual changes; do not delete it or bypass the safety check. Shell rechecks on every launch and remains usable without memory.";
217
+ // `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
218
+ export const ENGRAM_PROVISION_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not provision a new Engram store identity, so it cannot vouch for that store. Restart Shell to retry. If Shell keeps reporting unknown compatibility and ~/.engram holds only .instance-id files, back it up and remove it, then restart Shell. Shell remains usable without memory.";
219
+ export const ENGRAM_CORE_MISSING_NOTICE = "Engram memory disabled: the bundled Engram core is not installed yet. Run 'gentle-shell setup' to install the verified core, then restart Shell. Shell remains usable without memory.";
220
+ // The first Engram release whose server answers GET /compatibility (PR #1792).
221
+ export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
222
+ export function engramOutdatedServerNotice(version: string): string {
223
+ return `Engram memory disabled: the running Engram server reports version ${version}, which cannot prove store compatibility. Back up your Engram data, update that server to ${ENGRAM_COMPATIBILITY_MIN_VERSION} or later with the method you installed it with, and restart it; updating Shell alone does not replace a running server. Shell rechecks on every launch and remains usable without memory.`;
224
+ }
225
+ // A canonical numeric x.y.z (optionally v-prefixed, no leading zeros) older than
226
+ // the minimum. The version comes from the server, so anything else is never
227
+ // echoed to the user.
228
+ function outdatedServerVersion(version: string | undefined): string | undefined {
229
+ const match = /^v?(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/.exec(version ?? "");
230
+ if (match === null) return undefined;
231
+ const parts = match.slice(1).map(Number);
232
+ const minimum = ENGRAM_COMPATIBILITY_MIN_VERSION.split(".").map(Number);
233
+ const index = parts.findIndex((part, i) => part !== minimum[i]);
234
+ return index >= 0 && parts[index] < minimum[index] ? parts.join(".") : undefined;
235
+ }
236
+ // The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
237
+ export const ENGRAM_DEFAULT_PORT = 7437;
238
+ export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
239
+ const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"];
240
+ const HEX32 = /^[a-f0-9]{32}$/;
241
+
242
+ export type EngramProbe = { kind: "refused" } | { kind: "unknown" } | { kind: "listening"; compatible: boolean; healthInstanceId?: string; healthVersion?: string };
243
+ // `fresh`: Shell provisions a new store this run and may record its provenance.
244
+ // `url`: a running server verified through /compatibility, reached explicitly.
245
+ export type EngramStartup = { policy: "allow" | "disabled"; notice?: string; url?: string; fresh?: true };
246
+
247
+ // Without an invalid policy or an explicit Engram selection, the default
248
+ // endpoint may be probed; an explicit selection is never contacted.
249
+ export function engramProbeAllowed(env: Env): boolean {
250
+ const policy = env.ENGRAM_STARTUP_POLICY;
251
+ return (policy === undefined || policy === "allow") && !ENGRAM_SELECTIONS.some((key) => Boolean(env[key]?.trim()));
252
+ }
253
+
254
+ // Evidence only: never run an executable, open SQLite or infer compatibility
255
+ // from versions. `provenance` means Shell's own record matched the store
256
+ // (directory, .instance-id, core digest) and its header showed no newer schema.
257
+ // A server version only selects the wording of a disabled decision.
258
+ export function classifyEngramStartup(input: { env: Env; store: "absent" | "present" | "unknown"; endpoint: EngramProbe; core: boolean;
259
+ coreVersion: string; provenance?: { instanceId: string } }): EngramStartup {
260
+ if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
261
+ const endpoint = input.endpoint;
262
+ const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
263
+ const unknown: EngramStartup = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
264
+ if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
265
+ if (endpoint.kind === "refused" && input.store === "absent") {
266
+ return input.core ? { policy: "allow", fresh: true } : { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
267
+ }
268
+ const own = input.provenance;
269
+ if (own === undefined || !input.core || input.store !== "present") return unknown;
270
+ // Shell's own store, created by this exact core: offline, the provider starts
271
+ // that core; a running server must be that store and that exact core.
272
+ if (endpoint.kind === "refused") return { policy: "allow" };
273
+ if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow" };
274
+ return unknown;
275
+ }
276
+
277
+ // The exact contract a startup-verified store reports (Engram GET /compatibility).
278
+ export function isReadyCompatibility(text: string): boolean {
279
+ const body = parseJson(text);
280
+ return typeof body === "object" && body !== null && !Array.isArray(body) && body.status === "ready" && body.active_reuse_authorized === true &&
281
+ body.capability === "startup-verified-store-v1" && body.revision === 1 && body.source === "active" && body.offline_automatic_opening_authorized !== true &&
282
+ typeof body.instance_id === "string" && HEX32.test(body.instance_id) && typeof body.runtime_id === "string" && HEX32.test(body.runtime_id);
283
+ }
284
+
285
+ const PROBE_BODY_LIMIT = 16 * 1024;
286
+ type LocalAnswer = { kind: "refused" } | { kind: "unknown" } | { kind: "response"; status: number; body: string };
287
+
288
+ // One bounded GET on a direct loopback socket: no agent, proxy, redirects or
289
+ // credentials (ENGRAM_HTTP_TOKEN belongs to the user's server, never sent).
290
+ function localGet(port: number, path: string, timeoutMs: number): Promise<LocalAnswer> {
291
+ return new Promise((resolve) => {
292
+ let settled = false;
293
+ let socket: ReturnType<typeof createConnection> | undefined;
294
+ const finish = (answer: LocalAnswer) => {
295
+ if (settled) return;
296
+ settled = true;
297
+ clearTimeout(timer);
298
+ request.destroy();
299
+ socket?.destroy();
300
+ resolve(answer);
301
+ };
302
+ const request = httpRequest({ host: "127.0.0.1", port, path, method: "GET", headers: { accept: "application/json" },
303
+ createConnection: () => (socket = createConnection({ host: "127.0.0.1", port })) }, (response) => {
304
+ const chunks: Buffer[] = [];
305
+ let size = 0;
306
+ response.on("data", (chunk: Buffer) => {
307
+ size += chunk.length;
308
+ if (size > PROBE_BODY_LIMIT) finish({ kind: "response", status: 0, body: "" });
309
+ else chunks.push(chunk);
310
+ });
311
+ response.on("end", () => finish({ kind: "response", status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") }));
312
+ response.on("error", () => finish({ kind: "unknown" }));
313
+ });
314
+ const timer = setTimeout(() => finish({ kind: "unknown" }), timeoutMs);
315
+ request.on("error", (error: NodeJS.ErrnoException) => finish({ kind: error.code === "ECONNREFUSED" ? "refused" : "unknown" }));
316
+ request.end();
317
+ });
318
+ }
319
+
320
+ // Read-only probe of the default endpoint: /compatibility, then /health for
321
+ // the running instance and version. Never inferred from /health alone.
322
+ export async function probeEngramEndpoint(input: { port?: number; timeoutMs?: number } = {}): Promise<EngramProbe> {
323
+ const port = input.port ?? ENGRAM_DEFAULT_PORT;
324
+ const timeoutMs = input.timeoutMs ?? 1000;
325
+ const compatibility = await localGet(port, "/compatibility", timeoutMs);
326
+ if (compatibility.kind !== "response") return compatibility;
327
+ if (compatibility.status === 200 && isReadyCompatibility(compatibility.body)) return { kind: "listening", compatible: true };
328
+ const health = await localGet(port, "/health", timeoutMs);
329
+ const body = health.kind === "response" && health.status === 200 ? parseJson(health.body) : undefined;
330
+ const identified = typeof body?.instance_id === "string" && HEX32.test(body.instance_id) && typeof body.version === "string";
331
+ return { kind: "listening", compatible: false, healthInstanceId: identified ? body.instance_id : undefined, healthVersion: identified ? body.version : undefined };
332
+ }
333
+
334
+ // Shell's own record of a store it provisioned fresh with the verified core.
335
+ // Written only after Shell itself provisioned the store's identity, before Pi starts.
336
+ export type EngramProvenance = { schema: 1; dataDir: string; instanceId: string; coreSha256: string };
337
+
338
+ export function engramProvenancePath(prefix: string, platform: string = process.platform): string {
339
+ return pathFor(platform).join(bundledPiConfigParent(prefix, platform), "engram-provenance.json");
340
+ }
341
+
342
+ export function parseEngramProvenance(text: string | undefined): EngramProvenance | undefined {
343
+ const record = parseJson(text);
344
+ if (record?.schema !== 1 || typeof record.dataDir !== "string" || !(posix.isAbsolute(record.dataDir) || win32.isAbsolute(record.dataDir)) ||
345
+ typeof record.instanceId !== "string" || !HEX32.test(record.instanceId) || typeof record.coreSha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.coreSha256)) return undefined;
346
+ return { schema: 1, dataDir: record.dataDir, instanceId: record.instanceId, coreSha256: record.coreSha256 };
347
+ }
348
+
349
+ export function engramProvenanceMatches(record: EngramProvenance | undefined, observed: { dataDir: string; instanceId?: string; coreSha256?: string }): boolean {
350
+ return record !== undefined && record.dataDir === observed.dataDir && record.instanceId === observed.instanceId && record.coreSha256 === observed.coreSha256;
351
+ }
352
+
353
+ // The bundled core's own schema (SQLite user_version), verified from the
354
+ // tagged sources v3.3.2 and v3.4.0: internal/store/store.go `const schemaVersion = 1`.
355
+ // A pin without a verified entry leaves the header check unavailable.
356
+ const BUNDLED_ENGRAM_SCHEMA: Record<string, number> = { "3.3.2": 1, "3.4.0": 1 };
357
+
358
+ export function bundledEngramSchema(coreVersion: string): number | undefined {
359
+ return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
360
+ }
361
+
362
+ // SQLite's user_version from the raw database header (bytes 60-63), or
363
+ // undefined for anything that is not a well-formed rollback or WAL database.
364
+ export function sqliteHeaderUserVersion(header: Uint8Array, size: number): number | undefined {
365
+ if (header.length < 100 || Buffer.from(header.subarray(0, 16)).toString("latin1") !== "SQLite format 3\0") return undefined;
366
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
367
+ const raw = view.getUint16(16);
368
+ const pageSize = raw === 1 ? 65536 : raw;
369
+ if (pageSize < 512 || pageSize > 65536 || (pageSize & (pageSize - 1)) !== 0 || size < pageSize || size % pageSize !== 0) return undefined;
370
+ if (![1, 2].includes(header[18]) || ![1, 2].includes(header[19])) return undefined;
371
+ return view.getUint32(60);
372
+ }
373
+
374
+ // A tripwire, not a compatibility proof: a header newer than the bundled
375
+ // schema means another Engram migrated the store. Equal or lower (including a
376
+ // stale 0 before a WAL checkpoint) is no signal either way.
377
+ export function engramHeaderAllows(userVersion: number | undefined, bundledSchema: number | undefined): boolean {
378
+ return userVersion !== undefined && bundledSchema !== undefined && userVersion <= bundledSchema;
379
+ }
380
+
381
+ // Pi 1.1.0 options that always take the next token as their value (cli/args.js);
382
+ // options that skip a dash-led next token never hide a version or help flag.
383
+ const PI_VALUE_OPTIONS = new Set(["--provider", "--model", "--api-key", "--system-prompt", "--append-system-prompt", "--name", "-n", "--session",
384
+ "--session-id", "--fork", "--session-dir", "--models", "--tools", "-t", "--exclude-tools", "-xt", "--thinking", "--export", "--extension", "-e",
385
+ "--skill", "--prompt-template", "--theme"]);
386
+
387
+ // Whether Pi reads its arguments as a version or help request: such a token in
388
+ // flag position, before Pi's own `--` and not as another option's value.
389
+ export function piVersionOrHelpRequested(argv: string[]): boolean {
390
+ for (let i = 0; i < argv.length; i += 1) {
391
+ const arg = argv[i];
392
+ if (arg === "--") return false;
393
+ if (arg === "--version" || arg === "-v" || arg === "--help" || arg === "-h") return true;
394
+ if (PI_VALUE_OPTIONS.has(arg)) i += 1;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ export function supportsEngramStartupPolicy(metadata: string | undefined): boolean {
400
+ const pkg = parseJson(metadata);
401
+ return pkg?.name === "gentle-engram" && pkg.engram?.startupPolicy === 1;
402
+ }
403
+
404
+ export function bundledChildEnv(base: Env, input: { node: string; cli: string; prefix: string; engram?: string; guard?: string; memory?: EngramStartup }): Env {
405
+ const env: Env = { ...base, PI_SKIP_VERSION_CHECK: "1", [GENTLE_SHELL_PREFIX_ENV]: input.prefix };
406
+ delete env.GENTLE_SHELL_PI;
407
+ delete env[BUNDLED_VERIFY_ENV];
408
+ if (/\s/.test(`${input.node}${input.cli}`)) delete env.GENTLE_PI_AGENTS_PI;
409
+ else env.GENTLE_PI_AGENTS_PI = `${input.node} ${input.cli}`;
410
+ const memory = input.memory;
411
+ if (memory === undefined) {
412
+ if (input.engram !== undefined && !base.ENGRAM_BIN?.trim()) env.ENGRAM_BIN = input.engram;
413
+ return env;
414
+ }
415
+ if (input.guard === undefined) throw new Error("A memory decision needs the Engram guard path.");
416
+ env.ENGRAM_STARTUP_POLICY = memory.policy;
417
+ if (memory.notice) env.ENGRAM_STARTUP_NOTICE = [memory.notice, base.ENGRAM_STARTUP_NOTICE].filter(Boolean).join(" ");
418
+ // An explicit ENGRAM_BIN or ENGRAM_URL is the user's choice: never replaced.
419
+ // An explicit URL alone still gets the verified core (or the guard), never PATH.
420
+ if (base.ENGRAM_BIN?.trim()) return env;
421
+ if (base.ENGRAM_URL?.trim()) {
422
+ env.ENGRAM_BIN = input.engram ?? input.guard;
423
+ return env;
424
+ }
425
+ if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
426
+ const local = memory.policy === "allow" && memory.url === undefined ? input.engram : undefined;
427
+ env.ENGRAM_BIN = local ?? input.guard;
428
+ return env;
429
+ }
430
+
431
+ // `gentle-shell setup` in a bundled install: the child environment with `dirs`
432
+ // (our node's folder, then the version's node_modules/.bin) ahead of the user's
433
+ // PATH. Setup runs Gentle AI, which finds `pi` on PATH, and pnpm's `.bin/pi`
434
+ // runs the first `node` on PATH, so both must be ours there.
435
+ export function bundledSetupEnv(env: Env, input: { dirs: string[]; platform: string }): Env {
436
+ const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH") ?? (input.platform === "win32" ? "Path" : "PATH");
437
+ const rest = (env[key] ?? "").split(pathFor(input.platform).delimiter).filter((entry) => entry.length > 0);
438
+ return { ...env, [key]: [...input.dirs, ...rest].join(pathFor(input.platform).delimiter) };
439
+ }
440
+
441
+ export type BundledNpmCommandEdit = { status: "unchanged" } | { status: "malformed" } | { status: "write"; text: string };
442
+
443
+ // The agent home's settings.json with `npmCommand` set to the bundled npm
444
+ // (scripts/bundled-install.mjs bundledNpmCommand), every other key kept in
445
+ // order and the file's indentation and final newline kept. A file that already
446
+ // holds exactly that command is left as it is; one that is not a JSON object
447
+ // is never rewritten.
448
+ export function withBundledNpmCommand(settingsText: string | undefined, command: string[]): BundledNpmCommandEdit {
449
+ const settings = settingsText === undefined ? {} : parseJson(settingsText);
450
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
451
+ const current = settings.npmCommand;
452
+ if (Array.isArray(current) && current.length === command.length && current.every((part, index) => part === command[index])) return { status: "unchanged" };
453
+ const indent = settingsText?.match(/\{\r?\n([ \t]+)/)?.[1] ?? " ";
454
+ const serialized = JSON.stringify({ ...settings, npmCommand: command }, null, indent);
455
+ return { status: "write", text: settingsText === undefined || settingsText.endsWith("\n") ? `${serialized}\n` : serialized };
456
+ }
457
+
458
+ export interface BundledUpdatePlan {
459
+ // Pi's own update was asked for: redirect it to `gentle-shell upgrade`.
460
+ self: boolean;
461
+ // What Pi still runs, if anything: the extension part of `--all`, or the
462
+ // whole command when it never updates Pi itself.
463
+ piArgs?: string[];
464
+ }
465
+
466
+ // `gentle-shell update [target]` in a bundled install, read the way Pi's
467
+ // package-manager-cli.js reads it: the target is Pi itself for a bare
468
+ // `update`, `--self`, or a `self`/`pi` positional, and "all" for `--all`,
469
+ // `--self --extensions` or `self`/`pi` with `--extensions`. Only Pi's own part
470
+ // is redirected; "all" still sends `update --extensions` (with the other
471
+ // flags) to Pi. Packages, models, help, and forms Pi refuses (conflicting or
472
+ // unknown options) reach Pi unchanged.
473
+ export function bundledUpdatePlan(passthrough: string[]): BundledUpdatePlan {
474
+ const passThrough = { self: false, piArgs: passthrough };
475
+ const flags: string[] = [];
476
+ const positional: string[] = [];
477
+ let self = false;
478
+ let extensions = false;
479
+ let all = false;
480
+ for (const token of passthrough.slice(1)) {
481
+ if (token === "--self") self = true;
482
+ else if (token === "--extensions") extensions = true;
483
+ else if (token === "--all") all = true;
484
+ else if (["--approve", "-a", "--no-approve", "-na", "--force"].includes(token)) flags.push(token);
485
+ else if (token.startsWith("-")) return passThrough;
486
+ else positional.push(token);
487
+ }
488
+ if (positional.length > 1 || (all && (self || extensions || positional.length > 0))) return passThrough;
489
+ if (positional.length === 1) {
490
+ if ((positional[0] !== "self" && positional[0] !== "pi") || self) return passThrough;
491
+ self = true;
492
+ }
493
+ if (all || (self && extensions)) return { self: true, piArgs: ["update", "--extensions", ...flags] };
494
+ if (extensions) return passThrough;
495
+ return { self: true };
496
+ }
497
+
498
+ // What `gentle-shell update` does after the redirected upgrade (`upgradeCode`
499
+ // is undefined when it did not run): with nothing left for Pi it exits with the
500
+ // upgrade's code; otherwise Pi still runs, so `update --all` updates the
501
+ // extensions even when the upgrade could not complete.
502
+ export function bundledUpdateNext(plan: BundledUpdatePlan, upgradeCode: number | undefined): { exitCode: number } | { piArgs: string[]; note?: string } {
503
+ if (plan.piArgs === undefined) return { exitCode: upgradeCode ?? 0 };
504
+ if (plan.self && upgradeCode !== undefined && upgradeCode !== 0) {
505
+ return { piArgs: plan.piArgs, note: "gentle-shell: the Gentle Shell upgrade did not complete; updating the extensions anyway." };
506
+ }
507
+ return { piArgs: plan.piArgs };
508
+ }
509
+
510
+ export function bundledUpdateNotice(input: { piVersion: string; interactive: boolean }): string[] {
511
+ return [
512
+ `Pi ships pinned with Gentle Shell (Pi ${input.piVersion}): a newer Pi arrives with a Gentle Shell release.`,
513
+ input.interactive
514
+ ? "Running 'gentle-shell upgrade'..."
515
+ : "Run 'gentle-shell upgrade' to install the latest Gentle Shell release and its Pi.",
516
+ ];
517
+ }
518
+
519
+ export type BundledUpgradeAction = { action: "upgrade" } | { action: "rollback" } | { error: string; usage: boolean };
520
+
521
+ export const BUNDLED_UPGRADE_USAGE = "usage: gentle-shell upgrade [--channel release] | gentle-shell upgrade --rollback";
522
+
523
+ // `gentle-shell upgrade` arguments in a bundled install, which follows the
524
+ // release channel only (the main channel builds from source).
525
+ export function parseBundledUpgradeArgs(args: string[]): BundledUpgradeAction {
526
+ if (args.length === 0) return { action: "upgrade" };
527
+ if (args.length === 1 && args[0] === "--rollback") return { action: "rollback" };
528
+ const channel = args.length === 1 && args[0].startsWith("--channel=") ? args[0].slice("--channel=".length)
529
+ : args.length === 2 && args[0] === "--channel" ? args[1] : undefined;
530
+ if (channel === "release") return { action: "upgrade" };
531
+ if (channel === "main") {
532
+ return { error: "this bundled installation follows the release channel only; the main channel needs a pnpm or npm installation of Gentle Shell", usage: false };
533
+ }
534
+ return { error: BUNDLED_UPGRADE_USAGE, usage: true };
535
+ }