gentle-pi 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +336 -137
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/gentle-shell.mjs +170 -22
  12. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  13. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  15. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  16. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  18. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  19. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  20. package/docs/assets/diagrams/measured-routing.svg +49 -0
  21. package/docs/assets/diagrams/native-review.svg +52 -12
  22. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  23. package/docs/assets/diagrams/permission-modes.svg +51 -0
  24. package/docs/assets/features/audio-notifications.png +0 -0
  25. package/docs/assets/features/command-palette.png +0 -0
  26. package/docs/bundled-install.md +33 -3
  27. package/docs/delegated-verification.md +10 -8
  28. package/docs/gentle-agents-activity.md +11 -5
  29. package/docs/gentle-shell-uninstall.md +8 -1
  30. package/docs/gentle-shell.md +5 -4
  31. package/docs/install-wizard.md +8 -1
  32. package/docs/permission-modes.md +1 -1
  33. package/docs/readme-reference.md +107 -19
  34. package/docs/yolo-mode.md +21 -8
  35. package/extensions/codegraph-tools.ts +68 -6
  36. package/extensions/gentle-agents.ts +99 -12
  37. package/extensions/gentle-ai.ts +284 -52
  38. package/extensions/gentle-herdr-activity.ts +2 -3
  39. package/extensions/gentle-shell.ts +27 -12
  40. package/extensions/gentle-todo.ts +43 -9
  41. package/extensions/quiet-tools.ts +1 -1
  42. package/extensions/session-scratch.ts +365 -0
  43. package/extensions/startup-banner.ts +2 -3
  44. package/lib/agent-home.ts +8 -3
  45. package/lib/agents-protocol.ts +3 -0
  46. package/lib/agents-runner.ts +55 -6
  47. package/lib/agents-view.ts +3 -1
  48. package/lib/bounded-writer-admission.ts +51 -0
  49. package/lib/command-palette-catalog.ts +0 -4
  50. package/lib/commit-slice-signal.ts +100 -0
  51. package/lib/context-backstop.ts +84 -0
  52. package/lib/delivered-test-lint.ts +80 -0
  53. package/lib/destructive-command-guard.ts +120 -9
  54. package/lib/gauge-theme.ts +146 -0
  55. package/lib/gentle-ai-binary.ts +28 -7
  56. package/lib/gentle-shell-bundled.ts +39 -3
  57. package/lib/gentle-shell-import-pi.ts +233 -0
  58. package/lib/gentle-shell-launcher.ts +145 -11
  59. package/lib/gentle-shell-uninstall.ts +18 -1
  60. package/lib/herdr-activity.ts +24 -0
  61. package/lib/native-review-cli.ts +9 -1
  62. package/lib/orchestrator-discovery.ts +5 -1
  63. package/lib/orchestrator-presence.ts +9 -0
  64. package/lib/orchestrator-scope.ts +21 -3
  65. package/lib/provider-contract-bundle.ts +99 -53
  66. package/lib/review-candidate-view-owner.ts +29 -19
  67. package/lib/review-candidate-view.ts +36 -2
  68. package/lib/review-risk-assessment.ts +43 -38
  69. package/lib/review-session-standing-permission.ts +3 -1
  70. package/lib/session-profile-freeze.ts +30 -1
  71. package/lib/session-scratch.ts +474 -0
  72. package/lib/shell-bar.ts +25 -25
  73. package/lib/shell-changes-view.ts +80 -13
  74. package/lib/shell-gauge.ts +49 -6
  75. package/lib/shell-todo.ts +23 -4
  76. package/lib/shell-usage-view.ts +5 -1
  77. package/lib/shell-usage.ts +7 -7
  78. package/lib/verifier-admission.ts +29 -0
  79. package/lib/yolo-temp-scratch.ts +69 -0
  80. package/package.json +1 -2
  81. package/runtime/gentle-ai-binary.mjs +28 -7
  82. package/runtime/gentle-shell-bundled.mjs +38 -2
  83. package/runtime/gentle-shell-import-pi.mjs +234 -0
  84. package/runtime/gentle-shell-launcher.mjs +143 -9
  85. package/runtime/gentle-shell-uninstall.mjs +18 -1
  86. package/runtime/native-review-cli.mjs +8 -0
  87. package/runtime/review-risk-assessment.mjs +43 -38
  88. package/scripts/build-runtime-modules.mjs +1 -0
  89. package/scripts/bundled-engram-migration.mjs +125 -0
  90. package/scripts/gentle-ai-installer.mjs +10 -10
  91. package/scripts/installer-server.mjs +14 -4
  92. package/scripts/main-channel.mjs +3 -2
  93. package/scripts/run-test-suite.mjs +5 -0
  94. package/scripts/verify-package-files.mjs +19 -13
  95. package/skills/gentle-ai/SKILL.md +3 -3
  96. package/skills/work-unit-commits/SKILL.md +5 -3
  97. package/tests/agent-risk-escalation.test.ts +37 -10
  98. package/tests/agents-runner.test.ts +260 -3
  99. package/tests/autonomous-guard.test.ts +122 -0
  100. package/tests/bounded-writer-admission.test.ts +11 -3
  101. package/tests/card-content-policy.test.ts +19 -9
  102. package/tests/child-safety.test.ts +39 -0
  103. package/tests/codegraph-tools.test.ts +123 -2
  104. package/tests/command-palette.test.ts +14 -4
  105. package/tests/commit-slice-signal.test.ts +162 -0
  106. package/tests/context-backstop.test.ts +159 -0
  107. package/tests/delegate-for-reason-contract.test.ts +91 -24
  108. package/tests/delivered-test-lint.test.ts +105 -0
  109. package/tests/destructive-command-guard.test.ts +85 -1
  110. package/tests/engram-safety.test.ts +1 -1
  111. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  115. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  116. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  117. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  118. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  119. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  120. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  121. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  122. package/tests/gauge-theme.test.ts +120 -0
  123. package/tests/gentle-agents.test.ts +282 -3
  124. package/tests/gentle-ai-binary.test.ts +1 -1
  125. package/tests/gentle-ai-child-guards.test.ts +61 -0
  126. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  127. package/tests/gentle-ai-installer.test.ts +51 -51
  128. package/tests/gentle-shell-bin.test.ts +163 -0
  129. package/tests/gentle-shell-bundled.test.ts +169 -6
  130. package/tests/gentle-shell-import-pi.test.ts +262 -0
  131. package/tests/gentle-shell-launcher.test.ts +79 -0
  132. package/tests/gentle-shell-uninstall.test.ts +30 -0
  133. package/tests/gentle-shell.test.ts +42 -9
  134. package/tests/gentle-todo.test.ts +179 -0
  135. package/tests/headless-delivery-question.test.ts +58 -0
  136. package/tests/herdr-activity.test.ts +34 -1
  137. package/tests/installer-server.test.ts +10 -2
  138. package/tests/installer-windows-bootstrap.test.ts +33 -4
  139. package/tests/native-review-capability-contract.test.ts +7 -1
  140. package/tests/native-review-parity.test.ts +22 -1
  141. package/tests/no-self-pause-contract.test.ts +5 -2
  142. package/tests/odd-integration.test.ts +5 -1
  143. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  144. package/tests/odd-routing-contract.test.ts +115 -30
  145. package/tests/orchestrator-budget.test.ts +19 -8
  146. package/tests/orchestrator-discovery.test.ts +29 -0
  147. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  148. package/tests/orchestrator-scope.test.ts +66 -0
  149. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  150. package/tests/package-manifest.test.ts +24 -20
  151. package/tests/pi-update-launcher.test.ts +21 -2
  152. package/tests/primary-session-prompt.test.ts +101 -0
  153. package/tests/provider-contract-bundle.test.ts +118 -0
  154. package/tests/provider-contract-mirror.test.ts +24 -0
  155. package/tests/quiet-tool-rendering.test.ts +23 -0
  156. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  157. package/tests/rdd-status-line.test.ts +79 -0
  158. package/tests/review-agent-end-preflight.test.ts +6 -0
  159. package/tests/review-candidate-view.test.ts +49 -0
  160. package/tests/review-contract-prompt.test.ts +5 -2
  161. package/tests/review-controller-native-routing.test.ts +14 -1
  162. package/tests/review-controller.test.ts +1 -1
  163. package/tests/review-host-relay-routing.test.ts +59 -2
  164. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  165. package/tests/review-owner-identity.test.ts +132 -0
  166. package/tests/review-risk-assessment.test.ts +163 -76
  167. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  168. package/tests/review-session-standing-permission.test.ts +27 -1
  169. package/tests/run-test-suite.test.ts +15 -0
  170. package/tests/runtime-harness.mjs +16 -6
  171. package/tests/session-changes-shell.test.ts +1 -1
  172. package/tests/session-profile-freeze.test.ts +23 -0
  173. package/tests/session-scratch.test.ts +945 -0
  174. package/tests/session-subject-routing-contract.test.ts +2 -1
  175. package/tests/shell-bar.test.ts +7 -0
  176. package/tests/shell-changes-view.test.ts +137 -1
  177. package/tests/shell-gauge.test.ts +42 -0
  178. package/tests/small-task-inline-contract.test.ts +37 -0
  179. package/tests/standalone-config-home.test.ts +324 -0
  180. package/tests/startup-banner.test.ts +5 -4
  181. package/tests/startup-home-repo.test.ts +34 -0
  182. package/tests/support/orchestrator-modules.ts +1 -0
  183. package/tests/task-size-routing-contract.test.ts +69 -9
  184. package/tests/test-scratch-contract.test.ts +102 -0
  185. package/tests/test-strength-contract.test.ts +36 -1
  186. package/tests/tool-batching-contract.test.ts +64 -0
  187. package/tests/verification-proportionality.test.ts +46 -0
  188. package/tests/verifier-admission.test.ts +137 -0
  189. package/tests/verify-agent-contract.test.ts +8 -8
  190. package/tests/windows-hidden-processes.test.ts +5 -0
  191. package/tests/writer-edit-surface-scope.test.ts +3 -0
  192. package/tests/writer-feature-document-admission.test.ts +156 -0
  193. package/tests/yolo-mode.test.ts +18 -0
  194. package/tests/yolo-temp-scratch.test.ts +71 -0
  195. package/scripts/mirror-odd-routing.mjs +0 -242
  196. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  197. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  198. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  199. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  200. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  201. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  202. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -24,7 +24,7 @@ import {
24
24
  trustedSystemExtractor,
25
25
  } from "../scripts/gentle-ai-installer.mjs";
