gentle-pi 4.1.0 → 4.3.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 (249) hide show
  1. package/README.md +352 -128
  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/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -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
 
@@ -176,6 +176,29 @@ export function bundledGentlePiConfigHome(env ) {
176
176
  return prefix === undefined ? undefined : bundledPiConfigDir(prefix);
177
177
  }
178
178
 
179
+ // #2201: the standalone launcher gives Pi this variable in its isolated home
180
+ // only, naming the config folder next to the agent dir (standaloneConfigHome:
181
+ // ~/.gentle-shell/gentle-ai by default). It is launcher-owned, never
182
+ // GENTLE_PI_CONFIG_HOME: Gentle AI 4.0.0 reads that one as the parent of its
183
+ // own gentle-ai folder, and the agent's shell would pass it to any gentle-ai.
184
+ export const GENTLE_SHELL_CONFIG_HOME_ENV = "GENTLE_SHELL_CONFIG_HOME";
185
+
186
+ // gentle-pi's config home, the one rule every reader shares: a bundled
187
+ // prefix's folder, then an explicit GENTLE_PI_CONFIG_HOME, then the launcher's
188
+ // marker (absolute only), then ~/.pi/gentle-ai. Empty values count as unset.
189
+ // `launcherMarker: false` is for install-level state written outside a session
190
+ // (the main channel's channel.json, builds and dev-binary.json): the installer
191
+ // and `gentle-shell upgrade` never see the marker, so their readers ignore it.
192
+ export function resolveGentlePiConfigHome(env , home , options = {}) {
193
+ const path = pathFor(options.platform ?? process.platform);
194
+ const bundled = bundledGentlePiConfigHome(env);
195
+ if (bundled !== undefined) return bundled;
196
+ if (env.GENTLE_PI_CONFIG_HOME) return env.GENTLE_PI_CONFIG_HOME;
197
+ const marker = env[GENTLE_SHELL_CONFIG_HOME_ENV];
198
+ if (options.launcherMarker !== false && marker && path.isAbsolute(marker)) return marker;
199
+ return path.join(home, ".pi", "gentle-ai");
200
+ }
201
+
179
202
  // The exact Pi of the version (S8): the Pi the version's package.json pins,
180
203
  // installed inside the version folder with Pi's own bin entry. Never another Pi.
