gentle-pi 2.1.2 → 2.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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -13,20 +13,21 @@ import {
13
13
  GENTLE_AI_RELEASE_ASSETS,
14
14
  GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
15
15
  downloadGentleAiAsset,
16
+ gentleAiAssetForm,
16
17
  installGentleAi,
17
18
  resolveGentleAiInstallerPackageRoot,
18
19
  resolveGentleAiReleaseAsset,
19
20
  trustedSystemExtractor,
20
21
  } from "../scripts/gentle-ai-installer.mjs";
21
22
 
22
- // v2.2.2 digests pinned from the published release: archive sha256 values match
23
- // checksums.txt and freshly computed hashes; binary sha256 values were computed
24
- // from the extracted executables of each verified archive.
23
+ // v2.5.0 digests pinned from the published release: archive digests verified
24
+ // against the minisign-signed checksums.txt and freshly computed hashes; binary
25
+ // digests computed from the extracted executables.
25
26
  const EXPECTED_ASSETS = {
26
- "darwin/amd64": { name: "gentle-ai_2.2.2_darwin_amd64.tar.gz", sha256: "5ca67829903bf4c6b14665664f80f9d8216c84b10c8e50870d297f452cefb9dc", binarySha256: "9b239423450562d026384f482bbd2f1e3f2820431a84f0921743ac3df9d632de" },
27
- "darwin/arm64": { name: "gentle-ai_2.2.2_darwin_arm64.tar.gz", sha256: "0193e1a284444dccee2863d31b8dbb76a982e8f9111955908d6a9131c1a5490e", binarySha256: "149b97248552c5e03ebc4d991f86b1360fb847a40fc315555a8aa256f95baca0" },
28
- "linux/amd64": { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: "b85bbb20eb2236de97b261df16cfc8d8394dfd07a137e885c4889b62d0c20fa1", binarySha256: "00d5732e8dd3945956800217a4f60213c2d9ca63351092a2cb7f4e5f9ece54f9" },
29
- "linux/arm64": { name: "gentle-ai_2.2.2_linux_arm64.tar.gz", sha256: "61e7077342448273f0c43af49ce4d182594bab5e4f86f812975af2fbe69e3b0b", binarySha256: "a92685aa7dbea0cc4297d016569b3defe9bb30a7374a620021e245b74f50eb68" },
27
+ "darwin/amd64": { name: "gentle-ai_2.5.0_darwin_amd64.tar.gz", sha256: "8204ed692d7ff16fae2eb8bd7b3132b5e4e4dd7b9ea40e93bfced57394da27c0", binarySha256: "929817d320e94ad62be7ed546ad8746e62bd63ab97e3f38c94456f221f163eca" },
28
+ "darwin/arm64": { name: "gentle-ai_2.5.0_darwin_arm64.tar.gz", sha256: "ce06831cea9fd2ac7b2b36d9e98e63988246e0fbafbbd2cc7c003ec2f1463a23", binarySha256: "c859cd88138eb41e83ef9b75309697aaff2acb9f720c9a029b432095f66aea02" },
29
+ "linux/amd64": { name: "gentle-ai_2.5.0_linux_amd64.tar.gz", sha256: "2ba84a3a7ba2b1193019bde2acb05b02cbf222b667568c919686352b3caab113", binarySha256: "5acedd14b7927a7665636bf728c14a614597ce85dd91313bc50cbc96881c6cab" },
30
+ "linux/arm64": { name: "gentle-ai_2.5.0_linux_arm64.tar.gz", sha256: "16a6243d17c146e3fc0024f6f005c5bd141fbb2dda4b56e269318e499ddc170b", binarySha256: "ce75ba1174955813ef462f4e7a50dd59a8e8167575033f6e17aa8d8488630cb8" },
30
31
  } as const;
31
32
 
32
33
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -37,15 +38,31 @@ test("default installer package root is the package containing scripts, not its
37
38
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
38
39
  });
39
40
 