26
26
 
27
- // v4.1.1 archive digests independently match the minisign-signed release
27
+ // v4.2.0 archive digests independently match the minisign-signed release
28
28
  // checksums; binary digests were computed from the extracted executables.
29
29
  function releaseAssets(): Record<string, { name: string; sha256: string; binarySha256: string; url: string }> {
30
30
  const assets: Record<string, { name: string; sha256: string; binarySha256: string; url: string }> = {};
@@ -40,17 +40,17 @@ function releaseAssets(): Record<string, { name: string; sha256: string; binaryS
40
40
  }
41
41
 
42
42
  const EXPECTED_ASSETS = {
43
- "darwin/amd64": { name: "gentle-ai_4.1.1_darwin_amd64.tar.gz", sha256: "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", binarySha256: "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338" },
44
- "darwin/arm64": { name: "gentle-ai_4.1.1_darwin_arm64.tar.gz", sha256: "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", binarySha256: "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a" },
45
- "linux/amd64": { name: "gentle-ai_4.1.1_linux_amd64.tar.gz", sha256: "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", binarySha256: "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe" },
46
- "linux/arm64": { name: "gentle-ai_4.1.1_linux_arm64.tar.gz", sha256: "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", binarySha256: "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55" },
43
+ "darwin/amd64": { name: "gentle-ai_4.2.0_darwin_amd64.tar.gz", sha256: "8f6a91e0f99cac3aea138705d7d222ea342f53a47602d48cb01488952a1c12f8", binarySha256: "784855e99f1bc45579f01420c923b7dc1dc6d63492cdc0174a9a32e611849e57" },
44
+ "darwin/arm64": { name: "gentle-ai_4.2.0_darwin_arm64.tar.gz", sha256: "d5e9a6521de5f08a393635f083f64dc7afc1b8e97884f7aab5ae5eb1f17c7f37", binarySha256: "8ea490287ef796848b0d305cf2b26265e360d3f400efc016972844498fb87097" },
45
+ "linux/amd64": { name: "gentle-ai_4.2.0_linux_amd64.tar.gz", sha256: "552c1642fd10f5d96cfe9bb1cc3b66b30eb47d3074cc7ca0559b598eb086defc", binarySha256: "9212c7b16ea9abfe93e3a16b29d8390e7762b5c07730cf05df1f3e7892ff96a4" },
46
+ "linux/arm64": { name: "gentle-ai_4.2.0_linux_arm64.tar.gz", sha256: "2bebe91b8808ce48d86c4d3700d098e37cb6b0658f196c666d9deeeb7c7c1a7a", binarySha256: "e1b88bd3dcdc8a15ddbcaf8731fd44561f05b06fbcb59d9385bbe82c4b96b5c4" },
47
47
  } as const;
48
48
 
49
- test("published Gentle AI v4.1.1 is the installer pin", () => {
50
- assert.equal(INSTALLER_VERSION, "4.1.1");
49
+ test("published Gentle AI v4.2.0 is the installer pin", () => {
50
+ assert.equal(INSTALLER_VERSION, "4.2.0");
51
51
  // v4.0.0 moved the Go module path to the /v4 major-version suffix.
52
52
  assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4");
53
- assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=");
53
+ assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=");
54
54
  });
55
55
 
56
56
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -61,15 +61,15 @@ test("default installer package root is the package containing scripts, not its
61
61
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
62
62
  });
63
63
 
64
- test("release mapping selects only the supported official v4.1.1 assets and pinned digests", () => {
64
+ test("release mapping selects only the supported official v4.2.0 assets and pinned digests", () => {
65
65
  assert.deepEqual(
66
66
  Object.fromEntries(Object.entries(releaseAssets()).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
67
67
  EXPECTED_ASSETS,
68
68
  );
69
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.1.1_linux_amd64.tar.gz");
70
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.1.1_darwin_arm64.tar.gz");
69
+ assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.2.0_linux_amd64.tar.gz");
70
+ assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.2.0_darwin_arm64.tar.gz");
71
71
  for (const asset of Object.values(releaseAssets())) {
72
- assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.1\.1\//);
72
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.2\.0\//);
73
73
  }
74
74
  });
75
75
 
@@ -79,7 +79,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
79
79
  assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
80
80
  // A raw binary under a stable pin means the pin itself is wrong: stable
81
81
  // releases publish signed archives only, so this fails closed pre-download.
82
- assert.throws(() => gentleAiAssetForm("gentle-ai_4.1.1_linux_amd64", "4.1.1"), /only admitted for a prerelease pin/);
82
+ assert.throws(() => gentleAiAssetForm("gentle-ai_4.2.0_linux_amd64", "4.2.0"), /only admitted for a prerelease pin/);
83
83
  assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
84
84
  // The current stable pin admits every pinned asset row through the same
85
85
  // gate the installer uses at download time (default installerVersion
@@ -106,7 +106,7 @@ test("release digests are all-or-none and install fails closed while any digest
106
106
  }),
107
107
  /checksum mismatch/,
108
108
  );
109
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
109
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai")), false);
110
110
  }
111
111
  });
