gentle-pi 3.7.0 → 4.0.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 (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -95,9 +95,9 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
95
95
  await writeFile(manifestPath, `${JSON.stringify({
96
96
  version: GENTLE_AI_VERSION,
97
97
  method: "go-sumdb-source-build",
98
- package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
99
- module: "github.com/gentleman-programming/gentle-ai/v3",
100
- tag: "v3.7.0",
98
+ package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
99
+ module: "github.com/gentleman-programming/gentle-ai/v4",
100
+ tag: "v4.0.0",
101
101
  architecture: process.arch === "x64" ? "x64" : "arm64",
102
102
  binarySha256: createHash("sha256").update(binary).digest("hex"),
103
103
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ GENTLE_AI_TIMING_ENTRY,
5
+ GentleAiElapsedTimingLedger,
6
+ parseGentleAiTimingData,
7
+ readGentleAiTimings,
8
+ type GentleAiTimingEntry,
9
+ } from "../lib/gentle-ai-elapsed-store.ts";
10
+
11
+ function entry(data: unknown): GentleAiTimingEntry {
12
+ return { type: "custom", customType: GENTLE_AI_TIMING_ENTRY, data };
13
+ }
14
+
15
+ test("timing entry parsing accepts well-formed records and rejects the rest", () => {
16
+ assert.deepEqual(parseGentleAiTimingData({ toolCallId: "c1", startedAt: 1 }), { toolCallId: "c1", startedAt: 1 });
17
+ assert.deepEqual(parseGentleAiTimingData({ toolCallId: "c1", startedAt: 1, endedAt: 2 }), { toolCallId: "c1", startedAt: 1, endedAt: 2 });
18
+ for (const bad of [undefined, null, "x", 5, [], {}, { startedAt: 1 }, { toolCallId: "" }, { toolCallId: "c1" }, { toolCallId: "c1", startedAt: "x" }, { toolCallId: "c1", startedAt: Number.NaN }, { toolCallId: "c1", startedAt: 1, endedAt: "x" }]) {
19
+ assert.equal(parseGentleAiTimingData(bad), undefined, JSON.stringify(bad) ?? String(bad));
20
+ }
21
+ });
22
+
23
+ test("replay keeps the last timing entry per tool call and ignores foreign entries", () => {
24
+ const entries: GentleAiTimingEntry[] = [
25
+ { type: "message", customType: undefined, data: null },
26
+ { type: "custom", customType: "gentle-pi.session-worktree/v1", data: { root: "/x" } },
27
+ entry({ toolCallId: "a", startedAt: 1 }),
28
+ entry({ toolCallId: "a", startedAt: 1, endedAt: 9 }),
29
+ entry({ toolCallId: "b", startedAt: 2 }),
30
+ entry({ toolCallId: "a", startedAt: "corrupt" }),
31
+ ];
32
+ const timings = readGentleAiTimings(entries);
33
+ assert.deepEqual(timings.get("a"), { toolCallId: "a", startedAt: 1, endedAt: 9 }, "the corrupt last entry is ignored, not merged");
34
+ assert.deepEqual(timings.get("b"), { toolCallId: "b", startedAt: 2 });
35
+ assert.equal(timings.size, 2);
36
+ });
37
+
38
+ test("the ledger persists start and end once each and skips ends without a durable start", () => {
39
+ const appended: Array<{ type: string; data?: unknown }> = [];
40
+ const host = { appendEntry: (type: string, data?: unknown) => appended.push({ type, data }) };
41
+ let stored: GentleAiTimingEntry[] = [];
42
+ const session = { getEntries: () => stored };
43
+ const ledger = new GentleAiElapsedTimingLedger(session, host);
44
+ ledger.recordEnd("call-1", 500);
45
+ assert.equal(appended.length, 0, "a terminal without a durable start has nothing honest to freeze");
46
+ ledger.recordStart("call-1", 1_000);
47
+ ledger.recordStart("call-1", 2_000);
48
+ assert.equal(appended.length, 1, "a repeated start stage does not append again");
49
+ stored = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
50
+ ledger.recordEnd("call-1", 31_000);
51
+ ledger.recordEnd("call-1", 32_000);
52
+ stored = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
53
+ assert.equal(appended.length, 2, "a repeated end stage does not append again");
54
+ assert.deepEqual(appended.map((append) => append.data), [
55
+ { toolCallId: "call-1", startedAt: 1_000 },
56
+ { toolCallId: "call-1", startedAt: 1_000, endedAt: 31_000 },
57
+ ]);
58
+ assert.deepEqual(ledger.lookup("call-1"), { toolCallId: "call-1", startedAt: 1_000, endedAt: 31_000 });
59
+ });
60
+
61
+ test("a fresh ledger restores timings recorded by a previous one", () => {
62
+ const appended: Array<{ type: string; data?: unknown }> = [];
63
+ const first = new GentleAiElapsedTimingLedger({ getEntries: () => [] }, { appendEntry: (type, data) => appended.push({ type, data }) });
64
+ first.recordStart("call-1", 1_000);
65
+ const stored: GentleAiTimingEntry[] = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
66
+ const second = new GentleAiElapsedTimingLedger({ getEntries: () => stored });
67
+ assert.deepEqual(second.lookup("call-1"), { toolCallId: "call-1", startedAt: 1_000 });
68
+ });
@@ -12,6 +12,7 @@ import {
12
12
  GENTLE_AI_PENDING_DIGEST,
13
13
  INSTALLER_VERSION,
14
14
  GENTLE_AI_RELEASE_ASSETS,
15
+ GENTLE_AI_WINDOWS_SOURCE_MODULE,
15
16
  GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
16
17
  downloadGentleAiAsset,
17
18
  gentleAiAssetForm,
@@ -21,18 +22,20 @@ import {
21
22
  trustedSystemExtractor,
22
23
  } from "../scripts/gentle-ai-installer.mjs";
23
24
 
24
- // v3.7.0 archive digests independently match the minisign-signed release
25
+ // v4.0.0 archive digests independently match the minisign-signed release
25
26
  // checksums; binary digests were computed from the extracted executables.
26
27
  const EXPECTED_ASSETS = {
27
- "darwin/amd64": { name: "gentle-ai_3.7.0_darwin_amd64.tar.gz", sha256: "e55ff3ee06258a90e9195c0e3a593b85f6d79cc439e9e6c04b2596725120a610", binarySha256: "aa30a940e3b75ac218249b3f92d17afd2b972f833258e8212175a73d8e76d5bf" },
28
- "darwin/arm64": { name: "gentle-ai_3.7.0_darwin_arm64.tar.gz", sha256: "66eb5740c4506cb2cfd1cc5207439c61cfe07678854f4ac7c83027a95a0e666a", binarySha256: "ca726cf3f9a523dc0112aa113beb634cda389e8b47edc5851254c32979bee89e" },
29
- "linux/amd64": { name: "gentle-ai_3.7.0_linux_amd64.tar.gz", sha256: "a730a61a43758f04cc9a4ac644945cc0e8652a1e33d6997a0a3d3f0044d2fff5", binarySha256: "002d09fd2b9628a29986a660c1f51f8a5042ff7fd54c8ab15b7b27de96c6cccc" },
30
- "linux/arm64": { name: "gentle-ai_3.7.0_linux_arm64.tar.gz", sha256: "a3a3d3a974f3d9b67d935fe9e306ae83c305da4ec1baed4a5319c10b044cd0eb", binarySha256: "e29546c51d8d65528bf565239ed3fc174da73c5fa3e861e64f20f84b16f28f26" },
28
+ "darwin/amd64": { name: "gentle-ai_4.0.0_darwin_amd64.tar.gz", sha256: "b5b74f22b38ec3339b38e8c68f797dc76ff12ed6580826a6e425d5c718da80c1", binarySha256: "d4a5b16ff70e65331e17a62356941bb0c75ecb9dbe3a0d98a6b54cfbd76cd6b0" },
29
+ "darwin/arm64": { name: "gentle-ai_4.0.0_darwin_arm64.tar.gz", sha256: "d2159caf6d68f367b18830ece6af71ef26963d5f5320d7df6a794773f45cc7e9", binarySha256: "18a9f7fae55d85c95684b6d512a4a148d0cb24a856325f72573c34caf65159eb" },
30
+ "linux/amd64": { name: "gentle-ai_4.0.0_linux_amd64.tar.gz", sha256: "5f4417cf29c969c86da4799942fd673368840901be1bb09c779a12d7ed6096ea", binarySha256: "50ba217b5138c1a9c7d5bf2f79931b1bb89b89c4cf650dcd7ee037657c88158d" },
31
+ "linux/arm64": { name: "gentle-ai_4.0.0_linux_arm64.tar.gz", sha256: "1383b040c95cfc69206660d73c21907b14ad70ab913f410917c54f43d3147e57", binarySha256: "6703704f0c4a5b70c36fbdc44db641e810871cab16bc28040d06aa1d10704ad3" },
31
32
  } as const;
32
33
 
33
- test("published Gentle AI v3.7.0 is the installer pin", () => {
34
- assert.equal(INSTALLER_VERSION, "3.7.0");
35
- assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:MQbzHlLdPklUQn0rVE9Mz94UygHsN2OPe7xMfPn9aGw=");
34
+ test("published Gentle AI v4.0.0 is the installer pin", () => {
35
+ assert.equal(INSTALLER_VERSION, "4.0.0");
36
+ // v4.0.0 moved the Go module path to the /v4 major-version suffix.
37
+ assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4");
38
+ assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:pZ/XZ2Pk3U9lgXigOTY62zlxxFOHnc9CjQhLgaV/Hfc=");
36
39
  });
37
40
 
38
41
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -43,15 +46,15 @@ test("default installer package root is the package containing scripts, not its
43
46
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
44
47
  });
45
48
 
46
- test("release mapping selects only the supported official v3.7.0 assets and pinned digests", () => {
49
+ test("release mapping selects only the supported official v4.0.0 assets and pinned digests", () => {
47
50
  assert.deepEqual(
48
51
  Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
49
52
  EXPECTED_ASSETS,
50
53
  );
51
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_3.7.0_linux_amd64.tar.gz");
52
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_3.7.0_darwin_arm64.tar.gz");
54
+ assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.0.0_linux_amd64.tar.gz");
55
+ assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.0.0_darwin_arm64.tar.gz");
53
56
  for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
54
- assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v3\.7\.0\//);
57
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.0\.0\//);
55
58
  }
56
59
  });
57
60
 
@@ -61,7 +64,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
61
64
  assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
62
65
  // A raw binary under a stable pin means the pin itself is wrong: stable
63
66
  // releases publish signed archives only, so this fails closed pre-download.
64
- assert.throws(() => gentleAiAssetForm("gentle-ai_3.7.0_linux_amd64", "3.7.0"), /only admitted for a prerelease pin/);
67
+ assert.throws(() => gentleAiAssetForm("gentle-ai_4.0.0_linux_amd64", "4.0.0"), /only admitted for a prerelease pin/);
65
68
  assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
66
69
  // The current stable pin admits every pinned asset row through the same
67
70
  // gate the installer uses at download time (default installerVersion
@@ -88,7 +91,7 @@ test("release digests are all-or-none and install fails closed while any digest
88
91
  }),
89
92
  /checksum mismatch/,
90
93
  );
91
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
94
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
92
95
  }
93
96
  });
94
97
 
@@ -116,8 +119,8 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
116
119
  let goExecutable = "go";
117
120
  const metadata = [
118
121
  "gentle-ai.exe: go1.25.10",
119
- "\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
120
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.7.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
122
+ "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
123
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
121
124
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
122
125
  ].join("\n");
123
126
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -135,7 +138,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
135
138
  return { stdout: "", stderr: "" };
136
139
  }
137
140
  if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
138
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 3.7.0\n", stderr: "" };
141
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.0.0\n", stderr: "" };
139
142
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
140
143
  };
141
144
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -151,7 +154,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
151
154
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
152
155
  assert.equal(result.installed, true);
153
156
  assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
154
- ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.7.0"], ["version", "-m"],
157
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"], ["version", "-m"],
155
158
  ]);
156
159
  }
157
160
  });
@@ -170,7 +173,7 @@ test("Windows source install reports missing or too-old Go without publishing a
170
173
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
171
174
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
172
175
  );
173
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai.exe")), false);
176
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
174
177
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
175
178
  }
176
179
  });
@@ -189,7 +192,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
189
192
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
190
193
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
191
194
  );
192
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v3.7.0");
195
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
193
196
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
194
197
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
195
198
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -210,7 +213,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
210
213
  assert.equal(reused.installed, false);
211
214
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
212
215
 
213
- await writeFile(join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json"), "{}\n");
216
+ await writeFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "{}\n");
214
217
  const repaired = windowsGoFixture();
215
218
  repaired.setGoExecutable(goPath);
216
219
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -251,8 +254,8 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
251
254
  : undefined;
252
255
  const metadata = fixtureOptions.metadataOverride ?? [
253
256
  "gentle-ai.exe: go1.25.10",
254
- "\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
255
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.7.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
257
+ "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
258
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
256
259
  "\tbuild\t-buildmode=exe",
257
260
  "\tbuild\t-compiler=gc",
258
261
  "\tbuild\tCGO_ENABLED=0",
@@ -272,7 +275,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
272
275
  return { stdout: "", stderr: "" };
273
276
  }
274
277
  if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
275
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 3.7.0\n", stderr: "" };
278
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.0.0\n", stderr: "" };
276
279
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
277
280
  };
278
281
  return {
@@ -314,7 +317,7 @@ test("Windows source installation resolves one validated Go executable and seals
314
317
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
315
318
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
316
319
  ["version"],
317
- ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.7.0"],
320
+ ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"],
318
321
  ["version", "-m"],
319
322
  ]);
320
323
  for (const call of goCalls) {
@@ -340,14 +343,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
340
343
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
341
344
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
342
345
  const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
343
- const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json"), "utf8")) as Record<string, string>;
346
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "utf8")) as Record<string, string>;
344
347
  assert.equal(result.installed, true);
345
348
  assert.deepEqual(manifest, {
346
- version: "3.7.0",
349
+ version: "4.0.0",
347
350
  method: "go-sumdb-source-build",
348
- package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
349
- module: "github.com/gentleman-programming/gentle-ai/v3",
350
- tag: "v3.7.0",
351
+ package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
352
+ module: "github.com/gentleman-programming/gentle-ai/v4",
353
+ tag: "v4.0.0",
351
354
  architecture: "arm64",
352
355
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
353
356
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -367,12 +370,12 @@ test("Windows source installation rejects Go metadata for a different architectu
367
370
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
368
371
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
369
372
  );
370
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai.exe")), false);
373
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
371
374
  });
372
375
 
373
376
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
374
377
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
375
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
378
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
376
379
  await mkdir(lockPath, { recursive: true });
377
380
  const fixture = await hardenedWindowsGoFixture(packageRoot);
378
381
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -380,7 +383,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
380
383
 
381
384
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
382
385
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
383
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock", "owner.json");
386
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock", "owner.json");
384
387
  const fixture = await hardenedWindowsGoFixture(packageRoot);
385
388
  let replacedOwner = false;
386
389
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -397,7 +400,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
397
400
  });
398
401
 
399
402
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
400
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
403
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
401
404
  await assert.rejects(
402
405
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
403
406
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -407,18 +410,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
407
410
  }
408
411
 
409
412
  function tombstonePath(packageRoot: string, nonce: string): string {
410
- return join(packageRoot, ".gentle-ai", `.v3.7.0.install.tombstone-${nonce}`);
413
+ return join(packageRoot, ".gentle-ai", `.v4.0.0.install.tombstone-${nonce}`);
411
414
  }
412
415
 
413
416
  async function tombstones(packageRoot: string): Promise<string[]> {
414
417
  const runtimeRoot = join(packageRoot, ".gentle-ai");
415
418
  return existsSync(runtimeRoot)
416
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v3.7.0.install.tombstone-"))
419
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.0.0.install.tombstone-"))
417
420
  : [];
418
421
  }
419
422
 
420
423
  function backupBundlePath(packageRoot: string, nonce: string): string {
421
- return join(packageRoot, ".gentle-ai", `.v3.7.0.backup-${nonce}`);
424
+ return join(packageRoot, ".gentle-ai", `.v4.0.0.backup-${nonce}`);
422
425
  }
423
426
 
424
427
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -429,7 +432,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
429
432
 
430
433
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
431
434
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
432
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
435
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
433
436
  await mkdir(lockPath, { recursive: true });
434
437
  const fixture = await hardenedWindowsGoFixture(packageRoot);
435
438
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -437,7 +440,7 @@ test("Windows source installation fails closed for an ownerless lock even after
437
440
 
438
441
  test("Windows source installation fails closed for a stale owner lock", async () => {
439
442
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
440
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
443
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
441
444
  await mkdir(lockPath, { recursive: true });
442
445
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
443
446
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -489,7 +492,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
489
492
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
490
493
  );
491
494
  assert.equal(existsSync(foreignTombstone), true);
492
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock")), false);
495
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
493
496
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
494
497
  });
495
498
 
@@ -497,7 +500,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
497
500
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
498
501
  const fixture = await hardenedWindowsGoFixture(packageRoot);
499
502
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
500
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock")), false);
503
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
501
504
  assert.deepEqual(await tombstones(packageRoot), []);
502
505
  const reuse = await hardenedWindowsGoFixture(packageRoot);
503
506
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -507,7 +510,7 @@ test("Windows source recovers a valid backup after a crash between publication r
507
510
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
508
511
  const initial = await hardenedWindowsGoFixture(packageRoot);
509
512
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
510
- const live = join(packageRoot, ".gentle-ai", "v3.7.0");
513
+ const live = join(packageRoot, ".gentle-ai", "v4.0.0");
511
514
  const backup = backupBundlePath(packageRoot, "crash");
512
515
  await rename(live, backup);
513
516
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -543,7 +546,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
543
546
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
544
547
  const initial = await hardenedWindowsGoFixture(packageRoot);
545
548
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
546
- const live = join(packageRoot, ".gentle-ai", "v3.7.0");
549
+ const live = join(packageRoot, ".gentle-ai", "v4.0.0");
547
550
  const backup = backupBundlePath(packageRoot, "valid");
548
551
  await copyWindowsBundle(live, backup);
549
552
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -556,7 +559,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
556
559
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
557
560
  const initial = await hardenedWindowsGoFixture(packageRoot);
558
561
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
559
- const live = join(packageRoot, ".gentle-ai", "v3.7.0");
562
+ const live = join(packageRoot, ".gentle-ai", "v4.0.0");
560
563
  const backup = backupBundlePath(packageRoot, "valid");
561
564
  await copyWindowsBundle(live, backup);
562
565
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -574,7 +577,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
574
577
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
575
578
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
576
579
  await fixture.waitForInstall();
577
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
580
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
578
581
  await assert.rejects(
579
582
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
580
583
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -588,7 +591,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
588
591
 
589
592
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
590
593
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
591
- const versionDirectory = join(packageRoot, ".gentle-ai", "v3.7.0");
594
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
592
595
  await mkdir(versionDirectory, { recursive: true });
593
596
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
594
597
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -625,7 +628,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
625
628
  },
626
629
  };
627
630
  await installGentleAi(options);
628
- const manifestPath = join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json");
631
+ const manifestPath = join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json");
629
632
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
630
633
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
631
634
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
@@ -638,7 +641,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
638
641
  // under the current stable pin, whose form gate refuses raw binaries.
639
642
  const payload = Buffer.from("signed archive fixture");
640
643
  const binary = "mirrored binary";
641
- const baseAsset = { name: "gentle-ai_3.7.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" };
644
+ const baseAsset = { name: "gentle-ai_4.0.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" };
642
645
  const extractArchive = async (_archive: string, destination: string) => {
643
646
  await mkdir(destination, { recursive: true });
644
647
  await writeFile(join(destination, "gentle-ai"), binary);
@@ -756,7 +759,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
756
759
  }),
757
760
  /checksum mismatch/,
758
761
  );
759
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
762
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
760
763
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
761
764
  });
762
765
 
@@ -778,7 +781,7 @@ test("installer promotes only the expected regular executable with executable PO
778
781
  await chmod(extracted, 0o700);
779
782
  },
780
783
  });
781
- const binary = join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai");
784
+ const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
782
785
  assert.equal(existsSync(binary), true);
783
786
  assert.equal(await readFile(binary, "utf8"), "native executable");
784
787
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -809,7 +812,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
809
812
  }),
810
813
  /binary checksum mismatch/,
811
814
  );
812
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
815
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
813
816
  });
814
817
 
815
818
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -832,7 +835,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
832
835
  },
833
836
  };
834
837
  await installGentleAi(options);
835
- const binary = join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai");
838
+ const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
836
839
  await chmod(binary, 0o600);
837
840
  const repaired = await installGentleAi(options);
838
841
  assert.equal(repaired.installed, true);
@@ -869,7 +872,7 @@ test("installer rejects archives with multiple expected executable entries", asy
869
872
  }),
870
873
  /exactly one regular gentle-ai/,
871
874
  );
872
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
875
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
873
876
  });
874
877
 
875
878
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -887,7 +890,18 @@ test("installer rejects an archive without the expected regular executable", asy
887
890
  }),
888
891
  /non-regular gentle-ai/,
889
892
  );
890
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
893
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
894
+ });
895
+
896
+ test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => {
897
+ const installerSource = await readFile(new URL("../scripts/gentle-ai-installer.mjs", import.meta.url), "utf8");
898
+ const match = /async function safeRemoveDirectory\(path\) \{(?<body>[\s\S]*?)\n\}/.exec(installerSource);
899
+ assert.ok(match?.groups?.body, "safeRemoveDirectory must remain available for scoped cleanup verification");
900
+ assert.match(
901
+ match.groups.body,
902
+ /await rm\(path, \{ recursive: true, force: true, maxRetries: 3, retryDelay: 100 \}\);/,
903
+ "safeRemoveDirectory cleanup must retry Windows EPERM failures from read-only Go module cache files",
904
+ );
891
905
  });
892
906
 
893
907
  // #400: a clean Windows source build measured 232 s on a cold module cache,