gentle-pi 4.3.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 (208) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gentle-ai-worker.md +1 -1
  3. package/assets/design-canvas/canvas.css +648 -0
  4. package/assets/design-canvas/canvas.js +1497 -0
  5. package/assets/design-canvas/index.html +88 -0
  6. package/assets/design-canvas/inspector.js +290 -0
  7. package/assets/orchestrator-delegation-reference.md +2 -2
  8. package/assets/orchestrator-verification.md +1 -1
  9. package/assets/orchestrator.md +2 -2
  10. package/bin/engram-guard +1 -1
  11. package/bin/gentle-shell.mjs +101 -151
  12. package/docs/bundled-install.md +47 -42
  13. package/docs/delegated-verification.md +6 -0
  14. package/docs/gentle-agents-activity.md +5 -1
  15. package/docs/gentle-music.md +61 -0
  16. package/docs/gentle-shell.md +77 -7
  17. package/docs/install-wizard.md +30 -6
  18. package/docs/permission-modes.md +7 -2
  19. package/docs/readme-reference.md +19 -15
  20. package/docs/session-profile-format.md +26 -0
  21. package/docs/voice.md +259 -0
  22. package/docs/yolo-mode.md +17 -2
  23. package/extensions/ask-user-choice.ts +4 -1
  24. package/extensions/gentle-agents.ts +173 -38
  25. package/extensions/gentle-ai.ts +214 -69
  26. package/extensions/gentle-design.ts +158 -0
  27. package/extensions/gentle-music.ts +289 -0
  28. package/extensions/gentle-shell.ts +332 -73
  29. package/extensions/gentle-voice.ts +11 -0
  30. package/extensions/session-scratch.ts +41 -22
  31. package/extensions/skill-registry.ts +63 -8
  32. package/lib/agent-profiles.ts +26 -0
  33. package/lib/agents-config.ts +35 -0
  34. package/lib/agents-protocol.ts +57 -1
  35. package/lib/agents-runner.ts +52 -3
  36. package/lib/agents-session-transport.ts +101 -6
  37. package/lib/agents-widget.ts +14 -1
  38. package/lib/auto-permission-memory.ts +48 -18
  39. package/lib/auto-permission-runtime.ts +21 -6
  40. package/lib/clipboard-image.ts +147 -0
  41. package/lib/command-palette-catalog.ts +2 -0
  42. package/lib/commit-slice-signal.ts +1 -1
  43. package/lib/context-backstop.ts +36 -0
  44. package/lib/design-canvas-server.ts +947 -0
  45. package/lib/design-comments.ts +437 -0
  46. package/lib/design-handoffs.ts +313 -0
  47. package/lib/design-open-browser.ts +58 -0
  48. package/lib/design-sources.ts +120 -0
  49. package/lib/external-editor.ts +31 -0
  50. package/lib/gentle-design.ts +513 -0
  51. package/lib/gentle-shell-bundled.ts +123 -49
  52. package/lib/gentle-shell-launcher.ts +9 -4
  53. package/lib/gentle-shell-startup-update.ts +61 -9
  54. package/lib/gentle-shell-uninstall.ts +33 -1
  55. package/lib/image-attachments.ts +150 -0
  56. package/lib/inprocess-reviewer.ts +33 -2
  57. package/lib/nan-provider.ts +3 -0
  58. package/lib/native-review-cli.ts +11 -4
  59. package/lib/notification-audio-native.ts +149 -13
  60. package/lib/notification-audio.ts +4 -0
  61. package/lib/notification-pulse-worker.ts +82 -18
  62. package/lib/notification-service.ts +5 -1
  63. package/lib/orchestrator-presence.ts +78 -8
  64. package/lib/orchestrator-scope.ts +10 -0
  65. package/lib/orchestrator-state.ts +11 -3
  66. package/lib/permission-mode-policy.ts +2 -2
  67. package/lib/review-host-relay.ts +11 -2
  68. package/lib/review-reminder-receipt.ts +24 -1
  69. package/lib/session-profile-append-controller.ts +713 -0
  70. package/lib/session-profile-disk-reader.ts +182 -0
  71. package/lib/session-profile-persistence.ts +15 -7
  72. package/lib/session-scratch.ts +52 -3
  73. package/lib/session-worktree-registry.ts +7 -0
  74. package/lib/shell-bar.ts +4 -3
  75. package/lib/shell-changes.ts +81 -5
  76. package/lib/shell-prompt.ts +4 -4
  77. package/lib/shell-sidebar-layout.ts +26 -6
  78. package/lib/shell-todo.ts +1 -1
  79. package/lib/verifier-admission.ts +5 -4
  80. package/lib/vim-editor-adapter.ts +11 -8
  81. package/lib/voice-artifacts.ts +188 -0
  82. package/lib/voice-command.ts +576 -0
  83. package/lib/voice-install.ts +455 -0
  84. package/lib/voice-platform.ts +99 -0
  85. package/lib/voice-push-to-talk.ts +262 -0
  86. package/lib/voice-session.ts +460 -0
  87. package/lib/voice-settings.ts +50 -0
  88. package/lib/voice-ui.ts +158 -0
  89. package/lib/yolo-session-policy.ts +58 -22
  90. package/package.json +1 -1
  91. package/runtime/gentle-shell-bundled.mjs +122 -48
  92. package/runtime/gentle-shell-launcher.mjs +9 -4
  93. package/runtime/gentle-shell-startup-update.mjs +61 -9
  94. package/runtime/gentle-shell-uninstall.mjs +33 -1
  95. package/runtime/native-review-cli.mjs +11 -4
  96. package/scripts/bundled-engram-migration.mjs +0 -6
  97. package/scripts/gentle-ai-installer.mjs +10 -10
  98. package/scripts/installer-downloads.mjs +7 -7
  99. package/scripts/installer-preflight.mjs +5 -3
  100. package/scripts/installer-windows.mjs +37 -7
  101. package/scripts/main-channel.mjs +91 -17
  102. package/scripts/pi-update.mjs +34 -6
  103. package/scripts/verify-package-files.mjs +26 -2
  104. package/scripts/voice-worker.cjs +476 -0
  105. package/skills/design/SKILL.md +110 -0
  106. package/tests/agent-profiles.test.ts +24 -0
  107. package/tests/agents-config.test.ts +61 -0
  108. package/tests/agents-fake-child.ts +8 -1
  109. package/tests/agents-protocol.test.ts +32 -0
  110. package/tests/agents-runner.test.ts +226 -5
  111. package/tests/agents-session-transport.test.ts +53 -3
  112. package/tests/ask-user-choice.test.ts +30 -0
  113. package/tests/auto-mode.test.ts +17 -0
  114. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  115. package/tests/auto-permission-runtime.test.ts +85 -1
  116. package/tests/autonomous-guard.test.ts +20 -0
  117. package/tests/bundled-install.test.ts +3 -3
  118. package/tests/clipboard-image.test.ts +114 -0
  119. package/tests/command-palette.test.ts +1 -1
  120. package/tests/context-backstop.test.ts +70 -4
  121. package/tests/delegate-for-reason-contract.test.ts +2 -1
  122. package/tests/design-canvas-assets.test.ts +710 -0
  123. package/tests/design-canvas-server.test.ts +1096 -0
  124. package/tests/design-comments.test.ts +201 -0
  125. package/tests/design-handoffs.test.ts +282 -0
  126. package/tests/design-open-browser.test.ts +70 -0
  127. package/tests/design-sources.test.ts +132 -0
  128. package/tests/engram-safety.test.ts +132 -57
  129. package/tests/external-editor.test.ts +84 -0
  130. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
  131. package/tests/gentle-agents.test.ts +365 -20
  132. package/tests/gentle-ai-binary.test.ts +1 -1
  133. package/tests/gentle-ai-installer.test.ts +51 -51
  134. package/tests/gentle-ai.test.ts +292 -13
  135. package/tests/gentle-design.test.ts +1313 -0
  136. package/tests/gentle-music.test.ts +335 -0
  137. package/tests/gentle-shell-bundled.test.ts +301 -248
  138. package/tests/gentle-shell-launcher.test.ts +28 -2
  139. package/tests/gentle-shell-startup-update.test.ts +77 -1
  140. package/tests/gentle-shell-uninstall.test.ts +59 -0
  141. package/tests/gentle-shell.test.ts +183 -8
  142. package/tests/gentle-todo.test.ts +11 -0
  143. package/tests/image-attachments.test.ts +119 -0
  144. package/tests/image-paste-chips.test.ts +236 -0
  145. package/tests/inprocess-reviewer.test.ts +55 -2
  146. package/tests/installer-bundles.test.ts +12 -6
  147. package/tests/installer-posix-bootstrap.test.ts +8 -8
  148. package/tests/installer-preflight.test.ts +16 -0
  149. package/tests/installer-server.test.ts +2 -2
  150. package/tests/installer-windows-bootstrap.test.ts +61 -3
  151. package/tests/installer-windows-diagnostics.test.ts +111 -0
  152. package/tests/main-channel-upgrade.test.ts +232 -2
  153. package/tests/main-channel.test.ts +140 -21
  154. package/tests/nan-provider.test.ts +36 -0
  155. package/tests/native-review-capability-contract.test.ts +11 -4
  156. package/tests/native-review-parity.test.ts +1 -1
  157. package/tests/notification-audio-native.test.ts +128 -9
  158. package/tests/notification-customize.test.ts +17 -5
  159. package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
  160. package/tests/orchestrator-budget.test.ts +5 -2
  161. package/tests/orchestrator-presence.test.ts +100 -2
  162. package/tests/orchestrator-rdd-ownership.test.ts +2 -2
  163. package/tests/orchestrator-scope.test.ts +14 -0
  164. package/tests/orchestrator-state.test.ts +29 -0
  165. package/tests/package-files.test.ts +34 -0
  166. package/tests/package-manifest.test.ts +6 -6
  167. package/tests/permission-mode-policy.test.ts +10 -1
  168. package/tests/pi-update.test.ts +99 -4
  169. package/tests/rdd-aware-verification-contract.test.ts +1 -1
  170. package/tests/review-host-relay-routing.test.ts +88 -1
  171. package/tests/review-host-relay.test.ts +9 -2
  172. package/tests/review-json-arguments.test.ts +20 -3
  173. package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
  174. package/tests/review-relay-transport-agent.test.ts +26 -0
  175. package/tests/review-reminder-receipt.test.ts +31 -1
  176. package/tests/session-changes.test.ts +2 -1
  177. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  178. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  179. package/tests/session-profile-append-controller.test.ts +731 -0
  180. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  181. package/tests/session-profile-disk-reader.test.ts +311 -0
  182. package/tests/session-scratch.test.ts +68 -2
  183. package/tests/session-worktree-registry.test.ts +13 -1
  184. package/tests/shell-bar.test.ts +14 -2
  185. package/tests/shell-changes.test.ts +148 -1
  186. package/tests/shell-prompt.test.ts +3 -3
  187. package/tests/shell-render-cache.test.ts +134 -1
  188. package/tests/shell-sidebar-layout.test.ts +50 -0
  189. package/tests/short-check-routing-contract.test.ts +1 -1
  190. package/tests/skill-collision-prefixes.test.ts +3 -1
  191. package/tests/skill-registry-watcher.test.ts +17 -5
  192. package/tests/skill-registry.test.ts +47 -3
  193. package/tests/task-size-routing-contract.test.ts +1 -1
  194. package/tests/test-strength-contract.test.ts +1 -1
  195. package/tests/verifier-admission.test.ts +18 -1
  196. package/tests/voice-command.test.ts +773 -0
  197. package/tests/voice-install.test.ts +323 -0
  198. package/tests/voice-platform.test.ts +176 -0
  199. package/tests/voice-push-to-talk.test.ts +444 -0
  200. package/tests/voice-session.test.ts +434 -0
  201. package/tests/voice-settings.test.ts +49 -0
  202. package/tests/voice-ui.test.ts +97 -0
  203. package/tests/voice-worker.test.ts +445 -0
  204. package/tests/weak-model-economy-prompt.test.ts +32 -0
  205. package/tests/windows-hidden-processes.test.ts +25 -2
  206. package/tests/yolo-customize.test.ts +5 -1
  207. package/tests/yolo-mode.test.ts +1 -1
  208. package/tests/yolo-session-policy-capture.test.ts +104 -0
