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
@@ -39,7 +39,7 @@ function fakeFs({ real = {}, files = {}, links = {} }: { real?: Record<string, s
39
39
  };
40
40
  }
41
41
 
42
- const ID = "4.1.0-1.1.0";
42
+ const ID = "4.1.0-1.2.0";
43
43
  const POSIX_ROOT = "/home/ana/.gentle-shell";
44
44
  const POSIX_VERSION = posix.join(POSIX_ROOT, "versions", ID);
45
45
  const POSIX_PACKAGE = posix.join(POSIX_VERSION, "node_modules", ".pnpm", "gentle-pi@4.1.0", "node_modules", "gentle-pi");
@@ -106,29 +106,29 @@ const install = { root: POSIX_ROOT, id: ID, versionDir: POSIX_VERSION };
106
106
  const messageOf = (pi: BundledPi) => ("message" in pi ? pi.message : "");
107
107
 
108
108
  test("bundledPi returns only the Pi the version pins, inside the version folder", () => {
109
- assert.deepEqual(bundledPi(install, { platform: "linux", fs: fakeFs({ files: piFiles("1.1.0", "1.1.0") }) }),
110
- { ok: true, version: "1.1.0", cli: posix.join(PI_PACKAGE(POSIX_VERSION), "dist", "bundle", "cli.js") });
109
+ assert.deepEqual(bundledPi(install, { platform: "linux", fs: fakeFs({ files: piFiles("1.2.0", "1.2.0") }) }),
110
+ { ok: true, version: "1.2.0", cli: posix.join(PI_PACKAGE(POSIX_VERSION), "dist", "bundle", "cli.js") });
111
111
  const windows = { root: "C:\\G", id: ID, versionDir: `C:\\G\\versions\\${ID}` };
112
- assert.deepEqual(bundledPi(windows, { platform: "win32", fs: fakeFs({ files: piFiles("1.1.0", "1.1.0", win32, windows.versionDir) }) }),
113
- { ok: true, version: "1.1.0", cli: win32.join(PI_PACKAGE(windows.versionDir, win32), "dist", "bundle", "cli.js") });
112
+ assert.deepEqual(bundledPi(windows, { platform: "win32", fs: fakeFs({ files: piFiles("1.2.0", "1.2.0", win32, windows.versionDir) }) }),
113
+ { ok: true, version: "1.2.0", cli: win32.join(PI_PACKAGE(windows.versionDir, win32), "dist", "bundle", "cli.js") });
114
114
  });
115
115
 
116
116
  test("bundledPi fails with the version, the pinned Pi and the way to repair it", () => {
117
- const repair = /Gentle Shell 4\.1\.0-1\.1\.0 .*Pi 1\.1\.0.*gentle-shell upgrade.*installer/s;
118
- const mismatch = bundledPi(install, { platform: "linux", fs: fakeFs({ files: piFiles("1.1.0", "1.0.0") }) });
117
+ const repair = /Gentle Shell 4\.1\.0-1\.2\.0 .*Pi 1\.2\.0.*gentle-shell upgrade.*installer/s;
118
+ const mismatch = bundledPi(install, { platform: "linux", fs: fakeFs({ files: piFiles("1.2.0", "1.0.0") }) });
119
119
  assert.equal(mismatch.ok, false);
120
120
  assert.match(messageOf(mismatch), repair);
121
121
  assert.match(messageOf(mismatch), /found Pi 1\.0\.0/);
122
- const files = piFiles("1.1.0", "1.1.0");
122
+ const files = piFiles("1.2.0", "1.2.0");
123
123
  delete files[posix.join(PI_PACKAGE(POSIX_VERSION), "dist", "bundle", "cli.js")];
124
124
  const missing = bundledPi(install, { platform: "linux", fs: fakeFs({ files }) });
125
125
  assert.match(messageOf(missing), repair);
126
126
  // A Pi linked from outside the version folder is not the bundled one.
127
- const outside = fakeFs({ real: { [PI_PACKAGE(POSIX_VERSION)]: "/usr/lib/node_modules/@earendil-works/pi-coding-agent" }, files: piFiles("1.1.0", "1.1.0") });
127
+ const outside = fakeFs({ real: { [PI_PACKAGE(POSIX_VERSION)]: "/usr/lib/node_modules/@earendil-works/pi-coding-agent" }, files: piFiles("1.2.0", "1.2.0") });
128
128
  assert.equal(bundledPi(install, { platform: "linux", fs: outside }).ok, false);
129
129
  });