112
112
 
@@ -136,7 +136,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
136
136
  const metadata = [
137
137
  "gentle-ai.exe: go1.25.10",
138
138
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
139
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
139
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.2.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
140
140
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
141
141
  ].join("\n");
142
142
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -154,7 +154,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
154
154
  return { stdout: "", stderr: "" };
155
155
  }
156
156
  if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
157
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.1.1\n", stderr: "" };
157
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.2.0\n", stderr: "" };
158
158
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
159
159
  };
160
160
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -170,7 +170,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
170
170
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
171
171
  assert.equal(result.installed, true);
172
172
  assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
173
- ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"], ["version", "-m"],
173
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.2.0"], ["version", "-m"],
174
174
  ]);
175
175
  }
176
176
  });
@@ -189,7 +189,7 @@ test("Windows source install reports missing or too-old Go without publishing a
189
189
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
190
190
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
191
191
  );
192
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
192
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai.exe")), false);
193
193
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
194
194
  }
195
195
  });
@@ -235,7 +235,7 @@ test("Windows source install builds in a short private directory under the temp
235
235
  }
236
236
  for (const call of fixture.calls.filter((entry) => entry.file === goPath)) assert.ok(call.options.cwd?.startsWith(buildDirectory), call.arguments_.join(" "));