@@ -16,12 +16,16 @@ import {
16
16
  bundledUpdatePlan,
17
17
  parseBundledUpgradeArgs,
18
18
  withBundledNpmCommand,
19
+ withoutBundledNpmCommand,
20
+ withoutMissingBundledNpmCommand,
19
21
  resolveBundledInstall,
20
22
  setBundledInstallForTesting,
21
23
  type BundledFs,
24
+ isBundledNpmCommand,
22
25
  type BundledPi,
23
26
  } from "../lib/gentle-shell-bundled.ts";
24
27
  import { artifactFor, engramPinVersion } from "../scripts/installer-downloads.mjs";
28
+ import { bundledNpmCommand, prefixLayout } from "../scripts/bundled-install.mjs";
25
29
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
26
30
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
27
31
 
@@ -128,7 +132,7 @@ test("bundledPi fails with the version, the pinned Pi and the way to repair it",
128
132
  assert.equal(bundledPi(install, { platform: "linux", fs: outside }).ok, false);
129
133
  });
130
134
 
131
- const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.4.0/engram" };
135
+ const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.5.2/engram" };
132
136
 
133
137
  test("bundledChildEnv keeps the user's environment and adds only the bundled Pi, the prefix and Engram, no Pi override and no Pi update check", () => {
134
138
  const user = { PATH: "/home/ana/.nvm/bin:/usr/bin", NODE_OPTIONS: "--max-old-space-size=4096", npm_config_registry: "https://npm.example", npm_config_prefix: "/home/ana/.npm-global",
@@ -140,7 +144,7 @@ test("bundledChildEnv keeps the user's environment and adds only the bundled Pi,
140
144
  // folder from the prefix, and Gentle AI 4.0.0 reads that variable as a parent.
141
145
  assert.deepEqual(env, { PATH: user.PATH, NODE_OPTIONS: user.NODE_OPTIONS, npm_config_registry: user.npm_config_registry, npm_config_prefix: user.npm_config_prefix,
142
146
  GENTLE_PI_CONFIG_HOME: user.GENTLE_PI_CONFIG_HOME, GENTLE_PI_AGENTS_PI: "/g/node /g/cli.js", PI_SKIP_VERSION_CHECK: "1", GENTLE_SHELL_PREFIX: "/g",
143
- ENGRAM_BIN: "/g/runtime/engram-3.4.0/engram" });
147
+ ENGRAM_BIN: "/g/runtime/engram-3.5.2/engram" });
144
148
  // The subagent runner splits that variable on whitespace: a spaced path falls
145
149
  // back to its own default, the running node and cli.js.
146
150
  const spaced = bundledChildEnv({ GENTLE_PI_AGENTS_PI: "pi" }, { node: "C:\\Users\\Ana Li\\node.exe", cli: "C:\\cli.js", ...ISOLATION });
@@ -174,6 +178,83 @@ test("withBundledNpmCommand sets only npmCommand and leaves an already bundled s
174
178
  for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withBundledNpmCommand(malformed, command), { status: "malformed" });
175
179
  });
176
180
 
