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
@@ -6,13 +6,12 @@ import { join } from "node:path";
6
6
  import test from "node:test";
7
7
  import { __testing } from "../extensions/gentle-ai.ts";
8
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";
9
+ import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_PI_TIMEOUT_ENV, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
10
10
  import type { ReviewCaptureSubmissionV1, ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
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
12
+ // One-slot capture routing: the host relay runs only when the selected
14
13
  // provider-returned collect input carries the --materialize token. Every
15
- // other lane and slot stays untouched.
14
+ // other capture form stays untouched.
16
15
 
17
16
  const SHA = `sha256:${"1".repeat(64)}`;
18
17
  const TREE = "2".repeat(40);
@@ -69,8 +68,7 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
69
68
  contract: "gentle-ai.review-integration/v2",
70
69
  applicability: "current_target",
71
70
  authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
72
- receipt: { status: "none" },
73
- action: "finalize",
71
+ action: "stop",
74
72
  replayability: "unknown",
75
73
  targetIdentity: SHA,
76
74
  projection: {
@@ -89,14 +87,13 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
89
87
  },
90
88
  candidates: [],
91
89
  ...(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 },
90
+ raw: { schema: "gentle-ai.review-integration.status/v3", action: "stop", lineage_id: lineageId },
93
91
  } as unknown as ReviewStatusV3;
94
92
  }
95
93
 
96
94
  interface RoutingHarness {
97
95
  statusQueue: ReviewStatusV3[];
98
96
  statusCalls: Array<{ cwd: string; lineageId?: string }>;
99
- finalizeCalls: number;
100
97
  native: NativeReviewCli;
101
98
  }
102
99
 
@@ -104,19 +101,9 @@ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
104
101
  const harness: RoutingHarness = {
105
102
  statusQueue: [...statuses],
106
103
  statusCalls: [],
107
- finalizeCalls: 0,
108
104
  native: undefined as unknown as NativeReviewCli,
109
105
  };
110
106
  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
107
  targetStatus: async (request) => {
121
108
  harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
122
109
  const next = harness.statusQueue.shift();
@@ -127,63 +114,41 @@ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
127
114
  return harness;
128
115
  }
129
116
 
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),
117
+ async function runCapture(cwd: string, harness: RoutingHarness, lineageId: string, input: Record<string, unknown> = { reviewerRunAcknowledged: true }): Promise<Record<string, unknown>> {
118
+ const selected = harness.statusQueue[0]?.nextTransition?.collect?.inputs[0];
119
+ if (selected === undefined) throw new Error("capture test requires one current collect input");
120
+ return await __testing.executeReviewCaptureOperation(
121
+ { lineageId, collectBinding: JSON.stringify(selected), ...input },
142
122
  cwd,
143
- new Map(),
144
123
  harness.native,
145
124
  ) as Record<string, unknown>;
146
125
  }
147
126
 
148
- test("materialize-marked pi slots route through the host relay in provider order and re-query STATUS", async (t) => {
127
+ test("one materialize binding routes exactly one provider slot through the host relay", async (t) => {
149
128
  t.after(() => __testing.setReviewHostRelayRunnerForTesting());
150
129
  const cwd = repository(t);
151
130
  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)]);
131
+ const input = relayCollectInput(lineageId, "review-risk", 0);
132
+ const harness = nativeHarness([finalizeStatus(lineageId, [input])]);
157
133
  const relayed: ReviewHostRelayRequest[] = [];
158
134
  __testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
159
135
  relayed.push(request);
160
136
  return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
161
137
  });
162
138
 
163
- const result = await runFinalize(cwd, harness, lineageId);
139
+ const result = await runCapture(cwd, harness, lineageId);
164
140
 
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.
141
+ assert.equal(relayed.length, 1);
142
+ assert.deepEqual(relayed[0]!.captureArgumentTokens, input.arguments.map((argument) => argument.token));
168
143
  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");
144
+ assert.equal(harness.statusCalls.length, 1, "a nonterminal capture does not auto-follow STATUS");
145
+ assert.equal(result.status, "captured");
146
+ assert.equal((result.host_relay as { transport: string }).transport, "pi_host_relay");
181
147
  });
