gentle-pi 2.1.2 → 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 +108 -79
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -3
- package/assets/agents/review-reliability.md +4 -3
- package/assets/agents/review-resilience.md +4 -3
- package/assets/agents/review-risk.md +4 -3
- package/assets/agents/sdd-apply.md +11 -4
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +8 -1
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +10 -5
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +8 -4
- package/assets/agents/sdd-verify.md +27 -2
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +2 -2
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +2 -2
- package/assets/orchestrator-delegation.md +145 -160
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +25 -48
- package/assets/sdd-orchestrator-workflow.md +163 -25
- package/assets/support/sdd-status-contract.md +24 -6
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- 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/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +14 -16
- package/docs/review-integration.md +27 -353
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2710 -3211
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +21 -12
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +676 -781
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +836 -118
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +578 -0
- package/lib/review-integration-v2.ts +1254 -221
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +230 -67
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -79
- package/runtime/gentle-ai-binary.mjs +174 -3
- package/runtime/native-review-cli.mjs +610 -715
- package/runtime/review-integration-v2.mjs +1223 -190
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +85 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +20 -9
- package/scripts/verify-package-files.mjs +119 -43
- package/skills/_shared/review-ledger-contract.md +11 -19
- 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/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +9 -5
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- 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/background-subagents.test.ts +771 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +16 -0
- package/tests/delegated-key-learnings-contract.test.ts +240 -0
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -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/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -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-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +159 -0
- package/tests/gentle-ai-installer.test.ts +138 -48
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +601 -0
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +75 -2
- package/tests/native-review-cli.test.ts +588 -908
- package/tests/native-review-consent.test.ts +283 -38
- package/tests/native-review-parity-runtime.test.ts +108 -350
- package/tests/native-review-parity.test.ts +684 -732
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +158 -89
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +128 -100
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +252 -0
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +898 -12
- package/tests/review-compact-contract.test.ts +29 -122
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +366 -857
- package/tests/review-controller-native-routing.test.ts +1253 -4199
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +236 -70
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +134 -0
- package/tests/review-dispatch-hydration-gap.test.ts +145 -0
- 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 +352 -0
- package/tests/review-host-relay.test.ts +754 -0
- package/tests/review-integration-v2-forward.test.ts +654 -0
- package/tests/review-integration-v2.test.ts +317 -129
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +106 -60
- package/tests/review-recovered-lineage-routing.test.ts +199 -0
- package/tests/review-relay-transport-agent.test.ts +322 -0
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +573 -180
- package/tests/sdd-agent-tools.test.ts +53 -32
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +6 -8
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- 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/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/git-commit-transaction.ts +0 -801
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/runtime/git-commit-transaction.mjs +0 -802
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- 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 -302
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { mkdtemp } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
10
|
+
import {
|
|
11
|
+
GENTLE_AI_DEV_BINARY_ENV,
|
|
12
|
+
gentleAiDevBinaryRegistrationPath,
|
|
13
|
+
setGentleAiDevBinaryEnvironmentForTesting,
|
|
14
|
+
} from "../lib/gentle-ai-binary.ts";
|
|
15
|
+
|
|
16
|
+
// Loud surfacing for the dev-binary override: while an override is active,
|
|
17
|
+
// every diagnostic surface must say so, name the exact binary, its live
|
|
18
|
+
// version, and its content digest — the maintainer must never wonder which
|
|
19
|
+
// gentle-ai actually answered.
|
|
20
|
+
|
|
21
|
+
interface CommandRegistration {
|
|
22
|
+
handler: (args: string, ctx: ExtensionContext) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function harness(): { pi: ExtensionAPI; commands: Map<string, CommandRegistration> } {
|
|
26
|
+
const commands = new Map<string, CommandRegistration>();
|
|
27
|
+
const pi = {
|
|
28
|
+
on() {},
|
|
29
|
+
registerCommand(name: string, registration: CommandRegistration) {
|
|
30
|
+
commands.set(name, registration);
|
|
31
|
+
},
|
|
32
|
+
registerTool() {},
|
|
33
|
+
} as unknown as ExtensionAPI;
|
|
34
|
+
return { pi, commands };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function contextFor(cwd: string, notifications: Array<{ message: string; severity: string }>): ExtensionContext {
|
|
38
|
+
return {
|
|
39
|
+
cwd,
|
|
40
|
+
hasUI: true,
|
|
41
|
+
ui: {
|
|
42
|
+
notify(message: string, severity: string) {
|
|
43
|
+
notifications.push({ message, severity });
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
} as unknown as ExtensionContext;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function withDevOverride<T>(callback: (state: { devBinary: string; sha256: string; home: string }) => Promise<T>): Promise<T> {
|
|
50
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-home-"));
|
|
51
|
+
const bin = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-bin-"));
|
|
52
|
+
const devBinary = join(bin, "gentle-ai");
|
|
53
|
+
writeFileSync(devBinary, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+surface'\n");
|
|
54
|
+
chmodSync(devBinary, 0o755);
|
|
55
|
+
const sha256 = createHash("sha256").update(readFileSync(devBinary)).digest("hex");
|
|
56
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: { [GENTLE_AI_DEV_BINARY_ENV]: devBinary }, home });
|
|
57
|
+
try {
|
|
58
|
+
return await callback({ devBinary, sha256, home });
|
|
59
|
+
} finally {
|
|
60
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("gentle:doctor and gentle:status surface the active dev-binary override loudly", async () => {
|
|
65
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
66
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
67
|
+
try {
|
|
68
|
+
await withDevOverride(async ({ devBinary, sha256 }) => {
|
|
69
|
+
const { pi, commands } = harness();
|
|
70
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
71
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
72
|
+
const expected = `Gentle AI dev binary override active (unpinned, field-test only): ${devBinary} 9.9.9-dev+surface sha256:${sha256.slice(0, 16)}`;
|
|
73
|
+
for (const command of ["gentle:doctor", "gentle:status"]) {
|
|
74
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
75
|
+
await commands.get(command)!.handler("", contextFor(cwd, notifications));
|
|
76
|
+
assert.equal(notifications.length, 1, command);
|
|
77
|
+
assert.ok(notifications[0]!.message.includes(expected), `${command}: ${notifications[0]!.message}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
82
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("without an override the surfaces stay silent about dev binaries", async () => {
|
|
87
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
88
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
89
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-home-"));
|
|
90
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: {}, home });
|
|
91
|
+
try {
|
|
92
|
+
const { pi, commands } = harness();
|
|
93
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
94
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
95
|
+
for (const command of ["gentle:doctor", "gentle:status"]) {
|
|
96
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
97
|
+
await commands.get(command)!.handler("", contextFor(cwd, notifications));
|
|
98
|
+
assert.equal(notifications.length, 1, command);
|
|
99
|
+
assert.doesNotMatch(notifications[0]!.message, /dev binary/i, command);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
103
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
104
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("an invalid override is surfaced as a failure, never silently ignored", async () => {
|
|
109
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
110
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
111
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-home-"));
|
|
112
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: { [GENTLE_AI_DEV_BINARY_ENV]: "/nonexistent/gentle-ai" }, home });
|
|
113
|
+
try {
|
|
114
|
+
const { pi, commands } = harness();
|
|
115
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
116
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
117
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
118
|
+
await commands.get("gentle:doctor")!.handler("", contextFor(cwd, notifications));
|
|
119
|
+
assert.equal(notifications.length, 1);
|
|
120
|
+
assert.match(notifications[0]!.message, /fail: Gentle AI dev binary override/);
|
|
121
|
+
assert.match(notifications[0]!.message, new RegExp(GENTLE_AI_DEV_BINARY_ENV));
|
|
122
|
+
} finally {
|
|
123
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
124
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
125
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("gentle:dev-binary registers, reports, and clears the persistent override", async () => {
|
|
130
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-home-"));
|
|
131
|
+
const bin = await mkdtemp(join(tmpdir(), "gentle-pi-dev-surface-bin-"));
|
|
132
|
+
const devBinary = join(bin, "gentle-ai");
|
|
133
|
+
writeFileSync(devBinary, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+register'\n");
|
|
134
|
+
chmodSync(devBinary, 0o755);
|
|
135
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: {}, home });
|
|
136
|
+
try {
|
|
137
|
+
const { pi, commands } = harness();
|
|
138
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
139
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
140
|
+
const command = commands.get("gentle:dev-binary");
|
|
141
|
+
assert.ok(command, "gentle:dev-binary command is registered");
|
|
142
|
+
const registrationPath = gentleAiDevBinaryRegistrationPath({ env: {}, home });
|
|
143
|
+
|
|
144
|
+
let notifications: Array<{ message: string; severity: string }> = [];
|
|
145
|
+
await command!.handler("status", contextFor(cwd, notifications));
|
|
146
|
+
assert.match(notifications[0]!.message, /no dev binary override/i);
|
|
147
|
+
|
|
148
|
+
notifications = [];
|
|
149
|
+
await command!.handler(devBinary, contextFor(cwd, notifications));
|
|
150
|
+
assert.equal(existsSync(registrationPath), true);
|
|
151
|
+
assert.match(notifications[0]!.message, /dev binary override active \(unpinned, field-test only\)/);
|
|
152
|
+
assert.ok(notifications[0]!.message.includes(devBinary));
|
|
153
|
+
|
|
154
|
+
notifications = [];
|
|
155
|
+
await command!.handler("relative/gentle-ai", contextFor(cwd, notifications));
|
|
156
|
+
assert.equal(notifications[0]!.severity, "error");
|
|
157
|
+
|
|
158
|
+
notifications = [];
|
|
159
|
+
await command!.handler("off", contextFor(cwd, notifications));
|
|
160
|
+
assert.equal(existsSync(registrationPath), false);
|
|
161
|
+
assert.match(notifications[0]!.message, /removed|cleared/i);
|
|
162
|
+
} finally {
|
|
163
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("session start announces the active override once, loudly", async () => {
|
|
168
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
169
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
170
|
+
try {
|
|
171
|
+
await withDevOverride(async ({ devBinary, sha256 }) => {
|
|
172
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void>>();
|
|
173
|
+
const pi = {
|
|
174
|
+
on(name: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void>) {
|
|
175
|
+
handlers.set(name, handler);
|
|
176
|
+
},
|
|
177
|
+
registerCommand() {},
|
|
178
|
+
registerTool() {},
|
|
179
|
+
} as unknown as ExtensionAPI;
|
|
180
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
181
|
+
const sessionStart = handlers.get("session_start");
|
|
182
|
+
assert.equal(typeof sessionStart, "function");
|
|
183
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
184
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
185
|
+
await sessionStart!({}, contextFor(cwd, notifications));
|
|
186
|
+
const expected = `Gentle AI dev binary override active (unpinned, field-test only): ${devBinary} 9.9.9-dev+surface sha256:${sha256.slice(0, 16)}`;
|
|
187
|
+
const announcement = notifications.find((entry) => entry.message.includes(expected));
|
|
188
|
+
assert.ok(announcement, JSON.stringify(notifications));
|
|
189
|
+
assert.equal(announcement!.severity, "warning");
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
193
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
GENTLE_AI_DEV_BINARY_ENV,
|
|
9
|
+
GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE,
|
|
10
|
+
GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA,
|
|
11
|
+
GentleAiDevBinaryOverrideError,
|
|
12
|
+
PackageLocalGentleAiBinaryMissingError,
|
|
13
|
+
gentleAiDevBinaryRegistrationPath,
|
|
14
|
+
registerGentleAiDevBinary,
|
|
15
|
+
resolveGentleAiBinary,
|
|
16
|
+
resolveGentleAiDevBinaryOverride,
|
|
17
|
+
unregisterGentleAiDevBinary,
|
|
18
|
+
type GentleAiDevBinaryEnvironment,
|
|
19
|
+
} from "../lib/gentle-ai-binary.ts";
|
|
20
|
+
|
|
21
|
+
const PLATFORM = "linux";
|
|
22
|
+
|
|
23
|
+
async function scratch(prefix: string): Promise<string> {
|
|
24
|
+
return await mkdtemp(join(tmpdir(), prefix));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeDevBinary(directory: string, contents = "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+field'\n"): string {
|
|
28
|
+
const path = join(directory, "gentle-ai");
|
|
29
|
+
writeFileSync(path, contents);
|
|
30
|
+
chmodSync(path, 0o755);
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function environment(home: string, env: Record<string, string | undefined> = {}): GentleAiDevBinaryEnvironment {
|
|
35
|
+
return { env, home };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function registrationDocument(path: string): string {
|
|
39
|
+
return `${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path })}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeRegistration(home: string, contents: string): string {
|
|
43
|
+
const path = gentleAiDevBinaryRegistrationPath(environment(home));
|
|
44
|
+
mkdirSync(join(home, ".pi", "gentle-ai"), { recursive: true });
|
|
45
|
+
writeFileSync(path, contents);
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isOverrideError = (origin: string | RegExp) => (error: unknown): boolean =>
|
|
50
|
+
error instanceof GentleAiDevBinaryOverrideError
|
|
51
|
+
&& error.code === GENTLE_AI_DEV_BINARY_OVERRIDE_INVALID_CODE
|
|
52
|
+
&& (typeof origin === "string" ? error.message.includes(origin) : origin.test(error.message));
|
|
53
|
+
|
|
54
|
+
test("the explicit env override resolves a verified dev binary and never changes the pinned path", async () => {
|
|
55
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
56
|
+
const bin = await scratch("gentle-pi-dev-bin-");
|
|
57
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
58
|
+
const devBinary = writeDevBinary(bin);
|
|
59
|
+
const env = environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: devBinary });
|
|
60
|
+
|
|
61
|
+
const override = resolveGentleAiDevBinaryOverride(env, PLATFORM);
|
|
62
|
+
assert.equal(override?.source, "env");
|
|
63
|
+
assert.equal(override?.path, devBinary);
|
|
64
|
+
assert.match(override?.sha256 ?? "", /^[0-9a-f]{64}$/);
|
|
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
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a registration is strict, env wins, and unregister restores the pinned resolver", async () => {
|
|
76
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
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);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("invalid override sources fail closed instead of silently falling back to the pin", async () => {
|
|
92
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
93
|
+
const bin = await scratch("gentle-pi-dev-bin-");
|
|
94
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
95
|
+
const devBinary = writeDevBinary(bin);
|
|
96
|
+
const symlinked = join(bin, "gentle-ai-link");
|
|
97
|
+
symlinkSync(devBinary, symlinked);
|
|
98
|
+
const nonExecutable = join(bin, "gentle-ai-noexec");
|
|
99
|
+
writeFileSync(nonExecutable, "#!/bin/sh\n");
|
|
100
|
+
chmodSync(nonExecutable, 0o644);
|
|
101
|
+
|
|
102
|
+
for (const value of ["relative/gentle-ai", symlinked, nonExecutable, join(bin, "missing-gentle-ai")]) {
|
|
103
|
+
const env = environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: value });
|
|
104
|
+
assert.throws(() => resolveGentleAiDevBinaryOverride(env, PLATFORM), isOverrideError(GENTLE_AI_DEV_BINARY_ENV), value);
|
|
105
|
+
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(GENTLE_AI_DEV_BINARY_ENV), value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const registrationPath = writeRegistration(home, "not-json\n");
|
|
109
|
+
assert.throws(() => resolveGentleAiDevBinaryOverride(environment(home), PLATFORM), isOverrideError(registrationPath));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("an unset or empty dev-binary environment preserves the package-local resolver", async () => {
|
|
113
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
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
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("GENTLE_PI_CONFIG_HOME relocates only the local registration document", async () => {
|
|
122
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
123
|
+
const configHome = await scratch("gentle-pi-dev-config-");
|
|
124
|
+
assert.equal(gentleAiDevBinaryRegistrationPath(environment(home, { GENTLE_PI_CONFIG_HOME: configHome })), join(configHome, "dev-binary.json"));
|
|
125
|
+
assert.equal(gentleAiDevBinaryRegistrationPath(environment(home)), join(home, ".pi", "gentle-ai", "dev-binary.json"));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("malformed registration variants name their local registration path and never fall back", async () => {
|
|
129
|
+
const bin = await scratch("gentle-pi-dev-bin-");
|
|
130
|
+
const packageRoot = await scratch("gentle-pi-dev-package-");
|
|
131
|
+
const devBinary = writeDevBinary(bin);
|
|
132
|
+
for (const contents of [
|
|
133
|
+
"not json at all",
|
|
134
|
+
`${JSON.stringify({ schema: "gentle-pi.dev-binary/v0", path: devBinary })}\n`,
|
|
135
|
+
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA })}\n`,
|
|
136
|
+
`${JSON.stringify({ schema: GENTLE_AI_DEV_BINARY_REGISTRATION_SCHEMA, path: "relative/gentle-ai" })}\n`,
|
|
137
|
+
`${JSON.stringify([devBinary])}\n`,
|
|
138
|
+
]) {
|
|
139
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
140
|
+
const registrationPath = writeRegistration(home, contents);
|
|
141
|
+
const env = environment(home);
|
|
142
|
+
assert.throws(() => resolveGentleAiDevBinaryOverride(env, PLATFORM), isOverrideError(registrationPath));
|
|
143
|
+
assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(registrationPath));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("registered binary replacement is observed on the next resolution", async () => {
|
|
148
|
+
const home = await scratch("gentle-pi-dev-home-");
|
|
149
|
+
const bin = await scratch("gentle-pi-dev-bin-");
|
|
150
|
+
const devBinary = writeDevBinary(bin, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build1'\n");
|
|
151
|
+
writeRegistration(home, registrationDocument(devBinary));
|
|
152
|
+
const env = environment(home);
|
|
153
|
+
const first = resolveGentleAiDevBinaryOverride(env, PLATFORM);
|
|
154
|
+
writeFileSync(devBinary, "#!/bin/sh\necho 'gentle-ai 9.9.9-dev+build2'\n");
|
|
155
|
+
chmodSync(devBinary, 0o755);
|
|
156
|
+
const second = resolveGentleAiDevBinaryOverride(env, PLATFORM);
|
|
157
|
+
assert.notEqual(first?.sha256, second?.sha256);
|
|
158
|
+
assert.equal(second?.path, devBinary);
|
|
159
|
+
});
|