40
- test("release mapping selects only the supported official v2.2.2 archive and pinned digests", () => {
41
+ test("release mapping selects only the supported official v2.5.0 assets and pinned digests", () => {
41
42
  assert.deepEqual(
42
43
  Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
43
44
  EXPECTED_ASSETS,
44
45
  );
45
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.2.2_linux_amd64.tar.gz");
46
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.2.2_darwin_arm64.tar.gz");
46
+ assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.5.0_linux_amd64.tar.gz");
47
+ assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.5.0_darwin_arm64.tar.gz");
47
48
  for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
48
- assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.2\.2\//);
49
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.5\.0\//);
50
+ }
51
+ });
52
+
53
+ test("raw release assets are admitted only under a prerelease pin", () => {
54
+ assert.equal(gentleAiAssetForm("gentle-ai_2.4.0_linux_amd64.tar.gz", "2.4.0"), "archive");
55
+ assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_linux_amd64", "2.5.0-rc.3"), "raw-binary");
56
+ assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
57
+ // A raw binary under a stable pin means the pin itself is wrong: stable
58
+ // releases publish signed archives only, so this fails closed pre-download.
59
+ assert.throws(() => gentleAiAssetForm("gentle-ai_2.6.0_linux_amd64", "2.6.0"), /only admitted for a prerelease pin/);
60
+ assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
61
+ // The current stable pin admits every pinned asset row through the same
62
+ // gate the installer uses at download time (default installerVersion
63
+ // argument): signed archives, never raw binaries.
64
+ for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
65
+ assert.equal(gentleAiAssetForm(asset.name), "archive");
49
66
  }
50
67
  });
51
68
 
@@ -66,7 +83,7 @@ test("release digests are all-or-none and install fails closed while any digest
66
83
  }),
67
84
  /checksum mismatch/,
68
85
  );
69
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
86
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai")), false);
70
87
  }
71
88
  });
72
89
 
@@ -95,7 +112,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
95
112
  const metadata = [
96
113
  "gentle-ai.exe: go1.25.10",
97
114
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
98
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
115
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.5.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
99
116
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
100
117
  ].join("\n");
101
118
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -113,7 +130,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
113
130
  return { stdout: "", stderr: "" };
114
131
  }
115
132
  if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
116
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.2.2\n", stderr: "" };
133
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.5.0\n", stderr: "" };
117
134
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
118
135
  };
119
136
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -129,7 +146,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
129
146
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
130
147
  assert.equal(result.installed, true);
131
148
  assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
132
- ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"], ["version", "-m"],
149
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.5.0"], ["version", "-m"],
133
150
  ]);
134
151
  }
135
152
  });
@@ -148,7 +165,7 @@ test("Windows source install reports missing or too-old Go without publishing a
148
165
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
149
166
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
150
167
  );
151
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
168
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai.exe")), false);
152
169
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
153
170
  }
154
171
  });
@@ -167,7 +184,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
167
184
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
168
185
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
169
186
  );
170
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
187
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.5.0");
171
188
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
172
189
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
173
190
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -188,7 +205,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
188
205
  assert.equal(reused.installed, false);
189
206
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
190
207
 
191
- await writeFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "{}\n");
208
+ await writeFile(join(packageRoot, ".gentle-ai", "v2.5.0", "integrity.json"), "{}\n");
192
209
  const repaired = windowsGoFixture();
193
210
  repaired.setGoExecutable(goPath);
194
211
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -230,7 +247,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
230
247
  const metadata = fixtureOptions.metadataOverride ?? [
231
248
  "gentle-ai.exe: go1.25.10",
232
249
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
233
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.2.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
250
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.5.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
234
251
  "\tbuild\t-buildmode=exe",
235
252
  "\tbuild\t-compiler=gc",
236
253
  "\tbuild\tCGO_ENABLED=0",
@@ -250,7 +267,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
250
267
  return { stdout: "", stderr: "" };
251
268
  }
252
269
  if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
253
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.2.2\n", stderr: "" };
270
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.5.0\n", stderr: "" };
254
271
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
255
272
  };
256
273
  return {
@@ -292,7 +309,7 @@ test("Windows source installation resolves one validated Go executable and seals
292
309
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
293
310
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
294
311
  ["version"],
295
- ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.2.2"],
312
+ ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.5.0"],
296
313
  ["version", "-m"],
297
314
  ]);
