gentle-pi 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gentle-ai-worker.md +1 -1
  3. package/assets/design-canvas/canvas.css +648 -0
  4. package/assets/design-canvas/canvas.js +1497 -0
  5. package/assets/design-canvas/index.html +88 -0
  6. package/assets/design-canvas/inspector.js +290 -0
  7. package/assets/orchestrator-delegation-reference.md +2 -2
  8. package/assets/orchestrator-verification.md +1 -1
  9. package/assets/orchestrator.md +2 -2
  10. package/bin/engram-guard +1 -1
  11. package/bin/gentle-shell.mjs +101 -151
  12. package/docs/bundled-install.md +47 -42
  13. package/docs/delegated-verification.md +6 -0
  14. package/docs/gentle-agents-activity.md +5 -1
  15. package/docs/gentle-music.md +61 -0
  16. package/docs/gentle-shell.md +77 -7
  17. package/docs/install-wizard.md +30 -6
  18. package/docs/permission-modes.md +7 -2
  19. package/docs/readme-reference.md +19 -15
  20. package/docs/session-profile-format.md +26 -0
  21. package/docs/voice.md +259 -0
  22. package/docs/yolo-mode.md +17 -2
  23. package/extensions/ask-user-choice.ts +4 -1
  24. package/extensions/gentle-agents.ts +173 -38
  25. package/extensions/gentle-ai.ts +214 -69
  26. package/extensions/gentle-design.ts +158 -0
  27. package/extensions/gentle-music.ts +289 -0
  28. package/extensions/gentle-shell.ts +332 -73
  29. package/extensions/gentle-voice.ts +11 -0
  30. package/extensions/session-scratch.ts +41 -22
  31. package/extensions/skill-registry.ts +63 -8
  32. package/lib/agent-profiles.ts +26 -0
  33. package/lib/agents-config.ts +35 -0
  34. package/lib/agents-protocol.ts +57 -1
  35. package/lib/agents-runner.ts +52 -3
  36. package/lib/agents-session-transport.ts +101 -6
  37. package/lib/agents-widget.ts +14 -1
  38. package/lib/auto-permission-memory.ts +48 -18
  39. package/lib/auto-permission-runtime.ts +21 -6
  40. package/lib/clipboard-image.ts +147 -0
  41. package/lib/command-palette-catalog.ts +2 -0
  42. package/lib/commit-slice-signal.ts +1 -1
  43. package/lib/context-backstop.ts +36 -0
  44. package/lib/design-canvas-server.ts +947 -0
  45. package/lib/design-comments.ts +437 -0
  46. package/lib/design-handoffs.ts +313 -0
  47. package/lib/design-open-browser.ts +58 -0
  48. package/lib/design-sources.ts +120 -0
  49. package/lib/external-editor.ts +31 -0
  50. package/lib/gentle-design.ts +513 -0
  51. package/lib/gentle-shell-bundled.ts +123 -49
  52. package/lib/gentle-shell-launcher.ts +9 -4
  53. package/lib/gentle-shell-startup-update.ts +61 -9
  54. package/lib/gentle-shell-uninstall.ts +33 -1
  55. package/lib/image-attachments.ts +150 -0
  56. package/lib/inprocess-reviewer.ts +33 -2
  57. package/lib/nan-provider.ts +3 -0
  58. package/lib/native-review-cli.ts +11 -4
  59. package/lib/notification-audio-native.ts +149 -13
  60. package/lib/notification-audio.ts +4 -0
  61. package/lib/notification-pulse-worker.ts +82 -18
  62. package/lib/notification-service.ts +5 -1
  63. package/lib/orchestrator-presence.ts +78 -8
  64. package/lib/orchestrator-scope.ts +10 -0
  65. package/lib/orchestrator-state.ts +11 -3
  66. package/lib/permission-mode-policy.ts +2 -2
  67. package/lib/review-host-relay.ts +11 -2
  68. package/lib/review-reminder-receipt.ts +24 -1
  69. package/lib/session-profile-append-controller.ts +713 -0
  70. package/lib/session-profile-disk-reader.ts +182 -0
  71. package/lib/session-profile-persistence.ts +15 -7
  72. package/lib/session-scratch.ts +52 -3
  73. package/lib/session-worktree-registry.ts +7 -0
  74. package/lib/shell-bar.ts +4 -3
  75. package/lib/shell-changes.ts +81 -5
  76. package/lib/shell-prompt.ts +4 -4
  77. package/lib/shell-sidebar-layout.ts +26 -6
  78. package/lib/shell-todo.ts +1 -1
  79. package/lib/verifier-admission.ts +5 -4
  80. package/lib/vim-editor-adapter.ts +11 -8
  81. package/lib/voice-artifacts.ts +188 -0
  82. package/lib/voice-command.ts +576 -0
  83. package/lib/voice-install.ts +455 -0
  84. package/lib/voice-platform.ts +99 -0
  85. package/lib/voice-push-to-talk.ts +262 -0
  86. package/lib/voice-session.ts +460 -0
  87. package/lib/voice-settings.ts +50 -0
  88. package/lib/voice-ui.ts +158 -0
  89. package/lib/yolo-session-policy.ts +58 -22
  90. package/package.json +1 -1
  91. package/runtime/gentle-shell-bundled.mjs +122 -48
  92. package/runtime/gentle-shell-launcher.mjs +9 -4
  93. package/runtime/gentle-shell-startup-update.mjs +61 -9
  94. package/runtime/gentle-shell-uninstall.mjs +33 -1
  95. package/runtime/native-review-cli.mjs +11 -4
  96. package/scripts/bundled-engram-migration.mjs +0 -6
  97. package/scripts/gentle-ai-installer.mjs +10 -10
  98. package/scripts/installer-downloads.mjs +7 -7
  99. package/scripts/installer-preflight.mjs +5 -3
  100. package/scripts/installer-windows.mjs +37 -7
  101. package/scripts/main-channel.mjs +91 -17
  102. package/scripts/pi-update.mjs +34 -6
  103. package/scripts/verify-package-files.mjs +26 -2
  104. package/scripts/voice-worker.cjs +476 -0
  105. package/skills/design/SKILL.md +110 -0
  106. package/tests/agent-profiles.test.ts +24 -0
  107. package/tests/agents-config.test.ts +61 -0
  108. package/tests/agents-fake-child.ts +8 -1
  109. package/tests/agents-protocol.test.ts +32 -0
  110. package/tests/agents-runner.test.ts +226 -5
  111. package/tests/agents-session-transport.test.ts +53 -3
  112. package/tests/ask-user-choice.test.ts +30 -0
  113. package/tests/auto-mode.test.ts +17 -0
  114. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  115. package/tests/auto-permission-runtime.test.ts +85 -1
  116. package/tests/autonomous-guard.test.ts +20 -0
  117. package/tests/bundled-install.test.ts +3 -3
  118. package/tests/clipboard-image.test.ts +114 -0
  119. package/tests/command-palette.test.ts +1 -1
  120. package/tests/context-backstop.test.ts +70 -4
  121. package/tests/delegate-for-reason-contract.test.ts +2 -1
  122. package/tests/design-canvas-assets.test.ts +710 -0
  123. package/tests/design-canvas-server.test.ts +1096 -0
  124. package/tests/design-comments.test.ts +201 -0
  125. package/tests/design-handoffs.test.ts +282 -0
  126. package/tests/design-open-browser.test.ts +70 -0
  127. package/tests/design-sources.test.ts +132 -0
  128. package/tests/engram-safety.test.ts +132 -57
  129. package/tests/external-editor.test.ts +84 -0
  130. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
  131. package/tests/gentle-agents.test.ts +365 -20
  132. package/tests/gentle-ai-binary.test.ts +1 -1
  133. package/tests/gentle-ai-installer.test.ts +51 -51
  134. package/tests/gentle-ai.test.ts +292 -13
  135. package/tests/gentle-design.test.ts +1313 -0
  136. package/tests/gentle-music.test.ts +335 -0
  137. package/tests/gentle-shell-bundled.test.ts +301 -248
  138. package/tests/gentle-shell-launcher.test.ts +28 -2
  139. package/tests/gentle-shell-startup-update.test.ts +77 -1
  140. package/tests/gentle-shell-uninstall.test.ts +59 -0
  141. package/tests/gentle-shell.test.ts +183 -8
  142. package/tests/gentle-todo.test.ts +11 -0
  143. package/tests/image-attachments.test.ts +119 -0
  144. package/tests/image-paste-chips.test.ts +236 -0
  145. package/tests/inprocess-reviewer.test.ts +55 -2
  146. package/tests/installer-bundles.test.ts +12 -6
  147. package/tests/installer-posix-bootstrap.test.ts +8 -8
  148. package/tests/installer-preflight.test.ts +16 -0
  149. package/tests/installer-server.test.ts +2 -2
  150. package/tests/installer-windows-bootstrap.test.ts +61 -3
  151. package/tests/installer-windows-diagnostics.test.ts +111 -0
  152. package/tests/main-channel-upgrade.test.ts +232 -2
  153. package/tests/main-channel.test.ts +140 -21
  154. package/tests/nan-provider.test.ts +36 -0
  155. package/tests/native-review-capability-contract.test.ts +11 -4
  156. package/tests/native-review-parity.test.ts +1 -1
  157. package/tests/notification-audio-native.test.ts +128 -9
  158. package/tests/notification-customize.test.ts +17 -5
  159. package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
  160. package/tests/orchestrator-budget.test.ts +5 -2
  161. package/tests/orchestrator-presence.test.ts +100 -2
  162. package/tests/orchestrator-rdd-ownership.test.ts +2 -2
  163. package/tests/orchestrator-scope.test.ts +14 -0
  164. package/tests/orchestrator-state.test.ts +29 -0
  165. package/tests/package-files.test.ts +34 -0
  166. package/tests/package-manifest.test.ts +6 -6
  167. package/tests/permission-mode-policy.test.ts +10 -1
  168. package/tests/pi-update.test.ts +99 -4
  169. package/tests/rdd-aware-verification-contract.test.ts +1 -1
  170. package/tests/review-host-relay-routing.test.ts +88 -1
  171. package/tests/review-host-relay.test.ts +9 -2
  172. package/tests/review-json-arguments.test.ts +20 -3
  173. package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
  174. package/tests/review-relay-transport-agent.test.ts +26 -0
  175. package/tests/review-reminder-receipt.test.ts +31 -1
  176. package/tests/session-changes.test.ts +2 -1
  177. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  178. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  179. package/tests/session-profile-append-controller.test.ts +731 -0
  180. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  181. package/tests/session-profile-disk-reader.test.ts +311 -0
  182. package/tests/session-scratch.test.ts +68 -2
  183. package/tests/session-worktree-registry.test.ts +13 -1
  184. package/tests/shell-bar.test.ts +14 -2
  185. package/tests/shell-changes.test.ts +148 -1
  186. package/tests/shell-prompt.test.ts +3 -3
  187. package/tests/shell-render-cache.test.ts +134 -1
  188. package/tests/shell-sidebar-layout.test.ts +50 -0
  189. package/tests/short-check-routing-contract.test.ts +1 -1
  190. package/tests/skill-collision-prefixes.test.ts +3 -1
  191. package/tests/skill-registry-watcher.test.ts +17 -5
  192. package/tests/skill-registry.test.ts +47 -3
  193. package/tests/task-size-routing-contract.test.ts +1 -1
  194. package/tests/test-strength-contract.test.ts +1 -1
  195. package/tests/verifier-admission.test.ts +18 -1
  196. package/tests/voice-command.test.ts +773 -0
  197. package/tests/voice-install.test.ts +323 -0
  198. package/tests/voice-platform.test.ts +176 -0
  199. package/tests/voice-push-to-talk.test.ts +444 -0
  200. package/tests/voice-session.test.ts +434 -0
  201. package/tests/voice-settings.test.ts +49 -0
  202. package/tests/voice-ui.test.ts +97 -0
  203. package/tests/voice-worker.test.ts +445 -0
  204. package/tests/weak-model-economy-prompt.test.ts +32 -0
  205. package/tests/windows-hidden-processes.test.ts +25 -2
  206. package/tests/yolo-customize.test.ts +5 -1
  207. package/tests/yolo-mode.test.ts +1 -1
  208. package/tests/yolo-session-policy-capture.test.ts +104 -0
