gentle-pi 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { chmodSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { mkdtemp } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
@@ -15,34 +14,14 @@ import {
|
|
|
15
14
|
registerGentleAiDevBinary,
|
|
16
15
|
resolveGentleAiBinary,
|
|
17
16
|
resolveGentleAiDevBinaryOverride,
|
|
18
|
-
setGentleAiDevBinaryEnvironmentForTesting,
|
|
19
17
|
unregisterGentleAiDevBinary,
|
|
20
18
|
type GentleAiDevBinaryEnvironment,
|
|
21
19
|
} from "../lib/gentle-ai-binary.ts";
|
|
22
|
-
import {
|
|
23
|
-
NATIVE_REVIEW_ERROR_CODE,
|
|
24
|
-
NativeReviewCliError,
|
|
25
|
-
NativeReviewCliV214,
|
|
26
|
-
NativeReviewCliV216,
|
|
27
|
-
clearNativeReviewCapabilitiesCacheForTesting,
|
|
28
|
-
type ExecFileAdapter,
|
|
29
|
-
} from "../lib/native-review-cli.ts";
|
|
30
|
-
|
|
31
|
-
// Dev-binary override, maintainer field-test lane. The pinned supply-chain
|
|
32
|
-
// resolver stays byte-identical while neither the env var nor the persistent
|
|
33
|
-
// registration file is present; every override failure is a typed refusal that
|
|
34
|
-
// names its origin, never a silent fallback to the pin (silently running the
|
|
35
|
-
// pinned release while the maintainer believes he is testing main is the worst
|
|
36
|
-
// outcome this contract exists to prevent).
|
|
37
20
|
|
|
38
21
|
const PLATFORM = "linux";
|
|
39
22
|
|
|
40
|
-
function sha256(path: string): string {
|
|
41
|
-
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
23
|
async function scratch(prefix: string): Promise<string> {
|
|
45
|
-
return mkdtemp(join(tmpdir(), prefix));
|
|
24
|
+
return await mkdtemp(join(tmpdir(), prefix));
|
|
46
25
|
}
|
|
47
26
|
|
|
48
27
|
function writeDevBinary(directory: string, contents = "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+field'\n"): string {
|
|
@@ -56,6 +35,10 @@ function environment(home: string, env: Record<string, string | undefined> = {})
|
|
|
56
35
|
return { env, home };
|
|
57
36
|
}
|
|
58
37
|
|
|
38
|
+
function registrationDocument(path: string): string {
|
|
39
|
+
return `${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path })}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
59
42
|
function writeRegistration(home: string, contents: string): string {
|
|
60
43
|
const path = gentleAiDevBinaryRegistrationPath(environment(home));
|
|
61
44
|
mkdirSync(join(home, ".pi", "gentle-ai"), { recursive: true });
|
|
@@ -63,102 +46,105 @@ function writeRegistration(home: string, contents: string): string {
|
|
|
63
46
|
return path;
|
|
64
47
|
}
|
|
65
48
|
|
|
66
|
-
function registrationDocument(path: string): string {
|
|
67
|
-
return `${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path })}\n`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
49
|
const isOverrideError = (origin: string | RegExp) => (error: unknown): boolean =>
|
|
71
50
|
error instanceof GentleAiDevBinaryOverrideError
|
|
72
51
|
&& error.code === GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE
|
|
73
52
|
&& (typeof origin === "string" ? error.message.includes(origin) : origin.test(error.message));
|
|
74
53
|
|
|
75
|
-
test("the env override resolves
|
|
54
|
+
test("the explicit env override resolves a verified dev binary and never changes the pinned path", async () => {
|
|
76
55
|
const home = await scratch("gentle-pi-dev-home-");
|
|
77
56
|
const bin = await scratch("gentle-pi-dev-bin-");
|
|
57
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
78
58
|
const devBinary = writeDevBinary(bin);
|
|
79
59
|
const env = environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: devBinary });
|
|
60
|
+
|
|
80
61
|
const override = resolveGentleAiDevBinaryOverride(env, PLATFORM);
|
|
81
62
|
assert.equal(override?.source, "env");
|
|
82
63
|
assert.equal(override?.path, devBinary);
|
|
83
|
-
assert.
|
|
84
|
-
const packageRoot = await scratch("gentle-pi-dev-pkg-");
|
|
64
|
+
assert.match(override?.sha256 ?? "", /^[0-9a-f]{64}$/);
|
|
85
65
|
assert.equal(resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), devBinary);
|
|
66
|
+
|
|
67
|
+
const pinnedEnvironment = environment(home);
|
|
68
|
+
assert.equal(resolveGentleAiDevBinaryOverride(pinnedEnvironment, PLATFORM), undefined);
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, pinnedEnvironment),
|
|
71
|
+
PackageLocalGentleAiBinaryMissingError,
|
|
72
|
+
);
|
|
86
73
|
});
|
|
87
74
|
|
|
88
|
-
test("
|
|
75
|
+
test("a registration is strict, env wins, and unregister restores the pinned resolver", async () => {
|
|
89
76
|
const home = await scratch("gentle-pi-dev-home-");
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
const registeredDirectory = await scratch("gentle-pi-dev-bin-");
|
|
78
|
+
const envDirectory = await scratch("gentle-pi-dev-env-");
|
|
79
|
+
const registered = writeDevBinary(registeredDirectory);
|
|
80
|
+
const envBinary = writeDevBinary(envDirectory, "#!/bin/sh\necho 'gentle-ai 9.9.10-dev'\n");
|
|
81
|
+
const registeredResult = registerGentleAiDevBinary(registered, environment(home), PLATFORM);
|
|
82
|
+
|
|
83
|
+
assert.equal(readFileSync(registeredResult.registrationPath, "utf8"), registrationDocument(registered));
|
|
84
|
+
assert.equal(resolveGentleAiDevBinaryOverride(environment(home), PLATFORM)?.path, registered);
|
|
85
|
+
assert.equal(resolveGentleAiDevBinaryOverride(environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: envBinary }), PLATFORM)?.path, envBinary);
|
|
86
|
+
assert.equal(unregisterGentleAiDevBinary(environment(home)), true);
|
|
87
|
+
assert.equal(resolveGentleAiDevBinaryOverride(environment(home), PLATFORM), undefined);
|
|
88
|
+
assert.equal(unregisterGentleAiDevBinary(environment(home)), false);
|
|
96
89
|
});
|
|
97
90
|
|
|
98
|
-
test("
|
|
91
|
+
test("invalid override sources fail closed instead of silently falling back to the pin", async () => {
|
|
99
92
|
const home = await scratch("gentle-pi-dev-home-");
|
|
100
93
|
const bin = await scratch("gentle-pi-dev-bin-");
|
|
94
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
101
95
|
const devBinary = writeDevBinary(bin);
|
|
102
|
-
const packageRoot = await scratch("gentle-pi-dev-pkg-");
|
|
103
96
|
const symlinked = join(bin, "gentle-ai-link");
|
|
104
97
|
symlinkSync(devBinary, symlinked);
|
|
105
98
|
const nonExecutable = join(bin, "gentle-ai-noexec");
|
|
106
99
|
writeFileSync(nonExecutable, "#!/bin/sh\n");
|
|
107
100
|
chmodSync(nonExecutable, 0o644);
|
|
101
|
+
|
|
108
102
|
for (const value of ["relative/gentle-ai", symlinked, nonExecutable, join(bin, "missing-gentle-ai")]) {
|
|
109
103
|
const env = environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: value });
|
|
110
104
|
assert.throws(() => resolveGentleAiDevBinaryOverride(env, PLATFORM), isOverrideError(GENTLE_AI_DEV_BINARY_ENV), value);
|
|
111
|
-
// Never a silent fallback to the pin.
|
|
112
105
|
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(GENTLE_AI_DEV_BINARY_ENV), value);
|
|
113
106
|
}
|
|
107
|
+
|
|
108
|
+
const registrationPath = writeRegistration(home, "not-json\n");
|
|
109
|
+
assert.throws(() => resolveGentleAiDevBinaryOverride(environment(home), PLATFORM), isOverrideError(registrationPath));
|
|
114
110
|
});
|
|
115
111
|
|
|
116
|
-
test("
|
|
112
|
+
test("an unset or empty dev-binary environment preserves the package-local resolver", async () => {
|
|
117
113
|
const home = await scratch("gentle-pi-dev-home-");
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
assert.equal(fileOnly?.path, registered);
|
|
124
|
-
const envDirectory = await scratch("gentle-pi-dev-bin-");
|
|
125
|
-
const envBinary = writeDevBinary(envDirectory, "#!/bin/sh\necho 'gentle-ai 9.9.10-dev'\n");
|
|
126
|
-
const both = resolveGentleAiDevBinaryOverride(environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: envBinary }), PLATFORM);
|
|
127
|
-
assert.equal(both?.source, "env");
|
|
128
|
-
assert.equal(both?.path, envBinary);
|
|
114
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
115
|
+
for (const env of [environment(home), environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: "" })]) {
|
|
116
|
+
assert.equal(resolveGentleAiDevBinaryOverride(env, PLATFORM), undefined);
|
|
117
|
+
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), PackageLocalGentleAiBinaryMissingError);
|
|
118
|
+
}
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
test("GENTLE_PI_CONFIG_HOME relocates the registration
|
|
121
|
+
test("GENTLE_PI_CONFIG_HOME relocates only the local registration document", async () => {
|
|
132
122
|
const home = await scratch("gentle-pi-dev-home-");
|
|
133
123
|
const configHome = await scratch("gentle-pi-dev-config-");
|
|
134
|
-
|
|
135
|
-
assert.equal(gentleAiDevBinaryRegistrationPath(env), join(configHome, "dev-binary.json"));
|
|
124
|
+
assert.equal(gentleAiDevBinaryRegistrationPath(environment(home, { GENTLE_PI_CONFIG_HOME: configHome })), join(configHome, "dev-binary.json"));
|
|
136
125
|
assert.equal(gentleAiDevBinaryRegistrationPath(environment(home)), join(home, ".pi", "gentle-ai", "dev-binary.json"));
|
|
137
126
|
});
|
|
138
127
|
|
|
139
|
-
test("
|
|
128
|
+
test("malformed registration variants name their local registration path and never fall back", async () => {
|
|
140
129
|
const bin = await scratch("gentle-pi-dev-bin-");
|
|
141
|
-
const packageRoot = await scratch("gentle-pi-dev-
|
|
130
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
142
131
|
const devBinary = writeDevBinary(bin);
|
|
143
|
-
const
|
|
132
|
+
for (const contents of [
|
|
144
133
|
"not json at all",
|
|
145
134
|
`${JSON.stringify({ schema: "gentle-pi.dev-binary/v0", path: devBinary })}\n`,
|
|
146
|
-
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path: devBinary, sha256: "0".repeat(64) })}\n`,
|
|
147
135
|
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA })}\n`,
|
|
148
136
|
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path: "relative/gentle-ai" })}\n`,
|
|
149
|
-
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path: join(bin, "missing-gentle-ai") })}\n`,
|
|
150
137
|
`${JSON.stringify([devBinary])}\n`,
|
|
151
|
-
]
|
|
152
|
-
for (const contents of cases) {
|
|
138
|
+
]) {
|
|
153
139
|
const home = await scratch("gentle-pi-dev-home-");
|
|
154
140
|
const registrationPath = writeRegistration(home, contents);
|
|
155
141
|
const env = environment(home);
|
|
156
|
-
assert.throws(() => resolveGentleAiDevBinaryOverride(env, PLATFORM), isOverrideError(registrationPath)
|
|
157
|
-
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(registrationPath)
|
|
142
|
+
assert.throws(() => resolveGentleAiDevBinaryOverride(env, PLATFORM), isOverrideError(registrationPath));
|
|
143
|
+
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(registrationPath));
|
|
158
144
|
}
|
|
159
145
|
});
|
|
160
146
|
|
|
161
|
-
test("
|
|
147
|
+
test("registered binary replacement is observed on the next resolution", async () => {
|
|
162
148
|
const home = await scratch("gentle-pi-dev-home-");
|
|
163
149
|
const bin = await scratch("gentle-pi-dev-bin-");
|
|
164
150
|
const devBinary = writeDevBinary(bin, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build1'\n");
|
|
@@ -168,169 +154,6 @@ test("replacing the registered binary is picked up on the next resolution with t
|
|
|
168
154
|
writeFileSync(devBinary, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build2'\n");
|
|
169
155
|
chmodSync(devBinary, 0o755);
|
|
170
156
|
const second = resolveGentleAiDevBinaryOverride(env, PLATFORM);
|
|
171
|
-
assert.equal(first?.sha256, createHash("sha256").update("#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build1'\n").digest("hex"));
|
|
172
|
-
assert.equal(second?.sha256, createHash("sha256").update("#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build2'\n").digest("hex"));
|
|
173
157
|
assert.notEqual(first?.sha256, second?.sha256);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
test("register and unregister write and delete the strict registration document", async () => {
|
|
177
|
-
const home = await scratch("gentle-pi-dev-home-");
|
|
178
|
-
const bin = await scratch("gentle-pi-dev-bin-");
|
|
179
|
-
const devBinary = writeDevBinary(bin);
|
|
180
|
-
const env = environment(home);
|
|
181
|
-
const registered = registerGentleAiDevBinary(devBinary, env, PLATFORM);
|
|
182
|
-
assert.equal(registered.registrationPath, gentleAiDevBinaryRegistrationPath(env));
|
|
183
|
-
assert.equal(readFileSync(registered.registrationPath, "utf8"), registrationDocument(devBinary));
|
|
184
|
-
assert.equal(resolveGentleAiDevBinaryOverride(env, PLATFORM)?.path, devBinary);
|
|
185
|
-
assert.throws(() => registerGentleAiDevBinary("relative/gentle-ai", env, PLATFORM), isOverrideError(/relative\/gentle-ai/));
|
|
186
|
-
assert.equal(unregisterGentleAiDevBinary(env), true);
|
|
187
|
-
assert.equal(resolveGentleAiDevBinaryOverride(env, PLATFORM), undefined);
|
|
188
|
-
assert.equal(unregisterGentleAiDevBinary(env), false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Version gate: dev mode accepts the binary's reported version with the latest
|
|
193
|
-
// known contract row as the capability floor; pinned mode stays byte-identical.
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
interface QueuedResult { stdout: string; stderr?: string; exitCode?: number }
|
|
197
|
-
|
|
198
|
-
function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; calls: Array<{ file: string; arguments: readonly string[] }> } {
|
|
199
|
-
const calls: Array<{ file: string; arguments: readonly string[] }> = [];
|
|
200
|
-
return {
|
|
201
|
-
calls,
|
|
202
|
-
adapter: async (request) => {
|
|
203
|
-
calls.push({ file: request.file, arguments: request.arguments });
|
|
204
|
-
const result = results.shift();
|
|
205
|
-
if (!result) throw new Error("unexpected native invocation");
|
|
206
|
-
return { stdout: result.stdout, stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const START = { stdout: JSON.stringify({ operation: "review/start", lineage_id: "lineage-1", state: "reviewing", risk_level: "medium", selected_lenses: ["review-reliability"], changed_files: 1, changed_lines: 2, correction_budget: 1, action: "created", lenses_required: true, projection: "workspace" }) };
|
|
212
|
-
const DEV_VERSION_OUTPUTS = ["gentle-ai 2.4.0-rc.8+fix.verify-attestation-recovery\n", "gentle-ai 9.9.9\n"];
|
|
213
|
-
|
|
214
|
-
async function withDevEnvironment<T>(callback: () => Promise<T>): Promise<T> {
|
|
215
|
-
const home = await scratch("gentle-pi-dev-home-");
|
|
216
|
-
const bin = await scratch("gentle-pi-dev-bin-");
|
|
217
|
-
const devBinary = writeDevBinary(bin);
|
|
218
|
-
setGentleAiDevBinaryEnvironmentForTesting({ env: { [GENTLE_AI_DEV_BINARY_ENV]: devBinary }, home });
|
|
219
|
-
try {
|
|
220
|
-
return await callback();
|
|
221
|
-
} finally {
|
|
222
|
-
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function withPinnedEnvironment<T>(callback: () => Promise<T>): Promise<T> {
|
|
227
|
-
const home = await scratch("gentle-pi-dev-home-");
|
|
228
|
-
setGentleAiDevBinaryEnvironmentForTesting({ env: {}, home });
|
|
229
|
-
try {
|
|
230
|
-
return await callback();
|
|
231
|
-
} finally {
|
|
232
|
-
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
test("pinned mode still refuses an unknown reported version", async () => {
|
|
237
|
-
await withPinnedEnvironment(async () => {
|
|
238
|
-
for (const stdout of DEV_VERSION_OUTPUTS) {
|
|
239
|
-
const queue = queuedAdapter([{ stdout }]);
|
|
240
|
-
await assert.rejects(
|
|
241
|
-
() => new NativeReviewCliV214(queue.adapter, "/package/.gentle-ai/gentle-ai").start({ cwd: "/repo" }),
|
|
242
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
|
|
243
|
-
stdout,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("dev mode accepts the reported version and floors capabilities at the latest known contract row", async () => {
|
|
250
|
-
await withDevEnvironment(async () => {
|
|
251
|
-
for (const stdout of DEV_VERSION_OUTPUTS) {
|
|
252
|
-
const queue = queuedAdapter([{ stdout }, START]);
|
|
253
|
-
const result = await new NativeReviewCliV214(queue.adapter, "/package/.gentle-ai/gentle-ai").start({ cwd: "/repo" });
|
|
254
|
-
assert.equal(result.lineageId, "lineage-1", stdout);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("dev mode never rescues a pinned-mode unparseable version banner", async () => {
|
|
260
|
-
await withDevEnvironment(async () => {
|
|
261
|
-
const queue = queuedAdapter([{ stdout: "definitely not a version banner\n" }]);
|
|
262
|
-
await assert.rejects(
|
|
263
|
-
() => new NativeReviewCliV214(queue.adapter, "/package/.gentle-ai/gentle-ai").start({ cwd: "/repo" }),
|
|
264
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
|
|
265
|
-
);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Captured 2026-08-15 from /home/gentleman/.cargo/bin/gentle-ai reporting
|
|
270
|
-
// "gentle-ai 2.4.0-rc.8+fix.verify-attestation-recovery" (a gentle-ai main
|
|
271
|
-
// line dev build) via `gentle-ai review capabilities --contract
|
|
272
|
-
// gentle-ai.review-integration/v2` in a scratch git repository.
|
|
273
|
-
const CAPABILITIES_V22 = readFileSync(join(import.meta.dirname, "fixtures", "devbinary", "capabilities-v2.2.captured.json"), "utf8");
|
|
274
|
-
const CAPTURED_EXECUTABLE_DIGEST = "ffc91d8fa79c869aba9aa3d1ec80edebb5b1744e5a06fef75d4c8b73c0e46bc1";
|
|
275
|
-
const STATUS_V5 = readFileSync(join(import.meta.dirname, "fixtures", "devbinary", "status-v5.captured.json"), "utf8");
|
|
276
|
-
const FORECAST_STDERR = "Forecast horizon: partial\nstep 1: collect; reason_code=empty_candidate_base_ref_required; description=empty candidate base ref required\nRe-query STATUS after completing this partial head.\n";
|
|
277
|
-
|
|
278
|
-
function v216(adapter: ExecFileAdapter, digest: string): NativeReviewCliV216 {
|
|
279
|
-
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 16 * 1024 * 1024, async () => undefined, () => digest);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
test("pinned mode refuses a dev-version capabilities envelope on package version equality", async () => {
|
|
283
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
284
|
-
await withPinnedEnvironment(async () => {
|
|
285
|
-
const queue = queuedAdapter([{ stdout: CAPABILITIES_V22 }]);
|
|
286
|
-
await assert.rejects(
|
|
287
|
-
() => v216(queue.adapter, CAPTURED_EXECUTABLE_DIGEST).capabilities({ cwd: "/repo" }),
|
|
288
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("dev mode negotiates the captured v2.2 capabilities envelope without the pin equality", async () => {
|
|
294
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
295
|
-
await withDevEnvironment(async () => {
|
|
296
|
-
const queue = queuedAdapter([{ stdout: CAPABILITIES_V22 }]);
|
|
297
|
-
const capabilities = await v216(queue.adapter, CAPTURED_EXECUTABLE_DIGEST).capabilities({ cwd: "/repo" });
|
|
298
|
-
assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("dev mode tolerates the provider's forecast narration on negotiated STATUS stderr and still refuses other stderr", async () => {
|
|
303
|
-
await withDevEnvironment(async () => {
|
|
304
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
305
|
-
const queue = queuedAdapter([{ stdout: CAPABILITIES_V22 }, { stdout: STATUS_V5, stderr: FORECAST_STDERR }]);
|
|
306
|
-
const status = await v216(queue.adapter, CAPTURED_EXECUTABLE_DIGEST).targetStatus({ cwd: "/repo", projection: "workspace" });
|
|
307
|
-
assert.equal(status.action, "start");
|
|
308
|
-
assert.equal(status.forecast?.horizon, "partial");
|
|
309
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
310
|
-
const noisy = queuedAdapter([{ stdout: CAPABILITIES_V22 }, { stdout: STATUS_V5, stderr: "panic: something else\n" }]);
|
|
311
|
-
await assert.rejects(
|
|
312
|
-
() => v216(noisy.adapter, CAPTURED_EXECUTABLE_DIGEST).targetStatus({ cwd: "/repo", projection: "workspace" }),
|
|
313
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
314
|
-
);
|
|
315
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("pinned mode still refuses forecast narration stderr on negotiated STATUS", async () => {
|
|
320
|
-
await withPinnedEnvironment(async () => {
|
|
321
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
322
|
-
// A pinned-version capabilities envelope cannot be captured from the dev
|
|
323
|
-
// binary; assert one layer down instead: with the dev override absent the
|
|
324
|
-
// forecast narration is not in any tolerated stderr set, so the negotiated
|
|
325
|
-
// STATUS invocation itself reports UNEXPECTED_STDERR. Capability
|
|
326
|
-
// negotiation is bypassed by pre-seeding the cache through a dev-mode run
|
|
327
|
-
// being impossible here, so this test drives the legacy client, whose
|
|
328
|
-
// stderr discipline is the same code path.
|
|
329
|
-
const queue = queuedAdapter([{ stdout: "gentle-ai 2.1.5\n" }, { stdout: STATUS_V5, stderr: FORECAST_STDERR }]);
|
|
330
|
-
await assert.rejects(
|
|
331
|
-
() => new NativeReviewCliV214(queue.adapter, "/package/.gentle-ai/gentle-ai").reviewStatus({ cwd: "/repo" }),
|
|
332
|
-
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
333
|
-
);
|
|
334
|
-
clearNativeReviewCapabilitiesCacheForTesting();
|
|
335
|
-
});
|
|
158
|
+
assert.equal(second?.path, devBinary);
|
|
336
159
|
});
|