130
130
 
131
- const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.3.2/engram" };
131
+ const ISOLATION = { prefix: "/g", engram: "/g/runtime/engram-3.4.0/engram" };
132
132
 
133
133
  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
134
  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 +140,7 @@ test("bundledChildEnv keeps the user's environment and adds only the bundled Pi,
140
140
  // folder from the prefix, and Gentle AI 4.0.0 reads that variable as a parent.
141
141
  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
142
  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.3.2/engram" });
143
+ ENGRAM_BIN: "/g/runtime/engram-3.4.0/engram" });
144
144
  // The subagent runner splits that variable on whitespace: a spaced path falls
145
145
  // back to its own default, the running node and cli.js.
146
146
  const spaced = bundledChildEnv({ GENTLE_PI_AGENTS_PI: "pi" }, { node: "C:\\Users\\Ana Li\\node.exe", cli: "C:\\cli.js", ...ISOLATION });
@@ -205,10 +205,10 @@ test("bundledUpdateNext still updates extensions when the upgrade did not comple
205
205
  });
206
206
 
207
207
  test("bundledUpdateNotice explains the pinned Pi and runs or names gentle-shell upgrade", () => {
208
- const interactive = bundledUpdateNotice({ piVersion: "1.1.0", interactive: true }).join("\n");
209
- assert.match(interactive, /Pi ships pinned with Gentle Shell \(Pi 1\.1\.0\)/);
208
+ const interactive = bundledUpdateNotice({ piVersion: "1.2.0", interactive: true }).join("\n");
209
+ assert.match(interactive, /Pi ships pinned with Gentle Shell \(Pi 1\.2\.0\)/);
210
210
  assert.match(interactive, /Running 'gentle-shell upgrade'/);
211
- assert.match(bundledUpdateNotice({ piVersion: "1.1.0", interactive: false }).join("\n"), /Run 'gentle-shell upgrade'/);
211
+ assert.match(bundledUpdateNotice({ piVersion: "1.2.0", interactive: false }).join("\n"), /Run 'gentle-shell upgrade'/);
212
212
  });
213
213
 
