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
@@ -1,13 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
7
7
  import { __testing } from "../extensions/gentle-ai.ts";
8
- import type { NativeReviewCli } from "../lib/native-review-cli.ts";
8
+ import type { NativeReviewCli, NativeReviewUnachievableLensCaptureArtifact, NativeReviewUnachievableLensCaptureRequest } from "../lib/native-review-cli.ts";
9
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
- import { decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
10
+ import { decodeReviewNextTransitionV3, decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
11
 
12
12
  // One-slot capture routing: the host relay runs only when the selected
13
13
  // provider-returned collect input carries the --materialize token. Every
@@ -142,25 +142,44 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
142
142
  } as unknown as ReviewStatusV3;
143
143
  }
144
144
 
145
+ function providerRefuterRequiredStatus(lineageId: string): ReviewStatusV3 {
146
+ const refuter: ReviewCollectInputV3 = {
147
+ name: "provider_refuter",
148
+ schema: "https://gentle-ai.dev/schema/review/refuter/v1",
149
+ captureOperation: "review.capture-refuter",
150
+ arguments: bindingArguments(lineageId, "review-risk", 0),
151
+ };
152
+ return {
153
+ ...finalizeStatus(lineageId),
154
+ nextTransition: { kind: "collect", reasonCode: "provider_refuter_required", collect: { inputs: [refuter] } },
155
+ };
156
+ }
157
+
145
158
  interface RoutingHarness {
146
159
  statusQueue: ReviewStatusV3[];
147
- statusCalls: Array<{ cwd: string; lineageId?: string }>;
160
+ statusCalls: Array<{ cwd: string; lineageId?: string; agent?: "pi" }>;
161
+ unachievableCalls: NativeReviewUnachievableLensCaptureRequest[];
148
162
  native: NativeReviewCli;
149
163
  }
150
164
 
151
- function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
165
+ type UnachievableResponder = (request: NativeReviewUnachievableLensCaptureRequest) => Promise<NativeReviewUnachievableLensCaptureArtifact>;
166
+
167
+ // The declaration verb stays OPT-IN: an absent captureUnachievableLens is the invocation-adjacent capability gate, so every pre-existing failure test keeps its exact envelope through the unchanged fall-back.
168
+ function nativeHarness(statuses: readonly ReviewStatusV3[], unachievableResponder?: UnachievableResponder): RoutingHarness {
152
169
  const harness: RoutingHarness = {
153
170
  statusQueue: [...statuses],
154
171
  statusCalls: [],
172
+ unachievableCalls: [],
155
173
  native: undefined as unknown as NativeReviewCli,
156
174
  };
157
175
  harness.native = {
158
176
  targetStatus: async (request) => {
159
- harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
177
+ harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }), ...(request.agent === undefined ? {} : { agent: request.agent }) });
160
178
  const next = harness.statusQueue.shift();
161
179
  if (next === undefined) throw new Error("status queue exhausted");
162
180
  return next;
163
181
  },
182
+ ...(unachievableResponder === undefined ? {} : { captureUnachievableLens: async (request: NativeReviewUnachievableLensCaptureRequest) => { harness.unachievableCalls.push(request); return await unachievableResponder(request); } }),
164
183
  };
165
184
  return harness;
166
185
  }
@@ -226,7 +245,7 @@ function prepared(request: ReviewHostRelayRequest) { return { request, promptByt
226
245
  test("grouped capture forecasts once, reaches a four-reviewer barrier, and reconciles unclosed submissions", async (t) => {
227
246
  t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
228
247
  const cwd = repository(t), lineageId = "relay-group", inputs = groupInputs(lineageId), lenses = inputs.map((input) => input.arguments.find((argument) => argument.name === "lens")!.value);
229
- const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))), finalizeStatus(lineageId)]);
248
+ const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))), providerRefuterRequiredStatus(lineageId)]);
230
249
  let ready!: () => void;
231
250
  const allStarted = new Promise<void>((resolve) => { ready = resolve; });
232
251
  const releases = new Map<string, () => void>(), completed: string[] = [], submitted: string[] = [];
