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
@@ -0,0 +1,125 @@
1
+ // Explicit bundled-store recovery. The provider owns catalog validation and
2
+ // migration; Shell owns promotion and its core-bound provenance. Never called
3
+ // from ordinary startup or upgrade.
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { spawnSync } from "node:child_process";
9
+ import { engramProbeAllowed, engramProvenancePath, parseEngramProvenance } from "../runtime/gentle-shell-bundled.mjs";
10
+
11
+ export const MIGRATION_USAGE = "Usage: gentle-shell memory migrate --confirm-quiescent\nStop every Engram client before running this command.\n";
12
+ export const MIGRATION_PENDING_NOTICE = "Engram memory disabled: an incomplete memory migration needs recovery. Keep every Engram client stopped, retain the original backup, and follow docs/bundled-install.md#recovering-an-interrupted-migration. Do not remove the migration marker or create a new store before recovery.";
13
+ export const migrationMarkerPath = (prefix) => join(prefix, "config", "engram-migration.json");
14
+ export function migrationPending(prefix) {
15
+ try { lstatSync(migrationMarkerPath(prefix)); return true; }
16
+ catch (error) { return error.code !== "ENOENT"; }
17
+ }
18
+ function regularText(path, limit) {
19
+ const info = lstatSync(path);
20
+ if (!info.isFile() || info.size > limit) throw new Error("unsafe migration metadata");
21
+ return readFileSync(path, "utf8");
22
+ }
23
+ const digest = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
24
+ // Durably record a file or a directory entry change. Windows cannot open a
25
+ // directory for fsync; there NTFS metadata journaling covers the renames.
26
+ function syncPath(path, directory = false) {
27
+ if (directory && process.platform === "win32") return;
28
+ const fd = openSync(path, "r");
29
+ try { fsyncSync(fd); } finally { closeSync(fd); }
30
+ }
31
+ function coreHelp(core, env) {
32
+ // Older cores route unknown commands through legacy startup. Inspect their
33
+ // config-free help in an isolated home, never try an unknown data command.
34
+ const scratch = mkdtempSync(join(tmpdir(), "gentle-engram-capability-"));
35
+ try {
36
+ const probe = spawnSync(core, ["--help"], {
37
+ cwd: scratch, env: { ...env, HOME: scratch, USERPROFILE: scratch, ENGRAM_DATA_DIR: join(scratch, "store") },
38
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000, maxBuffer: 65536, windowsHide: true,
39
+ });
40
+ return !probe.error && !probe.signal && probe.status === 0 && probe.stderr === "" ? probe.stdout.split("\n").map((line) => line.trim()) : [];
41
+ } finally { rmSync(scratch, { recursive: true, force: true }); }
42
+ }
43
+ function supportsMigration(core, env) {
44
+ return coreHelp(core, env).includes("migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json");
45
+ }
46
+ export function supportsBundledSessionOwned(core, env) {
47
+ if (!core) return false;
48
+ const clean = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("ENGRAM_") && key !== "GH_TOKEN" && key !== "GITHUB_TOKEN"));
49
+ try { return coreHelp(core, { ...clean, ENGRAM_NO_UPDATE_CHECK: "1" }).some((line) => /^serve --session-owned(?:\s|$)/.test(line)); }
50
+ catch { return false; }
51
+ }
52
+
53
+ export function migrateBundledEngram({ prefix, core, home, env }) {
54
+ let markerOwned = false;
55
+ let promotionStarted = false;
56
+ let output;
57
+ let backup;
58
+ try {
59
+ if (!core) throw new Error("the verified bundled core is unavailable; run gentle-shell setup first");
60
+ if (!engramProbeAllowed(env)) throw new Error("explicit Engram selections or a disabled startup policy must be resolved before bundled migration");
61
+ if (migrationPending(prefix)) throw new Error(MIGRATION_PENDING_NOTICE);
62
+ const source = join(realpathSync(home), ".engram");
63
+ if (!lstatSync(source).isDirectory() || realpathSync(source) !== source) throw new Error("the source must be a regular default store directory");
64
+ const provenancePath = engramProvenancePath(prefix);
65
+ const original = regularText(provenancePath, 4096);
66
+ const record = parseEngramProvenance(original);
67
+ const identity = regularText(join(source, ".instance-id"), 128).trim();
68
+ if (!record || record.dataDir !== source || record.instanceId !== identity) throw new Error("the store does not match Shell's recorded identity; it cannot be adopted");
69
+ const coreHash = digest(core);
70
+ const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("ENGRAM_") && key !== "GH_TOKEN" && key !== "GITHUB_TOKEN"));
71
+ childEnv.ENGRAM_NO_UPDATE_CHECK = "1";
72
+ if (!supportsMigration(core, childEnv)) throw new Error("the bundled core does not support migrate-copy; wait for a supporting published bundle. The source and provenance are unchanged");
73
+ const suffix = randomUUID();
74
+ output = `${source}.migrating-${suffix}`;
75
+ backup = `${source}.before-${suffix}`;
76
+ const marker = migrationMarkerPath(prefix);
77
+ mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
78
+ // A recorded refusal precedes copying/promotion. A killed launcher must not
79
+ // mistake the rename gap for a fresh install and provision an empty store.
80
+ writeFileSync(marker, `${JSON.stringify({ source, output, backup, originalProvenance: record })}\n`, { flag: "wx", mode: 0o600 });
81
+ markerOwned = true;
82
+ syncPath(marker);
83
+ syncPath(dirname(marker), true);
84
+ const result = spawnSync(core, ["migrate-copy", "--data-dir", source, "--output-dir", output, "--confirm-quiescent", "--json"], {
85
+ env: childEnv, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000, maxBuffer: 16384, windowsHide: true,
86
+ });
87
+ if (result.error || result.signal) throw new Error("the provider failed or timed out; no source promotion was attempted");
88
+ if (result.status !== 0) throw new Error(`the provider rejected migration (exit ${result.status}): ${result.stderr.trim().slice(0, 2048) || "no diagnostic"}`);
89
+ if (result.stderr !== "") throw new Error("unexpected provider diagnostic with success evidence");
90
+ let proof;
91
+ try { proof = JSON.parse(result.stdout); } catch { throw new Error("invalid provider migration evidence"); }
92
+ if (!proof || proof.migration_completed !== true || proof.source_dir !== source || proof.output_dir !== output || proof.instance_id !== identity) {
93
+ throw new Error("provider migration evidence does not match the requested store and copy");
94
+ }
95
+ if (!lstatSync(output).isDirectory() || realpathSync(output) !== output ||
96
+ regularText(join(output, ".instance-id"), 128).trim() !== identity ||
97
+ regularText(join(source, ".instance-id"), 128).trim() !== identity ||
98
+ regularText(provenancePath, 4096) !== original || digest(core) !== coreHash) {
99
+ throw new Error("store, provenance or bundled core changed during migration");
100
+ }
101
+ // The original is retained, not deleted. From this point any failure keeps
102
+ // the marker: recovery must reconcile both paths before another launch.
103
+ promotionStarted = true;
104
+ renameSync(source, backup);
105
+ renameSync(output, source);
106
+ syncPath(dirname(source), true);
107
+ const temporary = `${provenancePath}.${suffix}.tmp`;
108
+ writeFileSync(temporary, `${JSON.stringify({ schema: 1, dataDir: source, instanceId: identity, coreSha256: coreHash })}\n`, { flag: "wx", mode: 0o600 });
109
+ syncPath(temporary);
110
+ renameSync(temporary, provenancePath);
111
+ syncPath(dirname(provenancePath), true);
112
+ // Only a durable promotion and provenance may release the fail-closed marker.
113
+ rmSync(marker);
114
+ // The migration is complete; a failed final sync must not report failure.
115
+ try { syncPath(dirname(marker), true); } catch { /* Best effort. */ }
116
+ return { code: 0, stdout: `${JSON.stringify({ status: "migrated", instance_id: identity, backup_dir: backup })}\n`, stderr: "" };
117
+ } catch (error) {
118
+ if (markerOwned && !promotionStarted) {
119
+ try { rmSync(migrationMarkerPath(prefix)); } catch { /* Keep the fail-closed marker. */ }
120
+ }
121
+ const retained = output ? ` Any partial copy at ${output} is retained; do not use it.` : "";
122
+ const recovery = promotionStarted ? ` Promotion may be incomplete; original backup: ${backup}. ${MIGRATION_PENDING_NOTICE}` : "";
123
+ return { code: 1, stdout: "", stderr: `gentle-shell: memory migration failed: ${error.message}.${retained}${recovery}\n` };
124
+ }
125
+ }
@@ -37,7 +37,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
37
37
  // version check below) derives from this constant instead of repeating the
