gentle-pi 3.6.0 → 3.7.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.
@@ -21,18 +21,18 @@ import {
21
21
  trustedSystemExtractor,
22
22
  } from "../scripts/gentle-ai-installer.mjs";
23
23
 
24
- // v3.6.1 archive digests independently match the minisign-signed release
24
+ // v3.7.0 archive digests independently match the minisign-signed release
25
25
  // checksums; binary digests were computed from the extracted executables.
26
26
  const EXPECTED_ASSETS = {
27
- "darwin/amd64": { name: "gentle-ai_3.6.1_darwin_amd64.tar.gz", sha256: "75f9839035eb25e2bc8eb324a365154cd6ecf869cc3db7885cbc9892008ac9bd", binarySha256: "fb9c286d58fddb0234660aa71a4ed0850f7d015f21c5f6c2059fdf70dc3bc0ae" },
28
- "darwin/arm64": { name: "gentle-ai_3.6.1_darwin_arm64.tar.gz", sha256: "be46c0884f31f485ca9663ee819156e058e1b1e665165a72105f42bb449560a2", binarySha256: "e13779ba1dfa23c31403650d76cceb0d523e56904530426c6bdedca9d7937550" },
29
- "linux/amd64": { name: "gentle-ai_3.6.1_linux_amd64.tar.gz", sha256: "251db626a3b774665066eeedfb9edbb1bfe60e874fbe98af1df65587b19b3c2f", binarySha256: "7208d5e3185293b70779f174a9deac4962f503a8873d4b3529ad7d444e1308a2" },
30
- "linux/arm64": { name: "gentle-ai_3.6.1_linux_arm64.tar.gz", sha256: "d6acc74454e6135af9714c497bc4791ed7149ee3c52d16f378cc085817274db5", binarySha256: "e0db95f4a7b9837060ba6de599f2ff15460ceb1b550c65d4225aaccc657ca979" },
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" },
31
31
  } as const;
32
32
 
33
- test("published Gentle AI v3.6.1 is the installer pin", () => {
34
- assert.equal(INSTALLER_VERSION, "3.6.1");
35
- assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:De+eaJuMPxsaiq8KQzWUKCC5FTaFiA3BqDg9nyh/k9Y=");
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=");
36
36
  });
37
37
 
38
38
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -43,15 +43,15 @@ test("default installer package root is the package containing scripts, not its
43
43
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
44
44
  });
45
45
 
46
- test("release mapping selects only the supported official v3.6.1 assets and pinned digests", () => {
46
+ test("release mapping selects only the supported official v3.7.0 assets and pinned digests", () => {
47
47
  assert.deepEqual(
48
48
  Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
49
49
  EXPECTED_ASSETS,
50
50
  );
51
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_3.6.1_linux_amd64.tar.gz");
52
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_3.6.1_darwin_arm64.tar.gz");
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");
53
53
  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\.6\.1\//);
54
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v3\.7\.0\//);
55
55
  }
56
56
  });
57
57
 
@@ -61,7 +61,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
61
61
  assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
62
62
  // A raw binary under a stable pin means the pin itself is wrong: stable
63
63
  // releases publish signed archives only, so this fails closed pre-download.
64
- assert.throws(() => gentleAiAssetForm("gentle-ai_3.6.1_linux_amd64", "3.6.1"), /only admitted for a prerelease pin/);
64
+ assert.throws(() => gentleAiAssetForm("gentle-ai_3.7.0_linux_amd64", "3.7.0"), /only admitted for a prerelease pin/);
65
65
  assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
66
66
  // The current stable pin admits every pinned asset row through the same
67
67
  // gate the installer uses at download time (default installerVersion
@@ -88,7 +88,7 @@ test("release digests are all-or-none and install fails closed while any digest
88
88
  }),
89
89
  /checksum mismatch/,
90
90
  );
91
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai")), false);
91
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
92
92
  }
93
93
  });
94
94
 