182
148
 
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.
149
+ test("Pi-authored review documents are rejected at the capture input boundary", async (t) => {
150
+ // The narrow capture schema rejects a caller-authored reviewer document
151
+ // before any relay launch or native call.
187
152
  t.after(() => __testing.setReviewHostRelayRunnerForTesting());
188
153
  const cwd = repository(t);
189
154
  const lineageId = "relay-lineage";
@@ -195,11 +160,10 @@ test("Pi-authored review documents are rejected at the FINALIZE input boundary",
195
160
  });
196
161
 
197
162
  await assert.rejects(
198
- () => runFinalize(cwd, harness, lineageId, { review_result: { lens_results: [{ findings: [], evidence: ["reviewed"] }] } }),
199
- /unknown field review_result/,
163
+ () => runCapture(cwd, harness, lineageId, { review_result: { lens_results: [{ findings: [], evidence: ["reviewed"] }] } }),
164
+ /does not accept review_result/,
200
165
  );
201
166
  assert.equal(relayCalls, 0);
202
- assert.equal(harness.finalizeCalls, 0);
203
167
  });
204
168
 
205
169
  test("an old binary reports the relay as unavailable without touching existing behavior", async (t) => {
@@ -211,14 +175,13 @@ test("an old binary reports the relay as unavailable without touching existing b
211
175
  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
176
  });
213
177
 
214
- const result = await runFinalize(cwd, harness, lineageId);
178
+ const result = await runCapture(cwd, harness, lineageId);
215
179
 
216
180
  assert.equal(result.status, "blocked");
217
181
  assert.equal(result.outcome, "pi-host-relay-unavailable");
218
182
  assert.equal(result.reason, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
219
183
  assert.equal(result.mutation_performed, false);
220
184
  assert.equal(result.mutation_outcome, "none");
221
- assert.equal(harness.finalizeCalls, 0);
222
185
  });
223
186
 
224
187
  test("a handshake refusal surfaces the provider refusal verbatim through the controller envelope", async (t) => {
@@ -231,43 +194,116 @@ test("a handshake refusal surfaces the provider refusal verbatim through the con
231
194
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", refusal, { exitCode: 1, stderr: refusal });
232
195
  });
233
196
 
234
- const result = await runFinalize(cwd, harness, lineageId);
197
+ const result = await runCapture(cwd, harness, lineageId);
235
198
 
236
199
  assert.equal(result.outcome, "pi-host-relay-handshake-refused");
237
200
  assert.equal(result.reason, refusal);
238
201
  assert.equal(result.refusal, refusal);
239
202
  });
240
203
 