181
+ // #2469: the npmCommand a bundled self-uninstall left behind names a node.exe
182
+ // the uninstall removed, so Pi package installs fail with ENOENT until it is
183
+ // gone; a command naming nothing inside the removed prefix stays.
184
+ test("withoutBundledNpmCommand removes the key when an entry names the removed prefix and keeps the rest", () => {
185
+ const text = '{\n "theme": "gentle",\n "npmCommand": ["/p/runtime/node.exe", "--update-notifier=false"]\n}\n';
186
+ const removed = withoutBundledNpmCommand(text, (path) => path === "/p/runtime/node.exe");
187
+ assert.ok(removed.status === "removed", removed.status);
188
+ assert.deepEqual(JSON.parse(removed.text), { theme: "gentle" });
189
+ assert.match(removed.text, /^\{\n {4}"theme"/);
190
+ assert.ok(removed.text.endsWith("}\n"));
191
+ assert.deepEqual(withoutBundledNpmCommand(undefined, () => true), { status: "unchanged" });
192
+ assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":"npm"}', () => true), { status: "unchanged" });
193
+ assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":["node"]}', () => true), { status: "unchanged" });
194
+ assert.deepEqual(withoutBundledNpmCommand('{"npmCommand":["/other/node"]}', () => false), { status: "unchanged" });
195
+ assert.deepEqual(withoutBundledNpmCommand('{"theme":"gentle"}', () => true), { status: "unchanged" });
196
+ for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withoutBundledNpmCommand(malformed, () => true), { status: "malformed" });
197
+ });
198
+
199
+ test("withoutMissingBundledNpmCommand removes only a bundled npmCommand whose executable is an absolute path that is gone", () => {
200
+ const text = '{\n "theme": "gentle",\n "npmCommand": ["/p/runtime/node-24.21.0/bin/node", "--userconfig=/p/pnpm/config/npmrc", "--globalconfig=/p/npm/npmrc", "--cache=/p/npm/cache", "--update-notifier=false"]\n}\n';
201
+ const removed = withoutMissingBundledNpmCommand(text, (path) => path !== "/p/runtime/node-24.21.0/bin/node");
202
+ assert.ok(removed.status === "removed", removed.status);
203
+ assert.deepEqual(JSON.parse(removed.text), { theme: "gentle" });
204
+ // An executable that exists, a PATH command, a non-array value, no key, no file.
205
+ assert.deepEqual(withoutMissingBundledNpmCommand(text, () => true), { status: "unchanged" });
206
+ assert.deepEqual(withoutMissingBundledNpmCommand('{"npmCommand":["npm"]}', () => false), { status: "unchanged" });
207
+ assert.deepEqual(withoutMissingBundledNpmCommand('{"npmCommand":"npm"}', () => false), { status: "unchanged" });
208
+ assert.deepEqual(withoutMissingBundledNpmCommand('{"theme":"gentle"}', () => false), { status: "unchanged" });
209
+ assert.deepEqual(withoutMissingBundledNpmCommand(undefined, () => false), { status: "unchanged" });
210
+ for (const malformed of ["{", "[]", "null"]) assert.deepEqual(withoutMissingBundledNpmCommand(malformed, () => false), { status: "malformed" });
211
+ });
212
+
213
+ // A missing program alone is not proof the command is ours: a user's
214
+ // npmCommand on an offline drive must survive the launcher repair, so only the
215
+ // shape a bundled install writes is ever dropped (PR #2472 review).
216
+ test("withoutMissingBundledNpmCommand never drops a user's npmCommand, even with a missing program", () => {
217
+ // The posix form the launcher sees on this platform is dropped. The Windows
218
+ // form is covered by isBundledNpmCommand: the launcher's isAbsolute is
219
+ // platform-specific, so a C:\ path is only absolute on Windows.
220
+ const posix = "/home/ana/.gentle-shell";
221
+ const flags = (root: string, sep: string) => [`--userconfig=${root}${sep}pnpm${sep}config${sep}npmrc`, `--globalconfig=${root}${sep}npm${sep}npmrc`, `--cache=${root}${sep}npm${sep}cache`];
222
+ const bundledForm = [`${posix}/runtime/node-24.21.0/bin/node`, `${posix}/runtime/node-24.21.0/lib/node_modules/npm/bin/npm-cli.js`, ...flags(posix, "/"), `--prefix=${posix}/npm/prefix`, "--update-notifier=false"];
223
+ {
224
+ const command = bundledForm;
225
+ const text = JSON.stringify({ theme: "gentle", npmCommand: command });
226
+ const removed = withoutMissingBundledNpmCommand(text, () => false);
227
+ assert.ok(removed.status === "removed", `${command[0]}: ${removed.status}`);
228
+ }
229
+ // What a user writes is never ours, even with a missing program.
230
+ const userCommands: unknown[][] = [
231
+ ["npm"],
232
+ ["C:\\tools\\node.exe", "C:\\tools\\npm-cli.js"],
233
+ ["C:\\Users\\ana\\AppData\\Local\\gentle-shell\\runtime\\node-24.21.0\\node.exe", "npm-cli.js"],
234
+ ["/opt/runtime/node-20.0.0/bin/node", "/opt/npm-cli.js", "--update-notifier=false"],
235
+ ];
236
+ for (const command of userCommands) {
237
+ const text = JSON.stringify({ theme: "gentle", npmCommand: command });
238
+ assert.deepEqual(withoutMissingBundledNpmCommand(text, () => false), { status: "unchanged" }, JSON.stringify(command));
239
+ }
240
+ });
241
+
242
+ test("isBundledNpmCommand recognizes only the npmCommand a bundled install writes, in every released form", () => {
243
+ const win = "C:\\Users\\ana\\AppData\\Local\\gentle-shell";
244
+ const posix = "/home/ana/.gentle-shell";
245
+ const flags = (root: string, sep: string) => [`--userconfig=${root}${sep}pnpm${sep}config${sep}npmrc`, `--globalconfig=${root}${sep}npm${sep}npmrc`, `--cache=${root}${sep}npm${sep}cache`];
246
+ // What the installer writes today on each platform, and its two released forms.
247
+ assert.equal(isBundledNpmCommand(bundledNpmCommand(prefixLayout({ platform: "win32", home: "C:\\Users\\ana", root: win }))), true);
248
+ assert.equal(isBundledNpmCommand(bundledNpmCommand(prefixLayout({ platform: "darwin", home: "/home/ana" }))), true);
249
+ assert.equal(isBundledNpmCommand([`${win}\\runtime\\node-24.21.0\\node.exe`, `${win}\\runtime\\node-24.21.0\\gentle-shell\\npm`, ...flags(win, "\\"), "--update-notifier=false"]), true);
250
+ assert.equal(isBundledNpmCommand([`${posix}/runtime/node-24.21.0/bin/node`, `${posix}/runtime/node-24.21.0/lib/node_modules/npm/bin/npm-cli.js`, ...flags(posix, "/"), `--prefix=${posix}/npm/prefix`, "--update-notifier=false"]), true);
251
+ // What a user writes is never ours, even with a missing program.
252
+ for (const command of [["npm"], ["C:\\tools\\node.exe", "C:\\tools\\npm-cli.js"], [`${win}\\runtime\\node-24.21.0\\node.exe`, "npm-cli.js"],
253
+ ["/opt/runtime/node-20.0.0/bin/node", "/opt/npm-cli.js", "--update-notifier=false"]]) {
254
+ assert.equal(isBundledNpmCommand(command), false, JSON.stringify(command));
255
+ }
256
+ });
257
+
177
258
  test("bundledUpdatePlan redirects Pi's own update and keeps the extension updates", () => {
178
259
  assert.deepEqual(bundledUpdatePlan(["update"]), { self: true });
179
260
  assert.deepEqual(bundledUpdatePlan(["update", "--self"]), { self: true });
@@ -235,48 +316,56 @@ const posixHost = process.platform === "win32" ? "the bundled prefix fixture use
235
316
  const SHELL = JSON.parse(readFileSync(join(repository, "package.json"), "utf8")).version;
236
317
  const FIXTURE_ID = `${SHELL}-1.2.0`;
237
318
  const FAKE_PI = [
238
- "import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs'; import {join, dirname} from 'node:path';",
319
+ "import {mkdirSync, readFileSync, writeFileSync} from 'node:fs'; import {join, dirname} from 'node:path';",
239
320
  "const args = process.argv.slice(2);",
240
321
  "if (args[0] === 'install' && args[1] === 'npm:gentle-engram@0.4.2' && process.env.FIXTURE_INSTALL_CAPABILITY !== '0') {",
241
322
  " const home = process.env.PI_CODING_AGENT_DIR; const metadata = join(home, 'npm/node_modules/gentle-engram/package.json'); mkdirSync(dirname(metadata), {recursive:true});",
242
- " writeFileSync(metadata, JSON.stringify({name:'gentle-engram',version:'0.4.2',engram:{startupPolicy:1,...(process.env.FIXTURE_OWNED_COMPANION === '0' ? {} : {sessionOwned:1})}}));",
323
+ " writeFileSync(metadata, JSON.stringify({name:'gentle-engram',version:'0.4.2',engram:{startupPolicy:1}}));",
243
324
  " const path = join(home,'settings.json'); let settings={}; try {settings=JSON.parse(readFileSync(path,'utf8'));} catch {}",
244
325
  " if (process.env.FIXTURE_INSTALL_CAPABILITY !== 'metadata-only') { const packages = settings.packages ?? []; const index = packages.findIndex(p => /^npm:gentle-engram(?:@|$)/.test(typeof p === 'string' ? p : p.source));",
245
326
  " if (index < 0) packages.push(args[1]); else packages[index] = typeof packages[index] === 'string' ? args[1] : {...packages[index], source:args[1]}; settings.packages = packages; writeFileSync(path, JSON.stringify(settings)); }",
246
327
  "}",
247
328
  "if (args.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
248
- // Stands in for gentle-engram creating the store through the bundled core during the session.
249
- "if (process.env.FIXTURE_CREATE_STORE && args[0] !== 'install' && args[0] !== 'list') {",
250
- " const data = join(process.env.HOME, '.engram');",
251
- " if (process.env.ENGRAM_BIN?.includes('engram-guard') !== true) { mkdirSync(data, {recursive:true});",
252
- " if (!existsSync(join(data, '.instance-id'))) writeFileSync(join(data, '.instance-id'), process.env.FIXTURE_CREATE_STORE + '\\n');",
253
- " const header = Buffer.alloc(4096); header.write('SQLite format 3\\0', 0, 'latin1'); header.writeUInt16BE(4096, 16); header[18] = 2; header[19] = 2;",
254
- " writeFileSync(join(data, 'engram.db'), header); writeFileSync(join(data, 'engram.db-wal'), '');",
255
- " }",
256
- "}",
257
329
  "const pick = ['PATH', 'GENTLE_SHELL_PI', 'GENTLE_PI_AGENTS_PI', 'PI_SKIP_VERSION_CHECK', 'npm_config_prefix', 'npm_config_registry', 'NODE_OPTIONS', 'PI_CODING_AGENT_DIR',",
258
330
  "\t'GENTLE_SHELL_PREFIX', 'GENTLE_PI_CONFIG_HOME', 'ENGRAM_BIN', 'ENGRAM_URL', 'ENGRAM_SERVER_MODE', 'ENGRAM_SOCKET', 'ENGRAM_STARTUP_POLICY', 'ENGRAM_STARTUP_NOTICE', 'GENTLE_AI_HOME', 'GENTLE_AI_PI_CONFIG_DIR', 'GENTLE_AI_ENGRAM_BIN', 'GENTLE_AI_NO_SELF_UPDATE', 'GH_TOKEN', 'ENGRAM_NO_UPDATE_CHECK'];",
259
- "const record = join(process.env.GENTLE_SHELL_PREFIX ?? '', 'config', 'engram-provenance.json');",
260
- "const provenance = (() => { try { return JSON.parse(readFileSync(record, 'utf8')); } catch { return null; } })();",
261
- "console.log(JSON.stringify({ cli: process.argv[1], args, provenance, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }));",
262
- // Stands in for a crash, kill or power loss of the launcher before its exit handler runs.
263
- "if (process.env.FIXTURE_KILL_LAUNCHER && args[0] !== 'install') process.kill(process.ppid, 'SIGKILL');",
331
+ "console.log(JSON.stringify({ cli: process.argv[1], args, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }));",
264
332
  ].join("\n");
333
+ const READY_COMPATIBILITY = { revision: 1, source: "active", status: "ready", offline_automatic_opening_authorized: false, active_reuse_authorized: true,
334
+ instance_id: "0123456789abcdef0123456789abcdef", runtime_id: "a".repeat(32), capability: "startup-verified-store-v1" };
335
+ // The bundled core: `serve` logs its invocation (FIXTURE_SERVE_LOG) and, like
336
+ // Engram 3.5.2, answers the ready /compatibility contract on a port it publishes
337
+ // in FIXTURE_ENGRAM_PORT_FILE, where the redirected default endpoint finds it.
338
+ // FIXTURE_SERVE=exit fails like a lost bind; race fails after starting the
339
+ // server a concurrent launch would. Every other command only prints a line.
265
340
  const FIXTURE_CORE = [
266
- "#!/bin/sh",
267
- 'if [ "$1" = --help ] && [ "$FIXTURE_OWNED_CORE" != 0 ]; then echo " serve --session-owned Private ephemeral HTTP server"; exit 0; fi',
268
- 'if [ "$1" != instance-id ]; then echo engram; exit 0; fi',
269
- '[ -z "$FIXTURE_PROVISION_ENV" ] || env > "$FIXTURE_PROVISION_ENV"',
270
- '[ -z "$FIXTURE_INSTANCE_ID_FAIL" ] || exit 3',
271
- 'mkdir -p "$HOME/.engram" || exit 1',
272
- '[ -f "$HOME/.engram/.instance-id" ] || printf \'%s\\n\' "${FIXTURE_INSTANCE_ID:-0123456789abcdef0123456789abcdef}" > "$HOME/.engram/.instance-id"',
273
- 'cat "$HOME/.engram/.instance-id"',
341
+ `#!${process.execPath}`,
342
+ "const { appendFileSync, renameSync, writeFileSync } = require('node:fs'); const { spawn } = require('node:child_process');",
343
+ "const args = process.argv.slice(2);",
344
+ "if (args[0] !== 'serve') { console.log('engram'); process.exit(0); }",
345
+ "const mode = process.env.FIXTURE_SERVE ?? 'ready';",
346
+ "const pick = ['HOME', 'GH_TOKEN', 'GITHUB_TOKEN', 'ENGRAM_NO_UPDATE_CHECK', 'ENGRAM_URL', 'ENGRAM_BIN', 'ENGRAM_HTTP_TOKEN', 'ENGRAM_STARTUP_POLICY'];",
347
+ "appendFileSync(process.env.FIXTURE_SERVE_LOG, JSON.stringify({ args, mode, cwd: process.cwd(), pid: process.pid, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }) + '\\n');",
348
+ "if (mode === 'exit') process.exit(1);",
349
+ "if (mode === 'race') { spawn(process.execPath, [process.argv[1], 'serve'], { env: { ...process.env, FIXTURE_SERVE: 'late' }, detached: true, stdio: 'ignore' }).unref(); process.exit(1); }",
350
+ `const ready = ${JSON.stringify(JSON.stringify(READY_COMPATIBILITY))};`,
351
+ "setTimeout(() => require('node:http').createServer((request, response) => {",
352
+ " response.writeHead(request.url === '/compatibility' ? 200 : 404, { 'content-type': 'application/json' }); response.end(request.url === '/compatibility' ? ready : '{}');",
353
+ "}).listen(0, '127.0.0.1', function () { const file = process.env.FIXTURE_ENGRAM_PORT_FILE; writeFileSync(file + '.tmp', String(this.address().port)); renameSync(file + '.tmp', file); }),",
354
+ " mode === 'late' ? 300 : 0);",
355
+ "setTimeout(() => process.exit(0), 60000);",
274
356
  "",
275
357
  ].join("\n");
276
358
 
277
359
  function bundledPrefix(t: TestContext, { pi = "1.2.0", core = true } = {}) {
278
360
  const base = realpathSync(mkdtempSync(join(tmpdir(), "gentle-shell-bundled-")));
279
- t.after(() => rmSync(base, { recursive: true, force: true }));
361
+ const serveLog = join(base, "engram-serve.log");
362
+ const serves = (): Array<{ args: string[]; mode: string; cwd: string; pid: number; env: Record<string, string | undefined> }> =>
363
+ existsSync(serveLog) ? readFileSync(serveLog, "utf8").trim().split("\n").map((line) => JSON.parse(line)) : [];
364
+ // The detached servers outlive their launcher by design: stop them with the fixture.
365
+ t.after(() => {
366
+ for (const { pid } of serves()) try { process.kill(pid); } catch { /* Already exited. */ }
367
+ rmSync(base, { recursive: true, force: true });
368
+ });
280
369
  const home = join(base, "home");
281
370
  const root = join(home, ".gentle-shell");
282
371
  const version = join(root, "versions", FIXTURE_ID);
@@ -339,33 +428,38 @@ function bundledPrefix(t: TestContext, { pi = "1.2.0", core = true } = {}) {
339
428
  mkdirSync(userBin);
340
429
  writeFileSync(join(userBin, "pi"), "#!/bin/sh\necho 9.9.9\n", { mode: 0o755 });
341
430
  // No test contacts the actual default Engram endpoint: connections to 7437
342
- // are redirected to a closed ephemeral port (refused) or to a local fixture
343
- // server (FIXTURE_ENGRAM_PORT). FIXTURE_NO_PROBE makes any probe fail the test.
431
+ // are redirected to a local fixture server (FIXTURE_ENGRAM_PORT), to the
432
+ // server the bundled core's `serve` published (FIXTURE_ENGRAM_PORT_FILE), or
433
+ // else to a closed ephemeral port (refused). FIXTURE_NO_PROBE makes any probe fail the test.
344
434
  const network = join(base, "network.mjs");
345
- writeFileSync(network, `import net from 'node:net'; import { syncBuiltinESMExports } from 'node:module';
435
+ const portFile = join(base, "engram-port");
436
+ writeFileSync(network, `import net from 'node:net'; import { readFileSync } from 'node:fs'; import { syncBuiltinESMExports } from 'node:module';
346
437
  const original = net.createConnection;
347
438
  const probe = net.createServer(); await new Promise((resolve) => probe.listen(0, '127.0.0.1', resolve));
348
439
  const closed = probe.address().port; await new Promise((resolve) => probe.close(resolve));
349
440
  net.createConnection = (options, ...rest) => {
350
441
  if (Number(options?.port ?? options) !== 7437) return original(options, ...rest);
351
442
  if (process.env.FIXTURE_NO_PROBE === '1') { process.stderr.write('unexpected Engram probe\\n'); process.exit(97); }
352
- return original({ ...options, port: Number(process.env.FIXTURE_ENGRAM_PORT || closed) }, ...rest);
443
+ const served = () => { try { return readFileSync(process.env.FIXTURE_ENGRAM_PORT_FILE, 'utf8'); } catch { return ''; } };
444
+ return original({ ...options, port: Number(process.env.FIXTURE_ENGRAM_PORT || served() || closed) }, ...rest);
353
445
  };
354
446
  syncBuiltinESMExports();`);
355
447
  const metadata = join(home, ".gentle-shell", "agent", "npm", "node_modules", "gentle-engram", "package.json");
356
448
  mkdirSync(dirname(metadata), { recursive: true });
357
449
  writeFileSync(metadata, JSON.stringify({ name: "gentle-engram", version: "0.4.2", engram: { startupPolicy: 1 } }));
358
450
  const env = { HOME: home, PATH: `${userBin}:/usr/bin:/bin`, GENTLE_SHELL_PI: join(userBin, "pi"), GENTLE_SHELL_NO_AUTO_SETUP: "1",
359
- NODE_OPTIONS: `--no-warnings --import=${network} --import=${registerHooks}`, npm_config_registry: "https://npm.example" };
451
+ NODE_OPTIONS: `--no-warnings --import=${network} --import=${registerHooks}`, npm_config_registry: "https://npm.example",
452
+ FIXTURE_SERVE_LOG: serveLog, FIXTURE_ENGRAM_PORT_FILE: portFile };
360
453
  const entry = join(root, "current", "node_modules", "gentle-pi", "bin", "gentle-shell.mjs");
361
454
  const run = (args: string[], extra: Record<string, string> = {}) => spawnSync(process.execPath, [entry, ...args], { env: { ...env, ...extra }, encoding: "utf8", input: "" });
362
- return { root, home, userBin, version, piDir, npmCli, engram, metadata, network, guard, run, env, entry: join(shell, "bin", "gentle-shell.mjs") };
455
+ return { root, home, userBin, version, piDir, npmCli, engram, metadata, network, guard, run, env, serves, portFile, entry: join(shell, "bin", "gentle-shell.mjs") };
363
456
  }
364
457
 
365
458
  const STORE_ID = "0123456789abcdef0123456789abcdef";
366
459
  const OTHER_STORE_ID = "fedcba9876543210fedcba9876543210";
460
+ const DEFAULT_URL = "http://127.0.0.1:7437";
367
461
  const lastLaunch = (result: { stdout: string }) => JSON.parse(result.stdout.trim().split("\n").at(-1)!);
368
- const memoryOf = (result: { stdout: string }) => [lastLaunch(result).env.ENGRAM_STARTUP_POLICY, lastLaunch(result).env.ENGRAM_BIN];
462
+ const memoryOf = (result: { stdout: string }) => [lastLaunch(result).env.ENGRAM_STARTUP_POLICY, lastLaunch(result).env.ENGRAM_BIN, lastLaunch(result).env.ENGRAM_URL];
369
463
  const provenancePath = (prefix: { root: string }) => join(prefix.root, "config", "engram-provenance.json");
370
464
  const sha256File = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex");
371
465
  function writeHeader(path: string, userVersion: number, magic = "SQLite format 3\0") {
@@ -390,164 +484,141 @@ async function engramServer(t: TestContext, routes: Record<string, [number, unkn
390
484
  return new Promise((resolve) => child.stdout.once("data", (chunk) => resolve(String(chunk).trim())));
391
485
  }
392
486
 
393
- test("a fresh launch provisions the store identity and finalizes provenance before Pi starts", { skip: posixHost }, (t) => {
487
+ // Whether the server a launch started still answers after the launcher exited.
488
+ async function stillServing(prefix: ReturnType<typeof bundledPrefix>): Promise<boolean> {
489
+ const response = await fetch(`http://127.0.0.1:${readFileSync(prefix.portFile, "utf8")}/compatibility`);
490
+ return response.ok && (await response.json()).status === "ready";
491
+ }
492
+
493
+ test("with nothing listening Shell starts the bundled core's shared server detached, with a clean environment, and Pi reuses it", { skip: posixHost }, async (t) => {
394
494
  const prefix = bundledPrefix(t);
395
- const provisioning = join(prefix.home, "provisioning-env");
396
- const result = prefix.run([], { FIXTURE_PROVISION_ENV: provisioning, GH_TOKEN: "fixture-token", GITHUB_TOKEN: "fixture-token" });
495
+ const result = prefix.run([], { GH_TOKEN: "fixture-token", GITHUB_TOKEN: "fixture-token", ENGRAM_HTTP_TOKEN: "fixture-http" });
397
496
  assert.equal(result.status, 0, result.stderr);
398
- const seen = lastLaunch(result);
399
- assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], ["allow", prefix.engram, undefined]);
400
- // Already finalized while Pi runs, from the identity file the core wrote.
401
- const record = { schema: 1, dataDir: join(prefix.home, ".engram"), instanceId: STORE_ID, coreSha256: sha256File(prefix.engram) };
402
- assert.deepEqual(seen.provenance, record);
403
- assert.deepEqual(JSON.parse(readFileSync(provenancePath(prefix), "utf8")), record);
404
- // Only the provisioning child skips Engram's update check and loses GitHub tokens; Pi keeps the user's environment.
405
- const child = Object.fromEntries(readFileSync(provisioning, "utf8").trim().split("\n").map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
406
- assert.deepEqual([child.ENGRAM_NO_UPDATE_CHECK, child.GH_TOKEN, child.GITHUB_TOKEN, child.ENGRAM_DATA_DIR, child.HOME], ["1", undefined, undefined, undefined, prefix.home]);
407
- assert.deepEqual([seen.env.GH_TOKEN, seen.env.ENGRAM_NO_UPDATE_CHECK], ["fixture-token", undefined]);
497
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
498
+ assert.doesNotMatch(result.stderr, /memory disabled/);
499
+ const [serve, ...others] = prefix.serves();
500
+ assert.deepEqual(others, []);
501
+ // Engram's own defaults: no port, data directory or URL chosen for it.
502
+ assert.deepEqual([serve.args, serve.cwd, serve.env.HOME], [["serve"], prefix.home, prefix.home]);
503
+ // S8: no update check, no GitHub tokens and no inherited Engram settings; Pi keeps the user's environment.
504
+ assert.deepEqual([serve.env.ENGRAM_NO_UPDATE_CHECK, serve.env.GH_TOKEN, serve.env.GITHUB_TOKEN, serve.env.ENGRAM_HTTP_TOKEN, serve.env.ENGRAM_URL, serve.env.ENGRAM_BIN],
505
+ ["1", undefined, undefined, undefined, undefined, undefined]);
506
+ assert.deepEqual([lastLaunch(result).env.GH_TOKEN, lastLaunch(result).env.ENGRAM_NO_UPDATE_CHECK], ["fixture-token", undefined]);
507
+ // Detached: it outlives the launcher and Pi, and the next launch reuses it without starting another.
508
+ assert.equal(await stillServing(prefix), true);
509
+ const second = prefix.run([]);
510
+ assert.equal(second.status, 0, second.stderr);
511
+ assert.deepEqual(memoryOf(second), ["allow", prefix.guard, DEFAULT_URL]);
512
+ assert.equal(prefix.serves().length, 1);
408
513
  });
409
514
 
410
- test("a session that never creates a database keeps Shell's identity; a foreign store with another identity is never adopted", { skip: posixHost }, (t) => {
411
- const prefix = bundledPrefix(t);
412
- assert.equal(prefix.run([]).status, 0);
413
- const data = join(prefix.home, ".engram");
414
- assert.deepEqual(readdirSync(data).sort(), [".instance-id"]);
415
- assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
416
- // Another Engram client replaces the shared directory with its own store.
417
- rmSync(data, { recursive: true });
418
- mkdirSync(data);
419
- writeFileSync(join(data, ".instance-id"), `${OTHER_STORE_ID}\n`);
420
- writeHeader(join(data, "engram.db"), 1);
421
- const foreign = prefix.run([]);
422
- assert.deepEqual(memoryOf(foreign), ["disabled", prefix.guard]);
423
- assert.equal(JSON.parse(readFileSync(provenancePath(prefix), "utf8")).instanceId, STORE_ID);
424
- });
425
- test("a later launch reuses Shell's own store, including a stale WAL header, without rewriting provenance", { skip: posixHost }, (t) => {
515
+ test("a server that fails to start or never reports ready disables memory for that launch and Shell still starts Pi", { skip: posixHost }, (t) => {
426
516
  const prefix = bundledPrefix(t);
427
- assert.equal(prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID }).status, 0);
428
- const recorded = readFileSync(provenancePath(prefix), "utf8");
429
- for (const userVersion of [0, 1]) {
430
- writeHeader(join(prefix.home, ".engram", "engram.db"), userVersion);
431
- const second = prefix.run([]);
432
- assert.equal(second.status, 0, second.stderr);
433
- const seen = lastLaunch(second);
434
- assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], ["allow", prefix.engram, undefined], String(userVersion));
435
- }
436
- assert.equal(readFileSync(provenancePath(prefix), "utf8"), recorded);
437
- });
438
-
439
- test("provenance mismatches disable memory behind the guard: instance, core digest, newer or malformed header", { skip: posixHost }, (t) => {
440
- const mismatches: Array<[string, (prefix: ReturnType<typeof bundledPrefix>) => void]> = [
441
- ["instance", (prefix) => writeFileSync(join(prefix.home, ".engram", ".instance-id"), `${OTHER_STORE_ID}\n`)],
442
- ["invalid identity", (prefix) => writeFileSync(join(prefix.home, ".engram", ".instance-id"), "not-an-id\n")],
443
- ["core digest", (prefix) => writeFileSync(prefix.engram, "#!/bin/sh\necho upgraded engram\n", { mode: 0o700 })],
444
- ["newer schema", (prefix) => writeHeader(join(prefix.home, ".engram", "engram.db"), 2)],
445
- ["malformed header", (prefix) => writeHeader(join(prefix.home, ".engram", "engram.db"), 1, "not sqlite at all")],
446
- ["symlinked database", (prefix) => {
447
- const db = join(prefix.home, ".engram", "engram.db");
448
- renameSync(db, `${db}.real`);
449
- symlinkSync(`${db}.real`, db);
450
- }],
451
- ["corrupt provenance", (prefix) => writeFileSync(provenancePath(prefix), "{")],
452
- ];
453
- for (const [name, change] of mismatches) {
454
- const prefix = bundledPrefix(t);
455
- assert.equal(prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID }).status, 0, name);
456
- change(prefix);
457
- const result = prefix.run([]);
458
- assert.equal(result.status, 0, `${name}: ${result.stderr}`);
459
- const seen = lastLaunch(result);
460
- assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN], ["disabled", prefix.guard], name);
461
- assert.match(result.stderr, name === "core digest" ? /memory disabled.*bundled core changed.*memory migrate --confirm-quiescent/s : /memory disabled.*could not verify.*compatibility.*Back up/s, name);
462
- }
517
+ const result = prefix.run([], { FIXTURE_SERVE: "exit" });
518
+ assert.equal(result.status, 0, result.stderr);
519
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
520
+ assert.match(result.stderr, /memory disabled.*could not start the shared Engram server/s);
521
+ assert.equal(lastLaunch(result).cli.endsWith(join("dist", "bundle", "cli.js")), true);
522
+ assert.equal(prefix.serves().length, 1);
463
523
  });
