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.
Files changed (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. 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 the dev binary with a fresh sha256", async () => {
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.equal(override?.sha256, sha256(devBinary));
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("with no env var and no registration file, pinned resolution is byte-identical", async () => {
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 packageRoot = await scratch("gentle-pi-dev-pkg-");
91
- const env = environment(home);
92
- assert.equal(resolveGentleAiDevBinaryOverride(env, PLATFORM), undefined);
93
- // An empty env value means unset, exactly like the pinned path today.
94
- assert.equal(resolveGentleAiDevBinaryOverride(environment(home, { [GENTLE_AI_DEV_BINARY_ENV]: "" }), PLATFORM), undefined);
95
- assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), PackageLocalGentleAiBinaryMissingError);
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("a relative path, symlink, or non-executable env override is a typed refusal naming the env var", async () => {
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("the registration file resolves the dev binary and the env var wins over it", async () => {
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 bin = await scratch("gentle-pi-dev-bin-");
119
- const registered = writeDevBinary(bin);
120
- writeRegistration(home, registrationDocument(registered));
121
- const fileOnly = resolveGentleAiDevBinaryOverride(environment(home), PLATFORM);
122
- assert.equal(fileOnly?.source, "registration");
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 file", async () => {
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
- const env = environment(home, { GENTLE_PI_CONFIG_HOME: configHome });
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("a malformed or unresolvable registration is a typed refusal naming the file, never the pin", async () => {
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-pkg-");
130
+ const packageRoot = await scratch("gentle-pi-dev-package-");
142
131
  const devBinary = writeDevBinary(bin);
143
- const cases = [
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), contents);
157
- assert.throws(() => resolveGentleAiBinary(packageRoot, PLATFORM, readFileSync, env), isOverrideError(registrationPath), contents);
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("replacing the registered binary is picked up on the next resolution with the new sha256", async () => {
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
  });