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.
Files changed (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. 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
+ });