@@ -249,7 +268,9 @@ test("grouped capture forecasts once, reaches a four-reviewer barrier, and recon
249
268
  assert.deepEqual(completed, [...lenses].reverse(), "reviewer completion order is not submission order");
250
269
  assert.deepEqual(submitted, lenses);
251
270
  assert.deepEqual({ outcome: result.outcome, statusCalls: harness.statusCalls.length, providerAction: result.provider_action }, { outcome: "native-reviewer-group-status-reconciled", statusCalls: 7, providerAction: "stop" });
252
- assert.equal(result.next_transition, undefined);
271
+ assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "post-last-capture reconciliation preserves the Pi host runtime");
272
+ assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.kind, "collect");
273
+ assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.reasonCode, "provider_refuter_required");
253
274
  });
254
275
 
255
276
  test("captured v5 STATUS accepts capture-phase Pn when authority has advanced to Rn at the forecast boundary", async (t) => {
@@ -465,6 +486,7 @@ test("a submission whose outcome is genuinely indeterminate still reconciles thr
465
486
  assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: null, timed_out: true, elapsed_ms: 120_004, timeout_ms: 120_000 });
466
487
  assert.match(String(result.reason), /exceeded its 120000ms bound/);
467
488
  assert.equal(harness.statusCalls.length, 2, "an indeterminate submission reconciles exactly once through STATUS");
489
+ assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "ordinary host-relay reconciliation preserves the Pi host runtime");
468
490
  });
469
491
 