@@ -203,10 +203,10 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
203
203
  // stream while still succeeding. That last qualifier is what keeps this list
204
204
  // honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
205
205
  // on by default; this was never explicitly chosen. ..."), which used to ride
206
- // with the skip notice whenever the resolved mode source was `default`. Under
207
- // opt-in receipt-driven development a default-source clone is refused long
208
- // before the consent ceremony runs, so the pinned binary can no longer emit
209
- // that line and it is removed here rather than left as dead tolerance. Multi-
206
+ // with the skip notice whenever the resolved mode source was `default`. Reviews
207
+ // are on by default again since gentle-ai v4.0.0, but the pinned binary no
208
+ // longer has that function and cannot emit the line, so it is removed here
209
+ // rather than left as dead tolerance. Multi-
210
210
  // line stderr is still expected in principle — these are separate Fprintln
211
211
  // calls, never one joined string — which is why membership is per line.
212
212
  export const REVIEW_CONSENT_NOTICES = Object.freeze([
@@ -1091,6 +1091,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1091
1091
  // 9d2927a84ba9a596f69029e3f854b16f86ca4c8b32cc5d63683764ccf2fb394b.
1092
1092
  // No new negotiated capability identity is asserted.
1093
1093
  "4.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1094
+ // v4.3.1 repeats 4.2.0: the published binary still advertises
1095
+ // capabilities/v2.6 under review-integration/v2, and the provider contract
1096
+ // stays at 1.4.0, with published tarball SHA-256
1097
+ // 58450471280aeb357443d6a28c819132d8c8eb78dd25b12bac64296b2f8c27c2.
1098
+ // The v4.3.0 tag has no release and is never pinned.
1099
+ // No new negotiated capability identity is asserted.
1100
+ "4.3.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1094
1101
  });