298
315
  for (const call of goCalls) {
@@ -318,14 +335,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
318
335
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
319
336
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
320
337
  const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
321
- const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json"), "utf8")) as Record<string, string>;
338
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.5.0", "integrity.json"), "utf8")) as Record<string, string>;
322
339
  assert.equal(result.installed, true);
323
340
  assert.deepEqual(manifest, {
324
- version: "2.2.2",
341
+ version: "2.5.0",
325
342
  method: "go-sumdb-source-build",
326
343
  package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
327
344
  module: "github.com/gentleman-programming/gentle-ai/v2",
328
- tag: "v2.2.2",
345
+ tag: "v2.5.0",
329
346
  architecture: "arm64",
330
347
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
331
348
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -345,12 +362,12 @@ test("Windows source installation rejects Go metadata for a different architectu
345
362
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
346
363
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
347
364
  );
348
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai.exe")), false);
365
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai.exe")), false);
349
366
  });
350
367
 
351
368
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
352
369
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
353
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
370
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock");
354
371
  await mkdir(lockPath, { recursive: true });
355
372
  const fixture = await hardenedWindowsGoFixture(packageRoot);
356
373
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -358,7 +375,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
358
375
 
359
376
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
360
377
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
361
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock", "owner.json");
378
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock", "owner.json");
362
379
  const fixture = await hardenedWindowsGoFixture(packageRoot);
363
380
  let replacedOwner = false;
364
381
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -375,7 +392,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
375
392
  });
376
393
 
377
394
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
378
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
395
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock");
379
396
  await assert.rejects(
380
397
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
381
398
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -385,18 +402,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
385
402
  }
386
403
 
387
404
  function tombstonePath(packageRoot: string, nonce: string): string {
388
- return join(packageRoot, ".gentle-ai", `.v2.2.2.install.tombstone-${nonce}`);
405
+ return join(packageRoot, ".gentle-ai", `.v2.5.0.install.tombstone-${nonce}`);
389
406
  }
390
407
 
391
408
  async function tombstones(packageRoot: string): Promise<string[]> {
392
409
  const runtimeRoot = join(packageRoot, ".gentle-ai");
393
410
  return existsSync(runtimeRoot)
394
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.2.2.install.tombstone-"))
411
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.5.0.install.tombstone-"))
395
412
  : [];
396
413
  }
397
414
 
398
415
  function backupBundlePath(packageRoot: string, nonce: string): string {
399
- return join(packageRoot, ".gentle-ai", `.v2.2.2.backup-${nonce}`);
416
+ return join(packageRoot, ".gentle-ai", `.v2.5.0.backup-${nonce}`);
400
417
  }
401
418
 
402
419
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -407,7 +424,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
407
424
 
408
425
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
409
426
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
410
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
427
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock");
411
428
  await mkdir(lockPath, { recursive: true });
412
429
  const fixture = await hardenedWindowsGoFixture(packageRoot);
413
430
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -415,7 +432,7 @@ test("Windows source installation fails closed for an ownerless lock even after
415
432
 
416
433
  test("Windows source installation fails closed for a stale owner lock", async () => {
417
434
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
418
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
435
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock");
419
436
  await mkdir(lockPath, { recursive: true });
420
437
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
421
438
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -467,7 +484,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
467
484
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
468
485
  );
469
486
  assert.equal(existsSync(foreignTombstone), true);
470
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
487
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock")), false);
471
488
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
472
489
  });
473
490
 
@@ -475,7 +492,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
475
492
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
476
493
  const fixture = await hardenedWindowsGoFixture(packageRoot);
477
494
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
478
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock")), false);
495
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock")), false);
479
496
  assert.deepEqual(await tombstones(packageRoot), []);
480
497
  const reuse = await hardenedWindowsGoFixture(packageRoot);