470
492
  test("a relay timeout reports its one-slot measurements and no auto-follow", async (t) => {
@@ -490,6 +512,360 @@ test("a relay timeout reports its one-slot measurements and no auto-follow", asy
490
512
  assert.equal(harness.statusCalls.length, 1);
491
513
  });
492
514
 
515
+ // gentle-pi#638: the typed stop a provider renders after a host declared a
516
+ // slot unachievable, built through the production decoder so the routing
517
+ // tests exercise the same typed shape the extension consumes.
518
+ function unachievableStopStatus(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, reason = "relay_transport_bound_exceeded"): ReviewStatusV3 {
519
+ const subjectHash = `sha256:${String(order).repeat(64)}`;
520
+ const repositoryContext = `rctx1_${"e".repeat(64)}`;
521
+ const rawTransition = {
522
+ kind: "stop",
523
+ reason_code: "unachievable_lens_slot",
524
+ unachievable_lens_slots: [
525
+ {
526
+ lens,
527
+ selected_order: order,
528
+ subject_hash: subjectHash,
529
+ reason,
530
+ withdraw: {
531
+ operation: "review.capture-unachievable",
532
+ command: `gentle-ai review capture-unachievable --lineage=${lineageId} --expected-revision=${SHA} --target=${SHA} --repository-context=${repositoryContext} --request-hash=${subjectHash} --withdraw=true`,
533
+ arguments: [
534
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
535
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
536
+ { name: "target", value: SHA, token: `--target=${SHA}` },
537
+ { name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
538
+ { name: "request-hash", value: subjectHash, token: `--request-hash=${subjectHash}` },
539
+ { name: "withdraw", value: "true", token: "--withdraw=true" },
540
+ ],
541
+ binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA, repository_context: repositoryContext },
542
+ },
543
+ },
544
+ ],
545
+ };
546
+ return {
547
+ ...finalizeStatus(lineageId),
548
+ nextTransition: decodeReviewNextTransitionV3(rawTransition),
549
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "stop", lineage_id: lineageId, target_identity: SHA, next_transition: rawTransition },
550
+ } as unknown as ReviewStatusV3;
551
+ }
552
+
553
+ function unachievableArtifact(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, reason = "relay_transport_bound_exceeded"): NativeReviewUnachievableLensCaptureArtifact {
554
+ return { schema: "gentle-ai.review-capture-unachievable/v1", lineageId, targetIdentity: SHA, lens, selectedOrder: order, reason, recorded: true };
555
+ }
556
+
557
+ // gentle-pi#638: a deterministic pi timeout is declared unachievable through
558
+ // the native verb, and one bound STATUS re-query renders the typed stop with
559
+ // its withdraw binding instead of reoffering the same slot.
560
+ test("a deterministic pi timeout declares the slot unachievable and renders the typed stop", async (t) => {
561
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
562
+ const cwd = repository(t);
563
+ const lineageId = "relay-lineage";
564
+ const input = relayCollectInput(lineageId, "review-risk", 0);
565
+ const stop = unachievableStopStatus(lineageId, "review-risk", 0);
566
+ const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
567
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
568
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
569
+ });
570
+
571
+ const result = await runCapture(cwd, harness, lineageId);
572
+
573
+ assert.equal(result.status, "blocked");
574
+ assert.equal(result.outcome, "unachievable-lens-slot-declared");
575
+ 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 });
576
+ assert.deepEqual(harness.unachievableCalls, [{ cwd: realpathSync(cwd), lineageId, targetIdentity: SHA, expectedRevision: SHA, requestHash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded", detail: "killed after 2256004ms against a 2256000ms relay bound", repositoryContext: `rctx1_${"e".repeat(64)}` }]);
577
+ assert.deepEqual(result.declaration, { lens: "review-risk", selected_order: 0, subject_hash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded" });
578
+ assert.deepEqual(result.result, stop.raw);
579
+ const slots = result.unachievable_lens_slots as Array<{ lens: string; subject_hash: string; withdraw: string }>;
580
+ assert.equal(slots.length, 1);
581
+ assert.equal(slots[0]!.lens, "review-risk");
582
+ assert.equal(slots[0]!.subject_hash, `sha256:${"0".repeat(64)}`);
583
+ assert.match(slots[0]!.withdraw, /capture-unachievable.*--withdraw=true/);
584
+ assert.match(String(result.next_action), /withdraw/);
585
+ assert.equal(result.mutation_performed, true);
586
+ // gentle-pi#822: the native declaration was recorded, so the flow reports the mutation as committed, not none.
587
+ assert.equal(result.mutation_outcome, "committed");
588
+ assert.equal(harness.statusCalls.length, 2, "one selection STATUS plus exactly one bound re-query");
589
+ assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "the bound re-query preserves the Pi host runtime");
590
+ });
591
+
592
+ // gentle-pi#638: generic admission rejections describe the submitted reviewer
593
+ // bytes, not a deterministic failure of the provider-bound slot. A fresh
594
+ // reviewer can repair malformed JSON or a binding mismatch, so both retain the
595
+ // ordinary exact-reoffer path and must never declare the slot unachievable.
596
+ test("repairable submission refusals retain the exact-slot retry path", async (t) => {
597
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
598
+ const cwd = repository(t);
599
+ const lineageId = "relay-lineage";
600
+ for (const refusal of [
601
+ "Error: reviewer payload contains no complete JSON object [invalid_request]\n",
602
+ "Error: reviewer artifact admission binding_mismatch [invalid_request]\n",
603
+ ]) {
604
+ const input = relayCollectInput(lineageId, "review-reliability", 0);
605
+ const harness = nativeHarness([finalizeStatus(lineageId, [input])], async () => {
606
+ throw new Error("a repairable refusal must not call capture-unachievable");
607
+ });
608
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
609
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", refusal.trim(), { exitCode: 1, stderr: refusal, mutationOutcome: "none" });
610
+ });
611
+
612
+ const result = await runCapture(cwd, harness, lineageId);
613
+
614
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
615
+ assert.equal(result.mutation_performed, false);
616
+ assert.equal(result.mutation_outcome, "none");
617
+ assert.match(String(result.next_action), /fresh STATUS/);
618
+ assert.deepEqual(harness.unachievableCalls, []);
619
+ assert.equal(harness.statusCalls.length, 1, "a repairable non-mutation needs no reconciliation before the provider reoffers it");
620
+ }
621
+ });
622
+
623
+ // gentle-pi#822: a stop carrying slots that do not match the identity this
624
+ // session declared is a reconciliation failure — never a success rendering
625
+ // someone else's withdraw command.
626
+ test("a stop whose slots do not match the declared identity reports a reconciliation failure", async (t) => {
627
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
628
+ const cwd = repository(t);
629
+ const lineageId = "relay-lineage";
630
+ const input = relayCollectInput(lineageId, "review-risk", 0);
631
+ const foreignStop = unachievableStopStatus(lineageId, "review-risk", 1);
632
+ const harness = nativeHarness([finalizeStatus(lineageId, [input]), foreignStop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
633
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
634
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
635
+ });
636
+
637
+ const result = await runCapture(cwd, harness, lineageId);
638
+
639
+ assert.equal(result.status, "blocked");
640
+ assert.equal(result.outcome, "unachievable-lens-declaration-reconciliation-failed");
641
+ assert.deepEqual(result.declaration, { lens: "review-risk", selected_order: 0, subject_hash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded" });
642
+ assert.equal(result.unachievable_lens_slots, undefined, "a foreign slot is never exposed as this session's withdraw command");
643
+ const reconciliationFailure = result.reconciliation_failure as { outcome?: string; reason?: string } | undefined;
644
+ assert.equal(reconciliationFailure?.outcome, "unachievable-lens-slot-declaration-unmatched");
645
+ assert.match(String(reconciliationFailure?.reason), /no unachievable_lens_slots entry matches the declared slot identity/);
646
+ assert.equal(result.mutation_performed, true);
647
+ assert.equal(result.mutation_outcome, "committed");
648
+ assert.match(String(result.next_action), /refused the unachievable declaration/);
649
+ assert.equal(harness.statusCalls.length, 2, "the bound re-query still runs exactly once");
650
+ });
651
+
652
+ // gentle-pi#822: with several declared entries on the stop, only the matching
653
+ // identity is exposed — never the withdraw commands of other runs.
654
+ test("a stop carrying several declared slots exposes only the one this session declared", async (t) => {
655
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
656
+ const cwd = repository(t);
657
+ const lineageId = "relay-lineage";
658
+ const input = relayCollectInput(lineageId, "review-risk", 0);
659
+ const stop = unachievableStopStatus(lineageId, "review-risk", 0);
660
+ const foreign = unachievableStopStatus(lineageId, "review-reliability", 2).nextTransition?.unachievableLensSlots?.[0];
661
+ assert.ok(foreign !== undefined);
662
+ stop.nextTransition = { ...stop.nextTransition!, unachievableLensSlots: [...stop.nextTransition!.unachievableLensSlots!, foreign] };
663
+ const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
664
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
665
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
666
+ });
667
+
668
+ const result = await runCapture(cwd, harness, lineageId);
669
+
670
+ assert.equal(result.outcome, "unachievable-lens-slot-declared");
671
+ const slots = result.unachievable_lens_slots as Array<{ lens: string; selected_order: number; subject_hash: string }>;
672
+ assert.equal(slots.length, 1, "only the declared entry is exposed");
673
+ assert.equal(slots[0]!.lens, "review-risk");
674
+ assert.equal(slots[0]!.selected_order, 0);
675
+ assert.equal(slots[0]!.subject_hash, `sha256:${"0".repeat(64)}`);
676
+ assert.equal(result.mutation_outcome, "committed");
677
+ });
678
+
679
+ // gentle-pi#822 (CodeRabbit finding): success must be proven by the unachievable_lens_slot stop itself — a bound STATUS that comes back with a collect reoffer instead of the stop is a reconciliation failure, never a silent success with no withdraw command.
680
+ test("a bound STATUS that reoffers the collect transition instead of the stop reports a reconciliation failure", async (t) => {
681
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
682
+ const cwd = repository(t);
683
+ const lineageId = "relay-lineage";
684
+ const input = relayCollectInput(lineageId, "review-risk", 0);
685
+ const harness = nativeHarness([finalizeStatus(lineageId, [input]), finalizeStatus(lineageId, [input])], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
686
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
687
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
688
+ });
689
+
690
+ const result = await runCapture(cwd, harness, lineageId);
691
+
692
+ assert.equal(result.status, "blocked");
693
+ assert.equal(result.outcome, "unachievable-lens-declaration-reconciliation-failed");
694
+ assert.equal(result.unachievable_lens_slots, undefined, "no stop means no withdraw command to expose");
695
+ const reconciliationFailure = result.reconciliation_failure as { outcome?: string; reason?: string };
696
+ assert.equal(reconciliationFailure?.outcome, "unachievable-lens-slot-declaration-unmatched");
697
+ assert.match(String(reconciliationFailure?.reason), /did not return the unachievable_lens_slot stop/);
698
+ assert.equal(result.mutation_performed, true, "the declaration was recorded");
699
+ assert.equal(result.mutation_outcome, "committed");
700
+ assert.equal(harness.statusCalls.length, 2, "the bound re-query still runs exactly once");
701
+ });
702
+
703
+ // gentle-pi#822 (CodeRabbit finding): the same proof requirement covers every STATUS shape that is not the stop — a foreign stop reason code or no transition at all.
704
+ test("a bound STATUS stop with a foreign reason code or no transition reports a reconciliation failure", async (t) => {
705
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
706
+ const cwd = repository(t);
707
+ const lineageId = "relay-lineage";
708
+ const input = relayCollectInput(lineageId, "review-risk", 0);
709
+ const responder = () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0));
710
+ const relayFailure = async () => {
711
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
712
+ };
713
+
714
+ const foreignStop = { ...finalizeStatus(lineageId), nextTransition: { kind: "stop", reasonCode: "rdd_disabled" } } as unknown as ReviewStatusV3;
715
+ const foreignHarness = nativeHarness([finalizeStatus(lineageId, [input]), foreignStop], responder);
716
+ __testing.setReviewHostRelayRunnerForTesting(relayFailure);
717
+ const foreign = await runCapture(cwd, foreignHarness, lineageId);
718
+
719
+ assert.equal(foreign.outcome, "unachievable-lens-declaration-reconciliation-failed");
720
+ assert.equal(foreign.unachievable_lens_slots, undefined);
721
+ const foreignFailure = foreign.reconciliation_failure as { outcome?: string; reason?: string };
722
+ assert.match(String(foreignFailure?.reason), /did not return the unachievable_lens_slot stop/);
723
+ assert.equal(foreign.mutation_performed, true);
724
+ assert.equal(foreign.mutation_outcome, "committed");
725
+
726
+ const bareHarness = nativeHarness([finalizeStatus(lineageId, [input]), finalizeStatus(lineageId)], responder);
727
+ __testing.setReviewHostRelayRunnerForTesting(relayFailure);
728
+ const bare = await runCapture(cwd, bareHarness, lineageId);
729
+
730
+ assert.equal(bare.outcome, "unachievable-lens-declaration-reconciliation-failed");
731
+ assert.equal(bare.unachievable_lens_slots, undefined);
732
+ const bareFailure = bare.reconciliation_failure as { outcome?: string; reason?: string };
733
+ assert.match(String(bareFailure?.reason), /did not return the unachievable_lens_slot stop/);
734
+ assert.equal(bare.mutation_outcome, "committed");
735
+ assert.equal(bareHarness.statusCalls.length, 2);
736
+ });
737
+
738
+ // gentle-pi#822: STATUS for one lineage renders only that lineage's withdraw
739
+ // command as the hint; a request for an unlisted lineage omits the hint rather
740
+ // than surfacing a potentially unrelated withdraw command.
741
+ test("an unachievable stop renders the withdraw hint only for the requested lineage", async (t) => {
742
+ const cwd = repository(t);
743
+ const matchedHarness = nativeHarness([unachievableStopStatus("other-lineage", "review-risk", 0)]);
744
+ const matched = (await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "other-lineage" }, cwd, matchedHarness.native)) as Record<string, unknown>;
745
+ assert.match(String(matched.hint), /capture-unachievable.*--withdraw=true/);
746
+ assert.equal(matched.requested_lineage_id, "other-lineage");
747
+
748
+ const unmatchedHarness = nativeHarness([unachievableStopStatus("other-lineage", "review-risk", 0)]);
749
+ const unmatched = (await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "relay-lineage" }, cwd, unmatchedHarness.native)) as Record<string, unknown>;
750
+ assert.equal(unmatched.hint, undefined, "no hint when no slot matches the requested lineage");
751
+ assert.equal(unmatched.requested_lineage_id, "relay-lineage");
752
+ });
753
+
754
+ // The fail-open capability gate: an older binary that refuses the whole verb
755
+ // keeps today's transport-failure envelope untouched.
756
+ test("an unknown-verb refusal falls back to today's transport-failure behavior", async (t) => {
757
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
758
+ const cwd = repository(t);
759
+ const lineageId = "relay-lineage";
760
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
761
+ throw Object.assign(new Error("native command returned empty output"), { diagnostics: { stderr: 'Error: unknown review command "capture-unachievable"\n' } });
762
+ });
763
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
764
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
765
+ });
766
+
767
+ const result = await runCapture(cwd, harness, lineageId);
768
+
769
+ assert.equal(result.status, "blocked");
770
+ assert.equal(result.outcome, "pi-host-relay-timeout");
771
+ assert.equal(harness.unachievableCalls.length, 1, "the verb was attempted exactly once");
772
+ assert.equal(harness.statusCalls.length, 1, "the fall-back performs no bound STATUS re-query");
773
+ assert.match(String(result.next_action), new RegExp(`${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV}=<milliseconds>`));
774
+ });
775
+
776
+ // Every non-capability declaration failure — here a typed binding-mismatch
777
+ // refusal — is surfaced with both failures, never hidden behind the relay
778
+ // failure it followed.
779
+ test("a refused declaration surfaces both failures instead of the transport fall-back", async (t) => {
780
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
781
+ const cwd = repository(t);
782
+ const lineageId = "relay-lineage";
783
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
784
+ throw Object.assign(new Error("review capture-unachievable binding does not match the current reviewing authority"), { diagnostics: { stderr: "Error: review capture-unachievable binding does not match [invalid_request]" } });
785
+ });
786
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
787
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
788
+ });
789
+
790
+ const result = await runCapture(cwd, harness, lineageId);
791
+
792
+ assert.equal(result.status, "blocked");
793
+ assert.equal(result.outcome, "unachievable-lens-declaration-failed");
794
+ assert.equal(result.mutation_performed, false);
795
+ assert.equal(result.mutation_outcome, "none");
796
+ 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 });
797
+ assert.ok(result.declaration_failure, "the declaration refusal rides the envelope");
798
+ assert.equal(harness.statusCalls.length, 1, "a refused declaration performs no bound STATUS re-query");
799
+ assert.match(String(result.next_action), /fresh STATUS/);
800
+ });
801
+
802
+ // gentle-pi#822 (outside-diff finding): the declaration failure's own envelope carries the mutation truth — a failure AFTER the provider recorded the declaration reports committed, never a hardcoded none.
803
+ test("a declaration failure whose envelope already committed propagates its mutation state", async (t) => {
804
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
805
+ const cwd = repository(t);
806
+ const lineageId = "relay-lineage";
807
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
808
+ throw Object.assign(new Error("review capture-unachievable failed after recording"), { failureEnvelope: { raw: { code: "invalid_request", message: "review capture-unachievable failed after recording" }, mutationOutcome: "committed" } });
809
+ });
810
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
811
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
812
+ });
813
+
814
+ const result = await runCapture(cwd, harness, lineageId);
815
+
816
+ assert.equal(result.status, "blocked");
817
+ assert.equal(result.outcome, "unachievable-lens-declaration-failed");
818
+ assert.equal(result.mutation_performed, true);
819
+ assert.equal(result.mutation_outcome, "committed");
820
+ assert.ok(result.declaration_failure, "the declaration failure still rides the envelope");
821
+ assert.equal(harness.statusCalls.length, 1, "a committed outcome needs no reconciliation re-query");
822
+ });
823
+
824
+ // gentle-pi#822 (outside-diff finding): an unknown declaration outcome is proven or disproven by one bound STATUS re-query without ever changing the failure outcome.
825
+ test("an unknown declaration outcome is proven committed by one bound STATUS re-query", async (t) => {
826
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
827
+ const cwd = repository(t);
828
+ const lineageId = "relay-lineage";
829
+ const input = relayCollectInput(lineageId, "review-risk", 0);
830
+ const stop = unachievableStopStatus(lineageId, "review-risk", 0);
831
+ const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], async () => {
832
+ throw Object.assign(new Error("native command timed out while recording the declaration"), { failureEnvelope: { raw: { code: "deadline_exceeded", message: "native command timed out" }, mutationOutcome: "unknown" } });
833
+ });
834
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
835
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
836
+ });
837
+
838
+ const result = await runCapture(cwd, harness, lineageId);
839
+
840
+ assert.equal(result.status, "blocked");
841
+ assert.equal(result.outcome, "unachievable-lens-declaration-failed", "the failure outcome never changes");
842
+ assert.equal(result.mutation_performed, true);
843
+ assert.equal(result.mutation_outcome, "committed");
844
+ assert.equal(harness.statusCalls.length, 2, "one selection STATUS plus exactly one bound reconciliation re-query");
845
+ assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "the reconciliation re-query preserves the Pi host runtime");
846
+ });
847
+
848
+ // gentle-pi#822 (outside-diff finding): a malformed declaration error with no envelope at all keeps the conservative none outcome and performs no re-query.
849
+ test("a malformed declaration error without an envelope keeps mutation none", async (t) => {
850
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
851
+ const cwd = repository(t);
852
+ const lineageId = "relay-lineage";
853
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
854
+ throw new TypeError("cannot read properties of undefined (reading 'detail')");
855
+ });
856
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
857
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
858
+ });
859
+
860
+ const result = await runCapture(cwd, harness, lineageId);
861
+
862
+ assert.equal(result.status, "blocked");
863
+ assert.equal(result.outcome, "unachievable-lens-declaration-failed");
864
+ assert.equal(result.mutation_performed, false);
865
+ assert.equal(result.mutation_outcome, "none");
866
+ assert.equal(harness.statusCalls.length, 1, "no envelope means no unknown outcome and no re-query");
867
+ });
868
+
493
869
  test("a non-timeout transport failure keeps its generic continuation and now carries its measurements", async (t) => {
494
870
  t.after(() => __testing.setReviewHostRelayRunnerForTesting());
495
871
  const cwd = repository(t);
@@ -18,6 +18,8 @@ import {
18
18
  resolveReviewHostRelayPiTimeoutMs,
19
19
  resolveReviewHostRelaySubmission,
20
20
  reviewHostRelaySlots,
21
+ reviewHostRelayUnachievableDetail,
22
+ reviewHostRelayUnachievableReason,
21
23
  prepareReviewHostRelaySlot,
22
24
  runReviewHostRelayReviewerGroup,
23
25
  runReviewHostRelaySlot,
@@ -779,6 +781,33 @@ test("refusal classification distinguishes unknown-flag, handshake, and other",
779
781
  assert.equal(classifyReviewHostRelayRefusal("some unrelated explosion"), "other");
780
782
  });
781
783
 
784
+ // gentle-pi#638: only a reviewer killed by the scaled relay bound is proven
785
+ // deterministic for this exact slot. Generic admission refusals are repairable
786
+ // by a fresh reviewer and retain the ordinary exact-reoffer behavior.
787
+ test("the unachievable predicate names only deterministic slot failures", () => {
788
+ const killed = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
789
+ assert.equal(reviewHostRelayUnachievableReason(killed), "relay_transport_bound_exceeded");
790
+ assert.equal(reviewHostRelayUnachievableDetail(killed), "killed after 2256004ms against a 2256000ms relay bound");
791
+
792
+ for (const refusal of ["reviewer payload contains no complete JSON object", "reviewer artifact admission binding_mismatch"]) {
793
+ const admitted = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", `${refusal} [invalid_request]`, { exitCode: 1, mutationOutcome: "none" });
794
+ assert.equal(reviewHostRelayUnachievableReason(admitted), undefined, `${refusal} is repairable by a fresh reviewer`);
795
+ assert.equal(reviewHostRelayUnachievableDetail(admitted), undefined);
796
+ }
797
+
798
+ const unknownOutcome = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", "gentle-ai capture submission exceeded its bound", { timedOut: true });
799
+ assert.equal(reviewHostRelayUnachievableReason(unknownOutcome), undefined, "an unknown mutation outcome is never deterministic");
800
+ assert.equal(reviewHostRelayUnachievableDetail(unknownOutcome), undefined);
801
+
802
+ for (const kind of [REVIEW_HOST_RELAY_FAILURE.PI_FAILED, REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED, REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH]) {
803
+ assert.equal(reviewHostRelayUnachievableReason(new ReviewHostRelayError(kind, "pi", "transient")), undefined, `${kind} stays transient`);
804
+ }
805
+
806
+ const unmeasured = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "killed at the bound");
807
+ assert.equal(reviewHostRelayUnachievableReason(unmeasured), "relay_transport_bound_exceeded");
808
+ assert.equal(reviewHostRelayUnachievableDetail(unmeasured), undefined, "no measurements means no detail, never a fabricated one");
809
+ });
810
+
782
811
  // ---------------------------------------------------------------------------
783
812
  // Slot detection — the provider decides; nothing is inferred.
784
813
  // ---------------------------------------------------------------------------
@@ -699,3 +699,47 @@ test("review-acknowledged/v1 is disjoint from every prior captured identity in b
699
699
  assert.throws(() => decoder(clone(acknowledged)), `${name} must reject the acknowledged envelope`);
700
700
  }
701
701
  });