1095
1102
 
1096
1103
 
@@ -43,12 +43,6 @@ function coreHelp(core, env) {
43
43
  function supportsMigration(core, env) {
44
44
  return coreHelp(core, env).includes("migrate-copy --data-dir <absolute-source> --output-dir <new-absolute-destination> --confirm-quiescent --json");
45
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
46
 
53
47
  export function migrateBundledEngram({ prefix, core, home, env }) {
54
48
  let markerOwned = false;
@@ -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.2.0";
40
+ export const INSTALLER_VERSION = "4.3.1";
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.2.0`
49
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v4@v4.3.1`
50
50
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
51
- // tag resolves to commit 4acaf4241beae1e0c977c92dfe406a270f8d40de, the published v4.2.0 release head.
51
+ // tag resolves to commit 2dba7784114fff57864519c4c0d067e952a8c98b, the published v4.3.1 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:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=";
53
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:00p8dnjJrhYFq1Qikk2oY6K/UXIty9A/IBUUOUiX9XA=";
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.2.0 digests are pinned from the published release:
72
+ // still holds it. The v4.3.1 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.2.0 publishes signed
114
+ // distributing unsigned Windows builds in c4b764d0, so v4.3.1 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.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"),
119
+ "darwin/amd64": asset("gentle-ai_4.3.1_darwin_amd64.tar.gz", "6cf57fee0574daef2abed07f54a355fec3b93ac8f01733dc6c4d3674f1746112", "ca44c3fce186b63a7e7ad455c338133ba1982f4a13485301ce4d66edb73d2928", "gentle-ai"),
120
+ "darwin/arm64": asset("gentle-ai_4.3.1_darwin_arm64.tar.gz", "13e8828447f9081465ee0c35a61b24fb49296d51a310c6999227e2aed178956b", "749d08062b0dec13681678a1a1c018613d223ddcca47b5f27681b1d8ab5d14c2", "gentle-ai"),
121
+ "linux/amd64": asset("gentle-ai_4.3.1_linux_amd64.tar.gz", "04ec685c56b42c29dff6db10fc7dc2c730217dc9c96736e53ad2acf3226dafb7", "86248792bde3fe5e9ca93b128c07e7caa6a5b31fac0f9fc84b5b3011b123cb90", "gentle-ai"),
122
+ "linux/arm64": asset("gentle-ai_4.3.1_linux_arm64.tar.gz", "99e7d313a2ff85eaf23a6415e9b3aae67bf74c2fa169b21b93070ada7613f6a0", "df3580506bb5ea63cc4ef5ccebe8b68cfedf66b72f3b51f84ff393183d8cea2b", "gentle-ai"),
123
123
  });
124
124
 
125
125
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -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.4.0",
47
+ version: "3.5.2",
48
48
  redirectHosts: Object.freeze(["objects.githubusercontent.com", "release-assets.githubusercontent.com"]),
49
49
  archives: Object.freeze({
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],
50
+ "darwin-arm64": ["darwin_arm64.tar.gz", "43d398b2fe3327a4a6436c26b2a17d3285ba80bf2f11a22d0796ea2a7fd2b78c", 7749520],
51
+ "darwin-x64": ["darwin_amd64.tar.gz", "aea28ebf461a4ea86c52b793c29311539d7123875354ed7411eae982fc82ec87", 8181300],
52
+ "linux-x64": ["linux_amd64.tar.gz", "106a43ca8f43a27c2c260a6787151c3be6d91c5ed3d5e454e6140527adf92e01", 8064598],
53
+ "linux-arm64": ["linux_arm64.tar.gz", "b0ba49cf5a7d5af436c9744a9a0d638b3f9ebb2a7321c8736d30e96305c2993c", 7472279],
54
+ "win32-x64": ["windows_amd64.zip", "ba56a36d37b2da85fcb1581bbdbed9ae1ee690e642aab8dbe1e18209bf264afe", 8272809],
55
+ "win32-arm64": ["windows_arm64.zip", "df5acc5dd6ee407d484074e570c77a79567c0b8dccf55e25ae25a644c1e7db5f", 7530282],
56
56
  }),
