gentle-pi 4.2.0 → 4.4.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 (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -10,6 +10,12 @@ export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate
10
10
 
11
11
  export const YOLO_STATUS_KEY = "gentle:yolo";
12
12
  export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
13
+ export const SUPER_YOLO_STATUS_TEXT = "💥 SUPER YOLO ON — no permission prompts; private paths stay blocked";
14
+ export const SUPER_YOLO_DIRECTIVE = `<gentle-super-yolo-session>
15
+ SUPER YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace. No permission questions are asked in this mode: proceed through ordinary and destructive shell work (including rm, resets, force operations and configured-guard confirmations) without confirmation dialogs, and make implementation and delivery choices directly.
16
+ The ONLY hard line is private material: never read, print, copy, transmit or commit secrets or private credentials — .env files, .ssh, keychains, *.pem/*.key/*.p12/*.pfx, secrets/ directories, cloud credential files. If a task seems to need them, stop and ask the human.
17
+ This does NOT override explicit human restrictions in the live conversation, and never auto-answers ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Children remain bounded by their delegated task; they receive no independent delivery grant or Super Yolo inheritance.
18
+ </gentle-super-yolo-session>`;
13
19
  export const YOLO_DIRECTIVE = `<gentle-yolo-session>
14
20
  YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
15
21
  Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
@@ -40,12 +46,14 @@ export class YoloSessionPolicy {
40
46
  }
41
47
  }
42
48
 
43
- /** Remove our exact block as well as adding it: bridge options can be reused. */
44
- export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
49
+ /** Remove our exact blocks as well as adding them: bridge options can be reused. */
50
+ export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean, superActive = false): void {
45
51
  if (!options) return;
46
52
  options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
47
- .replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "");
48
- if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
53
+ .replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "")
54
+ .replace(`\n\n${SUPER_YOLO_DIRECTIVE}`, "").replace(SUPER_YOLO_DIRECTIVE, "");
55
+ if (superActive) appendSystemPromptOnce(options, SUPER_YOLO_DIRECTIVE);
56
+ else if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
49
57
  }
50
58
  const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
51
59
  type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
@@ -86,7 +94,8 @@ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContex
86
94
  }
87
95
  export interface YoloSessionController {
88
96
  active(context: ExtensionContext): Promise<boolean>;
89
- mode(context: ExtensionContext): Promise<PermissionMode>;
97
+ superActive(context: ExtensionContext): Promise<boolean>;
98
+ mode(context: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode>;
90
99
  setMode(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
91
100
  defaultMode(): PermissionMode;
92
101
  saveDefault(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
@@ -101,7 +110,7 @@ interface ModeIdentity {
101
110
  }
102
111
  /** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
103
112
  export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
104
- options: { onChange?: () => void } = {}): YoloSessionController {
113
+ options: { onChange?: () => void; resolveIdentity?: typeof resolveCanonicalGitRepositoryIdentity } = {}): YoloSessionController {
105
114
  const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
106
115
  let generation = 0;
107
116
  let ownerGeneration = 0;
@@ -112,6 +121,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
112
121
  const seen = new WeakMap<object, Set<string>>();
113
122
  const observers = new Set<() => void>();
114
123
  const pendingCaptures = new Set<{ manager: ModeIdentity["manager"]; id: string }>();
124
+ // Identity is re-captured for every tool admission: freshness is a security
125
+ // invariant, so cross-admission reuse is never allowed (a mid-lookup mode
126
+ // reselection must invalidate). Within one admission the redundant mode()
127
+ // lookups of the same tool call share at most ONE capture, keyed by that
128
+ // admission's tool call id; a different or absent id re-captures, and a
129
+ // generation bump or cwd change still invalidates a shared slot.
130
+ let capturedMode: (ModeIdentity & { cwd: string; generation: number; admission: string }) | undefined;
131
+ // Rendered strings last published for each context; identical repaints are skipped.
132
+ const published = new WeakMap<ExtensionContext, { yolo: string | undefined; modeText: string }>();
115
133
  const primary = () => env.GENTLE_PI_AGENTS_CHILD !== "1";
116
134
  const interactive = (ctx: ExtensionContext): boolean => {
117
135
  if (!primary() || !ctx.hasUI) return false;
@@ -129,10 +147,15 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
129
147
  const ids = seen.get(scope.manager) ?? new Set<string>(); ids.add(scope.id); seen.set(scope.manager, ids);
130
148
  };
131
149
  const publish = (ctx: ExtensionContext): void => {
132
- const yolo = liveMode === "yolo";
133
- try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo ? YOLO_STATUS_TEXT : undefined); } catch { /* display only */ }
134
- try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [YOLO_STATUS_TEXT] : undefined); } catch { /* display only */ }
135
- try { ctx.ui.setStatus(MODE_STATUS_KEY, `Permissions: ${liveMode.toUpperCase()}`); } catch { /* display only */ }
150
+ const yolo = liveMode === "yolo" ? YOLO_STATUS_TEXT : liveMode === "super-yolo" ? SUPER_YOLO_STATUS_TEXT : undefined;
151
+ const modeText = `Permissions: ${liveMode.toUpperCase()}`;
152
+ const previous = published.get(ctx);
153
+ if (previous && previous.yolo === yolo && previous.modeText === modeText) return;
154
+ let painted = true;
155
+ try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo); } catch { painted = false; /* display only */ }
156
+ try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [yolo] : undefined); } catch { painted = false; /* display only */ }
157
+ try { ctx.ui.setStatus(MODE_STATUS_KEY, modeText); } catch { painted = false; /* display only */ }
158
+ if (painted) published.set(ctx, { yolo, modeText });
136
159
  };
137
160
  const changed = (ctx: ExtensionContext): void => {
138
161
  generation += 1; publish(ctx);
@@ -162,17 +185,25 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
162
185
  try {
163
186
  const canonical = await realpath(cwd);
164
187
  if (!current()) return undefined;
165
- const clone = await resolveCanonicalGitRepositoryIdentity(canonical);
188
+ const clone = await (options.resolveIdentity ?? resolveCanonicalGitRepositoryIdentity)(canonical);
166
189
  if (!current()) return undefined;
167
190
  return { ...scope, workspace: clone ?? `cwd:${canonical}` };
168
191
  } catch { return undefined; }
169
192
  finally { pendingCaptures.delete(scope); }
170
193
  };
171
194
  const defaultMode = (): PermissionMode => resolvePermissionModePolicy(store).mode;
172
- const mode = async (ctx: ExtensionContext): Promise<PermissionMode> => {
173
- const epoch = generation;
174
- const identity = await capture(ctx);
175
- if (epoch !== generation) return "normal";
195
+ const mode = async (ctx: ExtensionContext, admission?: { toolCallId: string }): Promise<PermissionMode> => {
196
+ const scope = session(ctx);
197
+ const admissionId = admission?.toolCallId;
198
+ let identity: ModeIdentity | undefined;
199
+ if (admissionId !== undefined && scope && capturedMode &&
200
+ capturedMode.admission === admissionId && capturedMode.generation === generation &&
201
+ capturedMode.manager === scope.manager && capturedMode.id === scope.id && capturedMode.cwd === ctx.cwd) identity = capturedMode;
202
+ if (!identity) {
203
+ const epoch = generation;
204
+ identity = await capture(ctx);
205
+ if (epoch !== generation) return "normal";
206
+ }
176
207
  if (binding && (!identity || binding.manager !== identity.manager || binding.id !== identity.id ||
177
208
  binding.workspace !== identity.workspace || (interactiveGrant && !interactive(ctx)))) {
178
209
  binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
@@ -182,12 +213,17 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
182
213
  remember(identity); binding = identity;
183
214
  if (fresh) { liveMode = defaultMode(); interactiveGrant = false; changed(ctx); }
184
215
  }
216
+ // Record after the binding pass: internal changed() bumps above must not strand
217
+ // a cache entry stamped with a superseded generation. Only admission-keyed
218
+ // lookups record; the slot is overwritten when a different admission id arrives.
219
+ if (identity && scope && admissionId !== undefined) capturedMode = { ...identity, cwd: ctx.cwd, generation, admission: admissionId };
185
220
  publish(ctx);
186
221
  return identity && binding ? liveMode : "normal";
187
222
  };
188
223
  const active = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "yolo";
224
+ const superActive = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "super-yolo";
189
225
  const reset = (ctx: ExtensionContext): void => {
190
- ownerGeneration += 1;
226
+ ownerGeneration += 1; capturedMode = undefined;
191
227
  // Initial session_start remains fresh; shutdown/reload of an observed session revokes.
192
228
  if (binding) remember(binding);
193
229
  const scope = session(ctx);
@@ -228,7 +264,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
228
264
  };
229
265
  const humanAction = async (action: HumanAction, ctx: ExtensionContext, current = () => true): Promise<boolean> => {
230
266
  if (!current()) return false;
231
- const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo");
267
+ const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo" && liveMode !== "super-yolo");
232
268
  const pending = setMode(enable ? "yolo" : "normal", ctx, current);
233
269
  const epoch = generation; // setMode revokes synchronously, then publishes once on success.
234
270
  const result = await pending;
@@ -253,7 +289,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
253
289
  const epoch = generation, value = await mode(ctx);
254
290
  if (!current()) return YOLO_DISPLAY.unavailable;
255
291
  if (epoch !== generation) return read();
256
- return binding ? value === "yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
292
+ return binding ? value === "yolo" || value === "super-yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
257
293
  };
258
294
  request.respond({
259
295
  read,
@@ -276,7 +312,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
276
312
  } catch { /* obsolete trusted context: discovery expires closed */ }
277
313
  });
278
314
  pi.registerCommand("gentle:mode", {
279
- description: "Permissions: normal|auto|yolo|status; default normal|auto|yolo saves for new primary sessions.",
315
+ description: "Permissions: normal|auto|yolo|super-yolo|status; default normal|auto|yolo|super-yolo saves for new primary sessions.",
280
316
  handler: async (args, ctx) => {
281
317
  const words = args.trim().split(/\s+/);
282
318
  if (!args.trim() || args.trim() === "status") {
@@ -290,7 +326,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
290
326
  else notify(ctx, "Mode unchanged — interactive primary session required or the session changed.", true);
291
327
  return;
292
328
  }
293
- notify(ctx, "Use /gentle:mode normal|auto|yolo|status or /gentle:mode default normal|auto|yolo. State unchanged.", true);
329
+ notify(ctx, "Use /gentle:mode normal|auto|yolo|super-yolo|status or /gentle:mode default normal|auto|yolo|super-yolo. State unchanged.", true);
294
330
  },
295
331
  });
296
332
  pi.registerCommand("gentle:yolo", {
@@ -301,7 +337,7 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
301
337
  if (!args.trim() && ctx.hasUI && typeof ctx.ui.select === "function") {
302
338
  current = bindCurrent(ctx);
303
339
  const state = await active(ctx) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
304
- const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
340
+ const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — standing permission (current: ${state})`, ["enable", "disable", "status"]);
305
341
  if (selected === undefined) return;