481
498
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -485,7 +502,7 @@ test("Windows source recovers a valid backup after a crash between publication r
485
502
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
486
503
  const initial = await hardenedWindowsGoFixture(packageRoot);
487
504
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
488
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
505
+ const live = join(packageRoot, ".gentle-ai", "v2.5.0");
489
506
  const backup = backupBundlePath(packageRoot, "crash");
490
507
  await rename(live, backup);
491
508
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -521,7 +538,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
521
538
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
522
539
  const initial = await hardenedWindowsGoFixture(packageRoot);
523
540
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
524
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
541
+ const live = join(packageRoot, ".gentle-ai", "v2.5.0");
525
542
  const backup = backupBundlePath(packageRoot, "valid");
526
543
  await copyWindowsBundle(live, backup);
527
544
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -534,7 +551,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
534
551
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
535
552
  const initial = await hardenedWindowsGoFixture(packageRoot);
536
553
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
537
- const live = join(packageRoot, ".gentle-ai", "v2.2.2");
554
+ const live = join(packageRoot, ".gentle-ai", "v2.5.0");
538
555
  const backup = backupBundlePath(packageRoot, "valid");
539
556
  await copyWindowsBundle(live, backup);
540
557
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -552,7 +569,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
552
569
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
553
570
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
554
571
  await fixture.waitForInstall();
555
- const lockPath = join(packageRoot, ".gentle-ai", ".v2.2.2.install.lock");
572
+ const lockPath = join(packageRoot, ".gentle-ai", ".v2.5.0.install.lock");
556
573
  await assert.rejects(
557
574
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
558
575
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -566,7 +583,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
566
583
 
567
584
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
568
585
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
569
- const versionDirectory = join(packageRoot, ".gentle-ai", "v2.2.2");
586
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v2.5.0");
570
587
  await mkdir(versionDirectory, { recursive: true });
571
588
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
572
589
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -590,7 +607,7 @@ test("Windows source publication rolls back a prior bundle when final directory
590
607
  test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
591
608
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
592
609
  const payload = Buffer.from("signed archive fixture");
593
- const asset = { name: "gentle-ai_2.2.2_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
610
+ const asset = { name: "gentle-ai_2.4.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), url: "https://example.invalid/gentle-ai.tar.gz", executable: "gentle-ai" };
594
611
  const options = {
595
612
  packageRoot,
596
613
  platform: "linux",
@@ -603,12 +620,66 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
603
620
  },
604
621
  };
605
622
  await installGentleAi(options);
606
- const manifestPath = join(packageRoot, ".gentle-ai", "v2.2.2", "integrity.json");
623
+ const manifestPath = join(packageRoot, ".gentle-ai", "v2.5.0", "integrity.json");
607
624
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
608
625
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
609
626
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
610
627
  });
611
628
 
629
+ test("a pinned asset falls back to its gentle-pi mirror only on download failure, and a stable pin declares no mirror", async () => {
630
+ // The mirror fallback is a property of the pinned row, not of the asset
631
+ // form: a prerelease row carries a mirrorUrl, a stable row never does.
632
+ // Drive the fallback through an archive-form asset so the same test holds
633
+ // under the current stable pin, whose form gate refuses raw binaries.
634
+ const payload = Buffer.from("signed archive fixture");
635
+ const binary = "mirrored binary";
636
+ const baseAsset = { name: "gentle-ai_2.5.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
637
+ const extractArchive = async (_archive: string, destination: string) => {
638
+ await mkdir(destination, { recursive: true });
639
+ await writeFile(join(destination, "gentle-ai"), binary);
640
+ };
641
+
642
+ const attempted: string[] = [];
643
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-"));
644
+ const result = await installGentleAi({
645
+ packageRoot, platform: "linux", arch: "x64",
646
+ releaseAssets: { "linux/amd64": baseAsset },
647
+ extractArchive,
648
+ download: async (url: string, destination: string) => {
649
+ attempted.push(url);
650
+ if (url === baseAsset.url) throw new Error("upstream asset deleted");
651
+ await writeFile(destination, payload);
652
+ },
653
+ });
654
+ assert.equal(result.installed, true);
655
+ assert.deepEqual(attempted, [baseAsset.url, baseAsset.mirrorUrl]);
656
+
657
+ // Integrity beats availability: wrong bytes from the upstream source fail
658
+ // immediately and the mirror is never consulted.
659
+ const mismatched: string[] = [];
660
+ await assert.rejects(installGentleAi({
661
+ packageRoot: await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-mismatch-")), platform: "linux", arch: "x64",
662
+ releaseAssets: { "linux/amd64": baseAsset },
663
+ extractArchive,
664
+ download: async (url: string, destination: string) => { mismatched.push(url); await writeFile(destination, "tampered bytes"); },
665
+ }), /checksum mismatch/);
666
+ assert.deepEqual(mismatched, [baseAsset.url]);
667
+
668
+ // Both sources unavailable: the failure names the documented recovery.
669
+ await assert.rejects(installGentleAi({
670
+ packageRoot: await mkdtemp(join(tmpdir(), "gentle-pi-installer-mirror-unavailable-")), platform: "linux", arch: "x64",
671
+ releaseAssets: { "linux/amd64": baseAsset },
672
+ extractArchive,
673
+ download: async () => { throw new Error("gone"); },
674
+ }), (error: unknown) => error instanceof Error && /GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1/.test(error.message) && (error as { code?: string }).code === "GENTLE_AI_ASSET_UNAVAILABLE");
675
+
676
+ // A stable pin downloads only from the signed gentle-ai release: no pinned
677
+ // row carries a mirror, so an upstream failure surfaces as the primary error.
678
+ for (const pinned of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
679
+ assert.equal((pinned as { mirrorUrl?: string }).mirrorUrl, undefined);
680
+ }
681
+ });
682
+
612
683
  test("Windows archive lookup remains unsupported and names Go SumDB source installation", () => {
613
684
  for (const arch of ["x64", "arm64"]) {
614
685
  assert.throws(() => resolveGentleAiReleaseAsset("win32", arch), /Go SumDB source installation/);
@@ -680,7 +751,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
680
751
  }),
681
752
  /checksum mismatch/,
682
753
  );
683
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
754
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai")), false);
684
755
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
685
756
  });
686
757
 
@@ -702,7 +773,7 @@ test("installer promotes only the expected regular executable with executable PO
702
773
  await chmod(extracted, 0o700);
703
774
  },
704
775
  });