241
- test("a transport failure mid-collection stops immediately and directs STATUS re-query", async (t) => {
204
+ test("a transport failure stops the selected capture without auto-follow", async (t) => {
242
205
  t.after(() => __testing.setReviewHostRelayRunnerForTesting());
243
206
  const cwd = repository(t);
244
207
  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;
208
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
251
209
  __testing.setReviewHostRelayRunnerForTesting(async () => {
252
- calls += 1;
253
- if (calls === 1) return { promptByteLength: 8, resultByteLength: 8, submission: '{"admission_decision":"completed"}' };
254
210
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
255
211
  });
256
212
 
257
- const result = await runFinalize(cwd, harness, lineageId);
213
+ const result = await runCapture(cwd, harness, lineageId);
258
214
 
259
- assert.equal(calls, 2);
260
215
  assert.equal(result.status, "blocked");
261
216
  assert.equal(result.outcome, "pi-host-relay-transport-failure");
262
217
  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);
218
+ assert.match(String(result.next_action), /fresh STATUS/);
268
219
  assert.equal(harness.statusCalls.length, 1, "no automatic relaunch after transport failure");
269
220
  });
270
221
 
222
+ // gentle-pi#522 / #524: a submission Go refused at admission is a proven
223
+ // non-mutation. The model must see the refusal text, mutation_outcome none,
224
+ // and a continuation for the reoffered slot, never an unknown outcome that the
225
+ // contract forbids replaying.
226
+ test("an admission refusal reaches the model as a proven non-mutation carrying the refusal and a continuation", async (t) => {
227
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
228
+ const cwd = repository(t);
229
+ const lineageId = "relay-lineage";
230
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
231
+ const refusal = "Error: reviewer artifact admission binding_mismatch: reviewer result echoed a different artifact subject: the rejected admission did not consume the lens slot, so re-run the lens and invoke gentle-ai review capture-result again on the same lineage with a result that echoes the binding's top-level subject_hash [invalid_request]\n";
232
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
233
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", refusal.trim(), { exitCode: 1, stderr: refusal, elapsedMs: 40, timeoutMs: 120_000, mutationOutcome: "none" });
234
+ });
235
+
236
+ const result = await runCapture(cwd, harness, lineageId);
237
+
238
+ assert.equal(result.status, "blocked");
239
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
240
+ assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: 1, timed_out: false, elapsed_ms: 40, timeout_ms: 120_000, stderr: refusal });
241
+ assert.equal(result.reason, refusal.trim());
242
+ assert.equal(result.mutation_performed, false);
243
+ assert.equal(result.mutation_outcome, "none");
244
+ assert.match(String(result.next_action), /did not consume the lens slot/);
245
+ assert.match(String(result.next_action), /fresh STATUS/);
246
+ assert.equal(harness.statusCalls.length, 1, "a proven non-mutation needs no STATUS reconciliation and no relaunch");
247
+ });
248
+
249
+ test("a submission whose outcome is genuinely indeterminate still reconciles through STATUS and carries its evidence", async (t) => {
250
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
251
+ const cwd = repository(t);
252
+ const lineageId = "relay-lineage";
253
+ const pending = finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)]);
254
+ const harness = nativeHarness([pending, pending]);
255
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
256
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", "gentle-ai capture submission exceeded its 120000ms bound after 120004ms", { exitCode: null, stderr: "", timedOut: true, elapsedMs: 120_004, timeoutMs: 120_000 });
257
+ });
258
+
259
+ const result = await runCapture(cwd, harness, lineageId);
260
+
261
+ assert.equal(result.status, "reconciled");
262
+ assert.equal(result.outcome, "native-capture-outcome-unknown");
263
+ assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: null, timed_out: true, elapsed_ms: 120_004, timeout_ms: 120_000 });
264
+ assert.match(String(result.reason), /exceeded its 120000ms bound/);
265
+ assert.equal(harness.statusCalls.length, 2, "an indeterminate submission reconciles exactly once through STATUS");
266
+ });
267
+
268
+ test("a relay timeout reports its one-slot measurements and no auto-follow", async (t) => {
269
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
270
+ const cwd = repository(t);
271
+ const lineageId = "relay-lineage";
272
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
273
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
274
+ throw new ReviewHostRelayError(
275
+ REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
276
+ "pi",
277
+ "pi reviewer subprocess exceeded the relay bound",
278
+ { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 },
279
+ );
280
+ });
281
+
282
+ const result = await runCapture(cwd, harness, lineageId);
283
+ assert.equal(result.status, "blocked");
284
+ assert.equal(result.outcome, "pi-host-relay-timeout");
285
+ assert.deepEqual(result.failure, { kind: "pi-timed-out", stage: "pi", exit_code: null, timed_out: true, elapsed_ms: 2_256_004, timeout_ms: 2_256_000 });
286
+ assert.match(String(result.next_action), new RegExp(`${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV}=<milliseconds>`));
287
+ assert.match(String(result.next_action), new RegExp(String(REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS)));
288
+ assert.equal(harness.statusCalls.length, 1);
289
+ });
290
+
291
+ test("a non-timeout transport failure keeps its generic continuation and now carries its measurements", async (t) => {
292
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
293
+ const cwd = repository(t);
294
+ const lineageId = "relay-lineage";
295
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
296
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
297
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4, elapsedMs: 1_200, timeoutMs: 900_000 });
298
+ });
299
+
300
+ const result = await runCapture(cwd, harness, lineageId);
301
+
302
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
303
+ assert.deepEqual(result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false, elapsed_ms: 1_200, timeout_ms: 900_000 });
304
+ assert.match(String(result.next_action), /fresh STATUS/);
305
+ });
306
+
271
307
  test("materialize tokens without a provider submission fail closed as a typed contract mismatch", async (t) => {
272
308
  t.after(() => __testing.setReviewHostRelayRunnerForTesting());
273
309
  const cwd = repository(t);
@@ -279,7 +315,7 @@ test("materialize tokens without a provider submission fail closed as a typed co
279
315
  return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
280
316
  });
281
317
 
282
- const result = await runFinalize(cwd, harness, lineageId);
318
+ const result = await runCapture(cwd, harness, lineageId);
283
319
 
284
320
  assert.equal(relayCalls, 0, "the completing form is never synthesized, so nothing launches");
285
321
  assert.equal(result.status, "blocked");
@@ -288,7 +324,6 @@ test("materialize tokens without a provider submission fail closed as a typed co
288
324
  assert.equal(result.reason, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
289
325
  assert.equal(result.mutation_performed, false);
290
326
  assert.equal(result.mutation_outcome, "none");
291
- assert.equal(harness.finalizeCalls, 0);
292
327
  assert.equal(harness.statusCalls.length, 1, "no relaunch and no post-capture STATUS after the contract mismatch");
293
328
  });
294
329
 
@@ -309,7 +344,7 @@ test("collect inputs without the provider-issued materialize token never reach t
309
344
  // The existing lane may fail on the synthetic projection; only the relay
310
345
  // boundary is under test here: it must never be consulted.
311
346
  try {
312
- await runFinalize(cwd, harness, lineageId);
347
+ await runCapture(cwd, harness, lineageId);
313
348
  } catch {
314
349
  // Existing-lane behavior for this synthetic fixture is out of scope.
315
350
  }