237
237
  assert.deepEqual(await readdir(temporaryDirectory), [], "the build directory is removed");
238
- if (!installError) assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), true, "the verified binary is published into the package");
238
+ if (!installError) assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai.exe")), true, "the verified binary is published into the package");
239
239
  } finally {
240
240
  await rm(packageRoot, { recursive: true, force: true });
241
241
  await rm(temporaryDirectory, { recursive: true, force: true });
@@ -257,7 +257,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
257
257
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
258
258
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
259
259
  );
260
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
260
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.2.0");
261
261
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
262
262
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
263
263
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -278,7 +278,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
278
278
  assert.equal(reused.installed, false);
279
279
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
280
280
 
281
- await writeFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "{}\n");
281
+ await writeFile(join(packageRoot, ".gentle-ai", "v4.2.0", "integrity.json"), "{}\n");
282
282
  const repaired = windowsGoFixture();
283
283
  repaired.setGoExecutable(goPath);
284
284
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -320,7 +320,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
320
320
  const metadata = fixtureOptions.metadataOverride ?? [
321
321
  "gentle-ai.exe: go1.25.10",
322
322
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
323
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
323
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.2.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
324
324
  "\tbuild\t-buildmode=exe",
325
325
  "\tbuild\t-compiler=gc",
326
326
  "\tbuild\tCGO_ENABLED=0",
@@ -340,7 +340,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
340
340
  return { stdout: "", stderr: "" };
341
341
  }
342
342
  if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
343
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.1.1\n", stderr: "" };
343
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.2.0\n", stderr: "" };
344
344
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
345
345
  };
346
346
  return {
@@ -382,7 +382,7 @@ test("Windows source installation resolves one validated Go executable and seals
382
382
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
383
383
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
384
384
  ["version"],
385
- ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"],
385
+ ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.2.0"],
386
386
  ["version", "-m"],
387
387
  ]);
388
388
  for (const call of goCalls) {
@@ -408,14 +408,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
408
408
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
409
409
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
410
410
  const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
411
- const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "utf8")) as Record<string, string>;
411
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.2.0", "integrity.json"), "utf8")) as Record<string, string>;
412
412
  assert.equal(result.installed, true);
413
413
  assert.deepEqual(manifest, {
414
- version: "4.1.1",
414
+ version: "4.2.0",
415
415
  method: "go-sumdb-source-build",
416
416
  package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
417
417
  module: "github.com/gentleman-programming/gentle-ai/v4",
418
- tag: "v4.1.1",
418
+ tag: "v4.2.0",
419
419
  architecture: "arm64",
420
420
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
421
421
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -435,12 +435,12 @@ test("Windows source installation rejects Go metadata for a different architectu
435
435
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
436
436
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
437
437
  );
438
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
438
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai.exe")), false);
439
439
  });
440
440
 
441
441
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
442
442
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
443
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
443
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock");
444
444
  await mkdir(lockPath, { recursive: true });