306
342
  action = selected;
307
343
  }
@@ -313,5 +349,5 @@ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessE
313
349
  notify(ctx, "YOLO unchanged — the session changed while the menu was open.", true);
314
350
  },
315
351
  });
316
- return { active, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
352
+ return { active, superActive, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
317
353
  }
@@ -0,0 +1,69 @@
1
+ // YOLO temp-scratch waiver: a recursive `rm` whose every literal target is the
2
+ // session's own scratch under a temp root needs no data-loss confirmation.
3
+ // Pure over an injectable filesystem view so tests never touch real paths.
4
+ // Callers own the policy gate: primary session with active YOLO only.
5
+ import { lstatSync, realpathSync } from "node:fs";
6
+ import { homedir, tmpdir } from "node:os";
7
+ import { posix } from "node:path";
8
+ import type { DestructiveCommandMatch } from "./destructive-command-guard.ts";
9
+
10
+ export interface TempScratchStat {
11
+ uid: number;
12
+ isDirectory(): boolean;
13
+ isFile(): boolean;
14
+ isSymbolicLink(): boolean;
15
+ }
16
+
17
+ export interface TempScratchFs {
18
+ /** Throws an error with code `ENOENT` when the path is missing. */
19
+ lstat(path: string): TempScratchStat;
20
+ /** Current process uid; undefined where POSIX ownership is unavailable. */
21
+ uid: number | undefined;
22
+ /** Resolved `os.tmpdir()`, when resolvable. */
23
+ tmpdir: string | undefined;
24
+ home: string;
25
+ }
26
+
27
+ const FIXED_TEMP_ROOTS = ["/tmp", "/var/tmp"];
28
+ const MKTEMP_NAME_RE = /^tmp\.[A-Za-z0-9]{6,}$/;
29
+ // A closed character set: expansion, quoting, glob, brace and redirection
30
+ // characters (`$`, backtick, `~`, `*?[{`, quotes, `<>`) make the shell
31
+ // target differ from the text or add a write elsewhere.
32
+ const LITERAL_PATH_RE = /^\/[A-Za-z0-9._@%+=:,/-]*$/;
33
+
34
+ export function defaultTempScratchFs(): TempScratchFs {
35
+ let resolvedTmpdir: string | undefined;
36
+ try { resolvedTmpdir = realpathSync(tmpdir()); } catch { resolvedTmpdir = undefined; }
37
+ return { lstat: (path) => lstatSync(path), uid: process.getuid?.(), tmpdir: resolvedTmpdir, home: homedir() };
38
+ }
39
+
40
+ /** A temp root equal to, above or below home would expose home entries as scratch. */
41
+ function tempRoots(fs: TempScratchFs): string[] {
42
+ const home = posix.normalize(fs.home);
43
+ return [...FIXED_TEMP_ROOTS, fs.tmpdir].filter((root): root is string =>
44
+ root !== undefined && posix.isAbsolute(root) && posix.normalize(root) === root && root !== "/" &&
45
+ root !== home && !home.startsWith(`${root}/`) && !root.startsWith(`${home}/`));
46
+ }
47
+
48
+ /** One literal path component directly under a temp root, proven own scratch by an admission-time lstat. */
49
+ export function isTempScratchTarget(target: string, fs: TempScratchFs): boolean {
50
+ if (fs.uid === undefined || !LITERAL_PATH_RE.test(target)) return false;
51
+ if (target.endsWith("/") || posix.normalize(target) !== target) return false;
52
+ const name = posix.basename(target);
53
+ if (name === "" || name === "." || name === ".." || !tempRoots(fs).includes(posix.dirname(target))) return false;
54
+ let stat: TempScratchStat;
55
+ try {
56
+ stat = fs.lstat(target);
57
+ } catch (error) {
58
+ return (error as { code?: unknown }).code === "ENOENT";
59
+ }
60
+ if (stat.isSymbolicLink()) return true;
61
+ if (stat.uid !== fs.uid) return false;
62
+ return stat.isFile() || (stat.isDirectory() && MKTEMP_NAME_RE.test(name));
63
+ }
64
+
65
+ /** A recognized effect is waivable temp scratch only when it is a direct recursive `rm` of temp scratch alone. */
66
+ export function isTempScratchRemoval(match: DestructiveCommandMatch, fs: TempScratchFs): boolean {
67
+ if (match.hardDeny || match.kind !== "filesystem" || match.removalTargets === undefined) return false;
68
+ return match.removalTargets.every((target) => isTempScratchTarget(target, fs));
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,6 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "check:provider-contract": "node scripts/check-provider-contract.mjs",
45
- "mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
46
45
  "postinstall": "node scripts/install-gentle-ai.mjs",
47
46
  "test": "node scripts/run-test-suite.mjs",
48
47
  "test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
@@ -15,7 +15,7 @@ import {
15
15
  resolveGentleAiReleaseAsset,
16
16
  } from "../scripts/gentle-ai-installer.mjs";
17
17
  import { engramPinVersion } from "../scripts/installer-downloads.mjs";
18
- import { bundledGentlePiConfigHome, bundledPiConfigDir, bundledPiConfigParent, runningBundledPrefix } from "./gentle-shell-bundled.mjs";
18
+ import { bundledPiConfigDir, bundledPiConfigParent, GENTLE_SHELL_CONFIG_HOME_ENV, resolveGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
20
 
21
21
  export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
@@ -87,9 +87,11 @@ export function bundledGentleAiPaths(prefix , platform = process.
87
87
  * (only when the pinned Engram is published), GENTLE_AI_NO_SELF_UPDATE=1 and
88
88
  * GENTLE_PI_CONFIG_HOME (the parent of GENTLE_AI_PI_CONFIG_DIR)
89
89
  * when this gentle-pi runs from the valid bundled install GENTLE_SHELL_PREFIX
90
- * names (runningBundledPrefix). Otherwise `env` itself, unchanged: a gentle-pi
91
- * installed elsewhere (the user's own, started from the agent's shell, which
92
- * inherits the variable) keeps its own Gentle AI.
90
+ * names (runningBundledPrefix). In the standalone launcher's isolated home
91
+ * (#2201) only the config pair, for the folder its marker names. Otherwise
92
+ * `env` itself, unchanged: a gentle-pi installed elsewhere (the user's own,
93
+ * started from the agent's shell, which inherits the variable) keeps its own
94
+ * Gentle AI.
93
95
  */
94
96
  export function gentleAiIsolationEnv(
95
97
  env ,
@@ -97,7 +99,7 @@ export function gentleAiIsolationEnv(
97
99
  ) {
98
100
  const platform = options.platform ?? process.platform;
99
101
  const prefix = runningBundledPrefix(env, { packageRoot: options.packageRoot, platform });
100
- if (prefix === undefined) return env;
102
+ if (prefix === undefined) return standaloneGentleAiEnv(env, platform);
101
103
  const paths = bundledGentleAiPaths(prefix, platform);
102
104
  let engram = false;
103
105
  try {
@@ -126,6 +128,23 @@ export function gentleAiIsolationEnv(
126
128
  };
127
129
  }
128
130
 
131
+ // #2201: the same config pair as a bundled install, for the folder the
132
+ // standalone launcher's marker names, when it decides the config home
133
+ // (resolveGentlePiConfigHome; an explicit GENTLE_PI_CONFIG_HOME keeps its
134
+ // existing meaning for Gentle AI). Gentle AI's own state stays where it was.
135
+ // The parent is given only for a folder named gentle-ai, the one Gentle AI
136
+ // 4.0.0 appends to it.
137
+ function standaloneGentleAiEnv(env , platform ) {
138
+ const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
139
+ if (marker === undefined || resolveGentlePiConfigHome(env, homedir(), { platform }) !== marker) return env;
140
+ const path = platform === "win32" ? win32 : posix;
141
+ return {
142
+ ...env,
143
+ GENTLE_AI_PI_CONFIG_DIR: marker,
144
+ ...(path.basename(marker) === "gentle-ai" ? { GENTLE_PI_CONFIG_HOME: path.dirname(marker) } : {}),
145
+ };
146
+ }
147
+
129
148
  function sha256(value ) {
130
149
  return createHash("sha256").update(value).digest("hex");
131
150
  }
@@ -137,7 +156,9 @@ function sha256(value ) {
137
156
  // 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
138
157
  // 2. the persistent registration file at
139
158
  // <config home>/dev-binary.json (a bundled prefix's gentle-pi, else
140
- // GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai) with the strict
159
+ // GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai, never the standalone
160
+ // launcher's marker: the main channel writes it outside any session,
161
+ // #2201) with the strict
141
162
  // shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
142
163
  //
143
164
  // The registration deliberately pins no digest: it is the unpinned field-test
@@ -195,7 +216,7 @@ function ambientDevBinaryEnvironment() {
195
216
  }
196
217
 
197
218
  export function gentleAiDevBinaryRegistrationPath(environment = ambientDevBinaryEnvironment()) {
198
- const configHome = bundledGentlePiConfigHome(environment.env) ?? environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
219
+ const configHome = resolveGentlePiConfigHome(environment.env, environment.home, { launcherMarker: false });
199
220
  return join(configHome, "dev-binary.json");
200
221
  }
201
222
 
@@ -2,7 +2,7 @@
2
2
  import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
3
  import { request as httpRequest } from "node:http";
4
4
  import { createConnection } from "node:net";
5
- import { dirname, posix, win32 } from "node:path";
5
+ import { dirname, isAbsolute, posix, win32 } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  // The launcher in a bundled install (S5, S8): pure decisions over an injected
@@ -28,6 +28,46 @@ import { fileURLToPath } from "node:url";
28
28
 
29
29
 
30
30
 
31
+ /**
32
+ * Builds the detached shared server's environment without retaining session scratch.
33
+ * Windows Node can reintroduce an omitted TEMP from its parent, so use the
34
+ * original stable ancestor there; POSIX omits scratch temp settings instead.
35
+ */
36
+ export function sharedEngramEnv(base , platform = process.platform) {
37
+ const path = pathFor(platform);
38
+ const env = {};
39
+ const scratch = (value ) => /^(gp|ga)[A-Za-z0-9]{6}$/.test(path.basename(path.normalize(value)));
40
+ let stableTemp ;
41
+ let tempKey ;
42
+ for (const [key, value] of Object.entries(base)) {
43
+ const name = platform === "win32" ? key.toUpperCase() : key;
44
+ if (name.startsWith("ENGRAM_") || name === "GH_TOKEN" || name === "GITHUB_TOKEN") continue;
45
+ if (["TMPDIR", "TMP", "TEMP"].includes(name) && value !== undefined && scratch(value)) {
46
+ if (platform !== "win32") continue;
47
+ // A drive-relative or root-relative path depends on ambient state.
48
+ const root = win32.parse(value).root;
49
+ if (!/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+[\\/]?)$/.test(root) || value.split(/[\\/]/).includes("..")) {
50
+ throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
51
+ }
52
+ let stable = win32.normalize(value);
53
+ while (scratch(stable)) {
54
+ const parent = win32.dirname(stable);
55
+ if (parent === stable) throw new Error("An absolute Windows scratch temp path is required for shared Engram.");
56
+ stable = parent;
57
+ }
58
+ env[key] = stable;
59
+ stableTemp ??= stable;
60
+ } else {
61
+ env[key] = value;
62
+ }
63
+ if (name === "TEMP") tempKey = key;
64
+ }
65
+ // Keep an existing custom TEMP, even if TMP/TMPDIR chose another root.
66
+ if (stableTemp !== undefined && (tempKey === undefined || env[tempKey] === undefined)) env[tempKey ?? "TEMP"] = stableTemp;
67
+ env.ENGRAM_NO_UPDATE_CHECK = "1";
68
+ return env;
69
+ }
70
+
31
71
  // The same rules scripts/bundled-install.mjs writes versions and markers with.
32
72
  const VERSION_ID = /^[0-9A-Za-z][0-9A-Za-z.-]*$/;
33
73
  const VERSION_MARKER = ".gentle-shell-version";
@@ -176,6 +216,29 @@ export function bundledGentlePiConfigHome(env ) {
176
216
  return prefix === undefined ? undefined : bundledPiConfigDir(prefix);
177
217
  }
178
218
 
219
+ // #2201: the standalone launcher gives Pi this variable in its isolated home
220
+ // only, naming the config folder next to the agent dir (standaloneConfigHome:
221
+ // ~/.gentle-shell/gentle-ai by default). It is launcher-owned, never
222
+ // GENTLE_PI_CONFIG_HOME: Gentle AI 4.0.0 reads that one as the parent of its
223
+ // own gentle-ai folder, and the agent's shell would pass it to any gentle-ai.
224
+ export const GENTLE_SHELL_CONFIG_HOME_ENV = "GENTLE_SHELL_CONFIG_HOME";
225
+
226
+ // gentle-pi's config home, the one rule every reader shares: a bundled
227
+ // prefix's folder, then an explicit GENTLE_PI_CONFIG_HOME, then the launcher's
228
+ // marker (absolute only), then ~/.pi/gentle-ai. Empty values count as unset.
229
+ // `launcherMarker: false` is for install-level state written outside a session
230
+ // (the main channel's channel.json, builds and dev-binary.json): the installer
231
+ // and `gentle-shell upgrade` never see the marker, so their readers ignore it.
232
+ export function resolveGentlePiConfigHome(env , home , options = {}) {
233
+ const path = pathFor(options.platform ?? process.platform);
234
+ const bundled = bundledGentlePiConfigHome(env);
235
+ if (bundled !== undefined) return bundled;
236
+ if (env.GENTLE_PI_CONFIG_HOME) return env.GENTLE_PI_CONFIG_HOME;
237
+ const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
238
+ if (options.launcherMarker !== false && marker && path.isAbsolute(marker)) return marker;
239
+ return path.join(home, ".pi", "gentle-ai");
240
+ }
241
+
179
242
  // The exact Pi of the version (S8): the Pi the version's package.json pins,
180
243
  // installed inside the version folder with Pi's own bin entry. Never another Pi.
181
244
  export function bundledPi(install , input ) {
@@ -210,13 +273,14 @@ export function bundledPi(install , input
210
273
  // Gentle AI's variables, never set here because the agent's shell would pass
211
274
  // them to a gentle-ai the user runs), and the published Engram as
212
275
  // gentle-engram's ENGRAM_BIN unless the user chose one. With a memory decision
213
- // (S2, S6): the startup policy, and ENGRAM_BIN is the verified core only when
214
- // memory is allowed locally; otherwise the Shell-owned guard, so no provider,
215
- // whatever loaded it, runs an Engram binary against an unverified store.
276
+ // (S2, S6): the startup policy, and ENGRAM_BIN is the Shell-owned guard, so no
277
+ // provider, whatever loaded it, runs an Engram binary against a store; allowed
278
+ // memory reaches the shared server through ENGRAM_URL only.
216
279
  export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
217
- 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.";
280
+ export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: Shell could not verify safe server reuse. Matching versions or updating alone do not prove compatibility. Reuse requires a server on 127.0.0.1:7437 with verified /compatibility evidence; Shell never stops or replaces a server already listening there. 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.";
281
+ export const ENGRAM_START_FAILED_NOTICE = "Engram memory disabled for this launch: Shell could not start the shared Engram server, or it did not report ready in time. Restart Shell to retry; if another program holds 127.0.0.1:7437, stop it first. Shell remains usable without memory.";
282
+ export const ENGRAM_NEWER_SCHEMA_NOTICE = "Engram memory disabled: ~/.engram was migrated by a newer Engram than Shell's bundled core, so Shell will not start a server on it. Start that newer Engram's server yourself (engram serve); Shell reuses a running server with verified /compatibility evidence. Back up the store; do not delete or downgrade it. Shell remains usable without memory.";
218
283
  // `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
219
- 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.";
220
284
  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.";
221
285
  // The first Engram release whose server answers GET /compatibility (PR #1792).
222
286
  export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
@@ -237,12 +301,13 @@ function outdatedServerVersion(version )
237
301
  // The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
238
302
  export const ENGRAM_DEFAULT_PORT = 7437;
239
303
  export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
240
- const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"];
304
+ const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL", "ENGRAM_SERVER_MODE"];
241
305
  const HEX32 = /^[a-f0-9]{32}$/;
242
306
 
243
307
 
244
- // `fresh`: Shell provisions a new store this run and may record its provenance.
245
- // `url`: a running server verified through /compatibility, reached explicitly.
308
+ // `url`: the shared server, reached explicitly once /compatibility verified it.
309
+ // `spawn`: nothing listens yet; Shell starts the bundled core's server and must
310
+ // see it verified before Pi starts, or memory is disabled for that launch.
246
311
 
247
312
 
248
313
  // Without an invalid policy or an explicit Engram selection, the default
@@ -253,26 +318,24 @@ export function engramProbeAllowed(env ) {
253
318
  }
254
319
 
255
320
  // Evidence only: never run an executable, open SQLite or infer compatibility
256
- // from versions. `provenance` means Shell's own record matched the store
257
- // (directory, .instance-id, core digest) and its header showed no newer schema.
258
- // A server version only selects the wording of a disabled decision.
259
- export function classifyEngramStartup(input
260
- ) {
321
+ // from versions. Like other agents' hooks, Shell shares the global server: a
322
+ // verified listener is reused, and with nothing listening the verified bundled
323
+ // core starts one on Engram's defaults, unless the default store's header
324
+ // (`userVersion`) shows a newer schema than that core's. A listener that does
325
+ // not verify is never replaced. A server version only selects the wording.
326
+ export function classifyEngramStartup(input ) {
261
327
  if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
262
328
  const endpoint = input.endpoint;
263
- const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
264
- const unknown = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
265
329
  if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
266
- if (endpoint.kind === "refused" && input.store === "absent") {
267
- return input.core ? { policy: "allow", fresh: true } : { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
330
+ if (endpoint.kind === "refused") {
331
+ if (!input.core) return { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
332
+ if (input.userVersion !== undefined && !engramHeaderAllows(input.userVersion, bundledEngramSchema(input.coreVersion))) {
333
+ return { policy: "disabled", notice: ENGRAM_NEWER_SCHEMA_NOTICE };
334
+ }
335
+ return { policy: "allow", url: ENGRAM_DEFAULT_URL, spawn: true };
268
336
  }
269
- const own = input.provenance;
270
- if (own === undefined || !input.core || input.store !== "present") return unknown;
271
- // Shell's own store, created by this exact core: offline, the provider starts
272
- // that core; a running server must be that store and that exact core.
273
- if (endpoint.kind === "refused") return { policy: "allow" };
274
- if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow" };
275
- return unknown;
337
+ const outdated = endpoint.kind === "listening" ? outdatedServerVersion(endpoint.healthVersion) : undefined;
338
+ return { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
276
339
  }
277
340
 
278
341
  // The exact contract a startup-verified store reports (Engram GET /compatibility).
@@ -332,8 +395,8 @@ export async function probeEngramEndpoint(input
332
395
  return { kind: "listening", compatible: false, healthInstanceId: identified ? body.instance_id : undefined, healthVersion: identified ? body.version : undefined };
333
396
  }
334
397
 
335
- // Shell's own record of a store it provisioned fresh with the verified core.
336
- // Written only after Shell itself provisioned the store's identity, before Pi starts.
398
+ // Shell's record of a store an earlier Shell provisioned with its bundled core,
399
+ // read only by the explicit `gentle-shell memory migrate` recovery.
337
400
 
338
401
 
339
402
  export function engramProvenancePath(prefix , platform = process.platform) {
@@ -347,14 +410,10 @@ export function parseEngramProvenance(text )
347
410
  return { schema: 1, dataDir: record.dataDir, instanceId: record.instanceId, coreSha256: record.coreSha256 };
348
411
  }
349
412
 
350
- export function engramProvenanceMatches(record , observed ) {
351
- return record !== undefined && record.dataDir === observed.dataDir && record.instanceId === observed.instanceId && record.coreSha256 === observed.coreSha256;
352
- }
353
-
354
413
  // The bundled core's own schema (SQLite user_version), verified from the
355
- // tagged sources v3.3.2 and v3.4.0: internal/store/store.go `const schemaVersion = 1`.
414
+ // tagged sources v3.3.2, v3.4.0 and v3.5.2: internal/store/store.go `const schemaVersion = 1`.
356
415
  // A pin without a verified entry leaves the header check unavailable.
357
- const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1 };
416
+ const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1, "3.5.2": 1 };
358
417
 
359
418
  export function bundledEngramSchema(coreVersion ) {
360
419
  return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
@@ -424,8 +483,7 @@ export function bundledChildEnv(base , input
424
483
  return env;
425
484
  }
426
485
  if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
427
- const local = memory.policy === "allow" && memory.url === undefined ? input.engram : undefined;
428
- env.ENGRAM_BIN = local ?? input.guard;
486
+ env.ENGRAM_BIN = input.guard;
429
487
  return env;
430
488
  }
431
489
 
@@ -458,6 +516,58 @@ export function withBundledNpmCommand(settingsText , command
458
516
 
459
517
 
460
518
 
519
+ // settings.json without `npmCommand`: every other key kept in order and the
520
+ // file's indentation and final newline kept.
521
+ function settingsWithoutNpmCommand(settingsText , settings ) {
522
+ const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1] ?? " ";
523
+ const { npmCommand: _removed, ...rest } = settings;
524
+ const serialized = JSON.stringify(rest, null, indent);
525
+ return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
526
+ }
527
+
528
+ // The agent home's settings.json without a `npmCommand` naming the prefix a
529
+ // bundled self-uninstall removed (gentle-shell-uninstall.ts): one absolute
530
+ // entry inside `withinRemoved` drops the whole key, every other key kept in
531
+ // order and the file's indentation and final newline kept. A file that is not
532
+ // a JSON object is never rewritten; a command naming nothing inside it stays.
533
+ export function withoutBundledNpmCommand(settingsText , withinRemoved ) {
534
+ if (settingsText === undefined) return { status: "unchanged" };
535
+ const settings = parseJson(settingsText);
536
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
537
+ const current = settings.npmCommand;
538
+ if (!Array.isArray(current) || !current.some((part) => typeof part === "string" && isAbsolute(part) && withinRemoved(part))) return { status: "unchanged" };
539
+ return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
540
+ }
541
+
542
+ // An `npmCommand` a bundled install wrote (scripts/bundled-install.mjs
543
+ // bundledNpmCommand, with npm-cli.js and --prefix in earlier releases): our
544
+ // node in runtime/node-<v>, then npm's settings and cache flags, and
545
+ // --update-notifier=false last. A command the user chose never matches, even
546
+ // when its program is missing.
547
+ export function isBundledNpmCommand(command ) {
548
+ const [node] = command;
549
+ return typeof node === "string" && /[\\/]runtime[\\/]node-[^\\/]+[\\/](node\.exe|bin[\\/]node)$/.test(node)
550
+ && ["--userconfig=", "--globalconfig=", "--cache="].every((flag) => command.some((part) => typeof part === "string" && part.startsWith(flag)))
551
+ && command[command.length - 1] === "--update-notifier=false";
552
+ }
553
+
554
+ // The agent home's settings.json without an `npmCommand` a bundled install
555
+ // wrote whose executable (its first entry) no longer exists, as after a
556
+ // bundled self-uninstall that kept the home (#2469): Pi runs that command for
557
+ // every package install, so a key naming a deleted node.exe fails them all
558
+ // with ENOENT. A command whose executable exists, that names no absolute
559
+ // path, or that the user set themselves is never touched.
560
+ export function withoutMissingBundledNpmCommand(settingsText , exists ) {
561
+ if (settingsText === undefined) return { status: "unchanged" };
562
+ const settings = parseJson(settingsText);
563
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return { status: "malformed" };
564
+ const executable = Array.isArray(settings.npmCommand) ? settings.npmCommand[0] : undefined;
565
+ if (typeof executable !== "string" || !isAbsolute(executable) || exists(executable) || !isBundledNpmCommand(settings.npmCommand )) return { status: "unchanged" };
566
+ return { status: "removed", text: settingsWithoutNpmCommand(settingsText, settings) };
567
+ }
568
+
569
+
570
+
461
571
 
462
572
 
463
573