gentle-pi 2.1.2 → 2.2.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 (171) hide show
  1. package/README.md +70 -12
  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 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  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 +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,317 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
10
+ import type { ReviewCaptureSubmissionV1, ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+
12
+ // Wiring contract (gentle-pi#311 P4): the compact controller FINALIZE lane
13
+ // routes pi-slot capture inputs through the host relay ONLY when the
14
+ // provider-returned collect input carries the --materialize token. Every
15
+ // other lane and slot stays untouched.
16
+
17
+ const SHA = `sha256:${"1".repeat(64)}`;
18
+ const TREE = "2".repeat(40);
19
+
20
+ function repository(t: test.TestContext): string {
21
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-relay-routing-"));
22
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
23
+ execFileSync("git", ["init", "-b", "main"], { cwd });
24
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
25
+ execFileSync("git", ["add", "."], { cwd });
26
+ execFileSync("git", ["-c", "user.name=Relay Test", "-c", "user.email=relay@example.invalid", "commit", "-m", "initial"], { cwd });
27
+ return cwd;
28
+ }
29
+
30
+ function bindingArguments(lineageId: string, lens: string, order: number): ReviewCollectInputV3["arguments"] {
31
+ return [
32
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
33
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
34
+ { name: "target", value: SHA, token: `--target=${SHA}` },
35
+ { name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
36
+ { name: "lens", value: lens, token: `--lens=${lens}` },
37
+ { name: "order", value: String(order), token: `--order=${order}` },
38
+ { name: "subject-hash", value: `sha256:${String(order).repeat(64)}`, token: `--subject-hash=sha256:${String(order).repeat(64)}` },
39
+ ];
40
+ }
41
+
42
+ function providerSubmission(lineageId: string, lens: string, order: number): ReviewCaptureSubmissionV1 {
43
+ const bindingTokens = bindingArguments(lineageId, lens, order).map((argument) => argument.token!);
44
+ return {
45
+ operationToken: "capture-result",
46
+ argumentTokens: [...bindingTokens, "--input={{value}}"],
47
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: bindingTokens.length }],
48
+ };
49
+ }
50
+
51
+ function relayCollectInput(lineageId: string, lens: string, order: number, materialize = true, submission: ReviewCaptureSubmissionV1 | "provider" | "absent" = "provider"): ReviewCollectInputV3 {
52
+ return {
53
+ name: "reviewer_result",
54
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
55
+ captureOperation: "review.capture-result",
56
+ arguments: [
57
+ ...bindingArguments(lineageId, lens, order),
58
+ ...(materialize ? [
59
+ { name: "agent", value: "pi", token: "--agent=pi" },
60
+ { name: "materialize", value: "true", token: "--materialize=true" },
61
+ ] : []),
62
+ ],
63
+ ...(materialize && submission !== "absent" ? { submission: submission === "provider" ? providerSubmission(lineageId, lens, order) : submission } : {}),
64
+ };
65
+ }
66
+
67
+ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV3[]): ReviewStatusV3 {
68
+ return {
69
+ contract: "gentle-ai.review-integration/v2",
70
+ applicability: "current_target",
71
+ authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
72
+ receipt: { status: "none" },
73
+ action: "finalize",
74
+ replayability: "unknown",
75
+ targetIdentity: SHA,
76
+ projection: {
77
+ schema: "gentle-ai.review-candidate-projection/v1",
78
+ kind: "current-changes",
79
+ projection: "workspace",
80
+ baseTree: TREE,
81
+ initialReviewTree: TREE,
82
+ currentCandidateTree: TREE,
83
+ pathsDigest: SHA,
84
+ paths: ["app.ts"],
85
+ intendedUntracked: [],
86
+ intendedUntrackedProof: SHA,
87
+ initialSnapshotIdentity: SHA,
88
+ currentSnapshotIdentity: SHA,
89
+ },
90
+ candidates: [],
91
+ ...(inputs === undefined ? {} : { nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [...inputs] } } }),
92
+ raw: { schema: "gentle-ai.review-integration.status/v3", action: "finalize", lineage_id: lineageId },
93
+ } as unknown as ReviewStatusV3;
94
+ }
95
+
96
+ interface RoutingHarness {
97
+ statusQueue: ReviewStatusV3[];
98
+ statusCalls: Array<{ cwd: string; lineageId?: string }>;
99
+ finalizeCalls: number;
100
+ native: NativeReviewCli;
101
+ }
102
+
103
+ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
104
+ const harness: RoutingHarness = {
105
+ statusQueue: [...statuses],
106
+ statusCalls: [],
107
+ finalizeCalls: 0,
108
+ native: undefined as unknown as NativeReviewCli,
109
+ };
110
+ harness.native = {
111
+ start: async () => { throw new Error("unexpected start"); },
112
+ finalize: async () => {
113
+ harness.finalizeCalls += 1;
114
+ return { lineageId: "relay-lineage", state: "approved", action: "approved", storeRevision: "r1" };
115
+ },
116
+ validate: async () => { throw new Error("unexpected validate"); },
117
+ bindSdd: async () => { throw new Error("unexpected bindSdd"); },
118
+ sddStatus: async () => ({ ready: false, artifactStore: "none", artifacts: {} as never, nextRecommended: "" }),
119
+ reviewStatus: async () => { throw new Error("unexpected reviewStatus"); },
120
+ targetStatus: async (request) => {
121
+ harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
122
+ const next = harness.statusQueue.shift();
123
+ if (next === undefined) throw new Error("status queue exhausted");
124
+ return next;
125
+ },
126
+ };
127
+ return harness;
128
+ }
129
+
130
+ function finalizeParameters(lineageId: string, input: Record<string, unknown> = {}): Record<string, unknown> {
131
+ return { operation: "finalize", lineageId, input: JSON.stringify(input) };
132
+ }
133
+
134
+ // Relay ROUTING is the subject here, not spend authorization: a host-relay
135
+ // finalize now forecasts its reviewer model runs and spends nothing until the
136
+ // caller acknowledges them (see review-relay-transport-agent.test.ts), so these
137
+ // routing cases authorize the run up front and keep asserting where the slots
138
+ // go. A case that must observe the forecast passes the acknowledgement off.
139
+ async function runFinalize(cwd: string, harness: RoutingHarness, lineageId: string, input: Record<string, unknown> = { reviewer_run_acknowledged: true }): Promise<Record<string, unknown>> {
140
+ return await __testing.executeReviewControllerOperation(
141
+ finalizeParameters(lineageId, input),
142
+ cwd,
143
+ new Map(),
144
+ harness.native,
145
+ ) as Record<string, unknown>;
146
+ }
147
+
148
+ test("materialize-marked pi slots route through the host relay in provider order and re-query STATUS", async (t) => {
149
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
150
+ const cwd = repository(t);
151
+ const lineageId = "relay-lineage";
152
+ const inputs = [
153
+ relayCollectInput(lineageId, "review-risk", 0),
154
+ relayCollectInput(lineageId, "review-reliability", 1),
155
+ ];
156
+ const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId)]);
157
+ const relayed: ReviewHostRelayRequest[] = [];
158
+ __testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
159
+ relayed.push(request);
160
+ return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
161
+ });
162
+
163
+ const result = await runFinalize(cwd, harness, lineageId);
164
+
165
+ assert.equal(relayed.length, 2);
166
+ assert.deepEqual(relayed[0]!.captureArgumentTokens, inputs[0]!.arguments.map((argument) => argument.token));
167
+ // The provider-owned completing form reaches the relay verbatim.
168
+ assert.deepEqual(relayed[0]!.submission, providerSubmission(lineageId, "review-risk", 0));
169
+ assert.deepEqual(relayed[1]!.captureArgumentTokens, inputs[1]!.arguments.map((argument) => argument.token));
170
+ assert.deepEqual(relayed[1]!.submission, providerSubmission(lineageId, "review-reliability", 1));
171
+
172
+ assert.equal(harness.finalizeCalls, 0, "the relay never invokes native finalize itself");
173
+ assert.equal(harness.statusCalls.length, 2, "STATUS is re-queried after all slots are captured");
174
+ assert.deepEqual(harness.statusCalls[1], { cwd, lineageId });
175
+
176
+ const hostRelay = result.host_relay as { transport: string; captured_slots: Array<Record<string, unknown>> };
177
+ assert.equal(hostRelay.transport, "pi_host_relay");
178
+ assert.equal(hostRelay.captured_slots.length, 2);
179
+ assert.deepEqual(hostRelay.captured_slots.map((slot) => slot.lens), ["review-risk", "review-reliability"]);
180
+ assert.equal(result.status, "in-progress");
181
+ });
182
+
183
+ test("Pi-authored review documents are rejected at the FINALIZE input boundary", async (t) => {
184
+ // gentle-pi#311 P5: `review_result` is no longer a legal FINALIZE input at
185
+ // all — the parse fails closed before any relay launch or native call, so
186
+ // a Pi-authored reviewer document can never race a host-mediated slot.
187
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
188
+ const cwd = repository(t);
189
+ const lineageId = "relay-lineage";
190
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
191
+ let relayCalls = 0;
192
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
193
+ relayCalls += 1;
194
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
195
+ });
196
+
197
+ await assert.rejects(
198
+ () => runFinalize(cwd, harness, lineageId, { review_result: { lens_results: [{ findings: [], evidence: ["reviewed"] }] } }),
199
+ /unknown field review_result/,
200
+ );
201
+ assert.equal(relayCalls, 0);
202
+ assert.equal(harness.finalizeCalls, 0);
203
+ });
204
+
205
+ test("an old binary reports the relay as unavailable without touching existing behavior", async (t) => {
206
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
207
+ const cwd = repository(t);
208
+ const lineageId = "relay-lineage";
209
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
210
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
211
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, { exitCode: 2, stderr: "flag provided but not defined: -materialize" });
212
+ });
213
+
214
+ const result = await runFinalize(cwd, harness, lineageId);
215
+
216
+ assert.equal(result.status, "blocked");
217
+ assert.equal(result.outcome, "pi-host-relay-unavailable");
218
+ assert.equal(result.reason, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
219
+ assert.equal(result.mutation_performed, false);
220
+ assert.equal(result.mutation_outcome, "none");
221
+ assert.equal(harness.finalizeCalls, 0);
222
+ });
223
+
224
+ test("a handshake refusal surfaces the provider refusal verbatim through the controller envelope", async (t) => {
225
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
226
+ const cwd = repository(t);
227
+ const lineageId = "relay-lineage";
228
+ const refusal = "the active runtime is not eligible for immutable receipt review; supported immutable review runtimes: claude-code, codex, opencode";
229
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
230
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
231
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", refusal, { exitCode: 1, stderr: refusal });
232
+ });
233
+
234
+ const result = await runFinalize(cwd, harness, lineageId);
235
+
236
+ assert.equal(result.outcome, "pi-host-relay-handshake-refused");
237
+ assert.equal(result.reason, refusal);
238
+ assert.equal(result.refusal, refusal);
239
+ });
240
+
241
+ test("a transport failure mid-collection stops immediately and directs STATUS re-query", async (t) => {
242
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
243
+ const cwd = repository(t);
244
+ const lineageId = "relay-lineage";
245
+ const inputs = [
246
+ relayCollectInput(lineageId, "review-risk", 0),
247
+ relayCollectInput(lineageId, "review-reliability", 1),
248
+ ];
249
+ const harness = nativeHarness([finalizeStatus(lineageId, inputs)]);
250
+ let calls = 0;
251
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
252
+ calls += 1;
253
+ if (calls === 1) return { promptByteLength: 8, resultByteLength: 8, submission: '{"admission_decision":"completed"}' };
254
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
255
+ });
256
+
257
+ const result = await runFinalize(cwd, harness, lineageId);
258
+
259
+ assert.equal(calls, 2);
260
+ assert.equal(result.status, "blocked");
261
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
262
+ assert.deepEqual(result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false });
263
+ assert.equal((result.captured_slots as unknown[]).length, 1);
264
+ assert.equal(result.mutation_performed, true);
265
+ assert.match(String(result.next_action), /Re-query negotiated STATUS/);
266
+ assert.match(String(result.next_action), /exact same bound slot/);
267
+ assert.equal(harness.finalizeCalls, 0);
268
+ assert.equal(harness.statusCalls.length, 1, "no automatic relaunch after transport failure");
269
+ });
270
+
271
+ test("materialize tokens without a provider submission fail closed as a typed contract mismatch", async (t) => {
272
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
273
+ const cwd = repository(t);
274
+ const lineageId = "relay-lineage";
275
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0, true, "absent")])]);
276
+ let relayCalls = 0;
277
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
278
+ relayCalls += 1;
279
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
280
+ });
281
+
282
+ const result = await runFinalize(cwd, harness, lineageId);
283
+
284
+ assert.equal(relayCalls, 0, "the completing form is never synthesized, so nothing launches");
285
+ assert.equal(result.status, "blocked");
286
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
287
+ assert.deepEqual(result.failure, { kind: "submission-contract-mismatch", stage: "binding", exit_code: null, timed_out: false });
288
+ assert.equal(result.reason, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
289
+ assert.equal(result.mutation_performed, false);
290
+ assert.equal(result.mutation_outcome, "none");
291
+ assert.equal(harness.finalizeCalls, 0);
292
+ assert.equal(harness.statusCalls.length, 1, "no relaunch and no post-capture STATUS after the contract mismatch");
293
+ });
294
+
295
+ test("collect inputs without the provider-issued materialize token never reach the relay", async (t) => {
296
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
297
+ const cwd = repository(t);
298
+ const lineageId = "relay-lineage";
299
+ const harness = nativeHarness([
300
+ finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0, false)]),
301
+ finalizeStatus(lineageId),
302
+ ]);
303
+ let relayCalls = 0;
304
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
305
+ relayCalls += 1;
306
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
307
+ });
308
+
309
+ // The existing lane may fail on the synthetic projection; only the relay
310
+ // boundary is under test here: it must never be consulted.
311
+ try {
312
+ await runFinalize(cwd, harness, lineageId);
313
+ } catch {
314
+ // Existing-lane behavior for this synthetic fixture is out of scope.
315
+ }
316
+ assert.equal(relayCalls, 0);
317
+ });