445
445
  const fixture = await hardenedWindowsGoFixture(packageRoot);
446
446
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -448,7 +448,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
448
448
 
449
449
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
450
450
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
451
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock", "owner.json");
451
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock", "owner.json");
452
452
  const fixture = await hardenedWindowsGoFixture(packageRoot);
453
453
  let replacedOwner = false;
454
454
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -465,7 +465,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
465
465
  });
466
466
 
467
467
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
468
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
468
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock");
469
469
  await assert.rejects(
470
470
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
471
471
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -475,18 +475,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
475
475
  }
476
476
 
477
477
  function tombstonePath(packageRoot: string, nonce: string): string {
478
- return join(packageRoot, ".gentle-ai", `.v4.1.1.install.tombstone-${nonce}`);
478
+ return join(packageRoot, ".gentle-ai", `.v4.2.0.install.tombstone-${nonce}`);
479
479
  }
480
480
 
481
481
  async function tombstones(packageRoot: string): Promise<string[]> {
482
482
  const runtimeRoot = join(packageRoot, ".gentle-ai");
483
483
  return existsSync(runtimeRoot)
484
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.1.1.install.tombstone-"))
484
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.2.0.install.tombstone-"))
485
485
  : [];
486
486
  }
487
487
 
488
488
  function backupBundlePath(packageRoot: string, nonce: string): string {
489
- return join(packageRoot, ".gentle-ai", `.v4.1.1.backup-${nonce}`);
489
+ return join(packageRoot, ".gentle-ai", `.v4.2.0.backup-${nonce}`);
490
490
  }
491
491
 
492
492
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -497,7 +497,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
497
497
 
498
498
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
499
499
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
500
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
500
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock");
501
501
  await mkdir(lockPath, { recursive: true });
502
502
  const fixture = await hardenedWindowsGoFixture(packageRoot);
503
503
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -505,7 +505,7 @@ test("Windows source installation fails closed for an ownerless lock even after
505
505
 
506
506
  test("Windows source installation fails closed for a stale owner lock", async () => {
507
507
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
508
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
508
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock");
509
509
  await mkdir(lockPath, { recursive: true });
510
510
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
511
511
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -557,7 +557,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
557
557
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
558
558
  );
559
559
  assert.equal(existsSync(foreignTombstone), true);
560
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
560
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock")), false);
561
561
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
562
562
  });
563
563
 
@@ -565,7 +565,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
565
565
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
566
566
  const fixture = await hardenedWindowsGoFixture(packageRoot);
567
567
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
568
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
568
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock")), false);
569
569
  assert.deepEqual(await tombstones(packageRoot), []);
570
570
  const reuse = await hardenedWindowsGoFixture(packageRoot);
571
571
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -575,7 +575,7 @@ test("Windows source recovers a valid backup after a crash between publication r
575
575
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
576
576
  const initial = await hardenedWindowsGoFixture(packageRoot);
577
577
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
578
- const live = join(packageRoot, ".gentle-ai", "v4.1.1");
578
+ const live = join(packageRoot, ".gentle-ai", "v4.2.0");
579
579
  const backup = backupBundlePath(packageRoot, "crash");
580
580
  await rename(live, backup);
581
581
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -611,7 +611,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
611
611
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
612
612
  const initial = await hardenedWindowsGoFixture(packageRoot);
613
613
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
614
- const live = join(packageRoot, ".gentle-ai", "v4.1.1");
614
+ const live = join(packageRoot, ".gentle-ai", "v4.2.0");
615
615
  const backup = backupBundlePath(packageRoot, "valid");
616
616
  await copyWindowsBundle(live, backup);
617
617
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -624,7 +624,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
624
624
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
625
625
  const initial = await hardenedWindowsGoFixture(packageRoot);
626
626
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
627
- const live = join(packageRoot, ".gentle-ai", "v4.1.1");
627
+ const live = join(packageRoot, ".gentle-ai", "v4.2.0");
628
628
  const backup = backupBundlePath(packageRoot, "valid");
629
629
  await copyWindowsBundle(live, backup);
630
630
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -642,7 +642,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
642
642
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
643
643
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
644
644
  await fixture.waitForInstall();
645
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
645
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.2.0.install.lock");
646
646
  await assert.rejects(
647
647
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
648
648
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -656,7 +656,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
656
656
 
657
657
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
658
658
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
659
- const versionDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
659
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v4.2.0");
660
660
  await mkdir(versionDirectory, { recursive: true });
661
661
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
662
662
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -1092,7 +1092,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
1092
1092
  },
1093
1093
  };
1094
1094
  await installGentleAi(options);
1095
- const manifestPath = join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json");
1095
+ const manifestPath = join(packageRoot, ".gentle-ai", "v4.2.0", "integrity.json");
1096
1096
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
1097
1097
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
1098
1098
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
@@ -1105,7 +1105,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
1105
1105
  // under the current stable pin, whose form gate refuses raw binaries.