181
204
  export function bundledPi(install , input ) {
@@ -214,14 +237,32 @@ export function bundledPi(install , input
214
237
  // memory is allowed locally; otherwise the Shell-owned guard, so no provider,
215
238
  // whatever loaded it, runs an Engram binary against an unverified store.
216
239
  export const ENGRAM_COMPANION_SOURCE = "npm:gentle-engram@0.4.2";
217
- export const ENGRAM_RECOVERY_NOTICE = "Engram memory disabled: the existing Engram data's compatibility is unknown. Back up your existing Engram data, update Engram and restart its server, then restart Shell to recheck memory safety (it rechecks on every launch). Shell remains usable without memory.";
240
+ export const ENGRAM_PRIVATE_REQUIRED_NOTICE = "Engram memory disabled: session-owned servers require a supporting published bundled core and companion. This bundle does not establish both capabilities; it will not fall back to a detached shared server. Install a supporting published bundle and restart Shell. Existing shared services are not stopped or adopted as session-owned.";
241
+ export const ENGRAM_CORE_CHANGED_NOTICE = "Engram memory disabled: the bundled core changed since Shell recorded this store. Install a published bundle supporting migrate-copy, stop every Engram client, retain a complete backup, then run gentle-shell memory migrate --confirm-quiescent. The command verifies and migrates a copy before updating provenance; older bundles refuse safely. Do not edit coreSha256 or delete the original store. Shell remains usable without memory.";
242
+ 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.";
218
243
  // `gentle-shell setup` publishes the verified core (ensureEngram) on every run.
219
244
  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
245
  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.";
246
+ // The first Engram release whose server answers GET /compatibility (PR #1792).
247
+ export const ENGRAM_COMPATIBILITY_MIN_VERSION = "3.4.0";
248
+ export function engramOutdatedServerNotice(version ) {
249
+ return `Engram memory disabled: the running Engram server reports version ${version}, which cannot prove store compatibility. Back up your Engram data, update that server to ${ENGRAM_COMPATIBILITY_MIN_VERSION} or later with the method you installed it with, and restart it; updating Shell alone does not replace a running server. Shell rechecks on every launch and remains usable without memory.`;
250
+ }
251
+ // A canonical numeric x.y.z (optionally v-prefixed, no leading zeros) older than
252
+ // the minimum. The version comes from the server, so anything else is never
253
+ // echoed to the user.
254
+ function outdatedServerVersion(version ) {
255
+ const match = /^v?(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/.exec(version ?? "");
256
+ if (match === null) return undefined;
257
+ const parts = match.slice(1).map(Number);
258
+ const minimum = ENGRAM_COMPATIBILITY_MIN_VERSION.split(".").map(Number);
259
+ const index = parts.findIndex((part, i) => part !== minimum[i]);
260
+ return index >= 0 && parts[index] < minimum[index] ? parts.join(".") : undefined;
261
+ }
221
262
  // The default endpoint gentle-engram uses without ENGRAM_URL or ENGRAM_PORT.
222
263
  export const ENGRAM_DEFAULT_PORT = 7437;
223
264
  export const ENGRAM_DEFAULT_URL = `http://127.0.0.1:${ENGRAM_DEFAULT_PORT}`;
224
- const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"];
265
+ const ENGRAM_SELECTIONS = ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL"];
225
266
  const HEX32 = /^[a-f0-9]{32}$/;
226
267
 
227
268
 
@@ -239,11 +280,13 @@ export function engramProbeAllowed(env ) {
239
280
  // Evidence only: never run an executable, open SQLite or infer compatibility
240
281
  // from versions. `provenance` means Shell's own record matched the store
241
282
  // (directory, .instance-id, core digest) and its header showed no newer schema.
283
+ // A server version only selects the wording of a disabled decision.
242
284
  export function classifyEngramStartup(input
243
285
  ) {
244
- const unknown = { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
245
- if (!engramProbeAllowed(input.env)) return unknown;
286
+ if (!engramProbeAllowed(input.env)) return { policy: "disabled", notice: ENGRAM_RECOVERY_NOTICE };
246
287
  const endpoint = input.endpoint;
288
+ const outdated = endpoint.kind === "listening" && !endpoint.compatible ? outdatedServerVersion(endpoint.healthVersion) : undefined;
289
+ const unknown = { policy: "disabled", notice: outdated === undefined ? ENGRAM_RECOVERY_NOTICE : engramOutdatedServerNotice(outdated) };
247
290
  if (endpoint.kind === "listening" && endpoint.compatible) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
248
291
  if (endpoint.kind === "refused" && input.store === "absent") {
249
292
  return input.core ? { policy: "allow", fresh: true } : { policy: "disabled", notice: ENGRAM_CORE_MISSING_NOTICE };
@@ -253,7 +296,7 @@ export function classifyEngramStartup(input
253
296
  // Shell's own store, created by this exact core: offline, the provider starts
254
297
  // that core; a running server must be that store and that exact core.
255
298
  if (endpoint.kind === "refused") return { policy: "allow" };
256
- if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow" };
299
+ if (endpoint.kind === "listening" && endpoint.healthInstanceId === own.instanceId && endpoint.healthVersion === input.coreVersion) return { policy: "allow", url: ENGRAM_DEFAULT_URL };
257
300
  return unknown;
258
301
  }
259
302
 
@@ -334,9 +377,9 @@ export function engramProvenanceMatches(record , ob
334
377
  }
335
378
 
336
379
  // The bundled core's own schema (SQLite user_version), verified from the
337
- // tagged source: v3.3.2:internal/store/store.go `const schemaVersion = 1`.
380
+ // tagged sources v3.3.2 and v3.4.0: internal/store/store.go `const schemaVersion = 1`.
338
381
  // A pin without a verified entry leaves the header check unavailable.
339
- const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1 };
382
+ const BUNDLED_ENGRAM_SCHEMA = { "3.3.2": 1, "3.4.0": 1 };
340
383
 
341
384
  export function bundledEngramSchema(coreVersion ) {
342
385
  return Object.hasOwn(BUNDLED_ENGRAM_SCHEMA, coreVersion) ? BUNDLED_ENGRAM_SCHEMA[coreVersion] : undefined;
@@ -384,6 +427,11 @@ export function supportsEngramStartupPolicy(metadata )
384
427
  return pkg?.name === "gentle-engram" && pkg.engram?.startupPolicy === 1;
385
428
  }
386
429
 
430
+ export function supportsEngramSessionOwned(metadata ) {
431
+ const pkg = parseJson(metadata);
432
+ return supportsEngramStartupPolicy(metadata) && pkg?.engram?.sessionOwned === 1;
433
+ }
434
+
387
435
  export function bundledChildEnv(base , input ) {
388
436
  const env = { ...base, PI_SKIP_VERSION_CHECK: "1", [GENTLE_SHELL_PREFIX_ENV]: input.prefix };
389
437
  delete env.GENTLE_SHELL_PI;
@@ -408,6 +456,12 @@ export function bundledChildEnv(base , input
408
456
  if (memory.url !== undefined) env.ENGRAM_URL = memory.url;
409
457
  const local = memory.policy === "allow" && memory.url === undefined ? input.engram : undefined;
410
458
  env.ENGRAM_BIN = local ?? input.guard;
459
+ if (local && memory.owned) {
460
+ env.ENGRAM_SERVER_MODE = "session-owned";
461
+ delete env.ENGRAM_URL;
462
+ delete env.ENGRAM_PORT;
463
+ delete env.ENGRAM_SOCKET;
464
+ }
411
465
  return env;
412
466
  }
413
467
 
@@ -0,0 +1,234 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ import { chmodSync, closeSync, linkSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { detectJsonFormatting } from "./gentle-shell-launcher.mjs";
5
+
6
+ // `gentle-shell home import-pi`: a one-time, opt-in copy of the user's pi
7
+ // sign-ins and MCP servers into a separate Gentle Shell home, as an
8
+ // alternative to --link. Only the files and settings keys below are read;
9
+ // packages, extensions, caches, AGENTS.md and sessions are never copied, so
10
+ // the target home keeps its own provisioning. Nothing is ever overwritten and
11
+ // the source is only read.
12
+
13
+ // auth.json holds OAuth tokens and API keys: always private in the copy.
14
+ const IMPORTED_FILES = [
15
+ { name: "auth.json", mode: 0o600 },
16
+ { name: "mcp.json" },
17
+ ];
18
+
19
+ // Pi 1.2.0's own settings for the default model (SettingsManager
20
+ // get/setDefaultProvider, DefaultModel, DefaultThinkingLevel).
21
+ export const IMPORTED_SETTINGS_KEYS = ["defaultProvider", "defaultModel", "defaultThinkingLevel"];
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+ function tempPathFor(path ) {
49
+ return join(dirname(path), `.${basename(path)}.gentle-shell-${process.pid}.tmp`);
50
+ }
51
+
52
+ function removeQuietly(path ) {
53
+ try {
54
+ unlinkSync(path);
55
+ } catch {
56
+ // Nothing left to clean up.
57
+ }
58
+ }
59
+
60
+ function writeTemp(path , data , mode ) {
61
+ const temp = tempPathFor(path);
62
+ const fd = openSync(temp, "wx", mode);
63
+ try {
64
+ try {
65
+ writeFileSync(fd, data);
66
+ } finally {
67
+ closeSync(fd);
68
+ }
69
+ chmodSync(temp, mode); // the umask must not change the mode
70
+ } catch (error) {
71
+ // A partial copy may hold credentials: never leave it behind.
72
+ removeQuietly(temp);
73
+ throw error;
74
+ }
75
+ return temp;
76
+ }
77
+
78
+ export const nodeImportPiFs = {
79
+ kind: (path) => {
80
+ try {
81
+ const stat = lstatSync(path);
82
+ if (stat.isSymbolicLink()) return "other";
83
+ return stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
84
+ } catch (error) {
85
+ if ((error ).code === "ENOENT") return "missing";
86
+ throw error;
87
+ }
88
+ },
89
+ mode: (path) => lstatSync(path).mode & 0o777,
90
+ readText: (path) => readFileSync(path, "utf8"),
91
+ copyNew: (from, to, mode) => {
92
+ const temp = writeTemp(to, readFileSync(from), mode);
93
+ try {
94
+ linkSync(temp, to); // EEXIST instead of replacing a file that appeared meanwhile
95
+ } finally {
96
+ removeQuietly(temp);
97
+ }
98
+ },
99
+ writeAtomic: (path, text) => {
100
+ const mode = lstatSync(path, { throwIfNoEntry: false })?.mode;
101
+ const temp = writeTemp(path, text, mode === undefined ? 0o600 : mode & 0o777);
102
+ try {
103
+ renameSync(temp, path);
104
+ } catch (error) {
105
+ removeQuietly(temp);
106
+ throw error;
107
+ }
108
+ },
109
+ };
110
+
111
+ function reason(error ) {
112
+ const code = (error )?.code;
113
+ return typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
114
+ }
115
+
116
+ function parseObject(text ) {
117
+ try {
118
+ const value = JSON.parse(text.replace(/^\uFEFF/, ""));
119
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? (value ) : undefined;
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ function importFile(name , mode , input ) {
126
+ const { fs } = input;
127
+ const from = join(input.source, name);
128
+ const to = join(input.target, name);
129
+ // Classification can throw too (ENOTDIR under a file): one failed item, never an aborted report.
130
+ try {
131
+ const kind = fs.kind(from);
132
+ if (kind === "missing") return { item: name, status: "not found" };
133
+ if (kind !== "file") return { item: name, status: "skipped", detail: "not a regular file" };
134
+ if (fs.kind(to) !== "missing") return { item: name, status: "present" };
135
+ const copyMode = mode ?? fs.mode(from);
136
+ const detail = `${copyMode.toString(8).padStart(4, "0")}`;
137
+ if (input.dryRun) return { item: name, status: "would copy", detail };
138
+ fs.copyNew(from, to, copyMode);
139
+ return { item: name, status: "copied", detail };
140
+ } catch (error) {
141
+ return { item: name, status: "failed", detail: reason(error) };
142
+ }
143
+ }
144
+
145
+ function importSettings(input ) {
146
+ try {
147
+ return planAndWriteSettings(input);
148
+ } catch (error) {
149
+ // An unreadable settings.json stays one failed item, never an aborted report.
150
+ return [{ item: "settings.json", status: "failed", detail: reason(error) }];
151
+ }
152
+ }
153
+
154
+ function planAndWriteSettings(input ) {
155
+ const { fs } = input;
156
+ const from = join(input.source, "settings.json");
157
+ const to = join(input.target, "settings.json");
158
+ const sourceKind = fs.kind(from);
159
+ if (sourceKind === "missing") return [{ item: "settings.json", status: "not found" }];
160
+ if (sourceKind !== "file") return [{ item: "settings.json", status: "skipped", detail: "not a regular file" }];
161
+ const source = parseObject(fs.readText(from));
162
+ if (source === undefined) return [{ item: "settings.json", status: "skipped", detail: `${from} is not a JSON object` }];
163
+ const targetKind = fs.kind(to);
164
+ if (targetKind !== "missing" && targetKind !== "file") return [{ item: "settings.json", status: "skipped", detail: `${to} is not a regular file` }];
165
+ const targetText = targetKind === "missing" ? undefined : fs.readText(to);
166
+ const target = targetText === undefined ? {} : parseObject(targetText);
167
+ if (target === undefined) return [{ item: "settings.json", status: "skipped", detail: `${to} is not a JSON object` }];
168
+
169
+ const items = [];
170
+ const added = {};
171
+ for (const key of IMPORTED_SETTINGS_KEYS) {
172
+ const item = `settings.json ${key}`;
173
+ if (!Object.hasOwn(source, key)) items.push({ item, status: "not found" });
174
+ else if (Object.hasOwn(target, key)) items.push({ item, status: "present" });
175
+ else if (typeof source[key] !== "string") items.push({ item, status: "skipped", detail: "not a string" });
176
+ else {
177
+ added[key] = source[key];
178
+ items.push({ item, status: input.dryRun ? "would copy" : "copied" });
179
+ }
180
+ }
181
+ if (input.dryRun || Object.keys(added).length === 0) return items;
182
+ try {
183
+ // Keep the target's own indentation and trailing-newline style.
184
+ const style = targetText === undefined ? { indent: undefined, trailingNewline: true } : detectJsonFormatting(targetText);
185
+ const body = JSON.stringify({ ...target, ...added }, null, style.indent ?? 2);
186
+ fs.writeAtomic(to, style.trailingNewline ? `${body}\n` : body);
187
+ return items;
188
+ } catch (error) {
189
+ const detail = reason(error);
190
+ return items.map((entry) => (entry.status === "copied" ? { ...entry, status: "failed", detail } : entry));
191
+ }
192
+ }
193
+
194
+ // Copies what the target lacks. A dry run reads the same files and writes
195
+ // nothing. The caller has already made sure `target` is a home this launcher
196
+ // owns (and exists, unless this is a dry run).
197
+ export function importPi(input ) {
198
+ const options = { ...input, fs: input.fs ?? nodeImportPiFs };
199
+ const items = IMPORTED_FILES.map((file) => importFile(file.name, file.mode, options));
200
+ items.push(...importSettings(options));
201
+ return { items, failed: items.some((item) => item.status === "failed") };
202
+ }
203
+
204
+ function describe(item ) {
205
+ switch (item.status) {
206
+ case "copied":
207
+ case "would copy":
208
+ return item.detail !== undefined && item.item === "auth.json" ? `${item.status} (${item.detail})` : item.status;
209
+ case "present":
210
+ return "already present, kept";
211
+ case "not found":
212
+ return "not found";
213
+ case "skipped":
214
+ return `skipped: ${item.detail}`;
215
+ case "failed":
216
+ return `failed: ${item.detail}`;
217
+ }
218
+ }
219
+
220
+ export function formatImportPiReport(result , context ) {
221
+ const lines = [
222
+ context.dryRun
223
+ ? `Dry run: would import from ${context.source} into ${context.target} (nothing is written)`
224
+ : `Importing from ${context.source} into ${context.target}`,
225
+ ];
226
+ for (const item of result.items) lines.push(` ${item.item}: ${describe(item)}`);
227
+ const count = (...statuses ) => result.items.filter((item) => statuses.includes(item.status)).length;
228
+ const imported = count("copied", "would copy");
229
+ const rest = `${count("present")} already present, ${count("not found")} not found, ${count("skipped")} skipped`;
230
+ if (result.failed) lines.push(`Import failed for ${count("failed")} item(s); ${count("copied")} copied and kept. ${rest}.`);
231
+ else if (imported === 0) lines.push(`Nothing to import: ${rest}.`);
232
+ else lines.push(`${context.dryRun ? "Would import" : "Imported"} ${imported} item(s); ${rest}.`);
233
+ return lines;
234
+ }