57
57
  });
58
58
  /** The Engram version the bundled install publishes under runtime/. */
@@ -248,9 +248,11 @@ export function planPreflight(inventory, { channel = "release" } = {}) {
248
248
  record("gentleAi", classify(inventory.gentleAi, requirements.gentleAi, (o) => o.compatible, true), requirements.gentleAi);
249
249
  const needsNative = tools.gentleAi.status === "unavailable";
250
250
  // The main channel builds Gentle AI from source on every platform.
251
- // gentle-pi's postinstall may build Gentle AI from source on Windows, so a
252
- // Windows release update needs Go like an installation; main always does.
253
- const windowsBuild = platform === "win32" && (update !== null || (needsNative && !npmCurrent));
251
+ // gentle-pi's postinstall always builds Gentle AI from source on Windows, even
252
+ // next to an installed gentle-ai, so installing or updating the Shell needs Go,
253
+ // as does provisioning a missing native binary; main always does.
254
+ const installsShell = tools.shell.status === "unavailable";
255
+ const windowsBuild = platform === "win32" && (update !== null || (!npmCurrent && (installsShell || needsNative)));
254
256
  const goStatus = windowsBuild || main ? classify(inventory.go, requirements.go) : "not-required";
255
257
  if (goStatus === "unavailable" || goStatus === "incompatible" || goStatus === "unknown") {
256
258
  // A missing, older or unreadable Go is left as it is: the build gets the
@@ -107,6 +107,36 @@ function exists(path) {
107
107
  try { lstatSync(path); return true; }
108
108
  catch (error) { if (error.code === "ENOENT") return false; throw error; }
109
109
  }
110
+ // Failure-only diagnostics, in UTF-16 code units. Suppress entire sensitive
111
+ // streams (including continuation lines), and individual path/SID/opaque lines.
112
+ // This is conservative diagnostic filtering, not a general-purpose secret parser.
113
+ function processDiagnosticText(value, limit) {
114
+ if (typeof value !== "string") return "";
115
+ if (/(?:^|[^a-z0-9])(?:authorization|bearer|token|password|passphrase|secret|credential|api[_-]?key|access[_-]?key|cookie)(?:$|[^a-z0-9])/i.test(value)) return "[redacted]";
116
+ return value.split(/\r\n?|\n/).map(line =>
117
+ /[A-Za-z]:[\\/]|\\|(?:^|[\s"'=])\/\S+|\bS-\d-(?:\d+-)+\d+\b|unsafe:[^|]*\||[A-Za-z0-9+/=_-]{64,}|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/i.test(line) ? "[redacted]" : line
118
+ ).join("\n").slice(0, limit);
119
+ }
120
+ function prerequisiteProcessCause(result) {
121
+ const failure = result.error;
122
+ const code = processDiagnosticText(failure?.code, 64);
123
+ const details = {
124
+ status: Number.isSafeInteger(result.status) ? result.status : null,
125
+ signal: processDiagnosticText(result.signal, 32) || null,
126
+ error: failure ? {
127
+ code, name: processDiagnosticText(failure.name, 64),
128
+ // Spawn text may contain a bare private executable name, not just paths.
129
+ message: /^E[A-Z0-9_]+$/.test(code) ? `Windows prerequisite process error: ${code}` : "Windows prerequisite process error",
130
+ errno: Number.isSafeInteger(failure.errno) ? failure.errno : processDiagnosticText(failure.errno, 64),
131
+ } : null,
132
+ stdout: processDiagnosticText(result.stdout, 1024), stderr: processDiagnosticText(result.stderr, 1024),
133
+ };
134
+ const cause = Object.assign(new Error("Windows prerequisite process diagnostics"), details);
135
+ // Existing evidence consumers read cause.stack only; omit local callsites and
136
+ // never attach the raw spawn Error (which carries command/path/argv fields).
137
+ cause.stack = `Error: ${cause.message}\n${JSON.stringify(details)}`.slice(0, 8192);
138
+ return cause;
139
+ }
110
140
  export function windowsProcessCheck(command, args, env) {
111
141
  // Owned direct children only, not a tree/global kill. Descendant-held stdio
112
142
  // is outside this guarantee. Never execute .cmd through a command interpreter.
@@ -114,7 +144,7 @@ export function windowsProcessCheck(command, args, env) {
114
144
  // A neutral cwd: inside a project (the bundle itself), pnpm 11 reports, and may
115
145
  // fetch, the version that project's packageManager pins instead of its own.
116
146
  const result = spawnSync(command, args, { cwd: parse(command).root || undefined, env, shell: false, encoding: "utf8", timeout: 15000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, windowsHide: true });
117
- if (result.error || result.status !== 0) throw new Error("Windows prerequisite process failed");
147
+ if (result.error || result.status !== 0) throw new Error("Windows prerequisite process failed", { cause: prerequisiteProcessCause(result) });
118
148
  return result.stdout.trim();
119
149
  }
120
150
 
@@ -151,9 +181,9 @@ try {
151
181
  $depth = 0;
152
182
  while ($path) {
153
183
  $role = 'ancestor'; if ($depth -eq 0) { $role = 'target' } elseif ($depth -eq 1) { $role = 'parent' };
154
- $item = Get-Item -LiteralPath $path -Force;
155
- if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
156
- if ($item.PSIsContainer) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
184
+ $attributes = [IO.File]::GetAttributes($path);
185
+ if ($attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
186
+ if ($attributes -band [IO.FileAttributes]::Directory) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
157
187
  $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]);
158
188
  if ($trusted -notcontains $owner.Value) { $detail = $path + '|' + $owner.Value + '|' + (& $account $owner) + '|'; throw ($role + '-owner') };
159
189
  $allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };
@@ -186,9 +216,9 @@ foreach ($start in $env:GENTLE_WINDOWS_CHECKS.Split('|')) {
186
216
  $depth = 0;
187
217
  while ($path) {
188
218
  $role = 'ancestor'; if ($depth -eq 0) { $role = 'target' } elseif ($depth -eq 1) { $role = 'parent' };
189
- $item = Get-Item -LiteralPath $path -Force;
190
- if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
191
- if ($item.PSIsContainer) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
219
+ $attributes = [IO.File]::GetAttributes($path);
220
+ if ($attributes -band [IO.FileAttributes]::ReparsePoint) { $detail = $path + '|||'; throw ($role + '-reparse') };
221
+ if ($attributes -band [IO.FileAttributes]::Directory) { $acl = [IO.Directory]::GetAccessControl($path) } else { $acl = [IO.File]::GetAccessControl($path) };
192
222
  $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]);
193
223
  if ($trusted -notcontains $owner.Value) { $detail = $path + '|' + $owner.Value + '|' + (& $account $owner) + '|'; throw ($role + '-owner') };
194
224
  $allowedRights = 0x1200a9; if ($depth -ge 2) { $allowedRights = 0x1200ad };
@@ -4,9 +4,11 @@
4
4
  // an exact commit SHA resolved at install or upgrade time:
5
5
  // - Gentle AI: `go install <module>@<sha>` in a sealed Go environment, so the
6
6
  // module source is verified by Go's checksum database (sum.golang.org), as
7
- // the Windows release build already is. The binary must report a version
8
- // naming that commit, then it is activated through the existing dev-binary
9
- // override, which never falls back to the pinned binary silently.
7
+ // the Windows release build already is. Go must resolve the commit to a
8
+ // module version (a tag or a pseudo-version) whose origin is that commit,
9
+ // and the binary must embed that version and report it, then it is
10
+ // activated through the existing dev-binary override, which never falls
11
+ // back to the pinned binary silently.
10
12
  // - Gentle Shell: the exact commit's source tarball, versioned
11
13
  // `<version>-main.<sha12>` and packed without `prepack` (which runs the full
12
14
  // test suite), then installed globally like any local tarball.
@@ -18,13 +20,14 @@ import { goAcquisition, pnpmGlobalBin, requirements } from "./installer-prefligh
18
20
 
19
21
  export const SHELL_REPOSITORY = "Gentleman-Programming/gentle-shell";
20
22
  export const GENTLE_AI_REPOSITORY = "Gentleman-Programming/gentle-ai";
23
+ export const GENTLE_AI_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
21
24
  export const GENTLE_AI_MAIN_PACKAGE = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
22
25
  export const CHANNEL_SCHEMA = "gentle-shell.channel/v1";
23
26
  export const CHANNELS = Object.freeze(["release", "main"]);
24
27
 
25
28
  const SHA = /^[0-9a-f]{40}$/;
26
29
  const MINUTE = 60_000;
27
- const deadlines = Object.freeze({ build: 15 * MINUTE, version: 30_000, extract: 2 * MINUTE, pack: 5 * MINUTE });
30
+ const deadlines = Object.freeze({ build: 15 * MINUTE, resolve: 2 * MINUTE, version: 30_000, extract: 2 * MINUTE, pack: 5 * MINUTE });
28
31
  const MAX_SOURCE_BYTES = 100 * 1024 * 1024;
29
32
 
30
33
  export class MainChannelError extends Error {
@@ -136,23 +139,57 @@ export async function buildMainGentleAi({ commit, ctx, platform, goPath, run, fs
136
139
  const executable = platform === "win32" ? "gentle-ai.exe" : "gentle-ai";
137
140
  const directory = join(configHome(ctx), "main", "gentle-ai", commit);
138
141
  const buildDirectory = join(configHome(ctx), "main", ".build");
142
+ // Verified in the work folder before it replaces anything, so a failed rebuild of
143
+ // an already registered commit never touches the override; the final move stays
144
+ // on one filesystem, and uninstall already accepts a leftover .build.
145
+ const staging = join(buildDirectory, commit);
139
146
  await fs.rm(buildDirectory, { recursive: true, force: true });
140
147
  const env = sealedGoEnvironment(goPath, buildDirectory, platform);
141
148
  for (const path of [env.GOBIN, env.GOPATH, env.GOMODCACHE, env.GOCACHE, join(buildDirectory, "tmp")]) await fs.mkdir(path, { recursive: true });
142
149
  try {
143
150
  const install = await run(goPath, ["install", `${GENTLE_AI_MAIN_PACKAGE}@${commit}`], { env, cwd: buildDirectory, deadlineMs: deadlines.build, stderrTail: 4096 });
144
151
  if (!succeeded(install)) throw new MainChannelError("main-gentle-ai-build-failed", `go install ${GENTLE_AI_MAIN_PACKAGE}@${commit} failed`, stderrCause(install));
145
- await fs.mkdir(directory, { recursive: true });
146
- const binaryPath = join(directory, executable);
147
- await fs.copyFile(join(env.GOBIN, executable), binaryPath);
148
- if (platform !== "win32") await fs.chmod(binaryPath, 0o755);
149
- // Go stamps the module pseudo-version, whose suffix is the commit's first 12 characters.
150
- const probe = await run(binaryPath, ["version"], { env: { PATH: env.PATH }, deadlineMs: deadlines.version });
152
+ await fs.mkdir(staging, { recursive: true });
153
+ const stagedPath = join(staging, executable);
154
+ await fs.copyFile(join(env.GOBIN, executable), stagedPath);
155
+ if (platform !== "win32") await fs.chmod(stagedPath, 0o755);
156
+ const reject = async (message, cause) => {
157
+ throw new MainChannelError("main-gentle-ai-unverified", message, cause);
158
+ };
159
+ const short = commit.slice(0, 12);
160
+ const goOptions = { env, cwd: buildDirectory, deadlineMs: deadlines.version, stderrTail: 4096 };
161
+ // Go resolves the commit through the proxy and checksum database: a tag commit is
162
+ // stamped with its tag, any other with a pseudo-version, so the version is not guessed.
163
+ // Resolving may reach the proxy, so it gets a network deadline, not the local one.
164
+ const listed = await run(goPath, ["list", "-m", "-json", `${GENTLE_AI_MODULE}@${commit}`], { ...goOptions, deadlineMs: deadlines.resolve });
165
+ if (!succeeded(listed)) return await reject(`Go could not resolve Gentle AI commit ${short}`, stderrCause(listed));
166
+ let resolved = null;
167
+ try {
168
+ resolved = JSON.parse(String(listed.stdout ?? ""));
169
+ } catch {
170
+ return await reject(`Go returned unreadable module information for Gentle AI commit ${short}`);
171
+ }
172
+ const resolvedVersion = typeof resolved?.Version === "string" ? resolved.Version : "";
173
+ const originHash = resolved?.Origin?.Hash;
174
+ if (resolvedVersion === "") return await reject(`Go resolved Gentle AI commit ${short} to no module version`);
175
+ if (originHash !== commit) {
176
+ return await reject(`Go resolved Gentle AI commit ${short} to ${originHash ? `commit ${originHash}` : "no commit"}, not the requested commit`);
177
+ }
178
+ const embedded = await run(goPath, ["version", "-m", stagedPath], goOptions);
179
+ if (!succeeded(embedded)) return await reject("the built Gentle AI's embedded module version could not be read", stderrCause(embedded));
180
+ const modLine = String(embedded.stdout ?? "").split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "mod" && fields[1] === GENTLE_AI_MODULE);
181
+ if (modLine?.[2] !== resolvedVersion) {
182
+ return await reject(`the built Gentle AI embeds module version ${modLine?.[2] ?? "none"}, expected ${resolvedVersion} for commit ${short}`);
183
+ }
184
+ const probe = await run(stagedPath, ["version"], { env: { PATH: env.PATH }, deadlineMs: deadlines.version });
151
185
  const match = succeeded(probe) && /^gentle-ai (\S+)$/m.exec(String(probe.stdout ?? ""));
152
- if (!match || !match[1].endsWith(`-${commit.slice(0, 12)}`)) {
153
- await fs.rm(directory, { recursive: true, force: true });
154
- throw new MainChannelError("main-gentle-ai-unverified", `the built Gentle AI does not report commit ${commit.slice(0, 12)}`);
186
+ const expectedVersion = resolvedVersion.replace(/^v/, "");
187
+ if (!match || match[1] !== expectedVersion) {
188
+ return await reject(`the built Gentle AI reports version ${match ? match[1] : "none"}, expected ${expectedVersion} for commit ${short}`);
155
189
  }
190
+ await fs.mkdir(directory, { recursive: true });
191
+ const binaryPath = join(directory, executable);
192
+ await fs.rename(stagedPath, binaryPath);
156
193
  registerGentleAiDevBinary(binaryPath, ctx, platform);
157
194
  return { binaryPath, version: match[1] };
158
195
  } finally {
@@ -387,6 +424,21 @@ async function mainGo({ userGo, pinnedGo, acquire, run }) {
387
424
  }
388
425
  }
389
426
 
427
+ /** A thrown value's message when it has one (an Error or not), otherwise the value as text. */
428
+ function describe(error) {
429
+ return typeof error?.message === "string" ? error.message : String(error);
430
+ }
431
+
432
+ /** `error` with `detail` appended to its message, keeping its code and cause; any
433
+ * other error becomes `main-upgrade-incomplete` with its message and itself as cause. */
434
+ function withDetail(error, detail) {
435
+ if (!(error instanceof MainChannelError)) {
436
+ return new MainChannelError("main-upgrade-incomplete", `${describe(error)}; ${detail}`, error);
437
+ }
438
+ const message = error.message.slice(`${error.code}: `.length);
439
+ return new MainChannelError(error.code, `${message}; ${detail}`, error.cause);
440
+ }
441
+
390
442
  /**
391
443
  * Updates Gentle Shell along its recorded channel (or the one `--channel` selects):
392
444
  * release goes to the latest npm release, main to the latest `main` commits of
@@ -441,11 +493,33 @@ export async function runUpgrade({ args, ctx, platform, arch = process.arch, pac
441
493
  out(`gentle-shell is already at the latest main: ${summary}.`);
442
494
  return 0;
443
495
  }
444
- if (!aiCurrent) await buildMainGentleAi({ commit: gentleAiCommit, ctx, platform, goPath: await mainGo({ userGo, pinnedGo, acquire, run }), run, fs });
496
+ // Fail forward (#2356): a Gentle AI step that fails still installs a main Shell that
497
+ // moved, so the retry runs the new Shell's code; the channel is recorded only when
498
+ // both steps succeed, so that retry rebuilds Gentle AI. Verification stays fail-closed.
499
+ // A flag, not the caught value: a rejection may be any value, even a falsy one.
500
+ let aiFailed = false;
501
+ let aiError;
502
+ if (!aiCurrent) {
503
+ try {
504
+ await buildMainGentleAi({ commit: gentleAiCommit, ctx, platform, goPath: await mainGo({ userGo, pinnedGo, acquire, run }), run, fs });
505
+ } catch (error) {
506
+ if (shellCurrent) throw error;
507
+ aiFailed = true;
508
+ aiError = error;
509
+ }
510
+ }
445
511
  if (!shellCurrent) {
446
- const manager = await ownerManager({ ctx, platform, packageRoot, adapters });
447
- const tgz = await packMainShell({ commit: shellCommit, ctx, fetch, run, pnpm, fs, platform });
448
- await installGlobal(manager, tgz, run);
512
+ try {
513
+ const manager = await ownerManager({ ctx, platform, packageRoot, adapters });
514
+ const tgz = await packMainShell({ commit: shellCommit, ctx, fetch, run, pnpm, fs, platform });
515
+ await installGlobal(manager, tgz, run);
516
+ } catch (error) {
517
+ throw aiFailed ? withDetail(error, `Gentle AI was not updated either (${describe(aiError)})`) : error;
518
+ }
519
+ }
520
+ if (aiFailed) {
521
+ out(`Updated Gentle Shell to the latest main (${shellCommit.slice(0, 12)}); Gentle AI was not updated.`);
522
+ throw withDetail(aiError, `Gentle Shell was updated to main ${shellCommit.slice(0, 12)} but Gentle AI was not: run gentle-shell upgrade --channel main to finish`);
449
523
  }
450
524
  await writeChannel(ctx, { channel: "main", shellCommit, gentleAiCommit }, fs);
451
525
  out(`Updated to the latest main: ${summary}.`);
@@ -1,6 +1,7 @@
1
1
  // Narrow update ownership proof. Deliberately independent of Shell upgrade/uninstall.
2
2
  import { posix, win32 } from 'node:path';
3
3
  import { bundledUpdatePlan } from '../runtime/gentle-shell-bundled.mjs';
4
+ import { setupErrorDetail } from './installer-runner.mjs';
4
5
 
5
6
  const PI = '@earendil-works/pi-coding-agent';
6
7
  const SHELL = 'gentle-pi';
@@ -8,6 +9,9 @@ const object = value => value !== null && typeof value === 'object' && !Array.is
8
9
  const succeeded = result => result?.code === 0 && !result.timedOut && result.signal == null;
9
10
  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
11
  const LINKED = /^(?:file|link|workspace):/i;
12
+ // A main-channel Shell (`<version>-main.<sha12>`, scripts/main-channel.mjs
13
+ // mainVersion) is never published; it exists only as the packed tarball.
14
+ const MAIN_BUILD = /^[0-9A-Za-z.+-]+-main\.[0-9a-f]{12}$/;
11
15
  const CLI_TAIL = /[\\/]dist[\\/]bundle[\\/]cli\.js$/i;
12
16
  // A quoted cli.js target inside a package-manager shim: pnpm's POSIX shim
13
17
  // (`"$basedir/../global/.../cli.js"`), its `.CMD`/npm's `.cmd` shim
@@ -136,7 +140,19 @@ async function listedEntries(groups, {adapters, path, key, canonical}) {
136
140
  * ownership. An adjacent runtime is owned by the Shell group that resolved
137
141
  * it: updating a separate global Pi never changes what that Shell runs.
138
142
  */
139
- export async function resolveUpdateOwner({runtime, packageRoot, platform = process.platform, adapters, cwd}) {
143
+ /** Where `gentle-shell upgrade --channel main` packed this Shell version: the
144
+ * same config home as scripts/main-channel.mjs configHome. A relative config
145
+ * home is refused, not resolved: `upgrade` resolved it from its own working
146
+ * directory, so it names no single folder, and the package manager runs from
147
+ * the neutral cwd.
148
+ */
149
+ function mainBuildTarball(version, {env, home, path}) {
150
+ const config = env?.GENTLE_PI_CONFIG_HOME || (typeof home === 'string' ? path.join(home, '.pi', 'gentle-ai') : null);
151
+ if (!config || !path.isAbsolute(config)) throw refusal('cannot locate the main-channel build without an absolute config home');
152
+ return path.join(config, 'main', 'packages', `${SHELL}-${version}.tgz`);
153
+ }
154
+
155
+ export async function resolveUpdateOwner({runtime, packageRoot, platform = process.platform, adapters, cwd, env, home}) {
140
156
  const {path, key} = pathsFor(platform);
141
157
  const canonical = canonicalWith(adapters, path);
142
158
  const selected = await selectedCliOf(runtime, {adapters, path, canonical});
@@ -154,7 +170,7 @@ export async function resolveUpdateOwner({runtime, packageRoot, platform = proce
154
170
  const root = await canonical(dependency.path);
155
171
  const manifest = await manifestOf(adapters, path, root);
156
172
  if (manifest.name !== pkg) throw refusal('listed package identity differs');
157
- let cli, spec;
173
+ let cli, spec, artifact = false;
158
174
  if (pkg === PI) {
159
175
  if (manifest.bin?.pi !== 'dist/bundle/cli.js') throw refusal('listed Pi bin is unsupported');
160
176
  cli = await canonical(path.join(root, manifest.bin.pi));
@@ -169,11 +185,12 @@ export async function resolveUpdateOwner({runtime, packageRoot, platform = proce
169
185
  // Reinstalling the same Shell version re-resolves its peer; `update`
170
186
  // never upgrades Shell itself (that is `gentle-shell upgrade`).
171
187
  if (typeof manifest.version !== 'string') throw refusal('listed Shell version is missing');
172
- spec = `${SHELL}@${manifest.version}`;
188
+ artifact = MAIN_BUILD.test(manifest.version);
189
+ spec = artifact ? mainBuildTarball(manifest.version, {env, home, path}) : `${SHELL}@${manifest.version}`;
173
190
  }
174
191
  if (key(cli) !== key(selected)) continue;
175
192
  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});
193
+ candidates.push({name, ...invocation, package:pkg, spec, artifact});
177
194
  }
178
195
  }
179
196
  if (candidates.length !== 1) throw refusal(candidates.length > 1 ? 'is ambiguous' : 'could not be verified from global listings');
@@ -207,11 +224,22 @@ export async function updateSelectedPi(input) {
207
224
  let owner;
208
225
  try { owner = await resolveUpdateOwner(input); }
209
226
  catch (error) { throw error.message?.includes('ownership') ? error : refusal('could not be resolved'); }
227
+ if (owner.artifact) {
228
+ try { await input.adapters.fs.realpath(owner.spec); }
229
+ catch { throw new Error(`gentle-shell update: the main-channel build ${owner.spec} is missing, and this unpublished build cannot be reinstalled from the registry; run 'gentle-shell upgrade --channel release' and then 'gentle-shell upgrade --channel main' to rebuild it. No installation changed.`); }
230
+ }
210
231
  // Exactly one approved package per command; never create a comma group or
211
232
  // reinstall arbitrary dependencies merely because they share a store.
212
233
  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}` : ''}`);
234
+ const result = await input.adapters.run(owner.command, [...owner.prefix, ...args], {deadlineMs:20 * 60_000, cwd: input.cwd, stderrTail:4096});
235
+ if (!succeeded(result)) {
236
+ // One sanitized line of the manager's own error: npm prints it on stderr,
237
+ // pnpm 11 prints ERR_PNPM_* on stdout. Read as one text, stderr last, an
238
+ // error line in either stream wins over a stderr warning, stderr's on a tie.
239
+ const output = [result?.stdout, result?.stderrTail].filter(text => typeof text === 'string').join('\n');
240
+ const detail = setupErrorDetail(output, input.home, input.platform);
241
+ throw new Error(`gentle-shell update: ${owner.name} (${[owner.command, ...owner.prefix].join(' ')}) could not install ${owner.spec}${detail ? `\n${detail}` : ''}`);
242
+ }
215
243
  const after = await suppliedPiVersion(owner, input);
216
244
  const before = input.currentVersion;
217
245
  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.`);
@@ -50,6 +50,29 @@ export const requiredPaths = [
50
50
  "assets/sounds/LICENSE.md",
51
51
  "docs/sound-notifications.md",
52
52
  "docs/sound-notifications-proposal.md",
53
+ "extensions/gentle-voice.ts",
54
+ "lib/voice-artifacts.ts",
55
+ "lib/voice-command.ts",
56
+ "lib/voice-install.ts",
57
+ "lib/voice-platform.ts",
58
+ "lib/voice-session.ts",
59
+ "lib/voice-ui.ts",
60
+ // The dictation worker is spawned by path, never imported, so nothing else
61
+ // would notice it missing from the packed tarball.
62
+ "scripts/voice-worker.cjs",
63
+ "docs/voice.md",
64
+ "extensions/gentle-design.ts",
65
+ "lib/gentle-design.ts",
66
+ // The design canvas server reads its page and scripts from assets/ by path.
67
+ "lib/design-canvas-server.ts",
68
+ "lib/design-open-browser.ts",
69
+ "lib/design-comments.ts",
70
+ "lib/design-handoffs.ts",
71
+ "lib/design-sources.ts",
72
+ "assets/design-canvas/index.html",
73
+ "assets/design-canvas/canvas.js",
74
+ "assets/design-canvas/canvas.css",
75
+ "assets/design-canvas/inspector.js",
53
76
  "scripts/npm-pack-result.mjs",
54
77
  "assets/orchestrator.md",
55
78
  "assets/orchestrator-delegation.md",
@@ -154,6 +177,7 @@ export const requiredPaths = [
154
177
  "skills/chained-pr/SKILL.md",
155
178
  "skills/cognitive-doc-design/SKILL.md",
156
179
  "skills/comment-writer/SKILL.md",
180
+ "skills/design/SKILL.md",
157
181
  "skills/gentle-ai/SKILL.md",
158
182
  "skills/issue-creation/SKILL.md",
159
183
  "skills/judgment-day/SKILL.md",
@@ -401,7 +425,7 @@ async function main() {
401
425
  });
402
426
 
403
427
  if (driftedContracts.length > 0) {
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:");
428
+ console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v4.3.1 runtime's vendored Gentle AI contract artifacts:");
405
429
  for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
406
430
  process.exit(1);
407
431
  }
@@ -446,7 +470,7 @@ async function main() {
446
470
  process.exit(1);
447
471
  }
448
472
 
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).`);
473
+ console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v4.3.1 runtime).`);
450
474
  }
451
475
 
452
476
  const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;