1106
1106
  const payload = Buffer.from("signed archive fixture");
1107
1107
  const binary = "mirrored binary";
1108
- const baseAsset = { name: "gentle-ai_4.1.1_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" };
1108
+ const baseAsset = { name: "gentle-ai_4.2.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" };
1109
1109
  const extractArchive = async (_archive: string, destination: string) => {
1110
1110
  await mkdir(destination, { recursive: true });
1111
1111
  await writeFile(join(destination, "gentle-ai"), binary);
@@ -1249,7 +1249,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
1249
1249
  }),
1250
1250
  /checksum mismatch/,
1251
1251
  );
1252
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
1252
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai")), false);
1253
1253
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
1254
1254
  });
1255
1255
 
@@ -1271,7 +1271,7 @@ test("installer promotes only the expected regular executable with executable PO
1271
1271
  await chmod(extracted, 0o700);
1272
1272
  },
1273
1273
  });
1274
- const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
1274
+ const binary = join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai");
1275
1275
  assert.equal(existsSync(binary), true);
1276
1276
  assert.equal(await readFile(binary, "utf8"), "native executable");
1277
1277
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -1302,7 +1302,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
1302
1302
  }),
1303
1303
  /binary checksum mismatch/,
1304
1304
  );
1305
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
1305
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai")), false);
1306
1306
  });
1307
1307
 
1308
1308
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -1325,7 +1325,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
1325
1325
  },
1326
1326
  };
1327
1327
  await installGentleAi(options);
1328
- const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
1328
+ const binary = join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai");
1329
1329
  await chmod(binary, 0o600);
1330
1330
  const repaired = await installGentleAi(options);
1331
1331
  assert.equal(repaired.installed, true);
@@ -1362,7 +1362,7 @@ test("installer rejects archives with multiple expected executable entries", asy
1362
1362
  }),
1363
1363
  /exactly one regular gentle-ai/,
1364
1364
  );
1365
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
1365
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai")), false);
1366
1366
  });
1367
1367
 
1368
1368
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -1380,7 +1380,7 @@ test("installer rejects an archive without the expected regular executable", asy
1380
1380
  }),
1381
1381
  /non-regular gentle-ai/,
1382
1382
  );
1383
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
1383
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.2.0", "gentle-ai")), false);
1384
1384
  });
1385
1385
 
1386
1386
  test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => {
@@ -661,6 +661,123 @@ test("first isolated run bootstraps the home, writes fullscreen, and prints the
661
661
  assert.doesNotMatch(second.stderr, /using a separate home at/);
662
662
  });
663
663
 