@@ -117,7 +117,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
117
117
  const metadata = [
118
118
  "gentle-ai.exe: go1.25.10",
119
119
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
120
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.6.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
120
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.7.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
121
121
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
122
122
  ].join("\n");
123
123
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -135,7 +135,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
135
135
  return { stdout: "", stderr: "" };
136
136
  }
137
137
  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.6.1\n", stderr: "" };
138
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 3.7.0\n", stderr: "" };
139
139
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
140
140
  };
141
141
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -151,7 +151,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
151
151
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
152
152
  assert.equal(result.installed, true);
153
153
  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.6.1"], ["version", "-m"],
154
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.7.0"], ["version", "-m"],
155
155
  ]);
156
156
  }
157
157
  });
@@ -170,7 +170,7 @@ test("Windows source install reports missing or too-old Go without publishing a
170
170
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
171
171
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
172
172
  );
173
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai.exe")), false);
173
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai.exe")), false);
174
174
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
175
175
  }
176
176
  });
@@ -189,7 +189,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
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
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v3.6.1");
192
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v3.7.0");
193
193
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
194
194
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
195
195
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -210,7 +210,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
210
210
  assert.equal(reused.installed, false);
211
211
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
212
212
 
213
- await writeFile(join(packageRoot, ".gentle-ai", "v3.6.1", "integrity.json"), "{}\n");
213
+ await writeFile(join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json"), "{}\n");
214
214
  const repaired = windowsGoFixture();
215
215
  repaired.setGoExecutable(goPath);
216
216
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -252,7 +252,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
252
252
  const metadata = fixtureOptions.metadataOverride ?? [
253
253
  "gentle-ai.exe: go1.25.10",
254
254
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
255
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.6.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
255
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.7.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
256
256
  "\tbuild\t-buildmode=exe",
257
257
  "\tbuild\t-compiler=gc",
258
258
  "\tbuild\tCGO_ENABLED=0",
@@ -272,7 +272,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
272
272
  return { stdout: "", stderr: "" };
273
273
  }
274
274
  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.6.1\n", stderr: "" };
275
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 3.7.0\n", stderr: "" };
276
276
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
277
277
  };
278
278
  return {
@@ -314,7 +314,7 @@ test("Windows source installation resolves one validated Go executable and seals
314
314
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
315
315
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
316
316
  ["version"],
317
- ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.6.1"],
317
+ ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.7.0"],
318
318
  ["version", "-m"],
319
319
  ]);
320
320
  for (const call of goCalls) {
@@ -340,14 +340,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
340
340
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
341
341
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
342
342
  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.6.1", "integrity.json"), "utf8")) as Record<string, string>;
343
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json"), "utf8")) as Record<string, string>;
344
344
  assert.equal(result.installed, true);
345
345
  assert.deepEqual(manifest, {
346
- version: "3.6.1",
346
+ version: "3.7.0",
347
347
  method: "go-sumdb-source-build",
348
348
  package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
349
349
  module: "github.com/gentleman-programming/gentle-ai/v3",
350
- tag: "v3.6.1",
350
+ tag: "v3.7.0",
351
351
  architecture: "arm64",
352
352
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
353
353
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -367,12 +367,12 @@ test("Windows source installation rejects Go metadata for a different architectu
367
367
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
368
368
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
369
369
  );
370
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai.exe")), false);
370
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai.exe")), false);
371
371
  });
372
372
 
373
373
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
374
374
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
375
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock");
375
+ const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
376
376
  await mkdir(lockPath, { recursive: true });
377
377
  const fixture = await hardenedWindowsGoFixture(packageRoot);
378
378
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -380,7 +380,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
380
380
 
381
381
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
382
382
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
383
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock", "owner.json");
383
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock", "owner.json");
384
384
  const fixture = await hardenedWindowsGoFixture(packageRoot);
385
385
  let replacedOwner = false;
386
386
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -397,7 +397,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
397
397
  });