702
+
703
+ // Producer-shaped projection: gentle-ai d521b5e, target_status.go and
704
+ // last-event-closure.schema.json#/properties/escalation (not a live capture).
705
+ function escalatedStatusV7(): JsonObject {
706
+ const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
707
+ body.schema = "gentle-ai.review-integration.status/v7";
708
+ (body.authority as JsonObject).state = "escalated";
709
+ body.action = "stop";
710
+ body.replayability = "manual_action_required";
711
+ body.next_transition = { kind: "stop", reason_code: "escalated" };
712
+ delete body.forecast;
713
+ body.escalation = { cause: "unknown_causality", finding_ids: ["R1"] };
714
+ return body;
715
+ }
716
+
717
+ test("v7 escalation is typed metadata and preserves native stop authority", () => {
718
+ const body = escalatedStatusV7();
719
+ const decoded = decodeReviewStatusV3(body);
720
+ assert.deepEqual(decoded.escalation, { cause: "unknown_causality", findingIds: ["R1"] });
721
+ delete body.escalation;
722
+ const older = decodeReviewStatusV3(body);
723
+ assert.deepEqual({ ...decoded, escalation: undefined, raw: undefined }, { ...older, escalation: undefined, raw: undefined });
724
+ assert.equal(decoded.action, "stop");
725
+ assert.equal(decoded.authority?.state, "escalated");
726
+ assert.deepEqual(decoded.nextTransition, { kind: "stop", reasonCode: "escalated" });
727
+ });
728
+
729
+ test("v7 escalation strictly decodes all causes and optional refuter evidence", () => {
730
+ for (const cause of ["unknown_causality", "insufficient_evidence", "missing_refuter_outcome", "targeted_validator_rejected", "correction_budget_exceeded", "unresolved_severe_findings"]) {
731
+ const body = escalatedStatusV7();
732
+ const rows = ["corroborated", "refuted", "inconclusive"].map((outcome) => ({ finding_id: "R2", outcome, proof: "observed evidence" }));
733
+ body.escalation = { cause, finding_ids: [], refuter_outcomes: rows };
734
+ assert.deepEqual(decodeReviewStatusV3(body).escalation, { cause, findingIds: [], refuterOutcomes: rows.map(({ finding_id, ...row }) => ({ findingId: finding_id, ...row })) });
735
+ }
736
+ for (const escalation of [null, undefined, [], {}, { cause: "future", finding_ids: [] }, { cause: "unknown_causality" }, { cause: "unknown_causality", finding_ids: [""] }, { cause: "unknown_causality", finding_ids: "R1" }, { cause: "unknown_causality", finding_ids: [], extra: true }]) {
737
+ assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), escalation }), /status\.escalation/);
738
+ }
739
+ for (const refuter_outcomes of [null, undefined, {}, [null], [{}], [{ finding_id: "R1", outcome: "future", proof: "proof" }], [{ finding_id: "", outcome: "refuted", proof: "proof" }], [{ finding_id: "R1", outcome: "refuted", proof: "" }], [{ finding_id: "R1", outcome: "refuted", proof: "proof", extra: true }]]) {
740
+ assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), escalation: { cause: "missing_refuter_outcome", finding_ids: ["R1"], refuter_outcomes } }), /refuter_outcomes/);
741
+ }
742
+ for (const version of [3, 5, 6]) {
743
+ assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), schema: `gentle-ai.review-integration.status/v${version}` }), /not allowed/);
744
+ }
745
+ });