38
38
  // literal, so a pin bump cannot leave a stale copy behind. See
39
39
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
40
- export const INSTALLER_VERSION = "4.1.1";
40
+ export const INSTALLER_VERSION = "4.2.0";
41
41
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
42
42
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
43
43
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -46,11 +46,11 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
48
48
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
49
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.1.1`
49
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.2.0`
50
50
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
51
- // tag resolves to commit 898d3976e8049a7238eaa7de60bff325c30d2b3d, the published v4.1.1 release head.
51
+ // tag resolves to commit 4acaf4241beae1e0c977c92dfe406a270f8d40de, the published v4.2.0 release head.
52
52
  // v4.0.0 moved the Go module path to the /v4 major-version suffix.
53
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=";
53
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=";
54
54
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
55
55
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
56
56
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -69,7 +69,7 @@ export class GentleAiInstallerError extends Error {
69
69
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
70
70
  // sentinel digest can never match a real SHA-256, so installation fails closed,
71
71
  // and verify-package-files.mjs refuses to pack/publish while any digest below
72
- // still holds it. The v4.1.1 digests are pinned from the published release:
72
+ // still holds it. The v4.2.0 digests are pinned from the published release:
73
73
  // archive sha256 values verified against the minisign-signed checksums.txt and
74
74
  // freshly computed hashes; binary sha256 values computed from the extracted
75
75
  // executables.
@@ -111,15 +111,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
111
111
  }
112
112
 
113
113
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
114
- // distributing unsigned Windows builds in c4b764d0, so v4.1.1 publishes signed
114
+ // distributing unsigned Windows builds in c4b764d0, so v4.2.0 publishes signed
115
115
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
116
116
  // exact-tag Go SumDB source-build path below; restore archive rows only when
117
117
  // upstream ships signed Windows assets.
