gentle-pi 2.4.0 → 2.6.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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -7,12 +7,16 @@ import {
7
7
  decodeReviewConsentV3,
8
8
  } from "../lib/review-integration-v2.ts";
9
9
  import {
10
+ decodeNativeSddStatusV2,
10
11
  NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES,
11
12
  NATIVE_REVIEW_ERROR_CODE,
12
13
  NativeReviewCliError,
13
14
  NativeReviewCliV216,
15
+ NativeReviewIntegrationError,
14
16
  createNodeExecFileAdapter,
17
+ isNativeReviewUnachievableVerbRefused,
15
18
  type ExecFileAdapter,
19
+ type NativeTargetStatusRequest,
16
20
  } from "../lib/native-review-cli.ts";
17
21
 
18
22
  const fixture = (name: string): Record<string, unknown> => JSON.parse(
@@ -56,6 +60,100 @@ function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
56
60
  return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024);
57
61
  }
58
62
 
63
+ // Structural test data follows the 2026-09-11 native 2.7.1-0.20260911070137-350f31554a4b
64
+ // capture: seven dependencies and four phaseInstructions groups, not a legacy alias.
65
+ function nativeSddStatus(changeName = "complete-native-review-lifecycle", workspaceRoot = "/repo"): Record<string, unknown> {
66
+ return {
67
+ schemaName: "gentle-ai.sdd-status",
68
+ schemaVersion: 2,
69
+ changeName,
70
+ artifactStore: "openspec",
71
+ planningHome: { mode: "repo-local", path: `${workspaceRoot}/openspec` },
72
+ changeRoot: `${workspaceRoot}/openspec/changes/${changeName}`,
73
+ actionContext: { mode: "repo-local", workspaceRoot, allowedEditRoots: [workspaceRoot] },
74
+ dependencies: { proposal: "all_done", specs: "all_done", design: "all_done", tasks: "all_done", apply: "all_done", verify: "all_done", archive: "ready" },
75
+ phaseInstructions: {
76
+ apply: ["Apply is complete."],
77
+ verify: ["Verification is complete."],
78
+ remediate: ["Bind remediation to failed evidence."],
79
+ archive: ["Archive the selected change."],
80
+ },
81
+ blockedReasons: [],
82
+ nextRecommended: "archive",
83
+ };
84
+ }
85
+
86
+ test("native SDD status executes its exact selected-root argv and returns only a validated v2 authority", async () => {
87
+ const body = nativeSddStatus();
88
+ const queue = queuedAdapter([{ stdout: JSON.stringify(body) }]);
89
+ const status = await (client(queue.adapter) as unknown as {
90
+ sddStatus(request: { changeName: string; workspaceRoot: string }): Promise<Record<string, unknown>>;
91
+ }).sddStatus({ changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" });
92
+
93
+ assert.deepEqual(status, body);
94
+ assert.deepEqual(queue.calls, [{
95
+ file: "/package/.gentle-ai/gentle-ai",
96
+ arguments: ["sdd-status", "complete-native-review-lifecycle", "--cwd", "/repo", "--json", "--instructions"],
97
+ cwd: "/repo",
98
+ timeoutMs: 30_000,
99
+ }]);
100
+ });
101
+
102
+ test("native SDD status rejects malformed v2 identities, dependencies, instructions, and blockers", async () => {
103
+ const invalidInstructions = { apply: ["ok"], verify: ["ok"], remediate: ["ok"], archive: [42] };
104
+ assert.doesNotThrow(() => decodeNativeSddStatusV2(
105
+ { ...nativeSddStatus(), phaseInstructions: { ...invalidInstructions, archive: ["ok"] } },
106
+ { changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" },
107
+ ));
108
+ const malformed = [
109
+ { ...nativeSddStatus(), schemaName: "gentle-pi.sdd-status" },
110
+ { ...nativeSddStatus(), schemaVersion: 1 },
111
+ { ...nativeSddStatus(), changeName: "other-change" },
112
+ { ...nativeSddStatus(), artifactStore: "future-store" },
113
+ { ...nativeSddStatus(), planningHome: { mode: "future-mode", path: "/repo/openspec" } },
114
+ { ...nativeSddStatus(), planningHome: { mode: "repo-local", path: "/outside" } },
115
+ { ...nativeSddStatus(), changeRoot: "bad\nroot" },
116
+ { ...nativeSddStatus(), actionContext: { mode: "repo-local", workspaceRoot: "/other", allowedEditRoots: ["/other"] } },
117
+ { ...nativeSddStatus(), actionContext: { mode: "future-mode", workspaceRoot: "/repo", allowedEditRoots: ["/repo"] } },
118
+ { ...nativeSddStatus(), actionContext: { mode: "repo-local", workspaceRoot: "/repo", allowedEditRoots: ["/outside"] } },
119
+ { ...nativeSddStatus(), dependencies: { apply: "all_done", verify: "all_done", archive: "future" } },
120
+ { ...nativeSddStatus(), phaseInstructions: invalidInstructions },
121
+ { ...nativeSddStatus(), blockedReasons: "not-an-array" },
122
+ { ...nativeSddStatus(), nextRecommended: "unknown" },
123
+ { ...nativeSddStatus(), instructions: {}, phaseInstructions: undefined },
124
+ { ...nativeSddStatus(), phaseInstructions: null },
125
+ ];
126
+ for (const body of malformed) {
127
+ await assert.rejects(
128
+ () => (client(queuedAdapter([{ stdout: JSON.stringify(body) }]).adapter) as unknown as {
129
+ sddStatus(request: { changeName: string; workspaceRoot: string }): Promise<Record<string, unknown>>;
130
+ }).sddStatus({ changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" }),
131
+ (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
132
+ );
133
+ }
134
+ });
135
+
136
+ test("native v2 read-only decoding preserves omitted optional phaseInstructions", () => {
137
+ const body = nativeSddStatus();
138
+ delete body.phaseInstructions;
139
+ assert.equal(decodeNativeSddStatusV2(body, { changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" }), body);
140
+ });
141
+
142
+ test("native SDD status keeps malformed JSON, timeout, and nonzero execution fail-closed", async () => {
143
+ for (const [result, code] of [
144
+ [{ stdout: "not-json" }, NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON],
145
+ [{ stdout: "", timedOut: true }, NATIVE_REVIEW_ERROR_CODE.TIMEOUT],
146
+ [{ stdout: "{}", exitCode: 1 }, NATIVE_REVIEW_ERROR_CODE.NON_ZERO],
147
+ ] as const) {
148
+ await assert.rejects(
149
+ () => (client(queuedAdapter([result]).adapter) as unknown as {
150
+ sddStatus(request: { changeName: string; workspaceRoot: string }): Promise<Record<string, unknown>>;
151
+ }).sddStatus({ changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" }),
152
+ (error: unknown) => error instanceof NativeReviewCliError && error.code === code && error.mutationOutcome === "none",
153
+ );
154
+ }
155
+ });
156
+
59
157
  test("negotiated STATUS accepts the pinned v5 receipt before routing its transition", async () => {
60
158
  const status = fixture("status-v5.captured.json");
61
159
  const queue = queuedAdapter([{ stdout: JSON.stringify(status) }]);
@@ -133,6 +231,125 @@ test("provider-owned refuter and targeted-validator vectors accept only their ma
133
231
  );
134
232
  });
135
233
 
234
+ // gentle-pi#638: the host relay's deterministic-failure exit declares the
235
+ // bound slot unachievable through the native verb instead of leaving the
236
+ // operator to re-spend an identical reviewer run. The invocation names the
237
+ // slot's frozen binding fields (--lineage/--target/--expected-revision/
238
+ // --request-hash), the machine-readable --reason, an optional bounded
239
+ // --detail, and the opaque --repository-context the slot carried; Go
240
+ // verifies all of them against the frozen authority before recording
241
+ // anything (internal/cli/review_capture_unachievable.go).
242
+ const UNACHIEVABLE_TARGET = `sha256:${"1".repeat(64)}`;
243
+ const UNACHIEVABLE_REVISION = `sha256:${"3".repeat(64)}`;
244
+ const UNACHIEVABLE_REQUEST_HASH = `sha256:${"0".repeat(64)}`;
245
+ const UNACHIEVABLE_ARTIFACT = { schema: "gentle-ai.review-capture-unachievable/v1", lineage_id: "relay-lineage", target_identity: UNACHIEVABLE_TARGET, lens: "review-reliability", selected_order: 0, reason: "relay_transport_bound_exceeded", recorded: true };
246
+ function unachievableRequest(cwd = "/repo") {
247
+ return { cwd, lineageId: "relay-lineage", targetIdentity: UNACHIEVABLE_TARGET, expectedRevision: UNACHIEVABLE_REVISION, requestHash: UNACHIEVABLE_REQUEST_HASH, reason: "relay_transport_bound_exceeded" };
248
+ }
249
+
250
+ test("capture-unachievable declares the exact slot binding and decodes its recorded artifact", async () => {
251
+ const queue = queuedAdapter([{ stdout: JSON.stringify(UNACHIEVABLE_ARTIFACT) }]);
252
+ const result = await client(queue.adapter).captureUnachievableLens({ ...unachievableRequest(), detail: "killed after 2256004ms against a 2256000ms relay bound", repositoryContext: "rctx1_" + "e".repeat(64) });
253
+ assert.deepEqual(result, { schema: "gentle-ai.review-capture-unachievable/v1", lineageId: "relay-lineage", targetIdentity: UNACHIEVABLE_TARGET, lens: "review-reliability", selectedOrder: 0, reason: "relay_transport_bound_exceeded", recorded: true });
254
+ // gentle-pi#822: --repository-context is authoritative and mutually exclusive with a path, so a declaration carrying one names no --cwd; the process still runs from request.cwd.
255
+ assert.deepEqual(queue.calls[0]?.arguments, ["review", "capture-unachievable", "--lineage", "relay-lineage", "--target", UNACHIEVABLE_TARGET, "--expected-revision", UNACHIEVABLE_REVISION, "--request-hash", UNACHIEVABLE_REQUEST_HASH, "--reason", "relay_transport_bound_exceeded", "--detail", "killed after 2256004ms against a 2256000ms relay bound", "--repository-context", "rctx1_" + "e".repeat(64)]);
256
+ assert.equal(queue.calls[0]?.cwd, "/repo", "the process working directory stays request.cwd even when --cwd is not passed as an argument");
257
+ assert.equal(queue.calls[0]?.timeoutMs, undefined, "the mutating declaration runs without the read-only negotiation timeout");
258
+
259
+ // the optional detail and repository context are genuinely optional
260
+ const bare = queuedAdapter([{ stdout: JSON.stringify(UNACHIEVABLE_ARTIFACT) }]);
261
+ await client(bare.adapter).captureUnachievableLens(unachievableRequest());
262
+ assert.deepEqual(bare.calls[0]?.arguments, ["review", "capture-unachievable", "--lineage", "relay-lineage", "--target", UNACHIEVABLE_TARGET, "--expected-revision", UNACHIEVABLE_REVISION, "--request-hash", UNACHIEVABLE_REQUEST_HASH, "--reason", "relay_transport_bound_exceeded", "--cwd", "/repo"]);
263
+ assert.equal(bare.calls[0]?.cwd, "/repo");
264
+ });
265
+
266
+ test("capture-unachievable measures detail in UTF-8 bytes, not UTF-16 code units", async () => {
267
+ const atLimit = queuedAdapter([{ stdout: JSON.stringify(UNACHIEVABLE_ARTIFACT) }]);
268
+ await client(atLimit.adapter).captureUnachievableLens({ ...unachievableRequest(), detail: "é".repeat(256) });
269
+ assert.equal(atLimit.calls.length, 1, "256 two-byte characters are exactly the 512-byte limit and must pass");
270
+
271
+ const oneCharBeyond = queuedAdapter([]);
272
+ await assert.rejects(() => client(oneCharBeyond.adapter).captureUnachievableLens({ ...unachievableRequest(), detail: "é".repeat(257) }), TypeError);
273
+ assert.equal(oneCharBeyond.calls.length, 0, "257 two-byte characters are 514 bytes and are refused before any process launches");
274
+
275
+ const shortButHeavy = queuedAdapter([]);
276
+ await assert.rejects(() => client(shortButHeavy.adapter).captureUnachievableLens({ ...unachievableRequest(), detail: "é".repeat(300) }), TypeError);
277
+ assert.equal(shortButHeavy.calls.length, 0, "300 code units pass a .length check but encode to 600 bytes and must throw");
278
+ });
279
+
280
+ test("capture-unachievable validates its request and artifact shape before and after the invocation", async () => {
281
+ for (const request of [
282
+ { ...unachievableRequest(), lineageId: "" },
283
+ { ...unachievableRequest(), targetIdentity: "not-a-sha" },
284
+ { ...unachievableRequest(), expectedRevision: " " },
285
+ { ...unachievableRequest(), requestHash: "sha256:short" },
286
+ { ...unachievableRequest(), reason: "" },
287
+ { ...unachievableRequest(), detail: "x".repeat(513) },
288
+ { ...unachievableRequest(), repositoryContext: "no\u0000context" },
289
+ ]) {
290
+ const queue = queuedAdapter([]);
291
+ await assert.rejects(() => client(queue.adapter).captureUnachievableLens(request), TypeError);
292
+ assert.equal(queue.calls.length, 0, "a malformed declaration is refused before any process launches");
293
+ }
294
+
295
+ for (const artifact of [
296
+ { ...UNACHIEVABLE_ARTIFACT, schema: "gentle-ai.review-capture-unachievable/v2" },
297
+ { ...UNACHIEVABLE_ARTIFACT, recorded: false },
298
+ { ...UNACHIEVABLE_ARTIFACT, selected_order: -1 },
299
+ ]) {
300
+ const queue = queuedAdapter([{ stdout: JSON.stringify(artifact) }]);
301
+ await assert.rejects(() => client(queue.adapter).captureUnachievableLens(unachievableRequest()));
302
+ assert.equal(queue.calls.length, 1);
303
+ }
304
+ });
305
+
306
+ test("an older binary's unknown-verb refusal classifies as a capability signal, not a capture failure", async () => {
307
+ const queue = queuedAdapter([{ stdout: "", stderr: 'Error: unknown review command "capture-unachievable"\n', exitCode: 1 }]);
308
+ const error = await client(queue.adapter).captureUnachievableLens(unachievableRequest()).then(() => undefined, (caught: unknown) => caught);
309
+ assert.ok(error instanceof NativeReviewCliError, "an empty-stdout refusal rejects with the typed CLI error carrying stderr diagnostics");
310
+ assert.equal(isNativeReviewUnachievableVerbRefused(error), true);
311
+
312
+ // Every other stderr — a typed binding-mismatch refusal, a timeout, or a
313
+ // clean run — is a real outcome, never a capability signal.
314
+ assert.equal(isNativeReviewUnachievableVerbRefused(new Error('unknown review command "capture-unachievable"')), false, "only the captured native stderr classifies");
315
+ const typedRefusal = new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, "review/capture-unachievable", true, true, "native capture-unachievable refused", { operation: "review/capture-unachievable", error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO, exit_code: 1, timed_out: false, output_limit_exceeded: false, stderr: "Error: review capture-unachievable binding does not match the current reviewing authority [invalid_request]" });
316
+ assert.equal(isNativeReviewUnachievableVerbRefused(typedRefusal), false);
317
+ });
318
+
319
+ test("nonzero targeted-validator capture preserves its dot-form typed failure", async () => {
320
+ const failure = {
321
+ schema: "gentle-ai.review-integration.failure/v2",
322
+ contract: "gentle-ai.review-integration/v2",
323
+ operation: "review.capture-validation",
324
+ phase: "native_running",
325
+ code: "targeted_validation_failed",
326
+ message: "the targeted validator rejected the candidate",
327
+ mutation_outcome: "unknown",
328
+ authority_applicability: "current_target",
329
+ retry_safe: false,
330
+ replayability: "status_required",
331
+ required_inputs: [],
332
+ next_action: "review.status",
333
+ };
334
+ const queue = queuedAdapter([{ stdout: JSON.stringify(failure), exitCode: 1 }]);
335
+ await assert.rejects(
336
+ () => client(queue.adapter).captureProviderRole({
337
+ captureOperation: "review.capture-validation",
338
+ argumentTokens: ["--repository-context=rctx1_" + "a".repeat(64), "--agent=pi", "--execute=true"],
339
+ cwd: "/repo",
340
+ }),
341
+ (error: unknown) => {
342
+ if (!(error instanceof NativeReviewIntegrationError)) return false;
343
+ assert.equal(error.failureEnvelope.operation, "review.capture-validation");
344
+ assert.equal(error.mutationOutcome, "unknown");
345
+ assert.equal(error.nextAction, "review.status");
346
+ assert.deepEqual(error.failureEnvelope.raw, failure);
347
+ return true;
348
+ },
349
+ );
350
+ assert.deepEqual(queue.calls[0]?.arguments, ["review", "capture-validation", "--repository-context=rctx1_" + "a".repeat(64), "--agent=pi", "--execute=true"]);
351
+ });
352
+
136
353
  test("malformed closure output remains a typed schema failure and never authorizes a retry", async () => {
137
354
  const queue = queuedAdapter([{ stdout: JSON.stringify({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review/capture-result" }) }]);
138
355
  await assert.rejects(
@@ -247,7 +464,8 @@ test("negotiated STATUS rejects malformed committed selectors before launching a
247
464
  { cwd: "/repo", baseRef: "refs/heads/main", committedOnly: "true" },
248
465
  ]) {
249
466
  const queue = queuedAdapter([]);
250
- await assert.rejects(() => client(queue.adapter).targetStatus(request), TypeError);
467
+ // gentle-pi#822: these fixtures are intentionally mistyped (a numeric baseRef, a string committedOnly) — the repo's `as unknown as` idiom keeps the invalid shapes reaching the validator instead of widening the request type.
468
+ await assert.rejects(() => client(queue.adapter).targetStatus(request as unknown as NativeTargetStatusRequest), TypeError);
251
469
  assert.equal(queue.calls.length, 0);
252
470
  }
253
471
  });
@@ -568,8 +786,8 @@ test("capture-result receives the controller AbortSignal without an automatic mu
568
786
  assert.equal(receivedTimeout, undefined);
569
787
  });
570
788
 
571
- test("native review client leaves SDD status resolution to the local SDD engine", () => {
572
- assert.equal("sddStatus" in client(queuedAdapter([]).adapter), false);
789
+ test("native review client exposes native v2 SDD status without adding review lifecycle behavior", () => {
790
+ assert.equal(typeof (client(queuedAdapter([]).adapter) as unknown as { sddStatus?: unknown }).sddStatus, "function");
573
791
  });
574
792
 
575
793
  test("read-only authority inventory rejects a repository identity mismatch after decoding", async () => {
@@ -609,6 +827,46 @@ test("current native process boundary preserves caps, timeout classes, and sanit
609
827
  );
610
828
  });
611
829
 
830
+ test("authority inventory accepts compact discarded work and historical entry statuses", async () => {
831
+ const repository = process.cwd();
832
+ const discardedWork = { captured_lens_results: [], findings_present: false };
833
+ const statuses = ["incomplete-store-entry", "historical-pre-receipt", "invalidated"] as const;
834
+ const body = {
835
+ schema: "gentle-ai.review-authority-status/v1",
836
+ operation: "review/status",
837
+ repository,
838
+ complete: true,
839
+ authoritative: true,
840
+ status: "active",
841
+ entries: statuses.map((status) => ({
842
+ version: "compact-v2",
843
+ lineage_id: `review-${status}`,
844
+ path: `${repository}/.git/gentle-ai/${status}`,
845
+ status,
846
+ discarded_work: discardedWork,
847
+ problems: [],
848
+ })),
849
+ locks: [],
850
+ diagnostics: [],
851
+ };
852
+ const decoded = await client(queuedAdapter([{ stdout: JSON.stringify(body) }]).adapter).reviewStatus({ cwd: repository });
853
+ assert.deepEqual(decoded.entries.map((entry) => ({ status: entry.status, discardedWork: entry.discardedWork })), statuses.map((status) => ({
854
+ status,
855
+ discardedWork: { capturedLensResults: [], findingsPresent: false },
856
+ })));
857
+
858
+ for (const entry of [
859
+ { ...body.entries[0], discarded_work: { captured_lens_results: "not-an-array", findings_present: false } },
860
+ { ...body.entries[0], discarded_work: { ...discardedWork, unexpected: true } },
861
+ { ...body.entries[0], status: "future-status" },
862
+ ]) {
863
+ await assert.rejects(
864
+ () => client(queuedAdapter([{ stdout: JSON.stringify({ ...body, entries: [entry] }) }]).adapter).reviewStatus({ cwd: repository }),
865
+ (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
866
+ );
867
+ }
868
+ });
869
+
612
870
  test("current review STATUS retains compact snapshot and released-lock wire fields", async (t) => {
613
871
  const repository = process.cwd();
614
872
  const snapshotIdentity = `sha256:${"d".repeat(64)}`;
@@ -742,3 +1000,59 @@ test("acknowledge-approved keeps its fail-closed stderr and typed refusal discip
742
1000
  (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT,
743
1001
  );
744
1002
  });
1003
+
1004
+ test("native continuation uses a distinct mutating operation and the same v2 decoder", async () => {
1005
+ const queue = queuedAdapter([{ stdout: JSON.stringify(nativeSddStatus()) }, { stdout: "{}" }]);
1006
+ const cli = client(queue.adapter);
1007
+ const request = { changeName: "complete-native-review-lifecycle", workspaceRoot: "/repo" };
1008
+ assert.equal((await cli.sddContinue(request)).nextRecommended, nativeSddStatus().nextRecommended);
1009
+ assert.deepEqual(queue.calls[0]!.arguments, ["sdd-continue", request.changeName, "--cwd", "/repo", "--json", "--instructions"]);
1010
+ assert.equal(queue.calls[0]!.timeoutMs, undefined);
1011
+ await assert.rejects(() => cli.sddContinue(request), (error: unknown) => error instanceof NativeReviewCliError && error.mutating === true);
1012
+ });
1013
+
1014
+ test("native discovery preserves nullable selection and rejects an invented selected identity", () => {
1015
+ const discovery = { ...nativeSddStatus(), changeName: null, changeRoot: null, nextRecommended: "select-change" };
1016
+ assert.equal(decodeNativeSddStatusV2(discovery, { workspaceRoot: "/repo" }).changeName, null);
1017
+ assert.throws(() => decodeNativeSddStatusV2(discovery, { changeName: "alpha", workspaceRoot: "/repo" }), /identity/);
1018
+ });
1019
+
1020
+ test("all twelve native actions retain their exact tokens without prose routing", () => {
1021
+ for (const nextRecommended of ["apply", "verify", "remediate", "archive", "archived", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"]) {
1022
+ const status = { ...nativeSddStatus(), nextRecommended, ...(nextRecommended === "remediate" ? { remediationState: { required: true, complete: false, failedEvidenceRevision: `sha256:${"a".repeat(64)}` } } : {}) };
1023
+ assert.equal(decodeNativeSddStatusV2(status, { workspaceRoot: "/repo" }).nextRecommended, nextRecommended);
1024
+ }
1025
+ });
1026
+
1027
+
1028
+ test("compact SDD bracket transports exact selection and remediation binding", async () => {
1029
+ const revision = `sha256:${"a".repeat(64)}`;
1030
+ const queued = queuedAdapter([{ stdout: JSON.stringify({ state: "proceed", token: revision }) }, { stdout: '{"state":"complete"}' }]);
1031
+ const cli = client(queued.adapter);
1032
+ const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "acquire-one", workUnit: "correction", evidenceGoal: "Observed correction", maxAttempts: 1, maxChangedLines: 300, remediatesEvidenceRevision: revision };
1033
+ assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "proceed", token: revision });
1034
+ assert.deepEqual(queued.calls[0].arguments, ["sdd-attempt", "acquire", "--cwd", "/repo", "--change", "fix", "--request-id", "acquire-one", "--work-unit", "correction", "--evidence-goal", "Observed correction", "--max-attempts", "1", "--max-changed-lines", "300", "--remediates-evidence-revision", revision]);
1035
+ assert.deepEqual(await cli.sddAttemptSettle({ workspaceRoot: "/repo", changeName: "fix", token: revision, requestId: "settle-one", outcome: "interrupted", diagnosis: "Child could not spawn", harnessDisposition: "invalidated", cleanupEvidence: "No process created", processEvidence: "Spawn rejected", remediatesEvidenceRevision: revision }), { state: "complete" });
1036
+ assert.equal(queued.calls[1].arguments.includes("--evidence-revision"), false);
1037
+ assert.equal(queued.calls[1].timeoutMs, undefined);
1038
+ });
1039
+
1040
+ test("compact SDD refuses malformed result and invalid terminal evidence before execution", async () => {
1041
+ const queued = queuedAdapter([{ stdout: '{"state":"proceed"}' }, { stdout: '{"state":"blocked","reason":"budget"}' }]);
1042
+ const cli = client(queued.adapter);
1043
+ const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "correction", evidenceGoal: "Observed correction" };
1044
+ await assert.rejects(cli.sddAttemptAcquire(request), /schema incompatible/);
1045
+ assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "blocked", reason: "budget" });
1046
+ await assert.rejects(cli.sddAttemptSettle({ ...request, token: `sha256:${"a".repeat(64)}`, outcome: "failed", diagnosis: "Test failed", harnessDisposition: "reused", cleanupEvidence: "Process exited", processEvidence: "Exit one" }), /evidence/i);
1047
+ assert.equal(queued.calls.length, 2);
1048
+ });
1049
+
1050
+
1051
+ test("compact admission accepts native empty CAS and never redirects uncertainty into review", async () => {
1052
+ const queued = queuedAdapter([{ stdout: '{"state":"blocked","reason":"budget"}' }, { stdout: "", timedOut: true }]);
1053
+ const cli = client(queued.adapter);
1054
+ const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "fix", evidenceGoal: "Observed correction", expectedRevision: "" };
1055
+ await cli.sddAttemptAcquire(request);
1056
+ assert.equal(queued.calls[0].arguments[queued.calls[0].arguments.indexOf("--expected-revision") + 1], "");
1057
+ await assert.rejects(cli.sddAttemptAcquire({ ...request, expectedRevision: undefined }), error => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === undefined);
1058
+ });
@@ -176,6 +176,97 @@ test("consent follow-up executes the provider-named invocation exactly once and
176
176
 
177
177
  // A binding mismatch is decided entirely inside Pi, before the provider is
178
178
  // launched, so it must not be reported as a provider failure (issue #247).
179
+ test("consent answer preserves quoted Windows provider cwd tokens and launches the provider exactly once", async () => {
180
+ const windowsCwd = "C:\\Users\\x\\repo with spaces";
181
+ const gitBashCwd = "/c/Users/x/repo with spaces";
182
+ const cwdPairs = process.platform === "win32"
183
+ ? [{ providerCwd: windowsCwd, requestedCwd: gitBashCwd }, { providerCwd: gitBashCwd, requestedCwd: windowsCwd }, { providerCwd: gitBashCwd, requestedCwd: gitBashCwd }, { providerCwd: windowsCwd, requestedCwd: windowsCwd }]
184
+ : [{ providerCwd: windowsCwd, requestedCwd: windowsCwd }];
185
+ for (const { providerCwd, requestedCwd } of cwdPairs) {
186
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
187
+ const lineage = "windows-cwd-lineage";
188
+ for (const choice of decoded.choices) {
189
+ choice.invocation = choice.invocation
190
+ .replace("--cwd /repo", `--cwd "${providerCwd}"`)
191
+ .replace("review-consent-fixture", lineage);
192
+ }
193
+ const started = JSON.parse(JSON.stringify(fixture<Record<string, unknown>>("start.fixture.json")).replaceAll("review-start-fixture", lineage)) as Record<string, unknown>;
194
+ const expected = [
195
+ "review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", providerCwd,
196
+ "--target", decoded.targetIdentity, "--projection", "workspace", "--lineage", lineage, "--consent", "granted",
197
+ ];
198
+ for (const createClient of [client, runtimeClient]) {
199
+ const queue = queuedAdapter([capabilities(), structuredClone(started)]);
200
+ const result = await createClient(queue.adapter).answerConsent({ cwd: requestedCwd, consent: decoded, answer: "granted" });
201
+ assert.equal(result.kind, "started");
202
+ assert.deepEqual(queue.calls, [expected]);
203
+ }
204
+ }
205
+ });
206
+
207
+ test("consent answer preserves quoted UNC and drive-root cwd tokens", async () => {
208
+ for (const providerCwd of ["\\\\server\\share", "C:\\"]) {
209
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
210
+ const lineage = "windows-root-lineage";
211
+ for (const choice of decoded.choices) {
212
+ choice.invocation = choice.invocation
213
+ .replace("--cwd /repo", `--cwd "${providerCwd}"`)
214
+ .replace("review-consent-fixture", lineage);
215
+ }
216
+ const started = JSON.parse(JSON.stringify(fixture<Record<string, unknown>>("start.fixture.json")).replaceAll("review-start-fixture", lineage)) as Record<string, unknown>;
217
+ const expected = [
218
+ "review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", providerCwd,
219
+ "--target", decoded.targetIdentity, "--projection", "workspace", "--lineage", lineage, "--consent", "granted",
220
+ ];
221
+ for (const createClient of [client, runtimeClient]) {
222
+ const queue = queuedAdapter([capabilities(), structuredClone(started)]);
223
+ const result = await createClient(queue.adapter).answerConsent({ cwd: providerCwd, consent: decoded, answer: "granted" });
224
+ assert.equal(result.kind, "started");
225
+ assert.deepEqual(queue.calls, [expected]);
226
+ }
227
+ }
228
+ });
229
+
230
+ test("unmatched consent invocation quotes reject before provider launch", async () => {
231
+ for (const quote of ["'", '"']) {
232
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
233
+ const choice = decoded.choices.find((candidate) => candidate.answer === "granted") as { invocation: string };
234
+ choice.invocation = `${choice.invocation} ${quote}`;
235
+ for (const createClient of [client, runtimeClient]) {
236
+ const queue = queuedAdapter([]);
237
+ await assert.rejects(
238
+ () => createClient(queue.adapter).answerConsent({ cwd: "/repo", consent: decoded, answer: "granted" }),
239
+ (error: unknown) => error instanceof TypeError && error.message === "Native consent invocation has invalid quoting",
240
+ );
241
+ assert.deepEqual(queue.calls, []);
242
+ }
243
+ }
244
+ });
245
+
246
+ test("different, duplicate, missing, and malformed consent cwd options reject before provider launch", async () => {
247
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
248
+ const drifted = (replace: (invocation: string) => string): ReviewConsentV2 => {
249
+ const consent = structuredClone(decoded);
250
+ const choice = consent.choices.find((candidate) => candidate.answer === "granted") as { invocation: string };
251
+ choice.invocation = replace(choice.invocation);
252
+ return consent;
253
+ };
254
+ const cases = [
255
+ { label: "different", reason: "consent-invocation-cwd-changed", consent: drifted((value) => value.replace("--cwd /repo", "--cwd /another-repo")) },
256
+ { label: "duplicate", reason: "consent-invocation-option-invalid", consent: drifted((value) => `${value} --cwd /repo`) },
257
+ { label: "missing", reason: "consent-invocation-option-invalid", consent: drifted((value) => value.replace("--cwd /repo ", "")) },
258
+ { label: "malformed", reason: "consent-invocation-cwd-changed", consent: drifted((value) => value.replace("--cwd /repo", '--cwd ""')) },
259
+ ] as const;
260
+ for (const scenario of cases) {
261
+ const queue = queuedAdapter([]);
262
+ await assert.rejects(
263
+ () => client(queue.adapter).answerConsent!({ cwd: "/repo", consent: scenario.consent, answer: "granted" }),
264
+ (error: unknown) => error instanceof NativeReviewConsentBindingError && error.reason === scenario.reason,
265
+ );
266
+ assert.deepEqual(queue.calls, [], `${scenario.label} cwd must not launch the provider`);
267
+ }
268
+ });
269
+
179
270
  test("a consent invocation binding mismatch is a typed pre-native error that never launches the provider", async () => {
180
271
  const consent = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
181
272
  const queue = queuedAdapter([]);
@@ -140,6 +140,8 @@ test("generated runtime decoder consumes the captured terminal closure directly"
140
140
 
141
141
  test("registered gentle_review surfaces the package-pinned Pi transport refusal before native START for a safe internal symlink candidate", async (t) => {
142
142
  await reviewEnabledHome(t);
143
+ const handshakeLessEnvironment = { ...process.env };
144
+ delete handshakeLessEnvironment.GENTLE_PI_REVIEW_RELAY_CONTRACT;
143
145
  const workspace = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-")));
144
146
  const repository = join(workspace, "repository");
145
147
  t.after(async () => {
@@ -174,11 +176,15 @@ test("registered gentle_review surfaces the package-pinned Pi transport refusal
174
176
  // refusal happens before native START, regardless of candidate materialization.
175
177
  const native = new NativeReviewCliV216(async (request) => {
176
178
  if (request.arguments[0] === "review" && request.arguments[1] === "start") nativeStartReached = true;
177
- const command = await run(binary, request.arguments, request.cwd, true);
179
+ const command = await run(binary, request.arguments, request.cwd, true, handshakeLessEnvironment);
178
180
  return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
179
181
  });
180
182
  const tools = new Map<string, RegisteredController>();
181
- createGentleAiExtension({ nativeReviewCli: native, candidateViews } as Parameters<typeof createGentleAiExtension>[0])({
183
+ // The extension declares the relay handshake in the session environment on
184
+ // load (gentle-pi#550). Keep registration isolated in a throwaway environment
185
+ // and spawn the pinned binary with an explicit handshake-less environment so
186
+ // ambient valid relay configuration cannot change this fixture's behavior.
187
+ createGentleAiExtension({ nativeReviewCli: native, candidateViews, processEnv: {} } as Parameters<typeof createGentleAiExtension>[0])({
182
188
  on() {},
183
189
  registerTool(definition: RegisteredController & { name: string }) { tools.set(definition.name, definition); },
184
190
  registerCommand() {},