664
+ // --- home import-pi ----------------------------------------------------------
665
+
666
+ function importPiFixture(t: test.TestContext) {
667
+ const f = fixture(t);
668
+ const env = { ...f.env, PI_CODING_AGENT_DIR: undefined };
669
+ const piHome = join(f.home, ".pi", "agent");
670
+ mkdirSync(piHome, { recursive: true });
671
+ writeFileSync(join(piHome, "auth.json"), JSON.stringify({ anthropic: { type: "oauth", refresh: "dummy-refresh" } }), { mode: 0o600 });
672
+ writeFileSync(join(piHome, "mcp.json"), JSON.stringify({ mcpServers: {} }));
673
+ writeFileSync(join(piHome, "settings.json"), JSON.stringify({ defaultProvider: "anthropic", defaultModel: "m", packages: ["npm:foreign"], extensions: ["./x.ts"] }));
674
+ writeFileSync(join(piHome, "mcp-cache.json"), "{}");
675
+ return { ...f, env, piHome };
676
+ }
677
+
678
+ test("first isolated run without a pi home keeps the plain --link hint", (t) => {
679
+ const f = fixture(t);
680
+ const first = run({ ...f.env, PI_CODING_AGENT_DIR: undefined }, []);
681
+ assert.equal(first.status, 0, first.stderr);
682
+ assert.match(first.stderr, /Run 'gentle-shell --link' to reuse your pi sign-ins and chats\.\n/);
683
+ assert.doesNotMatch(first.stderr, /import-pi/);
684
+ });
685
+
686
+ test("first isolated run with a pi sign-in also mentions home import-pi", (t) => {
687
+ const f = importPiFixture(t);
688
+ const first = run(f.env, []);
689
+ assert.equal(first.status, 0, first.stderr);
690
+ assert.match(first.stderr, /'gentle-shell --link' to reuse your pi sign-ins and chats, or 'gentle-shell home import-pi'/);
691
+ });
692
+
693
+ test("--help lists home import-pi", (t) => {
694
+ const f = fixture(t);
695
+ const result = run(f.env, ["--help"]);
696
+ assert.match(result.stdout, /gentle-shell \[home selectors\] home import-pi \[--dry-run\]/);
697
+ });
698
+
699
+ test("home import-pi copies sign-ins, MCP servers and default model into the isolated home only", (t) => {
700
+ const f = importPiFixture(t);
701
+ const result = run(f.env, ["home", "import-pi"]);
702
+ assert.equal(result.status, 0, result.stderr);
703
+ assert.match(result.stdout, /auth\.json: copied \(0600\)/);
704
+ assert.match(result.stdout, /settings\.json defaultProvider: copied/);
705
+ assert.doesNotMatch(result.stdout + result.stderr, /dummy-refresh/);
706
+ assert.ok(existsSync(join(f.gentleShellHome, "auth.json")));
707
+ assert.equal(existsSync(join(f.gentleShellHome, "mcp-cache.json")), false);
708
+ if (process.platform !== "win32") assert.equal(statSync(join(f.gentleShellHome, "auth.json")).mode & 0o777, 0o600);
709
+ const settings = JSON.parse(readFileSync(join(f.gentleShellHome, "settings.json"), "utf8"));
710
+ assert.equal(settings.defaultProvider, "anthropic");
711
+ assert.equal(settings.tuiMode, "fullscreen", "the home is bootstrapped as a normal first launch would");
712
+ assert.equal(settings.packages, undefined);
713
+ assert.equal(settings.extensions, undefined);
714
+ assert.ok(existsSync(join(f.gentleShellHome, ".gentle-shell-home")));
715
+
716
+ const again = run(f.env, ["home", "import-pi"]);
717
+ assert.equal(again.status, 0, again.stderr);
718
+ assert.match(again.stdout, /auth\.json: already present, kept/);
719
+ assert.match(again.stdout, /Nothing to import/);
720
+ });
721
+
722
+ test("home import-pi --dry-run prints the plan and creates nothing", (t) => {
723
+ const f = importPiFixture(t);
724
+ const result = run(f.env, ["home", "import-pi", "--dry-run"]);
725
+ assert.equal(result.status, 0, result.stderr);
726
+ assert.match(result.stdout, /auth\.json: would copy/);
727
+ assert.equal(existsSync(f.gentleShellHome), false);
728
+ assert.equal(existsSync(join(f.home, ".gentle-shell")), false);
729
+ });
730
+
731
+ test("home import-pi refuses link mode and a target that is the pi home itself", (t) => {
732
+ const f = importPiFixture(t);
733
+ const before = readFileSync(join(f.piHome, "settings.json"), "utf8");
734
+ for (const argv of [["--link", "home", "import-pi"], ["--home", f.piHome, "home", "import-pi"]]) {
735
+ const result = run(f.env, argv);
736
+ assert.equal(result.status, 1, argv.join(" "));
737
+ assert.match(result.stderr, /gentle-shell home import-pi: .*nothing to import/);
738
+ }
739
+ assert.equal(run(f.env, ["home", "link"]).status, 0);
740
+ assert.equal(run(f.env, ["home", "import-pi"]).status, 1);
741
+ assert.equal(readFileSync(join(f.piHome, "settings.json"), "utf8"), before);
742
+ assert.equal(run(f.env, ["home", "import-pi", "--force"]).status, 2);
743
+ });
744
+
745
+ test("a --home filled by home import-pi is still auto-provisioned as gentle-shell's own", (t) => {
746
+ const f = importPiFixture(t);
747
+ const fresh = join(f.root, "fresh-home");
748
+ const empty = join(f.root, "empty-home");
749
+ mkdirSync(empty);
750
+ const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
751
+ const counterPath = join(f.root, "gentle-ai-runs.log");
752
+ writeGentleAiScriptCountingRuns(gentleAiScript, counterPath);
753
+ const env = enableAutoProvision({ ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript, GENTLE_SHELL_GENTLE_AI_PIN: "3.6.0" });
754
+ for (const [index, target] of [fresh, empty].entries()) {
755
+ const imported = run(f.env, ["--home", target, "home", "import-pi"]);
756
+ assert.equal(imported.status, 0, imported.stderr);
757
+ assert.match(imported.stdout, /auth\.json: copied/);
758
+ const launch = run(env, ["--home", target, "--mode", "rpc"]);
759
+ assert.equal(launch.status, 0, launch.stderr);
760
+ assert.doesNotMatch(launch.stderr, /already has content and was not set up by gentle-shell/);
761
+ assert.equal(readFileSync(counterPath, "utf8").trim().split("\n").length, index + 1, `auto-provisioning ran for ${target}`);
762
+ }
763
+ });
764
+
765
+ test("home import-pi exits 1 and names the item when a write fails", { skip: process.platform === "win32" }, (t) => {
766
+ const f = importPiFixture(t);
767
+ mkdirSync(f.gentleShellHome, { recursive: true });
768
+ writeFileSync(join(f.gentleShellHome, "settings.json"), "{}");
769
+ writeFileSync(join(f.gentleShellHome, ".gentle-shell-home"), "{}");
770
+ chmodSync(f.gentleShellHome, 0o500);
771
+ let result;
772
+ try {
773
+ result = run(f.env, ["home", "import-pi"]);
774
+ } finally {
775
+ chmodSync(f.gentleShellHome, 0o700); // before the fixture's own cleanup
776
+ }
777
+ assert.equal(result.status, 1);
778
+ assert.match(result.stdout, /auth\.json: failed/);
779
+ });
780
+
664
781
  test("forwarded args reach pi after the injected extension flags, in order", (t) => {
665
782
  const f = fixture(t);
666
783
  const result = run(f.env, ["--mode", "rpc", "-p", "hi"]);
@@ -815,6 +932,17 @@ test("gentle-shell install npm:<pkg> forwards the subcommand and its argument ve
815
932
  assert.equal(payload.PI_CODING_AGENT_DIR, f.gentleShellHome);
816
933
  });
817
934
 
935
+ // #2140: pi dispatches `mcp` on argv[0] only, so an injected -e ahead of it
936
+ // turned `gentle-shell mcp list` into an interactive prompt.
937
+ test("gentle-shell mcp add <server> -- <cmd> forwards to pi's own mcp verbatim, with no injected extension flags", (t) => {
938
+ const f = fixture(t);
939
+ const result = run(f.env, ["mcp", "add", "demo", "--", "npx", "-y", "demo-server"]);
940
+ assert.equal(result.status, 0, result.stderr);
941
+ const payload = JSON.parse(result.stdout);
942
+ assert.deepEqual(payload.args, ["mcp", "add", "demo", "--", "npx", "-y", "demo-server"]);
943
+ assert.equal(payload.PI_CODING_AGENT_DIR, f.gentleShellHome);
944
+ });
945
+
818
946
  test("a too-old pi exits 1 naming both versions", (t) => {
819
947
  const f = fixture(t);
820
948
  writePiScript(f.piScript, "0.80.0");
@@ -2603,6 +2731,13 @@ test("--help mentions the setup subcommand", (t) => {
2603
2731
  assert.match(result.stdout, /\bsetup\b/);
2604
2732
  });
2605
2733
 
2734
+ test("--help lists the mcp subcommand", (t) => {
2735
+ const f = fixture(t);
2736
+ const result = run(f.env, ["--help"]);
2737
+ assert.equal(result.status, 0, result.stderr);
2738
+ assert.match(result.stdout, /gentle-shell mcp /);
2739
+ });
2740
+
2606
2741
  // --- --link takeover of a conflicting path package -----------------------
2607
2742
  //
2608
2743
  // Regression coverage for the settings.json shape that triggered the tool
@@ -3350,3 +3485,31 @@ test("on a TTY without colors the gentle-shell line has no ANSI styling", { skip
3350
3485
  const out = runInPty({ ...f.env, GENTLE_SHELL_PI: piScript, PTY_NO_COLOR: "1" }, ["--link"]);
3351
3486
  assert.ok(out.endsWith(`${PI_HINT}To resume in gentle-shell: gentle-shell --link --session abc\r\n`), JSON.stringify(out));
3352
3487
  });
3488
+
3489
+ // #2143: a Gentle AI that honors GENTLE_AI_PI_EXCLUDE_PACKAGES (gentle-ai#5503)
3490
+ // never declares npm:gentle-pi, so setup neither runs `pi remove` nor prints
3491
+ // the contradictory "removing npm:gentle-pi" line.
3492
+ test("gentle-shell setup asks Gentle AI to skip npm:gentle-pi, so nothing is installed only to be removed", (t) => {
3493
+ const f = fixture(t);
3494
+ const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
3495
+ writeFileSync(gentleAiScript, [
3496
+ "#!/usr/bin/env node",
3497
+ "import { readFileSync, writeFileSync } from 'node:fs';",
3498
+ "import { join } from 'node:path';",
3499
+ "const excluded = (process.env.GENTLE_AI_PI_EXCLUDE_PACKAGES ?? '').split(',').map((entry) => entry.trim());",
3500
+ "const settingsPath = join(process.env.PI_CODING_AGENT_DIR, 'settings.json');",
3501
+ "const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));",
3502
+ "settings.packages = ['npm:gentle-pi', 'npm:pi-btw'].filter((source) => !excluded.includes(source));",
3503
+ "writeFileSync(settingsPath, JSON.stringify(settings));",
3504
+ "process.stdout.write(JSON.stringify({ args: process.argv.slice(2), excluded: process.env.GENTLE_AI_PI_EXCLUDE_PACKAGES ?? null }) + '\\n');",
3505
+ "",
3506
+ ].join("\n"));
3507
+ chmodSync(gentleAiScript, 0o755);
3508
+ const result = run({ ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript }, ["setup"]);
3509
+ assert.equal(result.status, 0, result.stderr);
3510
+ assert.doesNotMatch(result.stderr, /removing npm:gentle-pi/);
3511
+ const lines = result.stdout.trim().split("\n").filter((line) => line.length > 0);
3512
+ assert.equal(lines.length, 1, "no pi remove ran: " + result.stdout);
3513
+ assert.deepEqual(JSON.parse(lines[0]), { args: ["install", "--agent", "pi", "--scope", "global"], excluded: "npm:gentle-pi" });
3514
+ });
3515
+