118
118
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
119
- "darwin/amd64": asset("gentle-ai_4.1.1_darwin_amd64.tar.gz", "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338", "gentle-ai"),
120
- "darwin/arm64": asset("gentle-ai_4.1.1_darwin_arm64.tar.gz", "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a", "gentle-ai"),
121
- "linux/amd64": asset("gentle-ai_4.1.1_linux_amd64.tar.gz", "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe", "gentle-ai"),
122
- "linux/arm64": asset("gentle-ai_4.1.1_linux_arm64.tar.gz", "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55", "gentle-ai"),
119
+ "darwin/amd64": asset("gentle-ai_4.2.0_darwin_amd64.tar.gz", "8f6a91e0f99cac3aea138705d7d222ea342f53a47602d48cb01488952a1c12f8", "784855e99f1bc45579f01420c923b7dc1dc6d63492cdc0174a9a32e611849e57", "gentle-ai"),
120
+ "darwin/arm64": asset("gentle-ai_4.2.0_darwin_arm64.tar.gz", "d5e9a6521de5f08a393635f083f64dc7afc1b8e97884f7aab5ae5eb1f17c7f37", "8ea490287ef796848b0d305cf2b26265e360d3f400efc016972844498fb87097", "gentle-ai"),
121
+ "linux/amd64": asset("gentle-ai_4.2.0_linux_amd64.tar.gz", "552c1642fd10f5d96cfe9bb1cc3b66b30eb47d3074cc7ca0559b598eb086defc", "9212c7b16ea9abfe93e3a16b29d8390e7762b5c07730cf05df1f3e7892ff96a4", "gentle-ai"),
122
+ "linux/arm64": asset("gentle-ai_4.2.0_linux_arm64.tar.gz", "2bebe91b8808ce48d86c4d3700d098e37cb6b0658f196c666d9deeeb7c7c1a7a", "e1b88bd3dcdc8a15ddbcaf8731fd44561f05b06fbcb59d9385bbe82c4b96b5c4", "gentle-ai"),
123
123
  });
124
124
 
125
125
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -10,7 +10,7 @@
10
10
  // Then open the printed one-time session URL. Not part of the installer entry.
11
11
 
12
12
  import { fileURLToPath, pathToFileURL } from "node:url";
13
- import { planPreflight, requirements } from "./installer-preflight.mjs";
13
+ import { PI_INSTALL_VERSION, planPreflight, requirements } from "./installer-preflight.mjs";
14
14
  import { createInstallerServer } from "./installer-server.mjs";
15
15
 
16
16
  const BIN = "/home/you/.local/share/pnpm/bin";
