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.
- package/docs/gentle-shell.md +5 -4
- package/docs/readme-reference.md +11 -35
- package/extensions/gentle-agents.ts +89 -22
- package/lib/agents-runner.ts +9 -0
- package/lib/foreign-target-grants.ts +32 -0
- package/lib/native-review-cli.ts +4 -0
- package/lib/session-change-capture.ts +10 -1
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +4 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/agents-runner.test.ts +22 -0
- package/tests/foreign-target-grants.test.ts +58 -0
- package/tests/gentle-agents.test.ts +362 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +50 -50
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/package-manifest.test.ts +6 -6
- package/tests/session-change-capture.test.ts +12 -1
|
@@ -21,18 +21,18 @@ import {
|
|
|
21
21
|
trustedSystemExtractor,
|
|
22
22
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
23
23
|
|
|
24
|
-
// v3.
|
|
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.
|
|
28
|
-
"darwin/arm64": { name: "gentle-ai_3.
|
|
29
|
-
"linux/amd64": { name: "gentle-ai_3.
|
|
30
|
-
"linux/arm64": { name: "gentle-ai_3.
|
|
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.
|
|
34
|
-
assert.equal(INSTALLER_VERSION, "3.
|
|
35
|
-
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:
|
|
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.
|
|
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.
|
|
52
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_3.
|
|
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\.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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\.
|
|
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:
|
|
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\.
|
|
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.
|
|
1566
|
+
test("Gentle Shell v3.7.0 package manifest declares the release version", () => {
|
|
1567
1567
|
const packageJson = readPackageJson();
|
|
1568
|
-
assert.equal(packageJson.version, "3.
|
|
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});
|