398
398
 
399
399
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
400
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock");
400
+ const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
401
401
  await assert.rejects(
402
402
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
403
403
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -407,18 +407,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
407
407
  }
408
408
 
409
409
  function tombstonePath(packageRoot: string, nonce: string): string {
410
- return join(packageRoot, ".gentle-ai", `.v3.6.1.install.tombstone-${nonce}`);
410
+ return join(packageRoot, ".gentle-ai", `.v3.7.0.install.tombstone-${nonce}`);
411
411
  }
412
412
 
413
413
  async function tombstones(packageRoot: string): Promise<string[]> {
414
414
  const runtimeRoot = join(packageRoot, ".gentle-ai");
415
415
  return existsSync(runtimeRoot)
416
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v3.6.1.install.tombstone-"))
416
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v3.7.0.install.tombstone-"))
417
417
  : [];
418
418
  }
419
419
 
420
420
  function backupBundlePath(packageRoot: string, nonce: string): string {
421
- return join(packageRoot, ".gentle-ai", `.v3.6.1.backup-${nonce}`);
421
+ return join(packageRoot, ".gentle-ai", `.v3.7.0.backup-${nonce}`);
422
422
  }
423
423
 
424
424
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -429,7 +429,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
429
429
 
430
430
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
431
431
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
432
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock");
432
+ const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
433
433
  await mkdir(lockPath, { recursive: true });
434
434
  const fixture = await hardenedWindowsGoFixture(packageRoot);
435
435
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -437,7 +437,7 @@ test("Windows source installation fails closed for an ownerless lock even after
437
437
 
438
438
  test("Windows source installation fails closed for a stale owner lock", async () => {
439
439
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
440
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock");
440
+ const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
441
441
  await mkdir(lockPath, { recursive: true });
442
442
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
443
443
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -489,7 +489,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
489
489
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
490
490
  );
491
491
  assert.equal(existsSync(foreignTombstone), true);
492
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock")), false);
492
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock")), false);
493
493
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
494
494
  });
495
495
 
@@ -497,7 +497,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
497
497
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
498
498
  const fixture = await hardenedWindowsGoFixture(packageRoot);
499
499
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
500
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock")), false);
500
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock")), false);
501
501
  assert.deepEqual(await tombstones(packageRoot), []);
502
502
  const reuse = await hardenedWindowsGoFixture(packageRoot);
503
503
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -507,7 +507,7 @@ test("Windows source recovers a valid backup after a crash between publication r
507
507
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
508
508
  const initial = await hardenedWindowsGoFixture(packageRoot);
509
509
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
510
- const live = join(packageRoot, ".gentle-ai", "v3.6.1");
510
+ const live = join(packageRoot, ".gentle-ai", "v3.7.0");
511
511
  const backup = backupBundlePath(packageRoot, "crash");
512
512
  await rename(live, backup);
513
513
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -543,7 +543,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
543
543
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
544
544
  const initial = await hardenedWindowsGoFixture(packageRoot);
545
545
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
546
- const live = join(packageRoot, ".gentle-ai", "v3.6.1");
546
+ const live = join(packageRoot, ".gentle-ai", "v3.7.0");
547
547
  const backup = backupBundlePath(packageRoot, "valid");
548
548
  await copyWindowsBundle(live, backup);
549
549
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -556,7 +556,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
556
556
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
557
557
  const initial = await hardenedWindowsGoFixture(packageRoot);
558
558
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
559
- const live = join(packageRoot, ".gentle-ai", "v3.6.1");
559
+ const live = join(packageRoot, ".gentle-ai", "v3.7.0");
560
560
  const backup = backupBundlePath(packageRoot, "valid");
561
561
  await copyWindowsBundle(live, backup);
562
562
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -574,7 +574,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
574
574
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
575
575
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
576
576
  await fixture.waitForInstall();
577
- const lockPath = join(packageRoot, ".gentle-ai", ".v3.6.1.install.lock");
577
+ const lockPath = join(packageRoot, ".gentle-ai", ".v3.7.0.install.lock");
578
578
  await assert.rejects(
579
579
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
580
580
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -588,7 +588,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
588
588
 
589
589
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
590
590
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
591
- const versionDirectory = join(packageRoot, ".gentle-ai", "v3.6.1");
591
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v3.7.0");
592
592
  await mkdir(versionDirectory, { recursive: true });
593
593
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
594
594
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -625,7 +625,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
625
625
  },
626
626
  };