@@ -36,7 +36,7 @@ const freshInventory = Object.freeze({
36
36
  // An earlier run installed the pinned stack, but its setup and `pnpm setup` did not finish.
37
37
  const recoveryInventory = Object.freeze({
38
38
  ...baseInventory,
39
- pi: { available: true, version: "1.0.0", usable: true },
39
+ pi: { available: true, version: PI_INSTALL_VERSION, usable: true },
40
40
  shell: { available: true, version: requirements.shell, usable: true, global: true },
41
41
  gentleAi: { available: true, version: requirements.gentleAi, usable: true, compatible: true },
42
42
  globalBin: { ...baseInventory.globalBin, onPath: false },
@@ -44,15 +44,15 @@ export const goPinVersion = go.version;
44
44
  // checksums.txt and GitHub's asset digests; every archive was downloaded and
45
45
  // re-hashed before pinning. GitHub serves the assets from its asset hosts.
46
46
  const engram = Object.freeze({
47
- version: "3.3.2",
47
+ version: "3.4.0",
48
48
  redirectHosts: Object.freeze(["objects.githubusercontent.com", "release-assets.githubusercontent.com"]),
49
49
  archives: Object.freeze({
50
- "darwin-arm64": ["darwin_arm64.tar.gz", "3d6b4ac0055883ec23d5b0a04a3cda041653cdf4916fd7e98944692e62fd0441", 7465858],
51
- "darwin-x64": ["darwin_amd64.tar.gz", "3da8d7befe8500ff09aa44c48f493eda96513fd3f63c7e4bfb95e3ae85dfae52", 7952089],
52
- "linux-x64": ["linux_amd64.tar.gz", "8694d3817b63c5fc5607b5919e88aa929bffa0bafda330513364e10c8ab61445", 7817468],
53
- "linux-arm64": ["linux_arm64.tar.gz", "b4546b6e00f68e56da52064f1865c6cbae1d5b0633aeebc36e1a2f20b17683e7", 7168173],
54
- "win32-x64": ["windows_amd64.zip", "e00afe91d1baade484d217606d78c2fd91e26a4f9823b603d050c1e4be78233c", 8013889],
55
- "win32-arm64": ["windows_arm64.zip", "251d467017f88e4eb7fc186c635a9bf64db5aa39cfc251d35dd4035f38941454", 7224229],
50
+ "darwin-arm64": ["darwin_arm64.tar.gz", "d0aa59c9a83d17ae3fc7f5dd31fdab6a26fd8c00fc5e692195753cd0719294f7", 7470202],
51
+ "darwin-x64": ["darwin_amd64.tar.gz", "3c5bdc73d226af5629cf4f4a781e223142e37c1ebb443c4b596f2e284352e381", 7961674],
52
+ "linux-x64": ["linux_amd64.tar.gz", "715fda625556e46d65ddb4526bb7f88a3fee5339ca3c44ce44cc652f17fa57f1", 7821191],
53
+ "linux-arm64": ["linux_arm64.tar.gz", "3309e9c0bbb539d183fd5aa9f9a13cd7aa0e81ec2512b5bedefb8b2bdd35b3e5", 7176726],
54
+ "win32-x64": ["windows_amd64.zip", "3065736ec7ff8137eb3fc607275f542ea13bb743a5157318c8a8770f11ce3289", 8021225],
55
+ "win32-arm64": ["windows_arm64.zip", "7463872a85a5e5434cf958bdffc71dda1926bced30d3b6b38c80fec9bb98fbba", 7231641],
56
56
  }),
57
57
  });
58
58
  /** The Engram version the bundled install publishes under runtime/. */
@@ -25,7 +25,7 @@ export const requirements = Object.freeze({
25
25
  /** Pi version the installer installs next to gentle-pi, and the version an older
26
26
  * Pi is updated to (never below requirements.pi; the runner asserts it).
27
27
  */
28
- export const PI_INSTALL_VERSION = "1.0.0";
28
+ export const PI_INSTALL_VERSION = "1.2.0";
29
29
 
30
30
  /** Runtime persisted under PNPM_HOME. Bootstrap-only Node: `pnpm runtime set
31
31
  * node <node> -g`, then `pnpm add -g npm@<npm> pnpm@<pnpm>`, or only npm when
@@ -170,6 +170,9 @@ export const guidance = Object.freeze({
170
170
  setupRateLimit: "`gentle-shell setup` could not finish because GitHub's limit for anonymous API requests was reached on this network. Wait up to an hour, then run the installer again.",
171
171
  // A failed acquire-go whose detail names a Go folder the installer did not publish.
172
172
  goDestinationConflict: `A Go folder from an earlier, interrupted installer run is in the way, and the installer never replaces a folder it cannot prove it published. Remove that folder (shown below), then run the installer again. Nothing was installed.`,
173
+ // A failed install whose detail shows Windows refused to start the gentle-ai.exe gentle-pi's postinstall built.
174
+ codeIntegrityBlocked: "Windows refused to run the Gentle AI program (gentle-ai.exe) that the installation builds on this computer, most likely because Smart App Control or another code integrity policy blocks unsigned programs. " +
175
+ "Windows offers no exception for a single program. Keep using Gentle Shell in WSL, or turn Smart App Control off in Windows Security and run the installer again; on Windows 11 it cannot be turned back on without reinstalling Windows.",
173
176
  // A failed persist-path whose detail shows pnpm could not tell the shell (POSIX SHELL missing or unsupported).
174
177
  persistPathShell: "`pnpm setup` could not tell which shell profile to edit because the SHELL environment variable is missing or names an unsupported shell. Open a regular terminal and run the installer again, or add `$PNPM_HOME/bin` to your PATH yourself.",
175
178
  fallback: "The installation stopped for an unexpected reason. Nothing else will run; check the terminal and run the installer again.",
@@ -186,16 +189,20 @@ function setupDetail(value) {
186
189
  const rateLimited = (detail) => /rate limit/i.test(detail) || (/GitHub API/i.test(detail) && /\b403\b/.test(detail));
187
190
  const unknownShell = (detail) => /ERR_PNPM_(?:UNKNOWN|UNSUPPORTED)_SHELL/.test(detail);
188
191
  const goConflict = (detail) => detail.startsWith("Conflicting Go destination: ");
192
+ // libuv reports a CreateProcess refusal it has no code for, such as a Windows code integrity block
193
+ // (Smart App Control, event 3077), as spawn UNKNOWN.
194
+ const codeIntegrityBlocked = (detail) => /\bspawn UNKNOWN\b/.test(detail);
189
195
  // The main channel's typed error when GitHub refused the latest main commit with HTTP 403.
190
196
  const githubLimited = (detail) => rateLimited(detail) || (/\bmain-commit-unavailable\b/.test(detail) && /\bHTTP 403\b/.test(detail));
191
197
  // Fixed steps whose sanitized last error line may reach the browser.
192
198
  // The bundled steps send a detail only for a migration (its error message); a new install's send none.
193
199
  const bundledDetail = Object.fromEntries(["claim-prefix", "copy-npm-settings", "install-runtime", "activate-version", "write-launcher", "verify-launcher", "bundled-path"]
194
200
  .map((step) => [step, () => false]));
195
- const detailSteps = Object.freeze({ ...bundledDetail, "shell-setup": rateLimited, "install-version": () => false, "persist-path": unknownShell, "acquire-go": goConflict,
196
- "install-global": githubLimited, "install-shell-main": githubLimited, "build-gentle-ai-main": githubLimited, "update-shell": githubLimited });
201
+ const detailSteps = Object.freeze({ ...bundledDetail, "shell-setup": rateLimited, "install-version": codeIntegrityBlocked, "persist-path": unknownShell, "acquire-go": goConflict,
202
+ "install-global": (detail) => githubLimited(detail) || codeIntegrityBlocked(detail), "install-shell-main": githubLimited, "build-gentle-ai-main": githubLimited, "update-shell": githubLimited });
203
+ // A step whose detail can match more than one pattern names its guidance by detail.
197
204
  const detailGuidance = Object.freeze({ "shell-setup": "setupRateLimit", "persist-path": "persistPathShell", "acquire-go": "goDestinationConflict",
198
- "install-global": "githubRateLimit", "install-shell-main": "githubRateLimit", "build-gentle-ai-main": "githubRateLimit", "update-shell": "githubRateLimit" });
205
+ "install-version": "codeIntegrityBlocked", "install-global": (detail) => (githubLimited(detail) ? "githubRateLimit" : "codeIntegrityBlocked"), "install-shell-main": "githubRateLimit", "build-gentle-ai-main": "githubRateLimit", "update-shell": "githubRateLimit" });
199
206
 
200
207
  const ID = /^[a-z][a-z0-9-]{0,63}$/;
201
208
  function identifier(value, fallback = "unknown") {
@@ -543,7 +550,10 @@ function outcomeView(result) {
543
550
  const detail = step ? setupDetail(result.detail) : null;
544
551
  if (detail) {
545
552
  view.detail = detail;
546
- if (detailSteps[step](detail)) view.guidance = guidance[detailGuidance[step]];
553
+ if (detailSteps[step](detail)) {
554
+ const key = detailGuidance[step];
555
+ view.guidance = guidance[typeof key === "function" ? key(detail) : key];
556
+ }
547
557
  }
548
558
  } else {
549
559
  view.guidance = guidance.outcomes[result.outcome];
@@ -35,9 +35,10 @@ export class MainChannelError extends Error {
35
35
  }
36
36
  }
37
37
 
38
- /** Same config home as the dev-binary override: GENTLE_PI_CONFIG_HOME or ~/.pi/gentle-ai. */
38
+ /** Same config home as the dev-binary override: a nonempty GENTLE_PI_CONFIG_HOME or ~/.pi/gentle-ai.
39
+ * Install-level state, so never the standalone launcher's per-home marker (#2201). */
39
40
  export function configHome({ env, home }) {
40
- return env.GENTLE_PI_CONFIG_HOME ?? join(home, ".pi", "gentle-ai");
41
+ return env.GENTLE_PI_CONFIG_HOME || join(home, ".pi", "gentle-ai");
41
42
  }
42
43
  export function channelStatePath(ctx) {
43
44
  return join(configHome(ctx), "channel.json");
@@ -0,0 +1,223 @@
1
+ // Narrow update ownership proof. Deliberately independent of Shell upgrade/uninstall.
2
+ import { posix, win32 } from 'node:path';
3
+ import { bundledUpdatePlan } from '../runtime/gentle-shell-bundled.mjs';
4
+
5
+ const PI = '@earendil-works/pi-coding-agent';
6
+ const SHELL = 'gentle-pi';
7
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ const succeeded = result => result?.code === 0 && !result.timedOut && result.signal == null;
9
+ const refusal = detail => new Error(`gentle-shell update: ownership ${detail}; update Pi with the package manager or source checkout that supplies the selected runtime. No installation changed.`);
10
+ const LINKED = /^(?:file|link|workspace):/i;
11
+ const CLI_TAIL = /[\\/]dist[\\/]bundle[\\/]cli\.js$/i;
12
+ // A quoted cli.js target inside a package-manager shim: pnpm's POSIX shim
13
+ // (`"$basedir/../global/.../cli.js"`), its `.CMD`/npm's `.cmd` shim
14
+ // (`"%~dp0\\...\\cli.js"`, `"%dp0%\\...\\cli.js"`) or an absolute path.
15
+ const SHIM_TARGET = /"((?:\$basedir[\\/]|%~?dp0%?[\\/])?[^"]*?[\\/]dist[\\/]bundle[\\/]cli\.js)"/g;
16
+
17
+ export function managedUpdatePlan(args) {
18
+ // The legacy extension-only alias must never trigger self-update.
19
+ if (args.includes('--extensions-only')) return {self:false, piArgs:args};
20
+ return bundledUpdatePlan(args);
21
+ }
22
+
23
+ function pathsFor(platform) {
24
+ const path = platform === 'win32' ? win32 : posix;
25
+ const key = value => platform === 'win32' ? path.normalize(value).toLowerCase() : path.normalize(value);
26
+ return {path, key};
27
+ }
28
+
29
+ function canonicalWith(adapters, path) {
30
+ return async value => {
31
+ if (typeof value !== 'string' || !path.isAbsolute(value)) throw refusal('has no absolute package path');
32
+ const result = await adapters.fs.realpath(value);
33
+ if (typeof result !== 'string' || !path.isAbsolute(result)) throw refusal('has an invalid canonical path');
34
+ return result;
35
+ };
36
+ }
37
+
38
+ /** The cli.js a selected PATH or GENTLE_SHELL_PI executable runs: itself when
39
+ * it is the entry (npm's bin symlink resolves there), otherwise the single
40
+ * target its shim names, relative to the shim's directory. Anything else
41
+ * cannot be matched against a listed package and is refused.
42
+ */
43
+ async function selectedCliOf(runtime, {adapters, path, canonical}) {
44
+ if (runtime.kind === 'bundled') return canonical(runtime.args[0]);
45
+ const file = await canonical(runtime.args.length ? runtime.args[0] : runtime.command);
46
+ if (CLI_TAIL.test(file)) return file;
47
+ let text;
48
+ try { text = String(await adapters.fs.readFile(file, 'utf8')); } catch { throw refusal('cannot read the selected executable'); }
49
+ const targets = new Set();
50
+ for (const [, quoted] of text.matchAll(SHIM_TARGET)) {
51
+ const relative = /^(?:\$basedir|%~?dp0%?)[\\/]/.exec(quoted);
52
+ const target = relative ? path.join(path.dirname(file), quoted.slice(relative[0].length)) : quoted;
53
+ if (!path.isAbsolute(target)) throw refusal('selected shim names a relative target');
54
+ targets.add(path.normalize(target));
55
+ }
56
+ if (targets.size !== 1) throw refusal(targets.size === 0 ? 'selected executable is not a Pi entry or shim' : 'selected shim names several targets');
57
+ return canonical([...targets][0]);
58
+ }
59
+
60
+ async function manifestOf(adapters, path, root) {
61
+ return JSON.parse(await adapters.fs.readFile(path.join(root, 'package.json'), 'utf8'));
62
+ }
63
+
64
+ /** The Pi package a canonical Shell root runs as its adjacent peer, as
65
+ * { root, manifest, cli }, or null when none resolves. pnpm's linked store
66
+ * places the peer beside the Shell package (`<...>/node_modules/<PI>`), npm
67
+ * nests it (`<shell>/node_modules/<PI>`); the global group's own
68
+ * `node_modules` is the last place a peer link can live.
69
+ */
70
+ async function adjacentPeer({shellRoot, groupRoot, adapters, path, key, canonical}) {
71
+ const places = [path.join(shellRoot, 'node_modules', PI), path.join(path.dirname(shellRoot), PI)];
72
+ if (groupRoot) places.push(path.join(groupRoot, PI));
73
+ for (const place of places) {
74
+ let root;
75
+ try { root = await canonical(place); } catch { continue; }
76
+ const manifest = await manifestOf(adapters, path, root);
77
+ if (manifest.name !== PI || manifest.bin?.pi !== 'dist/bundle/cli.js') throw refusal('adjacent Pi identity differs');
78
+ const cli = await canonical(path.join(root, manifest.bin.pi));
79
+ // The canonical package root must supply its own CLI file, not a link elsewhere.
80
+ if (key(cli) !== key(path.join(root, manifest.bin.pi))) throw refusal('adjacent Pi package root does not supply its CLI');
81
+ return {root, manifest, cli};
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Package managers run from a neutral directory: inside a project, pnpm
87
+ // honors that project's `packageManager` pin and may download another pnpm
88
+ // before answering, and the global installation does not depend on the cwd.
89
+ async function globalListing(name, invocation, adapters, cwd) {
90
+ const argv = name === 'pnpm' ? ['list', '-g', '--depth', '0', '--json'] : ['ls', '-g', '--depth=0', '--json', '--long'];
91
+ const result = await adapters.run(invocation.command, [...invocation.prefix, ...argv], {deadlineMs:30_000, cwd});
92
+ if (!succeeded(result)) throw refusal('listing failed');
93
+ const json = JSON.parse(result.stdout);
94
+ const groups = name === 'pnpm' ? json : [json];
95
+ if (!Array.isArray(groups) || groups.length === 0) throw refusal('listing is malformed');
96
+ return groups;
97
+ }
98
+
99
+ async function invocationFor(name, adapters, platform) {
100
+ const invocation = adapters.invocation ? await adapters.invocation(name) : await adapters.which(name).then(command => command ? {command, prefix:[]} : null);
101
+ if (!invocation) return null;
102
+ if (platform === 'win32' && /\.(cmd|bat)$/i.test(invocation.command)) throw refusal(`cannot run ${name} without its shell shim`);
103
+ return invocation;
104
+ }
105
+
106
+ /** Every listed top-level Pi or Shell package of one manager, with the
107
+ * canonical global group it links from and that group's member names.
108
+ */
109
+ async function listedEntries(groups, {adapters, path, key, canonical}) {
110
+ const entries = [];
111
+ const members = new Map();
112
+ for (const group of groups) {
113
+ if (!object(group) || typeof group.path !== 'string' || !object(group.dependencies)) throw refusal('listing is malformed');
114
+ for (const [pkg, dependency] of Object.entries(group.dependencies)) {
115
+ if (!object(dependency) || !/^(?:@[^/\\]+\/)?[^/\\]+$/.test(pkg)) throw refusal('listing is malformed');
116
+ // pnpm's JSON may aggregate several global groups. The dependency's
117
+ // listed link location, not the aggregate group.path or canonical store
118
+ // package root, identifies which old group `add -g` would replace.
119
+ const listed = dependency.path;
120
+ if (typeof listed !== 'string' || !path.isAbsolute(listed)) throw refusal('listed package path is missing');
121
+ const modules = pkg.startsWith('@') ? path.dirname(path.dirname(listed)) : path.dirname(listed);
122
+ if (path.basename(modules) !== 'node_modules' || key(path.join(modules, pkg)) !== key(listed)) throw refusal('listed top-level group is unknown');
123
+ const groupRoot = await canonical(modules);
124
+ const set = members.get(key(groupRoot)) ?? new Set();
125
+ set.add(pkg);
126
+ members.set(key(groupRoot), set);
127
+ if (pkg === PI || pkg === SHELL) entries.push({pkg, dependency, groupRoot});
128
+ }
129
+ }
130
+ return {entries, members};
131
+ }
132
+
133
+ /** A single listed package must supply the exact selected executable.
134
+ * pnpm v11 lists separate global groups whose packages resolve into a linked
135
+ * store outside PNPM_HOME. Neither containment nor a package name proves
136
+ * ownership. An adjacent runtime is owned by the Shell group that resolved
137
+ * it: updating a separate global Pi never changes what that Shell runs.
138
+ */
139
+ export async function resolveUpdateOwner({runtime, packageRoot, platform = process.platform, adapters, cwd}) {
140
+ const {path, key} = pathsFor(platform);
141
+ const canonical = canonicalWith(adapters, path);
142
+ const selected = await selectedCliOf(runtime, {adapters, path, canonical});
143
+ const shellRoot = await canonical(packageRoot);
144
+ const adjacent = runtime.kind === 'bundled';
145
+ const candidates = [];
146
+ for (const name of ['pnpm', 'npm']) {
147
+ const invocation = await invocationFor(name, adapters, platform);
148
+ if (!invocation) continue;
149
+ const groups = await globalListing(name, invocation, adapters, cwd);
150
+ const {entries, members} = await listedEntries(groups, {adapters, path, key, canonical});
151
+ for (const {pkg, dependency, groupRoot} of entries) {
152
+ if ((pkg === SHELL) !== adjacent) continue;
153
+ if (dependency.link === true || [dependency.resolved, dependency.version].some(value => typeof value === 'string' && LINKED.test(value))) throw refusal('listed supplier is a source or linked package');
154
+ const root = await canonical(dependency.path);
155
+ const manifest = await manifestOf(adapters, path, root);
156
+ if (manifest.name !== pkg) throw refusal('listed package identity differs');
157
+ let cli, spec;
158
+ if (pkg === PI) {
159
+ if (manifest.bin?.pi !== 'dist/bundle/cli.js') throw refusal('listed Pi bin is unsupported');
160
+ cli = await canonical(path.join(root, manifest.bin.pi));
161
+ if (key(cli) !== key(path.join(root, manifest.bin.pi))) throw refusal('listed Pi package root does not supply its CLI');
162
+ spec = `${PI}@latest`;
163
+ } else {
164
+ // Only the Shell package this launcher runs from can own its peer.
165
+ if (key(root) !== key(shellRoot)) continue;
166
+ const peer = await adjacentPeer({shellRoot: root, groupRoot, adapters, path, key, canonical});
167
+ if (!peer) continue;
168
+ cli = peer.cli;
169
+ // Reinstalling the same Shell version re-resolves its peer; `update`
170
+ // never upgrades Shell itself (that is `gentle-shell upgrade`).
171
+ if (typeof manifest.version !== 'string') throw refusal('listed Shell version is missing');
172
+ spec = `${SHELL}@${manifest.version}`;
173
+ }
174
+ if (key(cli) !== key(selected)) continue;
175
+ if (name === 'pnpm' && members.get(key(groupRoot)).size !== 1) throw refusal('selected supplier shares its global install group');
176
+ candidates.push({name, ...invocation, package:pkg, spec});
177
+ }
178
+ }
179
+ if (candidates.length !== 1) throw refusal(candidates.length > 1 ? 'is ambiguous' : 'could not be verified from global listings');
180
+ return candidates[0];
181
+ }
182
+
183
+ /** The Pi version the owner's package now supplies, read back from a fresh
184
+ * listing after the install; null when it cannot be read.
185
+ */
186
+ async function suppliedPiVersion(owner, {platform, adapters, cwd}) {
187
+ const {path, key} = pathsFor(platform);
188
+ const canonical = canonicalWith(adapters, path);
189
+ try {
190
+ const groups = await globalListing(owner.name, owner, adapters, cwd);
191
+ const {entries} = await listedEntries(groups, {adapters, path, key, canonical});
192
+ for (const {pkg, dependency, groupRoot} of entries) {
193
+ if (pkg !== owner.package) continue;
194
+ const root = await canonical(dependency.path);
195
+ if (pkg === PI) return (await manifestOf(adapters, path, root)).version ?? null;
196
+ if ((await manifestOf(adapters, path, root)).name !== SHELL) continue;
197
+ const peer = await adjacentPeer({shellRoot: root, groupRoot, adapters, path, key, canonical});
198
+ if (peer) return peer.manifest.version ?? null;
199
+ }
200
+ } catch {
201
+ return null;
202
+ }
203
+ return null;
204
+ }
205
+
206
+ export async function updateSelectedPi(input) {
207
+ let owner;
208
+ try { owner = await resolveUpdateOwner(input); }
209
+ catch (error) { throw error.message?.includes('ownership') ? error : refusal('could not be resolved'); }
210
+ // Exactly one approved package per command; never create a comma group or
211
+ // reinstall arbitrary dependencies merely because they share a store.
212
+ const args = owner.name === 'pnpm' ? ['add', '-g', owner.spec, ...(owner.package === SHELL ? ['--allow-build=gentle-pi'] : [])] : ['install', '-g', owner.spec, ...(owner.package === PI ? ['--ignore-scripts'] : [])];
213
+ const result = await input.adapters.run(owner.command, [...owner.prefix, ...args], {deadlineMs:20 * 60_000, cwd: input.cwd});
214
+ if (!succeeded(result)) throw new Error(`gentle-shell update: ${owner.name} could not install ${owner.spec}${result?.stderrTail ? `\n${result.stderrTail}` : ''}`);
215
+ const after = await suppliedPiVersion(owner, input);
216
+ const before = input.currentVersion;
217
+ if (after === null) input.out?.(`${owner.name} installed ${owner.spec}. The Pi version it now supplies could not be read back; run 'gentle-shell --version' to check it.`);
218
+ else if (before !== undefined && after === before) {
219
+ input.out?.(`${owner.name} installed ${owner.spec}, and the selected Pi is still ${after}.`);
220
+ if (owner.name === 'pnpm') input.out?.('pnpm installs only releases older than its minimumReleaseAge setting (pnpm 11 defaults to 4320 minutes); a newer Pi release is picked up once it is old enough.');
221
+ } else input.out?.(`${owner.name} installed ${owner.spec}; the selected Pi is now ${after}${before !== undefined ? ` (was ${before})` : ''}.`);
222
+ return 0;
223
+ }
@@ -30,6 +30,10 @@ export async function runStage(stage) {
30
30
  return !normalizedKey.startsWith("GIT_") && !normalizedKey.startsWith("GENTLE_PI_AGENTS_") && normalizedKey !== "GENTLE_PI_CONFIG_HOME";
31
31
  })) : process.env;
32
32
  if (configHome) env.GENTLE_PI_CONFIG_HOME = configHome;
33
+ // Unit fixtures also get a private temp dir, removed with the stage, so a
34
+ // fixture that leaves a subagent unfinished cannot leave its TMPDIR behind.
35
+ const unitTmp = configHome ? mkdtempSync(join(tmpdir(), "gentle-pi-unit-tmp-")) : undefined;
36
+ if (unitTmp) env.TMPDIR = unitTmp;
33
37
  try {
34
38
  return await new Promise((resolve) => {
35
39
  const child = spawn(stage.command, { shell: true, stdio: "inherit", env });
@@ -41,6 +45,7 @@ export async function runStage(stage) {
41
45
  });
42
46
  } finally {
43
47
  if (configHome) rmSync(configHome, { recursive: true, force: true });
48
+ if (unitTmp) rmSync(unitTmp, { recursive: true, force: true });
44
49
  }
45
50
  }
46
51
 
@@ -53,6 +53,7 @@ export const requiredPaths = [
53
53
  "scripts/npm-pack-result.mjs",
54
54
  "assets/orchestrator.md",
55
55
  "assets/orchestrator-delegation.md",
56
+ "assets/orchestrator-delegation-reference.md",
56
57
  "assets/orchestrator-tracking.md",
57
58
  "assets/orchestrator-verification.md",
58
59
  "assets/orchestrator-writer.md",
@@ -81,6 +82,7 @@ export const requiredPaths = [
81
82
  "docs/review-integration.md",
82
83
  "extensions/gentle-ai.ts",
83
84
  "extensions/resume-hint.ts",
85
+ "extensions/session-scratch.ts",
84
86
  "extensions/skill-registry.ts",
85
87
  "lib/child-package-injection.ts",
86
88
  "lib/gentle-ai-binary.ts",
@@ -90,6 +92,7 @@ export const requiredPaths = [
90
92
  "lib/gentle-shell-update-panel.ts",
91
93
  "lib/gentle-shell-startup-flow.ts",
92
94
  "lib/gentle-shell-resume-hint.ts",
95
+ "lib/gentle-shell-import-pi.ts",
93
96
  "lib/native-review-cli.ts",
94
97
  "lib/status-timing-diagnostics.ts",
95
98
  "lib/provider-contract-bundle.ts",
@@ -98,6 +101,7 @@ export const requiredPaths = [
98
101
  "lib/review-relay-contract.ts",
99
102
  "lib/agent-assets.ts",
100
103
  "lib/telemetry-trigger.ts",
104
+ "lib/session-scratch.ts",
101
105
  "runtime/child-package-injection.mjs",
102
106
  "runtime/gentle-ai-binary.mjs",
103
107
  "runtime/gentle-shell-launcher.mjs",
@@ -107,6 +111,7 @@ export const requiredPaths = [
107
111
  "runtime/gentle-shell-startup-flow.mjs",
108
112
  "runtime/gentle-shell-resume-hint.mjs",
109
113
  "runtime/gentle-shell-uninstall.mjs",
114
+ "runtime/gentle-shell-import-pi.mjs",
110
115
  "runtime/native-review-cli.mjs",
111
116
  "runtime/status-timing-diagnostics.mjs",
112
117
  "runtime/review-integration-v2.mjs",
@@ -131,17 +136,18 @@ export const requiredPaths = [
131
136
  // exact bytes are pinned by the lock-driven scripts/check-provider-contract.mjs
132
137
  // drift check, which runs in the same pnpm test flow.
133
138
  "contracts/review-provider-contract-mirror/provider-contract.lock.json",
134
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/README.md",
135
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/manifest.json",
136
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/orchestration/pi.md",
137
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/schemas/lens.schema.json",
138
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/schemas/refuter.schema.json",
139
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/schemas/targeted-validator.schema.json",
140
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/vectors/lens.json",
141
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/vectors/refuter.json",
142
- "contracts/review-provider-contract-mirror/v1.3.0/bundle/vectors/targeted-validator.json",
143
- "contracts/review-provider-contract-mirror/v1.3.0/generated/provider-capabilities.baseline.json",
144
- "contracts/review-provider-contract-mirror/v1.3.0/generated/provider-roles.baseline.json",
139
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/README.md",
140
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/manifest.json",
141
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/orchestration/pi.md",
142
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md",
143
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/schemas/lens.schema.json",
144
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/schemas/refuter.schema.json",
145
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/schemas/targeted-validator.schema.json",
146
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/vectors/lens.json",
147
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/vectors/refuter.json",
148
+ "contracts/review-provider-contract-mirror/v1.4.0/bundle/vectors/targeted-validator.json",
149
+ "contracts/review-provider-contract-mirror/v1.4.0/generated/provider-capabilities.baseline.json",
150
+ "contracts/review-provider-contract-mirror/v1.4.0/generated/provider-roles.baseline.json",
145
151
  "prompts/skill-creation.md",
146
152
  "skills/_shared/review-ledger-contract.md",
147
153
  "skills/branch-pr/SKILL.md",
@@ -395,7 +401,7 @@ async function main() {
395
401
  });
396
402
 
397
403
  if (driftedContracts.length > 0) {
398
- console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v4.1.1 runtime's vendored Gentle AI contract artifacts:");
404
+ console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v4.2.0 runtime's vendored Gentle AI contract artifacts:");
399
405
  for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
400
406
  process.exit(1);
401
407
  }
@@ -440,7 +446,7 @@ async function main() {
440
446
  process.exit(1);
441
447
  }
442
448
 
443
- console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v4.1.1 runtime).`);
449
+ console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v4.2.0 runtime).`);
444
450
  }
445
451
 
446
452
  const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;