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
@@ -6,6 +6,11 @@ export interface DestructiveCommandMatch {
6
6
  kind: DestructionKind;
7
7
  triggerIndex: number;
8
8
  hardDeny: boolean;
9
+ /**
10
+ * Literal targets of a direct, top-level, unwrapped and unquoted recursive
11
+ * `rm` that runs before every other segment of the command; absent otherwise.
12
+ */
13
+ removalTargets?: readonly string[];
9
14
  }
10
15
  interface Token {
11
16
  value: string;
@@ -14,6 +19,10 @@ interface Token {
14
19
  index: number;
15
20
  }
16
21
  const separators = new Set([";", "&&", "||", "|", "&", "\n", "(", ")"]);
22
+ // Separators after which the next segment starts only once this one finished.
23
+ // Pipes, background jobs and subshell parentheses run segments concurrently or
24
+ // out of textual order.
25
+ const SEQUENTIAL_SEPARATORS = new Set(["", ";", "&&", "||", "\n"]);
17
26
 
18
27
  /** Literal, named heredocs are data, not executable shell segments; offsets are preserved. */
19
28
  function blankHeredocs(command: string): string {
@@ -49,10 +58,14 @@ const WRAPPER_VALUE_OPTIONS: Record<string, readonly string[]> = {
49
58
  xargs: ["-n", "-I", "-P", "--max-args", "--replace", "--max-procs"],
50
59
  };
51
60
  const WRAPPERS = ["env", "sudo", "command", "exec", "nohup", "timeout", "xargs"];
61
+ // gentle-pi#2285: unquoted reserved words that open a compound body or a
62
+ // condition are followed by a command in command position.
63
+ const RESERVED_WORDS = new Set(["then", "else", "elif", "do", "{", "!", "if", "while", "until"]);
52
64
 
53
- /** Index of the executable token after assignments and familiar wrappers. */
65
+ /** Index of the executable token after reserved words, assignments and familiar wrappers. */
54
66
  function headCursor(segment: readonly Token[]): number {
55
67
  let cursor = 0;
68
+ while (cursor < segment.length && segment[cursor].raw === segment[cursor].value && RESERVED_WORDS.has(segment[cursor].value)) cursor++;
56
69
  while (cursor < segment.length) {
57
70
  const value = segment[cursor].value;
58
71
  if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(value)) {
@@ -85,29 +98,63 @@ const DATA_COMMANDS = new Set([
85
98
  const INERT_COMMANDS = new Set(["cd", "pwd", "true", "false", "test", "[", "mkdir", "touch"]);
86
99
  const HEREDOC_RE = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g;
87
100
 
101
+ type Ranges = Array<readonly [number, number]>;
102
+ // A simple `$(...)`: no nested substitution, quoting, expansion or escapes in its body.
103
+ const SIMPLE_SUBSTITUTION_RE = /\$\(([^()'"`$\\]*)\)/g;
104
+
105
+ /**
106
+ * Replace each simple `$(...)` whose body is itself only data and inert
107
+ * commands with a same-length placeholder word, so offsets are preserved.
108
+ * Such a substitution runs nothing guarded; its output is only more words.
109
+ * Returns undefined when any substitution is not provably data, or when a
110
+ * heredoc could expand one in its body.
111
+ */
112
+ function maskDataSubstitutions(command: string): { masked: string; ranges: Ranges } | undefined {
113
+ if (!command.includes("$(")) return { masked: command, ranges: [] };
114
+ if (command.includes("<<")) return undefined;
115
+ const ranges: Ranges = [];
116
+ let opaque = false;
117
+ const masked = command.replace(SIMPLE_SUBSTITUTION_RE, (raw: string, body: string, offset: number) => {
118
+ const inner = dataRanges(body);
119
+ if (inner === undefined) opaque = true;
120
+ else ranges.push(...inner.map(([from, to]) => [offset + 2 + from, offset + 2 + to] as const));
121
+ return "_".repeat(raw.length);
122
+ });
123
+ return opaque || masked.includes("$(") ? undefined : { masked, ranges };
124
+ }
125
+
88
126
  /**
89
127
  * Byte ranges of the command that are plain arguments of a known data command
90
128
  * (gentle-shell#2174). A guarded or denied pattern matching only inside these
91
129
  * ranges is prose, not an operation. Returns no ranges, keeping every lexical
92
- * match, when the command is opaque: command or process substitution, an
130
+ * match, when the command is opaque: a command substitution that is not
131
+ * itself only data commands, process substitution or backticks, an
93
132
  * interpreter or remote shell anywhere, or quoting the tokenizer cannot
94
133
  * account for.
95
134
  */
96
- export function dataArgumentRanges(command: string): Array<readonly [number, number]> {
97
- if (/\$\(|`|[<>]\(/.test(command)) return [];
135
+ export function dataArgumentRanges(command: string): Ranges {
136
+ return dataRanges(command) ?? [];
137
+ }
138
+
139
+ /** Undefined means opaque; an empty list means only inert commands. */
140
+ function dataRanges(original: string): Ranges | undefined {
141
+ const substitutions = maskDataSubstitutions(original);
142
+ if (substitutions === undefined) return undefined;
143
+ const command = substitutions.masked;
144
+ if (/`|[<>]\(/.test(command)) return undefined;
98
145
  // A heredoc counts only when its `<<` starts an unquoted token of the raw
99
146
  // command; quoted text such as `echo '<<x'` must not blank following lines.
100
147
  const rawStarts = new Set(tokenize(command, false).filter((token) => token.raw.startsWith("<<")).map((token) => token.index));
101
148
  const heredocs = [...command.matchAll(HEREDOC_RE)];
102
- if (heredocs.some((heredoc) => !rawStarts.has(heredoc.index))) return [];
149
+ if (heredocs.some((heredoc) => !rawStarts.has(heredoc.index))) return undefined;
103
150
  const blanked = blankHeredocs(command);
104
151
  const tokens = tokenize(command);
105
152
  const covered = new Uint8Array(blanked.length);
106
153
  for (const token of tokens) covered.fill(1, token.index, token.index + token.raw.length);
107
154
  for (let index = 0; index < blanked.length; index++) {
108
- if (!covered[index] && !/\s/.test(blanked[index])) return [];
155
+ if (!covered[index] && !/\s/.test(blanked[index])) return undefined;
109
156
  }
110
- const ranges: Array<readonly [number, number]> = [];
157
+ const ranges: Ranges = [...substitutions.ranges];
111
158
  let start = 0;
112
159
  for (let end = 0; end <= tokens.length; end++) {
113
160
  if (end < tokens.length && !tokens[end].operator) continue;
@@ -120,10 +167,12 @@ export function dataArgumentRanges(command: string): Array<readonly [number, num
120
167
  if (segment[0].value === "env") cursor++;
121
168
  while (cursor < segment.length && /^[A-Za-z_][A-Za-z_0-9]*=/.test(segment[cursor].value)) cursor++;
122
169
  const head = segment[cursor];
123
- if (head === undefined || head.value !== head.raw) return [];
170
+ // A segment of plain assignments only (`f=$(ls)`) runs no command.
171
+ if (head === undefined && cursor > 0 && segment[0].value !== "env") continue;
172
+ if (head === undefined || head.value !== head.raw) return undefined;
124
173
  const name = head.value;
125
174
  if (INERT_COMMANDS.has(name)) continue;
126
- if (!DATA_COMMANDS.has(name)) return [];
175
+ if (!DATA_COMMANDS.has(name)) return undefined;
127
176
  ranges.push([head.index + head.raw.length, tokens[end]?.index ?? command.length]);
128
177
  }
129
178
  // A heredoc body belongs to the segment of its `<<` header, which the
@@ -148,12 +197,58 @@ function sqlDestroys(sql: string): boolean {
148
197
  });
149
198
  }
150
199
 
200
+ /** Operands of a plain `rm` invocation, or undefined when any argument is quoted, escaped or disables root protection. */
201
+ function removalTargets(args: readonly Token[]): string[] | undefined {
202
+ const targets: string[] = [];
203
+ let options = true;
204
+ for (const arg of args) {
205
+ if (arg.raw !== arg.value || arg.value === "--no-preserve-root") return undefined;
206
+ if (options && arg.value === "--") options = false;
207
+ else if (options && arg.value.startsWith("-")) continue;
208
+ else targets.push(arg.value);
209
+ }
210
+ return targets.length > 0 ? targets : undefined;
211
+ }
212
+
213
+ /**
214
+ * Bodies of backtick command substitutions outside single quotes, with the
215
+ * backslash escapes that backticks remove, and the offset of each opening
216
+ * backtick. An unterminated substitution is a shell syntax error and runs
217
+ * nothing, so it yields no body. Heredoc bodies stay data, as elsewhere.
218
+ */
219
+ function backtickBodies(command: string): Array<{ body: string; index: number }> {
220
+ const bodies: Array<{ body: string; index: number }> = [];
221
+ const text = blankHeredocs(command);
222
+ let quote: "'" | '"' | undefined;
223
+ for (let index = 0; index < text.length; index++) {
224
+ const char = text[index];
225
+ if (quote === "'") {
226
+ if (char === "'") quote = undefined;
227
+ } else if (char === "\\") {
228
+ index++;
229
+ } else if (char === "'" && quote === undefined) {
230
+ quote = "'";
231
+ } else if (char === '"') {
232
+ quote = quote === '"' ? undefined : '"';
233
+ } else if (char === "`") {
234
+ let end = index + 1;
235
+ while (end < text.length && text[end] !== "`") end += text[end] === "\\" ? 2 : 1;
236
+ if (end >= text.length) break;
237
+ bodies.push({ body: text.slice(index + 1, end).replace(/\\([\\$`])/g, "$1"), index });
238
+ index = end;
239
+ }
240
+ }
241
+ return bodies;
242
+ }
243
+
151
244
  export function recognizeDestructiveCommands(command: string, depth = 0): DestructiveCommandMatch[] {
152
245
  if (depth > 4) return [];
153
246
  const tokens = tokenize(command);
154
247
  const matches: DestructiveCommandMatch[] = [];
155
248
  let previous: Token[] = [];
156
249
  let previousSeparator = "";
250
+ // True while every earlier segment was a leading literal recursive `rm`.
251
+ let leadingRemovals = depth === 0;
157
252
  let start = 0;
158
253
  for (let end = 0; end <= tokens.length; end++) {
159
254
  if (end < tokens.length && !tokens[end].operator) continue;
@@ -172,6 +267,11 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
172
267
  matches.push(...recognizeDestructiveCommands(args[flag + 1], depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
173
268
  }
174
269
  }
270
+ // `eval` runs its arguments joined by spaces, like a literal `sh -c` payload.
271
+ if (name === "eval") {
272
+ const payload = (args[0] === "--" ? args.slice(1) : args).join(" ");
273
+ matches.push(...recognizeDestructiveCommands(payload, depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
274
+ }
175
275
  if (["psql", "mysql", "mariadb", "sqlite3"].includes(name)) {
176
276
  const payloads = args.map((arg) => arg.replace(/^(?:--command|--execute)=/, ""));
177
277
  const heredoc = command.slice(head.index).match(/^[^\n]*<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/);
@@ -184,6 +284,13 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
184
284
  if (name === "rm" && args.some((arg) => /^-[^-]*[rR]/.test(arg) || arg === "--recursive")) {
185
285
  const rootTarget = args.some((arg) => /^(?:\/(?:\*+)?$|\.{1,2}\/?$|~(?:\/|$)|\$\{?HOME\}?(?:\/|$))/.test(arg));
186
286
  add(DESTRUCTION_KIND.FILESYSTEM, rootTarget);
287
+ const sequential = SEQUENTIAL_SEPARATORS.has(tokens[end]?.raw ?? "");
288
+ const targets = leadingRemovals && sequential && cursor === 0 && head.raw === "rm" ? removalTargets(segment.slice(1)) : undefined;
289
+ if (targets !== undefined) matches[matches.length - 1].removalTargets = targets;
290
+ else leadingRemovals = false;
291
+ } else if (segment.length > 0 || tokens[end]?.raw !== "\n") {
292
+ // Only blank lines may sit between leading removals.
293
+ leadingRemovals = false;
187
294
  }
188
295
  if (name === "find") {
189
296
  if (args.includes("-delete")) add(DESTRUCTION_KIND.FILESYSTEM);
@@ -216,5 +323,9 @@ export function recognizeDestructiveCommands(command: string, depth = 0): Destru
216
323
  previousSeparator = tokens[end]?.value ?? "";
217
324
  start = end + 1;
218
325
  }
326
+ // Backtick substitutions run like `$(...)`, also inside double quotes.
327
+ for (const { body, index } of backtickBodies(command)) {
328
+ matches.push(...recognizeDestructiveCommands(body, depth + 1).map((match) => ({ ...match, triggerIndex: index })));
329
+ }
219
330
  return matches;
220
331
  }
@@ -0,0 +1,31 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export interface ExternalEditorHost {
4
+ stop(): void;
5
+ start(): void;
6
+ requestRender(force?: boolean): void;
7
+ }
8
+
9
+ export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync, cwd?: string): boolean {
10
+ const command = env.VISUAL || env.EDITOR;
11
+ if (!command) return false;
12
+ const [editor, ...editorArgs] = command.split(" ");
13
+ // Repository-controlled paths must never become shell syntax. Batch
14
+ // editors require cmd.exe, so fail closed rather than quoting for a shell.
15
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(editor)) {
16
+ throw new Error("Could not open editor: Windows batch editors are not supported. Set $VISUAL or $EDITOR to a direct executable (.exe).");
17
+ }
18
+
19
+ host.stop();
20
+ try {
21
+ const result = spawn(editor, [...editorArgs, path], { cwd, stdio: "inherit", shell: false });
22
+ if (result.error) throw result.error;
23
+ if (result.status !== 0) throw new Error(result.signal ? `terminated by ${result.signal}` : `exit status ${result.status}`);
24
+ } catch (error) {
25
+ throw new Error(`Could not open editor: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
26
+ } finally {
27
+ host.start();
28
+ host.requestRender(true);
29
+ }
30
+ return true;
31
+ }
@@ -0,0 +1,146 @@
1
+ // Gauge theme roles from the installed theme source (issue #2222).
2
+ //
3
+ // Pi's theme schema is closed at every level (colors, export and the theme
4
+ // object itself reject unknown properties) and `theme.fg` throws on unknown
5
+ // tokens, so gauge roles cannot be theme colors or a top-level block. The
6
+ // one open map is `vars` (a free-form record of color values), so themes
7
+ // declare the roles there under reserved `gauge.` keys. Color values resolve
8
+ // like any Pi theme value: an existing theme token name (painted through
9
+ // `theme.fg`), or a direct color (hex, okhsl, or a 256-color index, painted
10
+ // as raw ANSI so the gauges alone recolor). Glyph values are literal single
11
+ // characters. Everything is optional and falls back per role to the gauge's
12
+ // historical roles, so themes without the keys — and invalid values — render
13
+ // exactly as before.
14
+
15
+ import { closeSync, constants, fstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
16
+ import { colorToRgb, parseColor } from "@earendil-works/pi-tui";
17
+ import { visibleWidth } from "@earendil-works/pi-tui";
18
+ import type { GaugePaint } from "./shell-gauge.ts";
19
+
20
+ const MAX_BYTES = 256_000;
21
+ const REFERENCE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
22
+ const COLOR_KEYS = ["ok", "warning", "error", "empty"] as const;
23
+ const GLYPH_KEYS = ["filled", "emptyGlyph"] as const;
24
+ // Reserved `vars` names that carry the gauge customization, e.g. "vars": { "gauge.ok": "#22c55e" }.
25
+ const gaugeVar = (key: string): string => `gauge.${key}`;
26
+
27
+ interface CacheEntry {
28
+ mtimeMs: number;
29
+ size: number;
30
+ paint: GaugePaint | undefined;
31
+ }
32
+
33
+ // The bar and panel render many times a second, so a render pays at most one
34
+ // statSync per call; the file is opened and parsed only when the stat changes,
35
+ // and a failed resolve is cached like any other result so a malformed theme
36
+ // costs no repeated reads on the render path.
37
+ const cache = new Map<string, CacheEntry>();
38
+
39
+ function record(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ /** One visible terminal column, single code point, no control characters or escape sequences. */
44
+ function isGlyph(value: unknown): value is string {
45
+ return typeof value === "string" && [...value].length === 1 && visibleWidth(value) === 1 && !/[\u0000-\u001f\u007f\x1b]/.test(value);
46
+ }
47
+
48
+ // A color entry resolves like a Pi theme value: variable references expand
49
+ // first, then either an existing theme token keeps its name (so the theme's
50
+ // own resolved ANSI applies) or the value delegates to pi-tui's `parseColor`,
51
+ // the same grammar themes use everywhere else (3 or 6 digit hex, oklch,
52
+ // case-insensitive okhsl), and becomes raw foreground ANSI. Direct colors are
53
+ // what let a theme recolor only the gauges, exactly like the issue's
54
+ // green/yellow/red statusline use case.
55
+ function resolveGaugeColor(value: unknown, colors: Record<string, unknown>, vars: Record<string, unknown>): string | undefined {
56
+ const seen = new Set<unknown>();
57
+ while (typeof value === "string" && REFERENCE.test(value) && Object.hasOwn(vars, value)) {
58
+ if (seen.has(value)) return undefined;
59
+ seen.add(value);
60
+ value = vars[value];
61
+ }
62
+ if (typeof value === "string" && Object.hasOwn(colors, value)) return value;
63
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255) return `\x1b[38;5;${value}m`;
64
+ if (typeof value === "string") {
65
+ try {
66
+ const { r, g, b } = colorToRgb(parseColor(value));
67
+ return `\x1b[38;2;${r};${g};${b}m`;
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /** Read only the installed theme source supplied by Pi; never fall back to the active theme. */
76
+ function readThemeSource(sourcePath: string): unknown {
77
+ const resolved = realpathSync(sourcePath);
78
+ const fd = openSync(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
79
+ let text: string;
80
+ try {
81
+ const stat = fstatSync(fd);
82
+ if (!stat.isFile() || stat.size > MAX_BYTES) throw new Error("Theme source unavailable or too large.");
83
+ text = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(fd));
84
+ } finally {
85
+ closeSync(fd);
86
+ }
87
+ return JSON.parse(text);
88
+ }
89
+
90
+ /**
91
+ * Gauge roles and glyphs declared by the theme source, or undefined when the
92
+ * theme declares none. Invalid entries are dropped individually, never
93
+ * throwing: a bad `gaugeWarning` keeps `warning` rather than disabling the rest.
94
+ */
95
+ export function resolveGaugePaint(sourcePath: string): GaugePaint | undefined {
96
+ const document: unknown = readThemeSource(sourcePath);
97
+ if (!record(document) || !record(document.vars)) return undefined;
98
+ const vars = document.vars;
99
+ const hasGaugeVars = COLOR_KEYS.some((key) => gaugeVar(key) in vars) || GLYPH_KEYS.some((key) => gaugeVar(key) in vars);
100
+ if (!hasGaugeVars) return undefined;
101
+ const colors = record(document.colors) ? document.colors : {};
102
+ const paint: GaugePaint = {};
103
+ for (const key of COLOR_KEYS) {
104
+ const declared = vars[gaugeVar(key)];
105
+ if (declared === undefined) continue;
106
+ const value = resolveGaugeColor(declared, colors, vars);
107
+ if (value !== undefined) paint[key] = value;
108
+ }
109
+ for (const key of GLYPH_KEYS) {
110
+ const value = vars[gaugeVar(key)];
111
+ if (isGlyph(value)) paint[key] = value;
112
+ }
113
+ return paint;
114
+ }
115
+
116
+ /** Memoized `resolveGaugePaint`; undefined whenever the theme source is unavailable or invalid. */
117
+ export function gaugePaintFor(sourcePath: string | undefined): GaugePaint | undefined {
118
+ if (!sourcePath) return undefined;
119
+ let stat;
120
+ try {
121
+ stat = statSync(sourcePath);
122
+ } catch {
123
+ // A missing source stays uncached: recreating it must take effect.
124
+ return undefined;
125
+ }
126
+ const cached = cache.get(sourcePath);
127
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.paint;
128
+ // Validate the stat before any open: a path that is not a regular file (a
129
+ // FIFO, a device) must never reach openSync, and an oversized file must
130
+ // never reach a read. Failures resolve to the historical roles.
131
+ let paint: GaugePaint | undefined;
132
+ if (stat.isFile() && stat.size <= MAX_BYTES) {
133
+ try {
134
+ paint = resolveGaugePaint(sourcePath);
135
+ } catch {
136
+ paint = undefined;
137
+ }
138
+ }
139
+ cache.set(sourcePath, { mtimeMs: stat.mtimeMs, size: stat.size, paint });
140
+ return paint;
141
+ }
142
+
143
+ /** Test hook: drop every cached theme source entry. */
144
+ export function clearGaugePaintCache(): void {
145
+ cache.clear();
146
+ }
@@ -14,7 +14,7 @@ import {
14
14
  resolveGentleAiReleaseAsset,
15
15
  } from "../scripts/gentle-ai-installer.mjs";
16
16
  import { engramPinVersion } from "../scripts/installer-downloads.mjs";
17
- import { bundledGentlePiConfigHome, bundledPiConfigDir, bundledPiConfigParent, runningBundledPrefix } from "./gentle-shell-bundled.ts";
17
+ import { bundledPiConfigDir, bundledPiConfigParent, GENTLE_SHELL_CONFIG_HOME_ENV, resolveGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.ts";
18
18
  import { fileURLToPath } from "node:url";
19
19
 
20
20
  export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
@@ -86,9 +86,11 @@ export function bundledGentleAiPaths(prefix: string, platform: string = process.
86
86
  * (only when the pinned Engram is published), GENTLE_AI_NO_SELF_UPDATE=1 and
87
87
  * GENTLE_PI_CONFIG_HOME (the parent of GENTLE_AI_PI_CONFIG_DIR)
88
88
  * when this gentle-pi runs from the valid bundled install GENTLE_SHELL_PREFIX
89
- * names (runningBundledPrefix). Otherwise `env` itself, unchanged: a gentle-pi
90
- * installed elsewhere (the user's own, started from the agent's shell, which
91
- * inherits the variable) keeps its own Gentle AI.
89
+ * names (runningBundledPrefix). In the standalone launcher's isolated home
90
+ * (#2201) only the config pair, for the folder its marker names. Otherwise
91
+ * `env` itself, unchanged: a gentle-pi installed elsewhere (the user's own,
92
+ * started from the agent's shell, which inherits the variable) keeps its own
93
+ * Gentle AI.
92
94
  */
93
95
  export function gentleAiIsolationEnv(
94
96
  env: NodeJS.ProcessEnv,
@@ -96,7 +98,7 @@ export function gentleAiIsolationEnv(
96
98
  ): NodeJS.ProcessEnv {
97
99
  const platform = options.platform ?? process.platform;
98
100
  const prefix = runningBundledPrefix(env, { packageRoot: options.packageRoot, platform });
99
- if (prefix === undefined) return env;
101
+ if (prefix === undefined) return standaloneGentleAiEnv(env, platform);
100
102
  const paths = bundledGentleAiPaths(prefix, platform);
101
103
  let engram = false;
102
104
  try {
@@ -125,6 +127,23 @@ export function gentleAiIsolationEnv(
125
127
  };
126
128
  }
127
129
 
130
+ // #2201: the same config pair as a bundled install, for the folder the
131
+ // standalone launcher's marker names, when it decides the config home
132
+ // (resolveGentlePiConfigHome; an explicit GENTLE_PI_CONFIG_HOME keeps its
133
+ // existing meaning for Gentle AI). Gentle AI's own state stays where it was.
134
+ // The parent is given only for a folder named gentle-ai, the one Gentle AI
135
+ // 4.0.0 appends to it.
136
+ function standaloneGentleAiEnv(env: NodeJS.ProcessEnv, platform: string): NodeJS.ProcessEnv {
137
+ const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
138
+ if (marker === undefined || resolveGentlePiConfigHome(env, homedir(), { platform }) !== marker) return env;
139
+ const path = platform === "win32" ? win32 : posix;
140
+ return {
141
+ ...env,
142
+ GENTLE_AI_PI_CONFIG_DIR: marker,
143
+ ...(path.basename(marker) === "gentle-ai" ? { GENTLE_PI_CONFIG_HOME: path.dirname(marker) } : {}),
144
+ };
145
+ }
146
+
128
147
  function sha256(value: Buffer): string {
129
148
  return createHash("sha256").update(value).digest("hex");
130
149
  }
@@ -136,7 +155,9 @@ function sha256(value: Buffer): string {
136
155
  // 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
137
156
  // 2. the persistent registration file at
138
157
  // <config home>/dev-binary.json (a bundled prefix's gentle-pi, else
139
- // GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai) with the strict
158
+ // GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai, never the standalone
159
+ // launcher's marker: the main channel writes it outside any session,
160
+ // #2201) with the strict
140
161
  // shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
141
162
  //
142
163
  // The registration deliberately pins no digest: it is the unpinned field-test
@@ -194,7 +215,7 @@ function ambientDevBinaryEnvironment(): GentleAiDevBinaryEnvironment {
194
215
  }
195
216
 
196
217
  export function gentleAiDevBinaryRegistrationPath(environment: GentleAiDevBinaryEnvironment = ambientDevBinaryEnvironment()): string {
197
- const configHome = bundledGentlePiConfigHome(environment.env) ?? environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
218
+ const configHome = resolveGentlePiConfigHome(environment.env, environment.home, { launcherMarker: false });
198
219
  return join(configHome, "dev-binary.json");
199
220
  }
200
221