627
627
  await installGentleAi(options);
628
- const manifestPath = join(packageRoot, ".gentle-ai", "v3.6.1", "integrity.json");
628
+ const manifestPath = join(packageRoot, ".gentle-ai", "v3.7.0", "integrity.json");
629
629
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
630
630
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
631
631
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
@@ -638,7 +638,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
638
638
  // under the current stable pin, whose form gate refuses raw binaries.
639
639
  const payload = Buffer.from("signed archive fixture");
640
640
  const binary = "mirrored binary";
641
- const baseAsset = { name: "gentle-ai_3.6.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" };
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" };
642
642
  const extractArchive = async (_archive: string, destination: string) => {
643
643
  await mkdir(destination, { recursive: true });
644
644
  await writeFile(join(destination, "gentle-ai"), binary);
@@ -756,7 +756,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
756
756
  }),
757
757
  /checksum mismatch/,
758
758
  );
759
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai")), false);
759
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
760
760
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
761
761
  });
762
762
 
@@ -778,7 +778,7 @@ test("installer promotes only the expected regular executable with executable PO
778
778
  await chmod(extracted, 0o700);
779
779
  },
780
780
  });
781
- const binary = join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai");
781
+ const binary = join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai");
782
782
  assert.equal(existsSync(binary), true);
783
783
  assert.equal(await readFile(binary, "utf8"), "native executable");
784
784
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -809,7 +809,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
809
809
  }),
810
810
  /binary checksum mismatch/,
811
811
  );
812
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai")), false);
812
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
813
813
  });
814
814
 
815
815
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -832,7 +832,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
832
832
  },
833
833
  };
834
834
  await installGentleAi(options);
835
- const binary = join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai");
835
+ const binary = join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai");
836
836
  await chmod(binary, 0o600);
837
837
  const repaired = await installGentleAi(options);
838
838
  assert.equal(repaired.installed, true);
@@ -869,7 +869,7 @@ test("installer rejects archives with multiple expected executable entries", asy
869
869
  }),
870
870
  /exactly one regular gentle-ai/,
871
871
  );
872
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai")), false);
872
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
873
873
  });
874
874
 
875
875
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -887,7 +887,7 @@ test("installer rejects an archive without the expected regular executable", asy
887
887
  }),
888
888
  /non-regular gentle-ai/,
889
889
  );
890
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.6.1", "gentle-ai")), false);
890
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.7.0", "gentle-ai")), false);
891
891
  });
892
892
 
893
893
  // #400: a clean Windows source build measured 232 s on a cold module cache,
@@ -361,11 +361,17 @@ test("3.6.1 explicitly repeats 3.6.0 because the negotiated provider lane is unc
361
361
  assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.6.0"] as Record<string, boolean>);
362
362
  });
363
363
 
364
+ test("3.7.0 explicitly repeats 3.6.1 because provider contract 1.2.0 is unchanged", () => {
365
+ const contract = NATIVE_CLI_CONTRACTS["3.7.0"] as Record<string, boolean>;
366
+ assert.ok(contract);
367
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.6.1"] as Record<string, boolean>);
368
+ });
369
+
364
370
  test("no shipped version key was added beyond the pin bump", () => {
365
371
  // Rows are promises to consumers, so a new key only ever appears in a
366
372
  // dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
367
373
  // v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
368
374
  // so they get no row: a row asserts ground truth measured against a binary
369
375
  // Pi actually ran, and the table only has to be ascending, not gapless.
370
- assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1"]);
376
+ assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.7.0"]);
371
377
  });