214
214
  test("parseBundledUpgradeArgs accepts the release channel and --rollback and refuses the main channel", () => {
@@ -233,18 +233,18 @@ test("parseBundledUpgradeArgs accepts the release channel and --rollback and ref
233
233
  const repository = dirname(dirname(fileURLToPath(import.meta.url)));
234
234
  const posixHost = process.platform === "win32" ? "the bundled prefix fixture uses a POSIX current symlink" : false;
235
235
  const SHELL = JSON.parse(readFileSync(join(repository, "package.json"), "utf8")).version;
236
- const FIXTURE_ID = `${SHELL}-1.1.0`;
236
+ const FIXTURE_ID = `${SHELL}-1.2.0`;
237
237
  const FAKE_PI = [
238
238
  "import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs'; import {join, dirname} from 'node:path';",
239
239
  "const args = process.argv.slice(2);",
240
240
  "if (args[0] === 'install' && args[1] === 'npm:gentle-engram@0.4.2' && process.env.FIXTURE_INSTALL_CAPABILITY !== '0') {",
241
241
  " 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}}));",
242
+ " writeFileSync(metadata, JSON.stringify({name:'gentle-engram',version:'0.4.2',engram:{startupPolicy:1,...(process.env.FIXTURE_OWNED_COMPANION === '0' ? {} : {sessionOwned:1})}}));",
243
243
  " const path = join(home,'settings.json'); let settings={}; try {settings=JSON.parse(readFileSync(path,'utf8'));} catch {}",
244
244
  " 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
245
  " 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
246
  "}",
247
- "if (args.includes('--version')) { console.log('1.1.0'); process.exit(0); }",
247
+ "if (args.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
248
248
  // Stands in for gentle-engram creating the store through the bundled core during the session.
249
249
  "if (process.env.FIXTURE_CREATE_STORE && args[0] !== 'install' && args[0] !== 'list') {",
250
250
  " const data = join(process.env.HOME, '.engram');",
@@ -255,7 +255,7 @@ const FAKE_PI = [
255
255
  " }",
256
256
  "}",
257
257
  "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
- "\t'GENTLE_SHELL_PREFIX', 'GENTLE_PI_CONFIG_HOME', 'ENGRAM_BIN', 'ENGRAM_URL', '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'];",
258
+ "\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
259
  "const record = join(process.env.GENTLE_SHELL_PREFIX ?? '', 'config', 'engram-provenance.json');",
260
260
  "const provenance = (() => { try { return JSON.parse(readFileSync(record, 'utf8')); } catch { return null; } })();",
261
261
  "console.log(JSON.stringify({ cli: process.argv[1], args, provenance, env: Object.fromEntries(pick.map((k) => [k, process.env[k]])) }));",
@@ -264,6 +264,7 @@ const FAKE_PI = [
264
264
  ].join("\n");
265
265
  const FIXTURE_CORE = [
266
266
  "#!/bin/sh",
267
+ 'if [ "$1" = --help ] && [ "$FIXTURE_OWNED_CORE" != 0 ]; then echo " serve --session-owned Private ephemeral HTTP server"; exit 0; fi',
267
268
  'if [ "$1" != instance-id ]; then echo engram; exit 0; fi',
268
269
  '[ -z "$FIXTURE_PROVISION_ENV" ] || env > "$FIXTURE_PROVISION_ENV"',
269
270
  '[ -z "$FIXTURE_INSTANCE_ID_FAIL" ] || exit 3',
@@ -273,7 +274,7 @@ const FIXTURE_CORE = [
273
274
  "",
274
275
  ].join("\n");
275
276
 
276
- function bundledPrefix(t: TestContext, { pi = "1.1.0", core = true } = {}) {
277
+ function bundledPrefix(t: TestContext, { pi = "1.2.0", core = true } = {}) {
277
278
  const base = realpathSync(mkdtempSync(join(tmpdir(), "gentle-shell-bundled-")));
278
279
  t.after(() => rmSync(base, { recursive: true, force: true }));
279
280
  const home = join(base, "home");
@@ -295,7 +296,7 @@ function bundledPrefix(t: TestContext, { pi = "1.1.0", core = true } = {}) {
295
296
  mkdirSync(join(piDir, "dist", "bundle"), { recursive: true });
296
297
  writeFileSync(join(piDir, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: pi, bin: { pi: "dist/bundle/cli.js" } }));
297
298
  writeFileSync(join(piDir, "dist", "bundle", "cli.js"), FAKE_PI);
298
- writeFileSync(join(version, "package.json"), JSON.stringify({ dependencies: { "gentle-pi": SHELL, "@earendil-works/pi-coding-agent": "1.1.0" } }));
299
+ writeFileSync(join(version, "package.json"), JSON.stringify({ dependencies: { "gentle-pi": SHELL, "@earendil-works/pi-coding-agent": "1.2.0" } }));
299
300
  writeFileSync(join(version, ".gentle-shell-version"), `${FIXTURE_ID}\n`);
300
301
  symlinkSync(`versions/${FIXTURE_ID}`, join(root, "current"));
301
302
  // The bundled npm the npmCommand entry loads.
@@ -457,10 +458,180 @@ test("provenance mismatches disable memory behind the guard: instance, core dige
457
458
  assert.equal(result.status, 0, `${name}: ${result.stderr}`);
458
459
  const seen = lastLaunch(result);
459
460
  assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN], ["disabled", prefix.guard], name);
460
- assert.match(result.stderr, /memory disabled.*Back up.*update.*restart.*recheck/s, 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);
461
462
  }
462
463
  });
463
464
 
465
+ function migrationFixture(t: TestContext, outcome = "success") {
466
+ 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
+ }
476
+
477
+ test("bundled local memory selects a verified session-owned provider instead of a detached shared server", { skip: posixHost }, (t) => {
478
+ const prefix = bundledPrefix(t);
479
+ const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
480
+ 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);
487
+ });
488
+
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" }]) {
491
+ const prefix = bundledPrefix(t);
492
+ const result = prefix.run([], { ...env, FIXTURE_CREATE_STORE: STORE_ID });
493
+ 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);
498
+ }
499
+ });
500
+
501
+ test("every selected companion must advertise private support, including mixed local and npm selections", { skip: posixHost }, (t) => {
502
+ const prefix = bundledPrefix(t);
503
+ const local = join(prefix.home, "legacy-companion");
504
+ mkdirSync(local);
505
+ 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
+ const settings = join(prefix.home, ".gentle-shell", "agent", "settings.json");
508
+ const selected = JSON.stringify({ packages: [local, "npm:gentle-engram@0.4.2"] });
509
+ writeFileSync(settings, selected);
510
+ const result = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID });
511
+ 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);
514
+ 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
+ });
518
+
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 });
523
+ 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);
529
+ }
530
+ });
531
+
532
+ test("an explicit Engram socket remains untouched and is never probed", { skip: posixHost }, (t) => {
533
+ const prefix = bundledPrefix(t);
534
+ const result = prefix.run([], { ENGRAM_SOCKET: "/fixture/selected.sock", FIXTURE_NO_PROBE: "1" });
535
+ 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);
539
+ });
540
+
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([]);
544
+ 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);
551
+ });
552
+
553
+ test("memory migrate explicitly promotes a verified copy and retains the original store", { skip: posixHost }, (t) => {
554
+ const prefix = migrationFixture(t);
555
+ const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1", GH_TOKEN: "fixture-secret", ENGRAM_CLOUD_TOKEN: "fixture-cloud" });
556
+ assert.equal(result.status, 0, result.stderr);
557
+ assert.equal(result.stderr, "");
558
+ const receipt = JSON.parse(result.stdout);
559
+ assert.deepEqual(Object.keys(receipt).sort(), ["backup_dir", "instance_id", "status"]);
560
+ assert.equal(receipt.status, "migrated");
561
+ assert.equal(receipt.instance_id, STORE_ID);
562
+ assert.deepEqual(readFileSync(join(receipt.backup_dir, "engram.db")), prefix.sourceBytes);
563
+ assert.equal(readFileSync(join(receipt.backup_dir, "config.json"), "utf8"), '{"preserved":true}\n');
564
+ assert.equal(readFileSync(join(prefix.source, "config.json"), "utf8"), '{"preserved":true}\n');
565
+ assert.deepEqual(JSON.parse(readFileSync(provenancePath(prefix), "utf8")), { schema: 1, dataDir: prefix.source, instanceId: STORE_ID, coreSha256: sha256File(prefix.engram) });
566
+ const invoked = JSON.parse(readFileSync(prefix.invocation, "utf8"));
567
+ assert.equal(invoked.args[0], "migrate-copy");
568
+ assert.equal(invoked.token, undefined);
569
+ assert.equal(invoked.cloud, undefined);
570
+ assert.equal(existsSync(join(prefix.root, "config", "engram-migration.json")), false);
571
+ assert.deepEqual(memoryOf(prefix.run([])), ["allow", prefix.engram]);
572
+ });
573
+
574
+ test("memory migrate rejects missing confirmation and malformed flags without invoking Engram", { skip: posixHost }, (t) => {
575
+ const prefix = migrationFixture(t);
576
+ for (const args of [["memory", "migrate"], ["memory", "migrate", "--confirm-quiescent", "--unknown"], ["memory", "migrate", "--confirm-quiescent", "--confirm-quiescent"]]) {
577
+ const result = prefix.run(args, { FIXTURE_NO_PROBE: "1" });
578
+ assert.equal(result.status, 2, result.stderr);
579
+ assert.equal(result.stdout, "");
580
+ assert.equal(result.stderr, "Usage: gentle-shell memory migrate --confirm-quiescent\nStop every Engram client before running this command.\n");
581
+ assert.equal(existsSync(prefix.invocation), false);
582
+ assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
583
+ assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
584
+ }
585
+ });
586
+
587
+ test("memory migrate never sends an unknown command to an older bundled core", { skip: posixHost }, (t) => {
588
+ const prefix = migrationFixture(t, "unsupported");
589
+ const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1" });
590
+ assert.equal(result.status, 1, result.stderr);
591
+ assert.equal(result.stdout, "");
592
+ assert.match(result.stderr, /does not support migrate-copy/);
593
+ assert.equal(existsSync(prefix.invocation), false);
594
+ assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
595
+ assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
596
+ });
597
+
598
+ test("memory migrate refuses provider failure and mismatched evidence without changing provenance or source", { skip: posixHost }, (t) => {
599
+ for (const outcome of ["failure", "wrong-identity"]) {
600
+ const prefix = migrationFixture(t, outcome);
601
+ const result = prefix.run(["memory", "migrate", "--confirm-quiescent"], { FIXTURE_NO_PROBE: "1" });
602
+ assert.equal(result.status, 1, result.stderr);
603
+ assert.equal(result.stdout, "");
604
+ assert.match(result.stderr, /^gentle-shell: memory migration failed:/);
605
+ assert.equal(readFileSync(provenancePath(prefix), "utf8"), prefix.originalRecord);
606
+ assert.deepEqual(readFileSync(join(prefix.source, "engram.db")), prefix.sourceBytes);
607
+ assert.equal(existsSync(join(prefix.root, "config", "engram-migration.json")), false);
608
+ }
609
+ });
610
+
611
+ test("an interrupted memory promotion blocks fresh provisioning until recovery", { skip: posixHost }, (t) => {
612
+ const prefix = bundledPrefix(t);
613
+ mkdirSync(join(prefix.root, "config"), { recursive: true });
614
+ const marker = join(prefix.root, "config", "engram-migration.json");
615
+ writeFileSync(marker, '{"interrupted":true}\n');
616
+ const result = prefix.run([], { FIXTURE_NO_PROBE: "1" });
617
+ assert.equal(result.status, 0, result.stderr);
618
+ assert.deepEqual(memoryOf(result), ["disabled", prefix.guard]);
619
+ assert.match(result.stderr, /incomplete memory migration/);
620
+ assert.equal(existsSync(join(prefix.home, ".engram")), false);
621
+ assert.equal(readFileSync(marker, "utf8"), '{"interrupted":true}\n');
622
+ assert.equal(existsSync(provenancePath(prefix)), false);
623
+ });
624
+
625
+ test("public help explains compatibility evidence without promising update recovery", { skip: posixHost }, (t) => {
626
+ const prefix = bundledPrefix(t);
627
+ const result = prefix.run(["--help"]);
628
+ assert.equal(result.status, 0, result.stderr);
629
+ assert.equal(result.stderr, "");
630
+ assert.match(result.stdout, /Matching versions or updating alone do not prove compatibility/);
631
+ assert.match(result.stdout, /\/compatibility/);
632
+ assert.doesNotMatch(result.stdout, /update Engram, restart/);
633
+ });
634
+
464
635
  test("a foreign pre-existing store stays disabled and never receives provenance", { skip: posixHost }, (t) => {
465
636
  const prefix = bundledPrefix(t);
466
637
  const data = join(prefix.home, ".engram");
@@ -487,7 +658,8 @@ test("a listening server is reused for Shell's store only with the recorded /hea
487
658
  assert.equal(result.status, 0, `${name}: ${result.stderr}`);
488
659
  const seen = lastLaunch(result);
489
660
  assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL],
490
- [policy, policy === "allow" ? prefix.engram : prefix.guard, undefined], name);
661
+ [policy, prefix.guard, policy === "allow" ? "http://127.0.0.1:7437" : undefined], name);
662
+ assert.equal(seen.env.ENGRAM_SERVER_MODE, undefined, name);
491
663
  }