705
- const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
776
+ const binary = join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai");
706
777
  assert.equal(existsSync(binary), true);
707
778
  assert.equal(await readFile(binary, "utf8"), "native executable");
708
779
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -733,7 +804,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
733
804
  }),
734
805
  /binary checksum mismatch/,
735
806
  );
736
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
807
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai")), false);
737
808
  });
738
809
 
739
810
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -756,7 +827,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
756
827
  },
757
828
  };
758
829
  await installGentleAi(options);
759
- const binary = join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai");
830
+ const binary = join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai");
760
831
  await chmod(binary, 0o600);
761
832
  const repaired = await installGentleAi(options);
762
833
  assert.equal(repaired.installed, true);
@@ -793,7 +864,7 @@ test("installer rejects archives with multiple expected executable entries", asy
793
864
  }),
794
865
  /exactly one regular gentle-ai/,
795
866
  );
796
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
867
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai")), false);
797
868
  });
798
869
 
799
870
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -811,5 +882,24 @@ test("installer rejects an archive without the expected regular executable", asy
811
882
  }),
812
883
  /non-regular gentle-ai/,
813
884
  );
814
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.2.2", "gentle-ai")), false);
885
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.5.0", "gentle-ai")), false);
886
+ });
887
+
888
+ // #400: a clean Windows source build measured 232 s on a cold module cache,
889
+ // so `go install` cannot share the 120 s bound that fits the `go version`
890
+ // probes. The probes keep their bound; the build gets its own.
891
+ test("Windows source build bounds go install separately from the Go toolchain probes", async () => {
892
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-timeout-"));
893
+ const fixture = windowsGoFixture();
894
+ const goPath = join(packageRoot, "go.exe");
895
+ await writeFile(goPath, "trusted local Go executable");
896
+ fixture.setGoExecutable(goPath);
897
+ await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath });
898
+ const goCalls = fixture.calls.filter((call) => call.file === goPath);
899
+ const install = goCalls.find((call) => call.arguments_[0] === "install");
900
+ assert.ok(install, "go install must run");
901
+ assert.ok(typeof install.options.timeout === "number" && install.options.timeout >= 600_000, `go install must get at least a ten-minute bound, received ${install.options.timeout}`);
902
+ const probes = goCalls.filter((call) => call.arguments_[0] === "version");
903
+ assert.ok(probes.length >= 2, "go version probes must run");
904
+ for (const probe of probes) assert.equal(probe.options.timeout, 120_000, `go ${probe.arguments_.join(" ")} keeps the probe bound`);
815
905
  });