@@ -298,20 +298,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
298
298
  );
299
299
  });
300
300
 
301
- test("package verification binds the published Gentle AI v3.6.1 runtime pin", () => {
301
+ test("package verification binds the published Gentle AI v3.7.0 runtime pin", () => {
302
302
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
303
303
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
304
304
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
305
305
 
306
- assert.match(installer, /INSTALLER_VERSION = "3\.6\.1"/);
306
+ assert.match(installer, /INSTALLER_VERSION = "3\.7\.0"/);
307
307
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
308
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:De\+eaJuMPxsaiq8KQzWUKCC5FTaFiA3BqDg9nyh\/k9Y="/);
308
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:MQbzHlLdPklUQn0rVE9Mz94UygHsN2OPe7xMfPn9aGw="/);
309
309
  assert.match(installer, /GOTOOLCHAIN: "local"/);
310
310
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
311
311
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
312
312
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
313
313
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
314
- assert.match(verifier, /v3\.6\.1/);
314
+ assert.match(verifier, /v3\.7\.0/);
315
315
  });
316
316
 
317
317
 
@@ -1563,9 +1563,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
1563
1563
  assert.match(wrapper, /quietToolsEnabled/);
1564
1564
  });
1565
1565
 
1566
- test("Gentle Shell v3.6.0 package and runtime stop before publication", () => {
1566
+ test("Gentle Shell v3.7.0 package manifest declares the release version", () => {
1567
1567
  const packageJson = readPackageJson();
1568
- assert.equal(packageJson.version, "3.6.0", "the release manifest must remain explicitly pinned to v3.6.0");
1568
+ assert.equal(packageJson.version, "3.7.0", "the release manifest must be explicitly pinned to v3.7.0");
1569
1569
  assert.equal(
1570
1570
  packageJson.scripts?.test,
1571
1571
  "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
@@ -3,7 +3,7 @@ import test from "node:test";
3
3
  import { mkdtemp, writeFile, rm, realpath } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { installSessionChangeCapture } from "../lib/session-change-capture.ts";
6
+ import { installSessionChangeCapture, publishForeignSessionChange } from "../lib/session-change-capture.ts";
7
7
  import { SessionChanges, SESSION_CHANGE_ENTRY, SESSION_CHANGE_EVENT } from "../lib/session-changes.ts";
8
8
 
9
9
  async function fixture(run: (f: any) => Promise<void>, child = false) {
@@ -21,6 +21,17 @@ async function fixture(run: (f: any) => Promise<void>, child = false) {
21
21
  finally { await rm(root, {recursive:true,force:true}); }
22
22
  }
23
23
 
24
+ test("foreign tool evidence uses a separate target-bound publisher, not the same-clone relay", async () => fixture(async ({ root, entries, pi, switchSession }) => {
25
+ const foreign = join(root, "independent");
26
+ const evidence = { id: "task:write", root: foreign, path: "file.txt", before: { kind: "absent" }, after: { kind: "text", text: "child output\n" } };
27
+ publishForeignSessionChange(pi as never, "session", evidence as never);
28
+ assert.equal(new SessionChanges("session", entries).worktrees[0]?.root, foreign);
29
+ assert.equal(entries.length, 1);
30
+ switchSession();
31
+ publishForeignSessionChange(pi as never, "session", { ...evidence, id: "second" } as never);
32
+ assert.equal(entries.length, 1);
33
+ }));
34
+
24
35
  test("capture reads no inventory at startup and ignores read-only tools", async () => fixture(async ({fire, entries}) => {
25
36
  await fire("tool_call", {toolCallId:"r",toolName:"read",input:{path:"missing"}});
26
37
  await fire("tool_result", {toolCallId:"r",toolName:"read",input:{path:"missing"},isError:false});