492
664
  });
493
665
 
@@ -510,6 +682,23 @@ test("a verified /compatibility server is reused through an explicit URL and the
510
682
  assert.equal(lastLaunch(unready.run([], { FIXTURE_ENGRAM_PORT: notReady })).env.ENGRAM_STARTUP_POLICY, "disabled");
511
683
  });
512
684
 
685
+ 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) => {
686
+ const prefix = bundledPrefix(t);
687
+ const data = join(prefix.home, ".engram");
688
+ mkdirSync(data);
689
+ writeHeader(join(data, "engram.db"), 1);
690
+ const before = readFileSync(join(data, "engram.db"));
691
+ // Engram 3.3.2 has no /compatibility route (the fixture answers 404).
692
+ const port = await engramServer(t, { "/health": [200, { status: "ok", service: "engram", version: "3.3.2", instance_id: OTHER_STORE_ID }] });
693
+ const result = prefix.run([], { FIXTURE_ENGRAM_PORT: port });
694
+ assert.equal(result.status, 0, result.stderr);
695
+ const seen = lastLaunch(result);
696
+ assert.deepEqual([seen.env.ENGRAM_STARTUP_POLICY, seen.env.ENGRAM_BIN, seen.env.ENGRAM_URL], ["disabled", prefix.guard, undefined]);
697
+ assert.match(result.stderr, /memory disabled.*reports version 3\.3\.2.*update that server to 3\.4\.0 or later/s);
698
+ assert.deepEqual(readFileSync(join(data, "engram.db")), before);
699
+ assert.equal(existsSync(provenancePath(prefix)), false);
700
+ });
701
+
513
702
  test("a fresh launch whose launcher is killed during Pi already has finalized provenance, and the next launch allows memory", { skip: posixHost }, (t) => {
514
703
  const prefix = bundledPrefix(t);
515
704
  const first = prefix.run([], { FIXTURE_CREATE_STORE: STORE_ID, FIXTURE_KILL_LAUNCHER: "1" });
@@ -634,7 +823,8 @@ test("setup with a foreign existing store runs the installer like the baseline,
634
823
  assert.equal(setup.status, 0, setup.stderr);
635
824
  assert.equal(installer.runs(), 1);
636
825
  assert.equal(existsSync(prefix.engram), true);
637
- assert.match(setup.stderr, /memory disabled.*Back up.*update.*restart.*recheck/s);
826
+ assert.match(setup.stderr, /memory disabled.*could not verify.*compatibility.*Back up/s);
827
+ assert.doesNotMatch(setup.stderr, /update Engram and restart/);
638
828
  assert.doesNotMatch(setup.stderr, /deferred/);
639
829
  const guarded = prefix.run([]);
640
830
  assert.equal(guarded.status, 0, guarded.stderr);
@@ -692,8 +882,9 @@ test("capability readback also requires Pi to declare the selected pinned compan
692
882
  const result = prefix.run([], { FIXTURE_INSTALL_CAPABILITY: "metadata-only" });
693
883
  assert.equal(result.status, 0, result.stderr);
694
884
  assert.match(result.stderr, /startupPolicy/);
695
- // Only the identity Shell provisions before Pi: no database is opened.
696
- assert.deepEqual(readdirSync(join(prefix.home, ".engram")), [".instance-id"]);
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);
697
888
  });
698
889
 
699
890
  test("pinning preserves resource filters and unrelated settings and does not reinstall next launch", { skip: posixHost }, (t) => {
@@ -768,7 +959,7 @@ test("the bundled launcher runs only its version's Pi, with the user's environme
768
959
  const prefix = bundledPrefix(t);
769
960
  const version = prefix.run(["--version"]);
770
961
  assert.equal(version.status, 0, version.stderr);
771
- assert.match(version.stdout, new RegExp(`^gentle-shell ${SHELL.replaceAll(".", "\\.")}\npi 1\\.1\\.0\n`));
962
+ assert.match(version.stdout, new RegExp(`^gentle-shell ${SHELL.replaceAll(".", "\\.")}\npi 1\\.2\\.0\n`));
772
963
  const launched = prefix.run(["list"]);
773
964
  assert.equal(launched.status, 0, launched.stderr);
774
965
  const seen = JSON.parse(launched.stdout.trim().split("\n").at(-1) ?? "{}");
@@ -894,19 +1085,19 @@ test("the version check before activation runs that version in bundled mode, nev
894
1085
  encoding: "utf8", input: "" });
895
1086
  const good = verify({ [BUNDLED_VERIFY_ENV]: FIXTURE_ID });
896
1087
  assert.equal(good.status, 0, good.stderr);
897
- assert.match(good.stdout, /\npi 1\.1\.0\n/);
1088
+ assert.match(good.stdout, /\npi 1\.2\.0\n/);
898
1089
  // The version's own Pi is broken, and the user's Pi prints the expected version.
899
- writeFileSync(join(prefix.userBin, "pi"), "#!/bin/sh\necho 1.1.0\n", { mode: 0o755 });
1090
+ writeFileSync(join(prefix.userBin, "pi"), "#!/bin/sh\necho 1.2.0\n", { mode: 0o755 });
900
1091
  rmSync(join(prefix.piDir, "dist", "bundle", "cli.js"));
901
1092
  const broken = verify({ [BUNDLED_VERIFY_ENV]: FIXTURE_ID });
902
1093
  assert.equal(broken.status, 1);
903
- assert.match(broken.stderr, /bundled Pi 1\.1\.0/);
1094
+ assert.match(broken.stderr, /bundled Pi 1\.2\.0/);
904
1095
  // Without the verification input, the same entry is an ordinary launcher that would pass.
905
- assert.match(verify({}).stdout, /\npi 1\.1\.0\n/);
1096
+ assert.match(verify({}).stdout, /\npi 1\.2\.0\n/);
906
1097
  // Only --version honors it.
907
1098
  const list = spawnSync(process.execPath, [prefix.entry, "list"], { env: { HOME: prefix.home, PATH: `${prefix.userBin}:/usr/bin:/bin`, [BUNDLED_VERIFY_ENV]: FIXTURE_ID,
908
1099
  GENTLE_SHELL_NO_AUTO_SETUP: "1" }, encoding: "utf8", input: "" });
909
- assert.equal(list.stdout, "1.1.0\n");
1100
+ assert.equal(list.stdout, "1.2.0\n");
910
1101
  });
911
1102
 
912
1103
  test("the bundled launcher refuses a Pi that is not the pinned one", { skip: posixHost }, (t) => {
@@ -919,7 +1110,7 @@ test("the bundled launcher refuses a Pi that is not the pinned one", { skip: pos
919
1110
  test("gentle-shell update in a bundled install explains the pinned Pi instead of updating it", { skip: posixHost }, (t) => {
920
1111
  const result = bundledPrefix(t).run(["update"]);
921
1112
  assert.equal(result.status, 0, result.stderr);
922
- assert.match(result.stdout, /Pi ships pinned with Gentle Shell \(Pi 1\.1\.0\)/);
1113
+ assert.match(result.stdout, /Pi ships pinned with Gentle Shell \(Pi 1\.2\.0\)/);
923
1114
  assert.match(result.stdout, /Run 'gentle-shell upgrade'/);
924
1115
  assert.doesNotMatch(result.stdout, /"args"/);
925
1116
  });
@@ -928,23 +1119,29 @@ test("gentle-shell update pi --extensions in a bundled install redirects Pi's pa
928
1119
  const prefix = bundledPrefix(t);
929
1120
  const result = prefix.run(["update", "pi", "--extensions"]);
930
1121
  assert.equal(result.status, 0, result.stderr);
931
- assert.match(result.stdout, /Pi ships pinned with Gentle Shell \(Pi 1\.1\.0\)/);
1122
+ assert.match(result.stdout, /Pi ships pinned with Gentle Shell \(Pi 1\.2\.0\)/);
932
1123
  const seen = JSON.parse(result.stdout.trim().split("\n").at(-1) ?? "{}");
933
1124
  assert.deepEqual(seen.args, ["update", "--extensions"]);
934
1125
  assert.ok(JSON.parse(readFileSync(join(prefix.home, ".gentle-shell", "agent", "settings.json"), "utf8")).npmCommand);
935
1126
  });
936
1127
 
937
- test("a kept version that current does not name keeps the ordinary launcher, Pi override and Pi update included", { skip: posixHost }, (t) => {
1128
+ test("a kept version that current does not name keeps the ordinary Pi override and refuses an unowned update", { skip: posixHost }, (t) => {
938
1129
  const prefix = bundledPrefix(t);
939
1130
  const other = join(prefix.root, "versions", "0.0.1-1.0.0");
940
1131
  mkdirSync(other);
941
1132
  writeFileSync(join(other, ".gentle-shell-version"), "0.0.1-1.0.0\n");
942
1133
  rmSync(join(prefix.root, "current"));
943
1134
  symlinkSync("versions/0.0.1-1.0.0", join(prefix.root, "current"));
944
- const update = spawnSync(process.execPath, [prefix.entry, "update"], { env: { HOME: dirname(prefix.root), PATH: "/usr/bin:/bin",
945
- GENTLE_SHELL_PI: join(dirname(dirname(prefix.root)), "user-bin", "pi"), GENTLE_SHELL_NO_AUTO_SETUP: "1" }, encoding: "utf8", input: "" });
946
- assert.equal(update.status, 0, update.stderr);
947
- assert.equal(update.stdout, "9.9.9\n");
1135
+ const options = { env: { HOME: dirname(prefix.root), PATH: "/usr/bin:/bin",
1136
+ GENTLE_SHELL_PI: join(dirname(dirname(prefix.root)), "user-bin", "pi"), GENTLE_SHELL_NO_AUTO_SETUP: "1" }, encoding: "utf8" as const, input: "" };
1137
+ const version = spawnSync(process.execPath, [prefix.entry, "--version"], options);
1138
+ assert.equal(version.status, 0, version.stderr);
1139
+ assert.match(version.stdout, /pi 9\.9\.9/);
1140
+ const update = spawnSync(process.execPath, [prefix.entry, "update"], options);
1141
+ assert.equal(update.status, 1);
1142
+ assert.equal(update.stdout, "");
1143
+ // The fake override is a plain script: neither a Pi entry nor a package-manager shim.
1144
+ assert.match(update.stderr, /ownership selected executable is not a Pi entry or shim.*No installation changed\./);
948
1145
  });
949
1146
 
950
1147
  test("gentle-shell upgrade in a bundled install refuses the main channel and rolls back to the kept version", { skip: posixHost }, (t) => {