gentle-pi 2.6.1 → 2.6.2
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/README.md +183 -943
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/docs/assets/brand/gentle-pi-banner.svg +33 -0
- package/docs/assets/brand/terminal-divider.svg +17 -0
- package/docs/assets/diagrams/agent-orchestration.svg +19 -0
- package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
- package/docs/assets/diagrams/native-review.svg +16 -0
- package/docs/assets/diagrams/sdd-cycle.svg +14 -0
- package/docs/assets/features/gentle-shell.png +0 -0
- package/docs/gentle-shell.md +151 -0
- package/docs/readme-reference.md +868 -0
- package/extensions/gentle-agents.ts +4 -1
- package/extensions/gentle-ai.ts +65 -22
- package/lib/agents-history.ts +7 -1
- package/lib/native-review-cli.ts +9 -0
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +9 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/gentle-agents.test.ts +22 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +3 -2
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/package-manifest.test.ts +19 -18
- package/tests/review-authority-recovery-docs.test.ts +13 -13
- package/tests/review-controller-native-routing.test.ts +49 -0
- package/tests/review-ledger-contract.test.ts +7 -5
- package/tests/sdd-managed-runtime-settlement.test.ts +37 -0
- package/tests/sdd-selection-transport.test.ts +57 -0
- package/tests/skill-collision-prefixes.test.ts +2 -2
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
trustedSystemExtractor,
|
|
21
21
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
22
22
|
|
|
23
|
-
// v2.8.
|
|
23
|
+
// v2.8.2 digests pinned from the published release: archive digests verified
|
|
24
24
|
// against the minisign-signed checksums.txt and freshly computed hashes; binary
|
|
25
25
|
// digests computed from the extracted executables.
|
|
26
26
|
const EXPECTED_ASSETS = {
|
|
27
|
-
"darwin/amd64": { name: "gentle-ai_2.8.
|
|
28
|
-
"darwin/arm64": { name: "gentle-ai_2.8.
|
|
29
|
-
"linux/amd64": { name: "gentle-ai_2.8.
|
|
30
|
-
"linux/arm64": { name: "gentle-ai_2.8.
|
|
27
|
+
"darwin/amd64": { name: "gentle-ai_2.8.2_darwin_amd64.tar.gz", sha256: "0daa28897e6e54ce584f12ccefebf0d0df84e4ea8fbbd3a07e596ca82b079fa2", binarySha256: "17069156869ceda8e23eaa4fe5d7565cd57c1d78528f121dabba7301b82a0c14" },
|
|
28
|
+
"darwin/arm64": { name: "gentle-ai_2.8.2_darwin_arm64.tar.gz", sha256: "12265017e0fb6d5dd1ddb1751f188fa8c47d95a95d7c7515ae174394da5f6e97", binarySha256: "491542e4b60e432048d4074f21c1b04417b980cf77eb34cd0a2e7447ca57dd77" },
|
|
29
|
+
"linux/amd64": { name: "gentle-ai_2.8.2_linux_amd64.tar.gz", sha256: "5b95b184606168685a4ff576c103b051ceb23346a7f81ba557e047fa4d744146", binarySha256: "a55f4d2e114128866810c25195e5efecdded4502660e9a9c35c0b7a41c909dd8" },
|
|
30
|
+
"linux/arm64": { name: "gentle-ai_2.8.2_linux_arm64.tar.gz", sha256: "a33c91ca0f5c5c85a4fd69f5169138089eb53e0a92c6ab80de5094d0688debb7", binarySha256: "e2759aca09ffe97638e984491319e43c9477d1ae117726ecbdf20d29d44c8950" },
|
|
31
31
|
} as const;
|
|
32
32
|
|
|
33
33
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -38,15 +38,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
38
38
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test("release mapping selects only the supported official v2.8.
|
|
41
|
+
test("release mapping selects only the supported official v2.8.2 assets and pinned digests", () => {
|
|
42
42
|
assert.deepEqual(
|
|
43
43
|
Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
44
44
|
EXPECTED_ASSETS,
|
|
45
45
|
);
|
|
46
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.8.
|
|
47
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.8.
|
|
46
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_2.8.2_linux_amd64.tar.gz");
|
|
47
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_2.8.2_darwin_arm64.tar.gz");
|
|
48
48
|
for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
|
|
49
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.8\.
|
|
49
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v2\.8\.2\//);
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
|
|
|
56
56
|
assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
|
|
57
57
|
// A raw binary under a stable pin means the pin itself is wrong: stable
|
|
58
58
|
// releases publish signed archives only, so this fails closed pre-download.
|
|
59
|
-
assert.throws(() => gentleAiAssetForm("gentle-ai_2.8.
|
|
59
|
+
assert.throws(() => gentleAiAssetForm("gentle-ai_2.8.2_linux_amd64", "2.8.2"), /only admitted for a prerelease pin/);
|
|
60
60
|
assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
|
|
61
61
|
// The current stable pin admits every pinned asset row through the same
|
|
62
62
|
// gate the installer uses at download time (default installerVersion
|
|
@@ -83,7 +83,7 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
83
83
|
}),
|
|
84
84
|
/checksum mismatch/,
|
|
85
85
|
);
|
|
86
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
86
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai")), false);
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -112,7 +112,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
112
112
|
const metadata = [
|
|
113
113
|
"gentle-ai.exe: go1.25.10",
|
|
114
114
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
115
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.8.
|
|
115
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.8.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
116
116
|
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
117
117
|
].join("\n");
|
|
118
118
|
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
@@ -130,7 +130,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
130
130
|
return { stdout: "", stderr: "" };
|
|
131
131
|
}
|
|
132
132
|
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
133
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.8.
|
|
133
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 2.8.2\n", stderr: "" };
|
|
134
134
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
135
135
|
};
|
|
136
136
|
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
@@ -146,7 +146,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
|
|
|
146
146
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
147
147
|
assert.equal(result.installed, true);
|
|
148
148
|
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
149
|
-
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.8.
|
|
149
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.8.2"], ["version", "-m"],
|
|
150
150
|
]);
|
|
151
151
|
}
|
|
152
152
|
});
|
|
@@ -165,7 +165,7 @@ test("Windows source install reports missing or too-old Go without publishing a
|
|
|
165
165
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
166
166
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
167
167
|
);
|
|
168
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
168
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai.exe")), false);
|
|
169
169
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
170
170
|
}
|
|
171
171
|
});
|
|
@@ -184,7 +184,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
|
|
|
184
184
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
185
185
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
186
186
|
);
|
|
187
|
-
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.8.
|
|
187
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v2.8.2");
|
|
188
188
|
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
189
189
|
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
190
190
|
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
@@ -205,7 +205,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
|
|
|
205
205
|
assert.equal(reused.installed, false);
|
|
206
206
|
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
207
207
|
|
|
208
|
-
await writeFile(join(packageRoot, ".gentle-ai", "v2.8.
|
|
208
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v2.8.2", "integrity.json"), "{}\n");
|
|
209
209
|
const repaired = windowsGoFixture();
|
|
210
210
|
repaired.setGoExecutable(goPath);
|
|
211
211
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
@@ -247,7 +247,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
247
247
|
const metadata = fixtureOptions.metadataOverride ?? [
|
|
248
248
|
"gentle-ai.exe: go1.25.10",
|
|
249
249
|
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
250
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.8.
|
|
250
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v2\tv2.8.2\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
251
251
|
"\tbuild\t-buildmode=exe",
|
|
252
252
|
"\tbuild\t-compiler=gc",
|
|
253
253
|
"\tbuild\tCGO_ENABLED=0",
|
|
@@ -267,7 +267,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
267
267
|
return { stdout: "", stderr: "" };
|
|
268
268
|
}
|
|
269
269
|
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
270
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.8.
|
|
270
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 2.8.2\n", stderr: "" };
|
|
271
271
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
272
272
|
};
|
|
273
273
|
return {
|
|
@@ -309,7 +309,7 @@ test("Windows source installation resolves one validated Go executable and seals
|
|
|
309
309
|
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
310
310
|
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
311
311
|
["version"],
|
|
312
|
-
["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.8.
|
|
312
|
+
["install", "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai@v2.8.2"],
|
|
313
313
|
["version", "-m"],
|
|
314
314
|
]);
|
|
315
315
|
for (const call of goCalls) {
|
|
@@ -335,14 +335,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
|
|
|
335
335
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
336
336
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
337
337
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
338
|
-
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.8.
|
|
338
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v2.8.2", "integrity.json"), "utf8")) as Record<string, string>;
|
|
339
339
|
assert.equal(result.installed, true);
|
|
340
340
|
assert.deepEqual(manifest, {
|
|
341
|
-
version: "2.8.
|
|
341
|
+
version: "2.8.2",
|
|
342
342
|
method: "go-sumdb-source-build",
|
|
343
343
|
package: "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai",
|
|
344
344
|
module: "github.com/gentleman-programming/gentle-ai/v2",
|
|
345
|
-
tag: "v2.8.
|
|
345
|
+
tag: "v2.8.2",
|
|
346
346
|
architecture: "arm64",
|
|
347
347
|
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
348
348
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -362,12 +362,12 @@ test("Windows source installation rejects Go metadata for a different architectu
|
|
|
362
362
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
363
363
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
364
364
|
);
|
|
365
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
365
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai.exe")), false);
|
|
366
366
|
});
|
|
367
367
|
|
|
368
368
|
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
369
369
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
370
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
370
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock");
|
|
371
371
|
await mkdir(lockPath, { recursive: true });
|
|
372
372
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
373
373
|
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
@@ -375,7 +375,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
|
|
|
375
375
|
|
|
376
376
|
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
377
377
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
378
|
-
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
378
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock", "owner.json");
|
|
379
379
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
380
380
|
let replacedOwner = false;
|
|
381
381
|
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
@@ -392,7 +392,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
395
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
395
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock");
|
|
396
396
|
await assert.rejects(
|
|
397
397
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
398
398
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
@@ -402,18 +402,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
405
|
-
return join(packageRoot, ".gentle-ai", `.v2.8.
|
|
405
|
+
return join(packageRoot, ".gentle-ai", `.v2.8.2.install.tombstone-${nonce}`);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
409
409
|
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
410
410
|
return existsSync(runtimeRoot)
|
|
411
|
-
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.8.
|
|
411
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v2.8.2.install.tombstone-"))
|
|
412
412
|
: [];
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
416
|
-
return join(packageRoot, ".gentle-ai", `.v2.8.
|
|
416
|
+
return join(packageRoot, ".gentle-ai", `.v2.8.2.backup-${nonce}`);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
@@ -424,7 +424,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
|
|
|
424
424
|
|
|
425
425
|
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
426
426
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
427
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
427
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock");
|
|
428
428
|
await mkdir(lockPath, { recursive: true });
|
|
429
429
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
430
430
|
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
@@ -432,7 +432,7 @@ test("Windows source installation fails closed for an ownerless lock even after
|
|
|
432
432
|
|
|
433
433
|
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
434
434
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
435
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
435
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock");
|
|
436
436
|
await mkdir(lockPath, { recursive: true });
|
|
437
437
|
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
438
438
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -484,7 +484,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
|
|
|
484
484
|
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
485
485
|
);
|
|
486
486
|
assert.equal(existsSync(foreignTombstone), true);
|
|
487
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.8.
|
|
487
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock")), false);
|
|
488
488
|
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
489
489
|
});
|
|
490
490
|
|
|
@@ -492,7 +492,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
|
|
|
492
492
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
493
493
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
494
494
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
495
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.8.
|
|
495
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock")), false);
|
|
496
496
|
assert.deepEqual(await tombstones(packageRoot), []);
|
|
497
497
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
498
498
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
@@ -502,7 +502,7 @@ test("Windows source recovers a valid backup after a crash between publication r
|
|
|
502
502
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
503
503
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
504
504
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
505
|
-
const live = join(packageRoot, ".gentle-ai", "v2.8.
|
|
505
|
+
const live = join(packageRoot, ".gentle-ai", "v2.8.2");
|
|
506
506
|
const backup = backupBundlePath(packageRoot, "crash");
|
|
507
507
|
await rename(live, backup);
|
|
508
508
|
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -538,7 +538,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
|
|
|
538
538
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
539
539
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
540
540
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
541
|
-
const live = join(packageRoot, ".gentle-ai", "v2.8.
|
|
541
|
+
const live = join(packageRoot, ".gentle-ai", "v2.8.2");
|
|
542
542
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
543
543
|
await copyWindowsBundle(live, backup);
|
|
544
544
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -551,7 +551,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
|
|
|
551
551
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
552
552
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
553
553
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
554
|
-
const live = join(packageRoot, ".gentle-ai", "v2.8.
|
|
554
|
+
const live = join(packageRoot, ".gentle-ai", "v2.8.2");
|
|
555
555
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
556
556
|
await copyWindowsBundle(live, backup);
|
|
557
557
|
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
@@ -569,7 +569,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
569
569
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
570
570
|
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
571
571
|
await fixture.waitForInstall();
|
|
572
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.
|
|
572
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v2.8.2.install.lock");
|
|
573
573
|
await assert.rejects(
|
|
574
574
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
575
575
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
@@ -583,7 +583,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
583
583
|
|
|
584
584
|
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
585
585
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
586
|
-
const versionDirectory = join(packageRoot, ".gentle-ai", "v2.8.
|
|
586
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v2.8.2");
|
|
587
587
|
await mkdir(versionDirectory, { recursive: true });
|
|
588
588
|
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
589
589
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -620,7 +620,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
|
|
|
620
620
|
},
|
|
621
621
|
};
|
|
622
622
|
await installGentleAi(options);
|
|
623
|
-
const manifestPath = join(packageRoot, ".gentle-ai", "v2.8.
|
|
623
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v2.8.2", "integrity.json");
|
|
624
624
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
625
625
|
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
626
626
|
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
@@ -633,7 +633,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
|
|
|
633
633
|
// under the current stable pin, whose form gate refuses raw binaries.
|
|
634
634
|
const payload = Buffer.from("signed archive fixture");
|
|
635
635
|
const binary = "mirrored binary";
|
|
636
|
-
const baseAsset = { name: "gentle-ai_2.8.
|
|
636
|
+
const baseAsset = { name: "gentle-ai_2.8.2_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
|
|
637
637
|
const extractArchive = async (_archive: string, destination: string) => {
|
|
638
638
|
await mkdir(destination, { recursive: true });
|
|
639
639
|
await writeFile(join(destination, "gentle-ai"), binary);
|
|
@@ -751,7 +751,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
751
751
|
}),
|
|
752
752
|
/checksum mismatch/,
|
|
753
753
|
);
|
|
754
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
754
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai")), false);
|
|
755
755
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
756
756
|
});
|
|
757
757
|
|
|
@@ -773,7 +773,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
773
773
|
await chmod(extracted, 0o700);
|
|
774
774
|
},
|
|
775
775
|
});
|
|
776
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.8.
|
|
776
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai");
|
|
777
777
|
assert.equal(existsSync(binary), true);
|
|
778
778
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
779
779
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -804,7 +804,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
804
804
|
}),
|
|
805
805
|
/binary checksum mismatch/,
|
|
806
806
|
);
|
|
807
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
807
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai")), false);
|
|
808
808
|
});
|
|
809
809
|
|
|
810
810
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -827,7 +827,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
827
827
|
},
|
|
828
828
|
};
|
|
829
829
|
await installGentleAi(options);
|
|
830
|
-
const binary = join(packageRoot, ".gentle-ai", "v2.8.
|
|
830
|
+
const binary = join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai");
|
|
831
831
|
await chmod(binary, 0o600);
|
|
832
832
|
const repaired = await installGentleAi(options);
|
|
833
833
|
assert.equal(repaired.installed, true);
|
|
@@ -864,7 +864,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
864
864
|
}),
|
|
865
865
|
/exactly one regular gentle-ai/,
|
|
866
866
|
);
|
|
867
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
867
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai")), false);
|
|
868
868
|
});
|
|
869
869
|
|
|
870
870
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -882,7 +882,7 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
882
882
|
}),
|
|
883
883
|
/non-regular gentle-ai/,
|
|
884
884
|
);
|
|
885
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.
|
|
885
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v2.8.2", "gentle-ai")), false);
|
|
886
886
|
});
|
|
887
887
|
|
|
888
888
|
// #400: a clean Windows source build measured 232 s on a cold module cache,
|
package/tests/gentle-ai.test.ts
CHANGED
|
@@ -800,8 +800,9 @@ test("a later alias keeps managed-root precedence and manifest ownership", (t) =
|
|
|
800
800
|
assert.equal(manifest.assets["agents/sdd-apply.md"], createHash("sha256").update(routed).digest("hex"));
|
|
801
801
|
});
|
|
802
802
|
|
|
803
|
-
test("runtime guidance keeps review policy out of the static orchestrator", () => {
|
|
804
|
-
const staticReferences = ["
|
|
803
|
+
test("runtime guidance keeps review policy out of the static orchestrator and technical reference", () => {
|
|
804
|
+
const staticReferences = ["docs/readme-reference.md", "skills/gentle-ai/SKILL.md"];
|
|
805
|
+
assert.match(readFileSync("README.md", "utf8"), /\]\(docs\/readme-reference\.md(?:#[^)]+)?\)/);
|
|
805
806
|
const forbiddenGenericRoutes = [
|
|
806
807
|
/fresh-context `reviewer`/,
|
|
807
808
|
/fresh reviewer audits/,
|
|
@@ -204,11 +204,23 @@ test("2.8.1 repeats 2.8.0 because the negotiated lane Pi consumes is unchanged",
|
|
|
204
204
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.8.0"] as Record<string, boolean>);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
test("2.8.2 repeats 2.8.1 because the negotiated lane Pi consumes is unchanged", () => {
|
|
208
|
+
// v2.8.2 only shipped OpenCode SDD preflight plugin fixes, community-tools
|
|
209
|
+
// RTK acquisition, and Claude Code Stop telemetry (no review/SDD contract
|
|
210
|
+
// changes); the closed fields Pi consumes did not change between 2.8.1 and
|
|
211
|
+
// 2.8.2, so this row repeats 2.8.1. riskEvidence and hint remain dark
|
|
212
|
+
// because neither is proven to reach Pi's negotiated START path.
|
|
213
|
+
const contract = NATIVE_CLI_CONTRACTS["2.8.2"] as Record<string, boolean>;
|
|
214
|
+
assert.equal(contract.riskEvidence, false);
|
|
215
|
+
assert.equal(contract.hint, false);
|
|
216
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.8.1"] as Record<string, boolean>);
|
|
217
|
+
});
|
|
218
|
+
|
|
207
219
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
208
220
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
209
221
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
210
222
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
211
223
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
212
224
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
213
|
-
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"]);
|
|
225
|
+
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"]);
|
|
214
226
|
});
|
|
@@ -101,13 +101,14 @@ function readPackageJson(): PackageJson {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
test("
|
|
104
|
+
test("technical reference declares the tested Pi minimum required for agent_settled", () => {
|
|
105
105
|
const manifest = readPackageJson();
|
|
106
106
|
assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=0.85.1");
|
|
107
107
|
assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], "0.85.1");
|
|
108
|
-
const
|
|
109
|
-
assert.match(
|
|
110
|
-
assert.match(
|
|
108
|
+
const reference = readFileSync(join(PACKAGE_ROOT, "docs", "readme-reference.md"), "utf8");
|
|
109
|
+
assert.match(reference, /Pi 0\.85\.1 or newer/);
|
|
110
|
+
assert.match(reference, /agent_settled/);
|
|
111
|
+
assert.match(readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8"), /\]\(docs\/readme-reference\.md(?:#[^)]+)?\)/);
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
test("package manifest has no obsolete native activation build surface", () => {
|
|
@@ -229,12 +230,12 @@ test("generated runtime modules and packed-package checks are deterministic", ()
|
|
|
229
230
|
test("package manifest ships and runs the checked-in package-local Gentle AI installer", () => {
|
|
230
231
|
const packageJson = readPackageJson();
|
|
231
232
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
232
|
-
const
|
|
233
|
+
const reference = readFileSync(join(PACKAGE_ROOT, "docs", "readme-reference.md"), "utf8");
|
|
233
234
|
|
|
234
235
|
assert.equal(packageJson.scripts?.postinstall, "node scripts/install-gentle-ai.mjs");
|
|
235
|
-
assert.match(
|
|
236
|
-
assert.match(
|
|
237
|
-
assert.match(
|
|
236
|
+
assert.match(reference, /run `node scripts\/install-gentle-ai\.mjs`/, "missing-binary recovery documentation must use the package postinstall entrypoint");
|
|
237
|
+
assert.match(reference, /installed `gentle-pi` package directory/, "recovery documentation must name the package working directory");
|
|
238
|
+
assert.match(reference, /if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before/i, "recovery documentation must prevent the installer skip from repeating");
|
|
238
239
|
assert.ok(packageJson.files?.includes("scripts/"));
|
|
239
240
|
assert.match(verifier, /"scripts\/install-gentle-ai\.mjs"/);
|
|
240
241
|
assert.match(verifier, /"scripts\/gentle-ai-installer\.mjs"/);
|
|
@@ -277,20 +278,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
277
278
|
);
|
|
278
279
|
});
|
|
279
280
|
|
|
280
|
-
test("package verification binds the published Gentle AI v2.8.
|
|
281
|
+
test("package verification binds the published Gentle AI v2.8.2 runtime pin", () => {
|
|
281
282
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
282
283
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
283
284
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
284
285
|
|
|
285
|
-
assert.match(installer, /INSTALLER_VERSION = "2\.8\.
|
|
286
|
+
assert.match(installer, /INSTALLER_VERSION = "2\.8\.2"/);
|
|
286
287
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
287
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
288
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:54VJ0ruRiPc6MbNw4iFmlcBkQFOyF5D8iHY0A54Ivug="/);
|
|
288
289
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
289
290
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
290
291
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
291
292
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
292
293
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
293
|
-
assert.match(verifier, /v2\.8\.
|
|
294
|
+
assert.match(verifier, /v2\.8\.2/);
|
|
294
295
|
});
|
|
295
296
|
|
|
296
297
|
|
|
@@ -1508,9 +1509,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
|
|
|
1508
1509
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1509
1510
|
});
|
|
1510
1511
|
|
|
1511
|
-
test("v2.6.
|
|
1512
|
+
test("v2.6.2 release package and runtime stop before publication", () => {
|
|
1512
1513
|
const packageJson = readPackageJson();
|
|
1513
|
-
assert.equal(packageJson.version, "2.6.
|
|
1514
|
+
assert.equal(packageJson.version, "2.6.2", "the release manifest must remain explicitly pinned to v2.6.2");
|
|
1514
1515
|
assert.equal(
|
|
1515
1516
|
packageJson.scripts?.test,
|
|
1516
1517
|
"node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
@@ -1540,8 +1541,8 @@ test("bounded review keeps the Judgment Day skill contract at canon metadata ver
|
|
|
1540
1541
|
assert.doesNotMatch(frontmatter, /^ version: "1\.4"$/m);
|
|
1541
1542
|
});
|
|
1542
1543
|
|
|
1543
|
-
test("
|
|
1544
|
-
const
|
|
1544
|
+
test("technical reference documents dynamic Gentle AI RDD ownership and the installed permission boundary", () => {
|
|
1545
|
+
const reference = readFileSync(join(PACKAGE_ROOT, "docs", "readme-reference.md"), "utf8");
|
|
1545
1546
|
for (const clause of [
|
|
1546
1547
|
"Gentle AI dynamically supplies runtime-specific RDD instructions",
|
|
1547
1548
|
"does not define an RDD lifecycle",
|
|
@@ -1549,9 +1550,9 @@ test("README documents dynamic Gentle AI RDD ownership and the installed permiss
|
|
|
1549
1550
|
"package-managed isolated installation",
|
|
1550
1551
|
"Project and user overrides may shadow a package asset",
|
|
1551
1552
|
]) {
|
|
1552
|
-
assert.ok(
|
|
1553
|
+
assert.ok(reference.includes(clause), `technical reference missing dynamic RDD clause: ${clause}`);
|
|
1553
1554
|
}
|
|
1554
|
-
assert.doesNotMatch(
|
|
1555
|
+
assert.doesNotMatch(reference, /New ordinary review uses compact `gentle_review` `start -> finalize -> validate`\./);
|
|
1555
1556
|
});
|
|
1556
1557
|
|
|
1557
1558
|
|
|
@@ -2,21 +2,21 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const TECHNICAL_REFERENCE = readFileSync("docs/readme-reference.md", "utf8");
|
|
6
6
|
const CONTROLLER = readFileSync("extensions/gentle-ai.ts", "utf8");
|
|
7
7
|
|
|
8
|
-
test("
|
|
9
|
-
assert.match(
|
|
10
|
-
assert.match(
|
|
11
|
-
assert.match(
|
|
12
|
-
assert.match(
|
|
13
|
-
assert.match(
|
|
14
|
-
assert.match(
|
|
15
|
-
assert.match(
|
|
16
|
-
assert.match(
|
|
17
|
-
assert.match(
|
|
18
|
-
assert.match(
|
|
19
|
-
assert.match(
|
|
8
|
+
test("technical reference documents the narrow published native maintenance contract", () => {
|
|
9
|
+
assert.match(TECHNICAL_REFERENCE, /abandon.*quarantine-legacy.*reconcile-authority.*explicit v2\.1\.11 maintenance/i);
|
|
10
|
+
assert.match(TECHNICAL_REFERENCE, /predecessor lineage and revision.*successor lineage and revision/i);
|
|
11
|
+
assert.match(TECHNICAL_REFERENCE, /exact seven-line.*anomalies=unchanged_target,malformed_recovery_authorization/i);
|
|
12
|
+
assert.match(TECHNICAL_REFERENCE, /fresh interactive approval/i);
|
|
13
|
+
assert.match(TECHNICAL_REFERENCE, /quarantine only the bound invalid compact-v2 recovery successor/i);
|
|
14
|
+
assert.match(TECHNICAL_REFERENCE, /predecessor stays untouched/i);
|
|
15
|
+
assert.match(TECHNICAL_REFERENCE, /repair-legacy-alias.*unsupported historical v1 operation alias/i);
|
|
16
|
+
assert.match(TECHNICAL_REFERENCE, /model supplies only lineage, actor, and reason/i);
|
|
17
|
+
assert.match(TECHNICAL_REFERENCE, /review dispose-result.*unsupported.*pending.*design/i);
|
|
18
|
+
assert.match(TECHNICAL_REFERENCE, /RESET.*RECOVER.*destructive/i);
|
|
19
|
+
assert.match(TECHNICAL_REFERENCE, /typed envelopes/i);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test("controller help keeps authorization, blocked outcomes, and recovery boundaries explicit", () => {
|
|
@@ -298,6 +298,55 @@ test("approved acknowledgement burn tears down the retained candidate view and k
|
|
|
298
298
|
assert.ok(registry.hasProjection(lineageId, contributorRoot), "terminal approved cleanup keeps the lineage projection");
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
+
// gentle-ai#4003: the native burn is the committed authority outcome. A Pi-side
|
|
302
|
+
// candidate-view teardown failure after it must never be reported as a failed
|
|
303
|
+
// acknowledgement, or the caller cannot tell that authority was consumed.
|
|
304
|
+
test("approved acknowledgement reports the burn truthfully when candidate-view cleanup fails after it", async (t) => {
|
|
305
|
+
const lineageId = "acknowledge-approved-deferred-cleanup";
|
|
306
|
+
const contributorRoot = candidateRepository(t);
|
|
307
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
308
|
+
let failWorktreeRemove = true;
|
|
309
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) => {
|
|
310
|
+
if (failWorktreeRemove && arguments_[0] === "worktree" && arguments_[1] === "remove") {
|
|
311
|
+
failWorktreeRemove = false;
|
|
312
|
+
throw Object.assign(new Error("simulated worktree removal failure"), { status: 128 });
|
|
313
|
+
}
|
|
314
|
+
return execFileSync(file, arguments_, options);
|
|
315
|
+
});
|
|
316
|
+
t.after(() => registry.cleanupAll());
|
|
317
|
+
const view = registry.create({ contributorRoot });
|
|
318
|
+
registry.retain(view.token, lineageId);
|
|
319
|
+
const statusRequests: unknown[] = [];
|
|
320
|
+
let acknowledgements = 0;
|
|
321
|
+
const native = {
|
|
322
|
+
targetStatus: async (request: unknown) => { statusRequests.push(request); return approvedAcknowledgementStatus(lineageId, contributorRoot); },
|
|
323
|
+
acknowledgeApproved: async () => { acknowledgements += 1; },
|
|
324
|
+
} as unknown as NativeReviewCli;
|
|
325
|
+
|
|
326
|
+
const completed = await __testing.executeReviewControllerOperation({ operation: "acknowledge-approved", lineageId }, contributorRoot, native, undefined, registry);
|
|
327
|
+
|
|
328
|
+
assert.equal(acknowledgements, 1, "exactly one native burn");
|
|
329
|
+
assert.equal(statusRequests.length, 1, "no STATUS reconciliation after a cleanup-only failure");
|
|
330
|
+
assert.equal(completed.status, "closed");
|
|
331
|
+
assert.equal(completed.outcome, "native-approved-acknowledgement-completed");
|
|
332
|
+
assert.equal(completed.authority, "burned");
|
|
333
|
+
assert.equal(completed.mutation_performed, true);
|
|
334
|
+
assert.equal(completed.mutation_outcome, "committed");
|
|
335
|
+
assert.deepEqual(completed.candidate_view_cleanup, {
|
|
336
|
+
status: "deferred",
|
|
337
|
+
diagnostics: { code: "candidate-view-git-failure", message: "candidate-view Git command worktree failed; inspect the candidate state before any new START" },
|
|
338
|
+
next_action: "retry-candidate-view-cleanup-or-remove-the-view-out-of-band",
|
|
339
|
+
});
|
|
340
|
+
assert.ok(existsSync(view.root), "a failed teardown preserves the candidate view");
|
|
341
|
+
assert.ok(registry.hasProjection(lineageId, contributorRoot), "the lineage projection survives the deferred cleanup");
|
|
342
|
+
|
|
343
|
+
// The residue stays recoverable: the registry still owns the view, so a
|
|
344
|
+
// later terminal cleanup removes it without replaying the burn.
|
|
345
|
+
registry.cleanupTerminal(lineageId, "approved", contributorRoot);
|
|
346
|
+
assert.equal(existsSync(view.root), false);
|
|
347
|
+
assert.equal(acknowledgements, 1);
|
|
348
|
+
});
|
|
349
|
+
|
|
301
350
|
test("ambiguous acknowledgement reconciles STATUS once without replaying the provider vector", async () => {
|
|
302
351
|
const lineageId = "ambiguous-acknowledgement";
|
|
303
352
|
let statusCalls = 0;
|