464
524
 
465
- function migrationFixture(t: TestContext, outcome = "success") {
525
+ test("concurrent starts converge: a failed bind followed by a ready /compatibility counts as started", { skip: posixHost }, (t) => {
466
526
  const prefix = bundledPrefix(t);
467
- assert.equal(prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID }).status, 0);
468
- const source = join(prefix.home, ".engram");
469
- writeFileSync(join(source, "config.json"), '{"preserved":true}\n');
470
- const originalRecord = readFileSync(provenancePath(prefix), "utf8");
471
- const sourceBytes = readFileSync(join(source, "engram.db"));
472
- const invocation = join(prefix.home, "migration-invoked");
473
- writeFileSync(prefix.engram, `#!${process.execPath}\nimport {cpSync,writeFileSync} from 'node:fs';\nconst args=process.argv.slice(2);\nif(args[0]==='--help'){console.log(${JSON.stringify(outcome === "unsupported" ? "Commands: compatibility" : " migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json\n serve --session-owned Private ephemeral HTTP server")});process.exit(0);}\nwriteFileSync(${JSON.stringify(invocation)},JSON.stringify({args,token:process.env.GH_TOKEN,cloud:process.env.ENGRAM_CLOUD_TOKEN}));\nif(args[0]!=='migrate-copy') process.exit(2);\nconst source=args[args.indexOf('--data-dir')+1],output=args[args.indexOf('--output-dir')+1];\nif(${JSON.stringify(outcome)}==='failure'){process.stderr.write('migration rejected\\n');process.exit(1);}\ncpSync(source,output,{recursive:true});\nconsole.log(JSON.stringify({source_dir:source,output_dir:output,instance_id:${JSON.stringify(outcome === "wrong-identity" ? OTHER_STORE_ID : STORE_ID)},migration_completed:true}));\n`, { mode: 0o700 });
474
- return { ...prefix, source, originalRecord, sourceBytes, invocation };
475
- }
527
+ const result = prefix.run([], { FIXTURE_SERVE: "race" });
528
+ assert.equal(result.status, 0, result.stderr);
529
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
530
+ assert.doesNotMatch(result.stderr, /memory disabled/);
531
+ assert.deepEqual(prefix.serves().map(({ mode }) => mode), ["race", "late"]);
532
+ });
476
533
 
