gentle-pi 2.1.2 → 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 (228) hide show
  1. package/README.md +108 -79
  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 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  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 +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,352 @@
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_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 type { ReviewCaptureSubmissionV1, ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+
12
+ // One-slot capture routing: the host relay runs only when the selected
13
+ // provider-returned collect input carries the --materialize token. Every
14
+ // other capture form stays untouched.
15
+
16
+ const SHA = `sha256:${"1".repeat(64)}`;
17
+ const TREE = "2".repeat(40);
18
+
19
+ function repository(t: test.TestContext): string {
20
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-relay-routing-"));
21
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
22
+ execFileSync("git", ["init", "-b", "main"], { cwd });
23
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
24
+ execFileSync("git", ["add", "."], { cwd });
25
+ execFileSync("git", ["-c", "user.name=Relay Test", "-c", "user.email=relay@example.invalid", "commit", "-m", "initial"], { cwd });
26
+ return cwd;
27
+ }
28
+
29
+ function bindingArguments(lineageId: string, lens: string, order: number): ReviewCollectInputV3["arguments"] {
30
+ return [
31
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
32
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
33
+ { name: "target", value: SHA, token: `--target=${SHA}` },
34
+ { name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
35
+ { name: "lens", value: lens, token: `--lens=${lens}` },
36
+ { name: "order", value: String(order), token: `--order=${order}` },
37
+ { name: "subject-hash", value: `sha256:${String(order).repeat(64)}`, token: `--subject-hash=sha256:${String(order).repeat(64)}` },
38
+ ];
39
+ }
40
+
41
+ function providerSubmission(lineageId: string, lens: string, order: number): ReviewCaptureSubmissionV1 {
42
+ const bindingTokens = bindingArguments(lineageId, lens, order).map((argument) => argument.token!);
43
+ return {
44
+ operationToken: "capture-result",
45
+ argumentTokens: [...bindingTokens, "--input={{value}}"],
46
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: bindingTokens.length }],
47
+ };
48
+ }
49
+
50
+ function relayCollectInput(lineageId: string, lens: string, order: number, materialize = true, submission: ReviewCaptureSubmissionV1 | "provider" | "absent" = "provider"): ReviewCollectInputV3 {
51
+ return {
52
+ name: "reviewer_result",
53
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
54
+ captureOperation: "review.capture-result",
55
+ arguments: [
56
+ ...bindingArguments(lineageId, lens, order),
57
+ ...(materialize ? [
58
+ { name: "agent", value: "pi", token: "--agent=pi" },
59
+ { name: "materialize", value: "true", token: "--materialize=true" },
60
+ ] : []),
61
+ ],
62
+ ...(materialize && submission !== "absent" ? { submission: submission === "provider" ? providerSubmission(lineageId, lens, order) : submission } : {}),
63
+ };
64
+ }
65
+
66
+ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV3[]): ReviewStatusV3 {
67
+ return {
68
+ contract: "gentle-ai.review-integration/v2",
69
+ applicability: "current_target",
70
+ authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
71
+ action: "stop",
72
+ replayability: "unknown",
73
+ targetIdentity: SHA,
74
+ projection: {
75
+ schema: "gentle-ai.review-candidate-projection/v1",
76
+ kind: "current-changes",
77
+ projection: "workspace",
78
+ baseTree: TREE,
79
+ initialReviewTree: TREE,
80
+ currentCandidateTree: TREE,
81
+ pathsDigest: SHA,
82
+ paths: ["app.ts"],
83
+ intendedUntracked: [],
84
+ intendedUntrackedProof: SHA,
85
+ initialSnapshotIdentity: SHA,
86
+ currentSnapshotIdentity: SHA,
87
+ },
88
+ candidates: [],
89
+ ...(inputs === undefined ? {} : { nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [...inputs] } } }),
90
+ raw: { schema: "gentle-ai.review-integration.status/v3", action: "stop", lineage_id: lineageId },
91
+ } as unknown as ReviewStatusV3;
92
+ }
93
+
94
+ interface RoutingHarness {
95
+ statusQueue: ReviewStatusV3[];
96
+ statusCalls: Array<{ cwd: string; lineageId?: string }>;
97
+ native: NativeReviewCli;
98
+ }
99
+
100
+ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
101
+ const harness: RoutingHarness = {
102
+ statusQueue: [...statuses],
103
+ statusCalls: [],
104
+ native: undefined as unknown as NativeReviewCli,
105
+ };
106
+ harness.native = {
107
+ targetStatus: async (request) => {
108
+ harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
109
+ const next = harness.statusQueue.shift();
110
+ if (next === undefined) throw new Error("status queue exhausted");
111
+ return next;
112
+ },
113
+ };
114
+ return harness;
115
+ }
116
+
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 },
122
+ cwd,
123
+ harness.native,
124
+ ) as Record<string, unknown>;
125
+ }
126
+
127
+ test("one materialize binding routes exactly one provider slot through the host relay", async (t) => {
128
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
129
+ const cwd = repository(t);
130
+ const lineageId = "relay-lineage";
131
+ const input = relayCollectInput(lineageId, "review-risk", 0);
132
+ const harness = nativeHarness([finalizeStatus(lineageId, [input])]);
133
+ const relayed: ReviewHostRelayRequest[] = [];
134
+ __testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
135
+ relayed.push(request);
136
+ return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
137
+ });
138
+
139
+ const result = await runCapture(cwd, harness, lineageId);
140
+
141
+ assert.equal(relayed.length, 1);
142
+ assert.deepEqual(relayed[0]!.captureArgumentTokens, input.arguments.map((argument) => argument.token));
143
+ assert.deepEqual(relayed[0]!.submission, providerSubmission(lineageId, "review-risk", 0));
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");
147
+ });
148
+
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.
152
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
153
+ const cwd = repository(t);
154
+ const lineageId = "relay-lineage";
155
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
156
+ let relayCalls = 0;
157
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
158
+ relayCalls += 1;
159
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
160
+ });
161
+
162
+ await assert.rejects(
163
+ () => runCapture(cwd, harness, lineageId, { review_result: { lens_results: [{ findings: [], evidence: ["reviewed"] }] } }),
164
+ /does not accept review_result/,
165
+ );
166
+ assert.equal(relayCalls, 0);
167
+ });
168
+
169
+ test("an old binary reports the relay as unavailable without touching existing behavior", async (t) => {
170
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
171
+ const cwd = repository(t);
172
+ const lineageId = "relay-lineage";
173
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
174
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
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" });
176
+ });
177
+
178
+ const result = await runCapture(cwd, harness, lineageId);
179
+
180
+ assert.equal(result.status, "blocked");
181
+ assert.equal(result.outcome, "pi-host-relay-unavailable");
182
+ assert.equal(result.reason, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
183
+ assert.equal(result.mutation_performed, false);
184
+ assert.equal(result.mutation_outcome, "none");
185
+ });
186
+
187
+ test("a handshake refusal surfaces the provider refusal verbatim through the controller envelope", async (t) => {
188
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
189
+ const cwd = repository(t);
190
+ const lineageId = "relay-lineage";
191
+ const refusal = "the active runtime is not eligible for immutable receipt review; supported immutable review runtimes: claude-code, codex, opencode";
192
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
193
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
194
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", refusal, { exitCode: 1, stderr: refusal });
195
+ });
196
+
197
+ const result = await runCapture(cwd, harness, lineageId);
198
+
199
+ assert.equal(result.outcome, "pi-host-relay-handshake-refused");
200
+ assert.equal(result.reason, refusal);
201
+ assert.equal(result.refusal, refusal);
202
+ });
203
+
204
+ test("a transport failure stops the selected capture without auto-follow", async (t) => {
205
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
206
+ const cwd = repository(t);
207
+ const lineageId = "relay-lineage";
208
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
209
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
210
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
211
+ });
212
+
213
+ const result = await runCapture(cwd, harness, lineageId);
214
+
215
+ assert.equal(result.status, "blocked");
216
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
217
+ assert.deepEqual(result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false });
218
+ assert.match(String(result.next_action), /fresh STATUS/);
219
+ assert.equal(harness.statusCalls.length, 1, "no automatic relaunch after transport failure");
220
+ });
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
+
307
+ test("materialize tokens without a provider submission fail closed as a typed contract mismatch", async (t) => {
308
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
309
+ const cwd = repository(t);
310
+ const lineageId = "relay-lineage";
311
+ const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0, true, "absent")])]);
312
+ let relayCalls = 0;
313
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
314
+ relayCalls += 1;
315
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
316
+ });
317
+
318
+ const result = await runCapture(cwd, harness, lineageId);
319
+
320
+ assert.equal(relayCalls, 0, "the completing form is never synthesized, so nothing launches");
321
+ assert.equal(result.status, "blocked");
322
+ assert.equal(result.outcome, "pi-host-relay-transport-failure");
323
+ assert.deepEqual(result.failure, { kind: "submission-contract-mismatch", stage: "binding", exit_code: null, timed_out: false });
324
+ assert.equal(result.reason, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
325
+ assert.equal(result.mutation_performed, false);
326
+ assert.equal(result.mutation_outcome, "none");
327
+ assert.equal(harness.statusCalls.length, 1, "no relaunch and no post-capture STATUS after the contract mismatch");
328
+ });
329
+
330
+ test("collect inputs without the provider-issued materialize token never reach the relay", async (t) => {
331
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
332
+ const cwd = repository(t);
333
+ const lineageId = "relay-lineage";
334
+ const harness = nativeHarness([
335
+ finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0, false)]),
336
+ finalizeStatus(lineageId),
337
+ ]);
338
+ let relayCalls = 0;
339
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
340
+ relayCalls += 1;
341
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
342
+ });
343
+
344
+ // The existing lane may fail on the synthetic projection; only the relay
345
+ // boundary is under test here: it must never be consulted.
346
+ try {
347
+ await runCapture(cwd, harness, lineageId);
348
+ } catch {
349
+ // Existing-lane behavior for this synthetic fixture is out of scope.
350
+ }
351
+ assert.equal(relayCalls, 0);
352
+ });