477
- test("bundled local memory selects a verified session-owned provider instead of a detached shared server", { skip: posixHost }, (t) => {
534
+ test("bundled memory never requires or selects a session-owned server, with the 0.4.2 companion and an explicit mode kept", { skip: posixHost }, (t) => {
478
535
  const prefix = bundledPrefix(t);
479
- const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
536
+ const result = prefix.run([]);
480
537
  assert.equal(result.status, 0, result.stderr);
481
- assert.deepEqual(memoryOf(result), ["allow", prefix.engram]);
482
- const seen = lastLaunch(result);
483
- assert.equal(seen.env.ENGRAM_SERVER_MODE, "session-owned");
484
- assert.equal(seen.env.ENGRAM_URL, undefined);
485
- assert.equal(seen.env.ENGRAM_SOCKET, undefined);
486
- assert.equal(seen.provenance.instanceId, STORE_ID);
538
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
539
+ assert.equal(lastLaunch(result).env.ENGRAM_SERVER_MODE, undefined);
540
+ assert.doesNotMatch(result.stderr, /session-owned/);
541
+ assert.deepEqual(JSON.parse(readFileSync(prefix.metadata, "utf8")).engram, { startupPolicy: 1 });
487
542
  });
488
543
 
489
- test("missing private capabilities never silently fall back or provision a store", { skip: posixHost }, (t) => {
490
- for (const env of [{ FIXTURE_OWNED_CORE: "0" }, { FIXTURE_OWNED_COMPANION: "0" }]) {
544
+ test("an explicit Engram server mode is a selection: kept, never probed or started, memory disabled", { skip: posixHost }, (t) => {
545
+ for (const mode of ["session-owned", "bogus"]) {
491
546
  const prefix = bundledPrefix(t);
492
- const result = prefix.run([], { ...env, FIXTURE_CREATE_STORE: STORE_ID });
547
+ const result = prefix.run([], { ENGRAM_SERVER_MODE: mode, FIXTURE_NO_PROBE: "1" });
493
548
  assert.equal(result.status, 0, result.stderr);
494
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
495
- assert.match(result.stderr, /session-owned.*supporting published/s);
496
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
497
- assert.equal(existsSync(provenancePath(prefix)), false);
549
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined], mode);
550
+ assert.equal(lastLaunch(result).env.ENGRAM_SERVER_MODE, mode);
551
+ assert.deepEqual(prefix.serves(), [], mode);
498
552
  }
499
553
  });
500
554
 
501
- test("every selected companion must advertise private support, including mixed local and npm selections", { skip: posixHost }, (t) => {
555
+ test("a mixed local and npm companion selection is preserved and memory still uses the shared server", { skip: posixHost }, (t) => {
502
556
  const prefix = bundledPrefix(t);
503
557
  const local = join(prefix.home, "legacy-companion");
504
558
  mkdirSync(local);
505
559
  writeFileSync(join(local, "package.json"), JSON.stringify({ name: "gentle-engram", engram: { startupPolicy: 1 } }));
506
- writeFileSync(prefix.metadata, JSON.stringify({ name: "gentle-engram", engram: { startupPolicy: 1, sessionOwned: 1 } }));
507
560
  const settings = join(prefix.home, ".gentle-shell", "agent", "settings.json");
508
561
  const selected = JSON.stringify({ packages: [local, "npm:gentle-engram@0.4.2"] });
509
562
  writeFileSync(settings, selected);
510
- const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
563
+ const result = prefix.run([]);
511
564
  assert.equal(result.status, 0, result.stderr);
512
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
513
- assert.match(result.stderr, /session-owned.*supporting published/s);
565
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL]);
514
566
  assert.deepEqual(JSON.parse(readFileSync(settings, "utf8")).packages, JSON.parse(selected).packages);
515
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
516
- assert.equal(existsSync(provenancePath(prefix)), false);
517
567
  });
518
568
 
519
- test("explicit empty or conflicting private modes remain invalid without provisioning", { skip: posixHost }, (t) => {
520
- for (const mode of ["", "shared", " "]) {
521
- const prefix = bundledPrefix(t);
522
- const result = prefix.run([], { ENGRAM_SERVER_MODE: mode, FIXTURE_CREATE_STORE: STORE_ID });
569
+ test("an existing store, including one the global CLI created, starts the shared server and Shell never touches it", { skip: posixHost }, (t) => {
570
+ const prefix = bundledPrefix(t);
571
+ const data = join(prefix.home, ".engram");
572
+ mkdirSync(data);
573
+ writeFileSync(join(data, ".instance-id"), `${OTHER_STORE_ID}\n`);
574
+ for (const userVersion of [0, 1]) {
575
+ writeHeader(join(data, "engram.db"), userVersion);
576
+ const before = readFileSync(join(data, "engram.db"));
577
+ const result = prefix.run([]);
523
578
  assert.equal(result.status, 0, result.stderr);
524
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
525
- assert.equal(lastLaunch(result).env.ENGRAM_SERVER_MODE, mode);
526
- assert.match(result.stderr, /explicit ENGRAM_SERVER_MODE.*unset.*session-owned/s);
527
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
528
- assert.equal(existsSync(provenancePath(prefix)), false);
579
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL], String(userVersion));
580
+ assert.deepEqual(readFileSync(join(data, "engram.db")), before);
529
581
  }
582
+ assert.deepEqual(readdirSync(data).sort(), [".instance-id", "engram.db"]);
583
+ assert.equal(existsSync(provenancePath(prefix)), false);
530
584
  });
531
585
 
532
- test("an explicit Engram socket remains untouched and is never probed", { skip: posixHost }, (t) => {
586
+ test("a store a newer Engram migrated keeps the server stopped and memory disabled", { skip: posixHost }, (t) => {
533
587
  const prefix = bundledPrefix(t);
534
- const result = prefix.run([], { ENGRAM_SOCKET: "/fixture/selected.sock", FIXTURE_NO_PROBE: "1" });
588
+ const data = join(prefix.home, ".engram");
589
+ mkdirSync(data);
590
+ writeHeader(join(data, "engram.db"), 2);
591
+ const result = prefix.run([]);
535
592
  assert.equal(result.status, 0, result.stderr);
536
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
537
- assert.equal(lastLaunch(result).env.ENGRAM_SOCKET, "/fixture/selected.sock");
538
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
593
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
594
+ assert.match(result.stderr, /memory disabled.*newer Engram/s);
595
+ assert.deepEqual(prefix.serves(), []);
539
596
  });
540
597
 
541
- test("a stopped Shell store with a changed core gets migration guidance, not a server upgrade guess", { skip: posixHost }, (t) => {
542
- const prefix = migrationFixture(t);
543
- const result = prefix.run([]);
598
+ function migrationFixture(t: TestContext, outcome = "success") {
599
+ const prefix = bundledPrefix(t);
600
+ // A store an earlier Shell provisioned and recorded with the bundled core.
601
+ const source = join(prefix.home, ".engram");
602
+ mkdirSync(source);
603
+ writeFileSync(join(source, ".instance-id"), `${STORE_ID}\n`);
604
+ writeHeader(join(source, "engram.db"), 1);
605
+ writeFileSync(join(source, "config.json"), '{"preserved":true}\n');
606
+ mkdirSync(dirname(provenancePath(prefix)), { recursive: true });
607
+ writeFileSync(provenancePath(prefix), `${JSON.stringify({ schema: 1, dataDir: source, instanceId: STORE_ID, coreSha256: sha256File(prefix.engram) }, null, 2)}\n`);
608
+ const originalRecord = readFileSync(provenancePath(prefix), "utf8");
609
+ const sourceBytes = readFileSync(join(source, "engram.db"));
610
+ const invocation = join(prefix.home, "migration-invoked");
611
+ writeFileSync(prefix.engram, `#!${process.execPath}\nimport {cpSync,writeFileSync} from 'node:fs';\nconst args=process.argv.slice(2);\nif(args[0]==='--help'){console.log(${JSON.stringify(outcome === "unsupported" ? "Commands: compatibility" : " migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json")});process.exit(0);}\nwriteFileSync(${JSON.stringify(invocation)},JSON.stringify({args,token:process.env.GH_TOKEN,cloud:process.env.ENGRAM_CLOUD_TOKEN}));\nif(args[0]!=='migrate-copy') process.exit(2);\nconst source=args[args.indexOf('--data-dir')+1],output=args[args.indexOf('--output-dir')+1];\nif(${JSON.stringify(outcome)}==='failure'){process.stderr.write('migration rejected\\n');process.exit(1);}\ncpSync(source,output,{recursive:true});\nconsole.log(JSON.stringify({source_dir:source,output_dir:output,instance_id:${JSON.stringify(outcome === "wrong-identity" ? OTHER_STORE_ID : STORE_ID)},migration_completed:true}));\n`, { mode: 0o700 });
612
+ return { ...prefix, source, originalRecord, sourceBytes, invocation };
613
+ }
614
+
615
+ test("an explicit Engram socket remains untouched and is never probed", { skip: posixHost }, (t) => {
616
+ const prefix = bundledPrefix(t);
617
+ const result = prefix.run([], { ENGRAM_SOCKET: "/fixture/selected.sock", FIXTURE_NO_PROBE: "1" });
544
618
  assert.equal(result.status, 0, result.stderr);
545
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
546
- assert.match(result.stderr, /bundled core changed/);
547
- assert.match(result.stderr, /gentle-shell memory migrate --confirm-quiescent/);
548
- assert.equal(existsSync(prefix.invocation), false);
549
- assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
550
- assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
619
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
620
+ assert.equal(lastLaunch(result).env.ENGRAM_SOCKET, "/fixture/selected.sock");
621
+ assert.deepEqual(prefix.serves(), []);
551
622
  });
552
623
 
553
624
  test("memory migrate explicitly promotes a verified copy and retains the original store", { skip: posixHost }, (t) => {
@@ -568,7 +639,6 @@ test("memory migrate explicitly promotes a verified copy and retains the origina
568
639
  assert.equal(invoked.token, undefined);
569
640
  assert.equal(invoked.cloud, undefined);
570
641
  assert.equal(existsSync(join(prefix.root, "config", "engram-migration.json")), false);
571
- assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
572
642
  });
573
643
 
574
644
  test("memory migrate rejects missing confirmation and malformed flags without invoking Engram", { skip: posixHost }, (t) => {
@@ -608,18 +678,17 @@ test("memory migrate refuses provider failure and mismatched evidence without ch
608
678
  }
609
679
  });
610
680
 
611
- test("an interrupted memory promotion blocks fresh provisioning until recovery", { skip: posixHost }, (t) => {
681
+ test("an interrupted memory promotion keeps the server stopped until recovery", { skip: posixHost }, (t) => {
612
682
  const prefix = bundledPrefix(t);
613
683
  mkdirSync(join(prefix.root, "config"), { recursive: true });
614
684
  const marker = join(prefix.root, "config", "engram-migration.json");
615
685
  writeFileSync(marker, '{"interrupted":true}\n');
616
686
  const result = prefix.run([], { FIXTURE_NO_PROBE: "1" });
617
687
  assert.equal(result.status, 0, result.stderr);
618
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
688
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
619
689
  assert.match(result.stderr, /incomplete memory migration/);
620
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
690
+ assert.deepEqual(prefix.serves(), []);
621
691
  assert.equal(readFileSync(marker, "utf8"), '{"interrupted":true}\n');
622
- assert.equal(existsSync(provenancePath(prefix)), false);
623
692
  });
624
693
 
625
694
  test("public help explains compatibility evidence without promising update recovery", { skip: posixHost }, (t) => {
@@ -629,41 +698,21 @@ test("public help explains compatibility evidence without promising update recov
629
698
  assert.equal(result.stderr, "");
630
699
  assert.match(result.stdout, /Matching versions or updating alone do not prove compatibility/);
631
700
  assert.match(result.stdout, /\/compatibility/);
632
- assert.doesNotMatch(result.stdout, /update Engram, restart/);
701
+ assert.match(result.stdout, /shares the global Engram server.*nothing listens.*engram serve/s);
702
+ assert.doesNotMatch(result.stdout, /update Engram, restart|Shell-provisioned|session-owned/);
633
703
  });
634
704
 
635
- test("a foreign pre-existing store stays disabled and never receives provenance", { skip: posixHost }, (t) => {
705
+ test("a listener without the ready contract is never replaced, even reporting the bundled core version", { skip: posixHost }, async (t) => {
636
706
  const prefix = bundledPrefix(t);
637
- const data = join(prefix.home, ".engram");
638
- mkdirSync(data);
639
- writeFileSync(join(data, ".instance-id"), `${STORE_ID}\n`);
640
- writeHeader(join(data, "engram.db"), 1);
641
- const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
707
+ const port = await engramServer(t, { "/health": [200, { status: "ok", instance_id: STORE_ID, version: engramPinVersion }] });
708
+ const result = prefix.run([], { FIXTURE_ENGRAM_PORT: port });
642
709
  assert.equal(result.status, 0, result.stderr);
643
- assert.deepEqual([lastLaunch(result).env.ENGRAM_STARTUP_POLICY, lastLaunch(result).env.ENGRAM_BIN], ["disabled", prefix.guard]);
644
- assert.equal(existsSync(provenancePath(prefix)), false);
645
- });
646
-
647
- test("a listening server is reused for Shell's store only with the recorded /health instance and the exact core version", { skip: posixHost }, async (t) => {
648
- const cases: Array<[string, unknown, string]> = [
649
- ["match", { status: "ok", instance_id: STORE_ID, version: engramPinVersion }, "allow"],
650
- ["other instance", { status: "ok", instance_id: OTHER_STORE_ID, version: engramPinVersion }, "disabled"],
651
- ["other version", { status: "ok", instance_id: STORE_ID, version: "9.9.9" }, "disabled"],
652
- ];
653
- for (const [name, health, policy] of cases) {
654
- const prefix = bundledPrefix(t);
655
- assert.equal(prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID }).status, 0, name);
656
- const port = await engramServer(t, { "/health": [200, health] });
657
- const result = prefix.run([], { FIXTURE_ENGRAM_PORT: port });
658
- assert.equal(result.status, 0, `${name}: ${result.stderr}`);
659
- const seen = lastLaunch(result);
660
- assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL],
661
- [policy, prefix.guard, policy === "allow" ? "http://127.0.0.1:7437" : undefined], name);
662
- assert.equal(seen.env.ENGRAM_SERVER_MODE, undefined, name);
663
- }
710
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
711
+ assert.match(result.stderr, /memory disabled.*could not verify/s);
712
+ assert.deepEqual(prefix.serves(), []);
664
713
  });
665
714
 
666
- test("a verified /compatibility server is reused through an explicit URL and the guard, without provenance", { skip: posixHost }, async (t) => {
715
+ test("a verified /compatibility server is reused unchanged through an explicit URL and the guard", { skip: posixHost }, async (t) => {
667
716
  const ready = { revision: 1, source: "active", status: "ready", offline_automatic_opening_authorized: false, active_reuse_authorized: true,
668
717
  instance_id: STORE_ID, runtime_id: "a".repeat(32), capability: "startup-verified-store-v1" };
669
718
  const prefix = bundledPrefix(t);
@@ -675,11 +724,12 @@ test("a verified /compatibility server is reused through an explicit URL and the
675
724
  assert.equal(result.status, 0, result.stderr);
676
725
  const seen = lastLaunch(result);
677
726
  assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_URL, seen.env.ENGRAM_BIN], ["allow", "http://127.0.0.1:7437", prefix.guard]);
678
- assert.equal(existsSync(provenancePath(prefix)), false);
727
+ assert.deepEqual(prefix.serves(), []);
679
728
  const unready = bundledPrefix(t);
680
729
  mkdirSync(join(unready.home, ".engram"));
681
730
  const notReady = await engramServer(t, { "/compatibility": [200, { ...ready, status: "unknown", active_reuse_authorized: false }] });
682
731
  assert.equal(lastLaunch(unready.run([], { FIXTURE_ENGRAM_PORT: notReady })).env.ENGRAM_STARTUP_POLICY, "disabled");
732
+ assert.deepEqual(unready.serves(), []);
683
733
  });
684
734
 
685
735
  test("a foreign store behind a 3.3.2 server stays guarded and the launch names the server version to update", { skip: posixHost }, async (t) => {
@@ -696,34 +746,10 @@ test("a foreign store behind a 3.3.2 server stays guarded and the launch names t
696
746
  assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], ["disabled", prefix.guard, undefined]);
697
747
  assert.match(result.stderr, /memory disabled.*reports version 3\.3\.2.*update that server to 3\.4\.0 or later/s);
698
748
  assert.deepEqual(readFileSync(join(data, "engram.db")), before);
699
- assert.equal(existsSync(provenancePath(prefix)), false);
700
- });
701
-
702
- test("a fresh launch whose launcher is killed during Pi already has finalized provenance, and the next launch allows memory", { skip: posixHost }, (t) => {
703
- const prefix = bundledPrefix(t);
704
- const first = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID, FIXTURE_KILL_LAUNCHER: "1" });
705
- assert.equal(first.signal, "SIGKILL");
706
- assert.equal(JSON.parse(readFileSync(provenancePath(prefix), "utf8")).instanceId, STORE_ID);
707
- assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
708
- });
709
- test("a failed identity provisioning disables memory for that launch and records nothing", { skip: posixHost }, (t) => {
710
- const cases: Array<[string, Record<string, string>, (prefix: ReturnType<typeof bundledPrefix>) => void]> = [
711
- ["instance-id failure", { FIXTURE_INSTANCE_ID_FAIL: "1" }, () => {}],
712
- ["invalid identity file", { FIXTURE_INSTANCE_ID: "not-an-id" }, () => {}],
713
- ["provenance write failure", {}, (prefix) => mkdirSync(provenancePath(prefix), { recursive: true })],
714
- ];
715
- for (const [name, extra, prepare] of cases) {
716
- const prefix = bundledPrefix(t);
717
- prepare(prefix);
718
- const result = prefix.run([], extra);
719
- assert.equal(result.status, 0, `${name}: ${result.stderr}`);
720
- assert.deepEqual(memoryOf(result), ["disabled", prefix.guard], name);
721
- assert.match(result.stderr, /could not provision/, name);
722
- assert.equal(lastLaunch(result).provenance, null, name);
723
- }
749
+ assert.deepEqual(prefix.serves(), []);
724
750
  });
725
751
 
726
- test("--version and forwarded Pi version or help flags never probe Engram, install the companion or print memory notices", { skip: posixHost }, (t) => {
752
+ test("--version and forwarded Pi version or help flags never probe or start Engram, install the companion or print memory notices", { skip: posixHost }, (t) => {
727
753
  const prefix = bundledPrefix(t);
728
754
  writeFileSync(prefix.metadata, '{"name":"gentle-engram","version":"0.4.1"}');
729
755
  mkdirSync(join(prefix.home, ".engram"));
@@ -737,7 +763,7 @@ test("--version and forwarded Pi version or help flags never probe Engram, insta
737
763
  assert.equal(readFileSync(settings, "utf8"), before, args.join(" "));
738
764
  assert.equal(JSON.parse(readFileSync(prefix.metadata, "utf8")).version, "0.4.1", args.join(" "));
739
765
  }
740
- assert.equal(existsSync(provenancePath(prefix)), false);
766
+ assert.equal(prefix.serves().length, 1);
741
767
  });
742
768
 
743
769
  test("-v or --help as a prompt or an option value never switches memory off", { skip: posixHost }, (t) => {
@@ -745,8 +771,8 @@ test("-v or --help as a prompt or an option value never switches memory off", {
745
771
  const prefix = bundledPrefix(t);
746
772
  const result = prefix.run(args);
747
773
  assert.equal(result.status, 0, `${args.join(" ")}: ${result.stderr}`);
748
- assert.deepEqual(memoryOf(result), ["allow", prefix.engram], args.join(" "));
749
- assert.equal(lastLaunch(result).provenance.instanceId, STORE_ID, args.join(" "));
774
+ assert.deepEqual(memoryOf(result), ["allow", prefix.guard, DEFAULT_URL], args.join(" "));
775
+ assert.equal(prefix.serves().length, 1, args.join(" "));
750
776
  }
751
777
  });
752
778
  test("a missing bundled core uses the guard, never an engram on PATH", { skip: posixHost }, (t) => {
@@ -771,20 +797,23 @@ function countingGentleAi(prefix: ReturnType<typeof bundledPrefix>) {
771
797
  return { bin, runs: () => (existsSync(log) ? readFileSync(log, "utf8").split("\n").filter(Boolean).length : 0) };
772
798
  }
773
799
 
774
- test("setup on a fresh machine publishes the bundled core; the next launch is fresh, records provenance and is reused", { skip: posixHost }, (t) => {
800
+ test("setup on a fresh machine publishes the bundled core; the next launch starts the shared server and later launches reuse it", { skip: posixHost }, (t) => {
775
801
  const prefix = bundledPrefix(t, { core: false });
776
802
  const installer = countingGentleAi(prefix);
777
803
  const setup = prefix.run(["setup"], { GENTLE_SHELL_GENTLE_AI_BIN: installer.bin });
778
804
  assert.equal(setup.status, 0, setup.stderr);
779
805
  assert.equal(installer.runs(), 1);
780
806
  assert.equal(existsSync(prefix.engram), true);
781
- const first = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
807
+ // Setup itself never starts a server.
808
+ assert.deepEqual(prefix.serves(), []);
809
+ const first = prefix.run([]);
782
810
  assert.equal(first.status, 0, first.stderr);
783
- assert.deepEqual([lastLaunch(first).env.ENGRAM_STARTUP_POLICY, lastLaunch(first).env.ENGRAM_BIN], ["allow", prefix.engram]);
784
- assert.equal(JSON.parse(readFileSync(provenancePath(prefix), "utf8")).instanceId, STORE_ID);
811
+ assert.deepEqual(memoryOf(first), ["allow", prefix.guard, DEFAULT_URL]);
785
812
  const second = prefix.run([]);
786
813
  assert.equal(second.status, 0, second.stderr);
787
- assert.deepEqual([lastLaunch(second).env.ENGRAM_STARTUP_POLICY, lastLaunch(second).env.ENGRAM_BIN], ["allow", prefix.engram]);
814
+ assert.deepEqual(memoryOf(second), ["allow", prefix.guard, DEFAULT_URL]);
815
+ assert.equal(prefix.serves().length, 1);
816
+ assert.equal(existsSync(provenancePath(prefix)), false);
788
817
  });
789
818
 
790
819
  test("an explicit ENGRAM_URL alone keeps the verified core, or the guard without it, as ENGRAM_BIN", { skip: posixHost }, (t) => {
@@ -808,12 +837,13 @@ test("a failed companion check after setup is a warning, not a setup failure", {
808
837
  assert.equal(setup.status, 0, setup.stderr);
809
838
  assert.equal(installer.runs(), 1);
810
839
  assert.match(setup.stderr, /startupPolicy/);
811
- assert.deepEqual(memoryOf(prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "0" })), ["disabled", prefix.guard]);
840
+ // The guard, not the companion check, protects memory: the shared server is still reused.
841
+ assert.deepEqual(memoryOf(prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "0" })), ["allow", prefix.guard, DEFAULT_URL]);
812
842
  assert.deepEqual(readdirSync(data), ["engram.db"]);
813
843
  assert.equal(readFileSync(join(data, "engram.db"), "utf8"), "untouched database sentinel");
814
844
  });
815
845
 
816
- test("setup with a foreign existing store runs the installer like the baseline, while memory stays behind the guard", { skip: posixHost }, (t) => {
846
+ test("setup with an existing store runs the installer like the baseline and the launch reuses the shared server behind the guard", { skip: posixHost }, (t) => {
817
847
  const prefix = bundledPrefix(t, { core: false });
818
848
  const data = join(prefix.home, ".engram");
819
849
  mkdirSync(data);
@@ -823,13 +853,11 @@ test("setup with a foreign existing store runs the installer like the baseline,
823
853
  assert.equal(setup.status, 0, setup.stderr);
824
854
  assert.equal(installer.runs(), 1);
825
855
  assert.equal(existsSync(prefix.engram), true);
826
- assert.match(setup.stderr, /memory disabled.*could not verify.*compatibility.*Back up/s);
827
- assert.doesNotMatch(setup.stderr, /update Engram and restart/);
828
- assert.doesNotMatch(setup.stderr, /deferred/);
856
+ assert.doesNotMatch(setup.stderr, /memory disabled|deferred/);
829
857
  const guarded = prefix.run([]);
830
858
  assert.equal(guarded.status, 0, guarded.stderr);
831
859
  const env = lastLaunch(guarded).env;
832
- assert.deepEqual([env.ENGRAM_STARTUP_POLICY, env.ENGRAM_BIN, env.ENGRAM_URL], ["disabled", prefix.guard, undefined]);
860
+ assert.deepEqual([env.ENGRAM_STARTUP_POLICY, env.ENGRAM_BIN, env.ENGRAM_URL], ["allow", prefix.guard, DEFAULT_URL]);
833
861
  // Subagents receive the same environment (GENTLE_PI_AGENTS_PI runs the same Pi with it).
834
862
  assert.equal(env.GENTLE_PI_AGENTS_PI.endsWith(join("dist", "bundle", "cli.js")), true);
835
863
  const provider = spawnSync(env.ENGRAM_BIN, ["instance-id"], { env: { ...prefix.env, ...env }, encoding: "utf8" });
@@ -844,15 +872,14 @@ test("setup with a foreign existing store runs the installer like the baseline,
844
872
  assert.equal(existsSync(provenancePath(prefix)), false);
845
873
  });
846
874
 
847
- test("a gentle-pi version change re-provisions Shell's own store once and keeps its memory", { skip: posixHost }, (t) => {
875
+ test("a gentle-pi version change reruns setup once and keeps memory on the shared server", { skip: posixHost }, (t) => {
848
876
  const prefix = bundledPrefix(t, { core: false });
849
877
  const installer = countingGentleAi(prefix);
850
878
  const auto = { GENTLE_SHELL_NO_AUTO_SETUP: "", GENTLE_SHELL_GENTLE_AI_BIN: installer.bin };
851
- const first = prefix.run([], { ...auto, FIXTURE_CREATE_STORE: STORE_ID });
879
+ const first = prefix.run([], auto);
852
880
  assert.equal(first.status, 0, first.stderr);
853
881
  assert.equal(installer.runs(), 1);
854
- assert.deepEqual([lastLaunch(first).env.ENGRAM_STARTUP_POLICY, lastLaunch(first).env.ENGRAM_BIN], ["allow", prefix.engram]);
855
- assert.equal(existsSync(provenancePath(prefix)), true);
882
+ assert.deepEqual(memoryOf(first), ["allow", prefix.guard, DEFAULT_URL]);
856
883
  assert.equal(prefix.run([], auto).status, 0);
857
884
  assert.equal(installer.runs(), 1);
858
885
  const manifest = join(dirname(dirname(prefix.entry)), "package.json");
@@ -861,7 +888,8 @@ test("a gentle-pi version change re-provisions Shell's own store once and keeps
861
888
  assert.equal(changed.status, 0, changed.stderr);
862
889
  assert.equal(installer.runs(), 2);
863
890
  assert.doesNotMatch(changed.stderr, /automatic setup failed|deferred/);
864
- assert.deepEqual([lastLaunch(changed).env.ENGRAM_STARTUP_POLICY, lastLaunch(changed).env.ENGRAM_BIN], ["allow", prefix.engram]);
891
+ assert.deepEqual(memoryOf(changed), ["allow", prefix.guard, DEFAULT_URL]);
892
+ assert.equal(prefix.serves().length, 1);
865
893
  assert.equal(prefix.run([], auto).status, 0);
866
894
  assert.equal(installer.runs(), 2);
867
895
  });
@@ -882,9 +910,7 @@ test("capability readback also requires Pi to declare the selected pinned compan
882
910
  const result = prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "metadata-only" });
883
911
  assert.equal(result.status, 0, result.stderr);
884
912
  assert.match(result.stderr, /startupPolicy/);
885
- // Unverified companion capability cannot authorize even fresh provisioning.
886
- assert.equal(existsSync(join(prefix.home, ".engram")), false);
887
- assert.equal(existsSync(provenancePath(prefix)), false);
913
+ assert.equal(lastLaunch(result).env.ENGRAM_BIN, prefix.guard);
888
914
  });
889
915
 
890
916
  test("pinning preserves resource filters and unrelated settings and does not reinstall next launch", { skip: posixHost }, (t) => {
@@ -900,16 +926,19 @@ test("pinning preserves resource filters and unrelated settings and does not rei
900
926
  assert.doesNotMatch(prefix.run([]).stderr, /installing policy-capable companion/);
901
927
  });
902
928
 
903
- test("final startup rechecks data created during automatic setup and records no provenance for it", { skip: posixHost }, (t) => {
929
+ test("final startup rechecks a store a newer Engram migrated during automatic setup and never starts a server on it", { skip: posixHost }, (t) => {
904
930
  const prefix = bundledPrefix(t);
905
931
  const fake = join(prefix.userBin, "gentle-ai");
906
- writeFileSync(fake, `#!/bin/sh\nmkdir -p "$HOME/.engram"\nprintf '${STORE_ID}\\n' > "$HOME/.engram/.instance-id"\nprintf fixture > "$HOME/.engram/engram.db"\n`, { mode: 0o755 });
932
+ const db = join(prefix.home, ".engram", "engram.db");
933
+ writeFileSync(fake, `#!${process.execPath}\nconst { mkdirSync, writeFileSync } = require('node:fs');\nmkdirSync(${JSON.stringify(dirname(db))}, { recursive: true });\n` +
934
+ `const header = Buffer.alloc(4096); header.write('SQLite format 3\\0', 0, 'latin1'); header.writeUInt16BE(4096, 16); header[18] = 2; header[19] = 2; header.writeUInt32BE(2, 60);\n` +
935
+ `writeFileSync(${JSON.stringify(db)}, header);\n`, { mode: 0o755 });
907
936
  const result = prefix.run([], { GENTLE_SHELL_NO_AUTO_SETUP: "", GENTLE_SHELL_GENTLE_AI_BIN: fake });
908
937
  assert.equal(result.status, 0, result.stderr);
909
- const seen = lastLaunch(result);
910
- assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN], ["disabled", prefix.guard]);
911
- assert.equal(readFileSync(join(prefix.home, ".engram", "engram.db"), "utf8"), "fixture");
912
- assert.equal(existsSync(provenancePath(prefix)), false);
938
+ assert.equal(existsSync(db), true);
939
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard, undefined]);
940
+ assert.match(result.stderr, /memory disabled.*newer Engram/s);
941
+ assert.deepEqual(prefix.serves(), []);
913
942
  });
914
943
 
915
944
  test("explicit endpoints and invalid policies never trigger availability probes or override executables", { skip: posixHost }, (t) => {
@@ -923,6 +952,7 @@ test("explicit endpoints and invalid policies never trigger availability probes
923
952
  const policyOnly = prefix.run([], { FIXTURE_NO_PROBE: "1", ENGRAM_STARTUP_POLICY: "invalid" });
924
953
  assert.equal(policyOnly.status, 0, policyOnly.stderr);
925
954
  assert.equal(lastLaunch(policyOnly).env.ENGRAM_BIN, prefix.guard);
955
+ assert.deepEqual(prefix.serves(), []);
926
956
  });
927
957
 
928
958
  test("startup offer precedes Pi, not-now preserves config and omit suppresses just that release", { skip: posixHost }, (t) => {
@@ -939,15 +969,36 @@ test("startup offer precedes Pi, not-now preserves config and omit suppresses ju
939
969
  env: { ...prefix.env, NODE_OPTIONS: `${prefix.env.NODE_OPTIONS} --import=data:text/javascript,${encodeURIComponent(preload(latest))}` },
940
970
  encoding: "utf8", input, timeout: 10000,
941
971
  });
972
+ // A recorded lookup answers the next launch without the network, so these
973
+ // runs simulate later days by aging the cache back past the TTL; the live
974
+ // lookup then records its own answer again.
975
+ const ageCache = () => {
976
+ const parsed = JSON.parse(readFileSync(config, "utf8"));
977
+ const latest = typeof parsed.startupReleaseCheck?.latest === "string" ? parsed.startupReleaseCheck.latest : "999.0.0";
978
+ parsed.startupReleaseCheck = { checkedAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), latest };
979
+ writeFileSync(config, `${JSON.stringify(parsed, null, 2)}\n`);
980
+ };
942
981
  const skipped = run("999.0.0", "2\r");
943
982
  assert.equal(skipped.status, 0, skipped.stderr);
944
983
  assert.match(skipped.stdout, /Gentle Shell update available/);
945
984
  assert.ok(skipped.stdout.indexOf("Gentle Shell update available") < skipped.stdout.indexOf('"cli":'));
946
- assert.equal(readFileSync(config, "utf8"), '{"other":"preserved"}\n');
985
+ // A not-now choice still records no decision, but the live lookup it paid
986
+ // for lands in the config as the cache tomorrow's launch reuses.
987
+ const afterSkipped = JSON.parse(readFileSync(config, "utf8"));
988
+ assert.equal(afterSkipped.other, "preserved");
989
+ assert.equal(afterSkipped.skippedStartupRelease, undefined);
990
+ assert.equal(afterSkipped.startupReleaseCheck.latest, "999.0.0");
991
+ assert.ok(!Number.isNaN(Date.parse(afterSkipped.startupReleaseCheck.checkedAt)));
992
+ ageCache();
947
993
  const omitted = run("999.0.0", "3\r");
948
994
  assert.equal(omitted.status, 0, omitted.stderr);
949
- assert.deepEqual(JSON.parse(readFileSync(config, "utf8")), { other: "preserved", skippedStartupRelease: "999.0.0" });
995
+ const afterOmit = JSON.parse(readFileSync(config, "utf8"));
996
+ assert.equal(afterOmit.other, "preserved");
997
+ assert.equal(afterOmit.skippedStartupRelease, "999.0.0");
998
+ assert.equal(afterOmit.startupReleaseCheck.latest, "999.0.0");
999
+ ageCache();
950
1000
  assert.doesNotMatch(run("999.0.0", "").stdout, /Gentle Shell update available/);
1001
+ ageCache();
951
1002
  assert.match(run("999.0.1", "2\r").stdout, /Gentle Shell update available/);
952
1003
  const command = run("999.0.1", "", ["list"]);
953
1004
  assert.equal(command.status, 0, command.stderr);
@@ -973,11 +1024,13 @@ test("the bundled launcher runs only its version's Pi, with the user's environme
973
1024
  assert.equal(seen.env.NODE_OPTIONS, prefix.env.NODE_OPTIONS);
974
1025
  assert.equal(seen.env.npm_config_registry, "https://npm.example");
975
1026
  assert.equal(seen.env.npm_config_prefix, undefined);
976
- // S9: gentle-pi's config folder inside the prefix, the pinned Engram for
977
- // gentle-engram, and none of Gentle AI's own variables.
1027
+ // S9: gentle-pi's config folder inside the prefix, the shared server's URL
1028
+ // behind the guard for gentle-engram, and none of Gentle AI's own variables.
1029
+ // A Pi subcommand never starts the server.
978
1030
  assert.equal(seen.env.GENTLE_SHELL_PREFIX, prefix.root);
979
1031
  assert.equal(seen.env.GENTLE_PI_CONFIG_HOME, undefined);
980
- assert.equal(seen.env.ENGRAM_BIN, prefix.engram);
1032
+ assert.deepEqual([seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], [prefix.guard, DEFAULT_URL]);
1033
+ assert.deepEqual(prefix.serves(), []);
981
1034
  for (const name of ["GENTLE_AI_HOME", "GENTLE_AI_PI_CONFIG_DIR", "GENTLE_AI_ENGRAM_BIN", "GENTLE_AI_NO_SELF_UPDATE"]) assert.equal(seen.env[name], undefined, name);
982
1035
  // Pi's own npm is ours, configured only from the prefix, through the isolated home's settings.
983
1036
  const settings = JSON.parse(readFileSync(join(prefix.home, ".gentle-shell", "agent", "settings.json"), "utf8"));