gentle-pi 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +70 -12
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,631 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import {
6
+ decodeReviewCapabilitiesV2,
7
+ decodeReviewConsentV2,
8
+ decodeReviewConsentV3,
9
+ decodeReviewFailureV2,
10
+ decodeReviewOperationV2,
11
+ decodeReviewResultArtifactV2,
12
+ decodeReviewStartV3,
13
+ decodeReviewStatusV3,
14
+ } from "../lib/review-integration-v2.ts";
15
+
16
+ // Forward decoders for the gentle-ai main line (dev-binary field-test lane).
17
+ //
18
+ // Fixture provenance:
19
+ // - capabilities-v2.2.captured.json and status-v5.captured.json were captured
20
+ // 2026-08-15 from the real binary at /home/gentleman/.cargo/bin/gentle-ai
21
+ // reporting "gentle-ai 2.4.0-rc.8+fix.verify-attestation-recovery" (built
22
+ // from the gentle-ai main line), in a scratch git repository:
23
+ // gentle-ai review capabilities --contract gentle-ai.review-integration/v2
24
+ // GENTLE_PI_REVIEW_RELAY_CONTRACT=gentle-pi.review-relay/v1 \
25
+ // gentle-ai review status --contract gentle-ai.review-integration/v2 \
26
+ // --cwd <scratch> --projection workspace --next-transition
27
+ // - capabilities-v2.1.derived.json is derived from the captured v2.2 payload
28
+ // plus gentle-ai origin/main contracts/review-integration/v2/schemas/
29
+ // capabilities-v2.1.schema.json (schema const v2.1, protocol minor 1,
30
+ // consent/v3 + status/v3 schema advertisements, 17 optional features): no
31
+ // installed binary emits v2.1 to capture from.
32
+ // - The v5-only next_transition samples (correction_request, provider_task,
33
+ // submission descriptor forms) are constructed from gentle-ai origin/main
34
+ // contracts/review-integration/v2/schemas/status-v5.schema.json and
35
+ // contracts/review-integration/v1/schemas/correction-plan-request.schema.json.
36
+ // - consent-v3.captured.json, start-v3-consent-granted.captured.json, and
37
+ // start-v3-consent-declined.captured.json were captured 2026-08-16 from the
38
+ // real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
39
+ // "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
40
+ // committed internal/runner/{runner.go,runner_test.go} baseline plus 12
41
+ // uncommitted changed lines that add a process-starting helper (risk signal
42
+ // shell_process, high tier):
43
+ // gentle-ai review status --contract gentle-ai.review-integration/v2 \
44
+ // --cwd <scratch> --projection workspace --next-transition # target source
45
+ // gentle-ai review start --contract gentle-ai.review-integration/v2 \
46
+ // --cwd <scratch> --target <identity> --projection workspace \
47
+ // --consent relay # consent-v3
48
+ // then the envelope's exact declined invocation (declined capture, which
49
+ // persists nothing) followed by its exact granted invocation (granted
50
+ // start/v3 capture). Note: the live granted/declined invocations carry no
51
+ // --agent token even though the envelope pins agent: claude-code — the
52
+ // published consent-v3.schema.json invocation pattern is narrower than the
53
+ // emitter, so the capture is authoritative (parity playbook, known traps).
54
+ // - status-v5-repository-context.captured.json was captured 2026-08-16 from
55
+ // the real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
56
+ // "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository with one
57
+ // medium-tier lineage in state `reviewing` whose single reviewer result was
58
+ // already admitted (consent granted start, then review capture-result):
59
+ // gentle-ai review status --contract gentle-ai.review-integration/v2 \
60
+ // --cwd <scratch> --agent claude-code --lineage <lineage> \
61
+ // --projection workspace --next-transition
62
+ // The top-level `repository_context` reference it carries is emitted by the
63
+ // live binary but MISSING from the published status-v5.schema.json
64
+ // (additionalProperties: false) at the same commit — the capture is
65
+ // authoritative (parity playbook, known traps).
66
+ // - result-artifact-v2.captured.json and result-artifact-v2-path.captured.json
67
+ // were captured 2026-08-16 from the real binary at
68
+ // /home/gentleman/.cargo/bin/gentle-ai reporting
69
+ // "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
70
+ // committed src/greet.js baseline plus an uncommitted shout() helper
71
+ // (medium tier, one review-reliability lens): consent-granted START, then
72
+ // the STATUS transition's exact `review capture-result` collect arguments
73
+ // with a completed reviewer input document. The reference form is the
74
+ // stdout of the --repository-context invocation (opaque rart1_ locator);
75
+ // the path form re-ran the exact same admitted slot with --cwd instead,
76
+ // which discovers the identical canonical bytes and answers with the
77
+ // provider-owned store path. Both agree on every binding field.
78
+ // - status-v5-capture-result-submission.captured.json was captured 2026-08-16
79
+ // from the real binary at /home/gentleman/.cargo/bin/gentle-ai reporting
80
+ // "gentle-ai 2.4.0-main.b1afef46", in a scratch git repository holding a
81
+ // committed src/add.js baseline plus an uncommitted double() helper
82
+ // (medium tier, one review-reliability lens), with
83
+ // GENTLE_PI_REVIEW_RELAY_CONTRACT=gentle-pi.review-relay/v1 exported:
84
+ // negotiated STATUS (--agent pi), the rendered consent/v3 START, its exact
85
+ // granted invocation, then
86
+ // gentle-ai review status --contract gentle-ai.review-integration/v2 \
87
+ // --cwd <scratch> --agent pi --next-transition
88
+ // at reviewer_results_required. Its materialize capture-result collect
89
+ // input carries the submission descriptor as a SINGULAR `value` object
90
+ // with a `schema` key (emitter: gentle-ai f1a95179, singular from its
91
+ // first commit) — not the `values` array this decoder was originally
92
+ // written against. The capture is authoritative.
93
+
94
+ const fixtureRoot = join(import.meta.dirname, "fixtures", "devbinary");
95
+ const fixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as T;
96
+ const v2FixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
97
+ const v2Fixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
98
+ const capturedDigest = "ffc91d8fa79c869aba9aa3d1ec80edebb5b1744e5a06fef75d4c8b73c0e46bc1";
99
+ const pinnedFixtureDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
100
+
101
+ type JsonObject = Record<string, unknown>;
102
+ const clone = <T>(value: T): T => structuredClone(value);
103
+ const sha = (fill: string): string => `sha256:${fill.repeat(64)}`;
104
+
105
+ test("the captured capabilities/v2.2 payload decodes and reports its own package version", () => {
106
+ const capabilities = decodeReviewCapabilitiesV2(fixture("capabilities-v2.2.captured.json"), capturedDigest);
107
+ assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
108
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v5"), true);
109
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
110
+ assert.equal(capabilities.optionalFeatures.has("provider_bound_native_git_context"), true);
111
+ });
112
+
113
+ test("the derived capabilities/v2.1 payload decodes with protocol minor 1", () => {
114
+ const capabilities = decodeReviewCapabilitiesV2(fixture("capabilities-v2.1.derived.json"), capturedDigest);
115
+ assert.equal(capabilities.packageVersion, "2.4.0-rc.8+fix.verify-attestation-recovery");
116
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
117
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v3"), true);
118
+ });
119
+
120
+ test("the pinned capabilities/v2 fixture still decodes unchanged", () => {
121
+ const capabilities = decodeReviewCapabilitiesV2(v2Fixture("capabilities.fixture.json"), pinnedFixtureDigest);
122
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.status/v3"), true);
123
+ assert.equal(capabilities.schemas.has("gentle-ai.review-integration.consent/v2"), true);
124
+ });
125
+
126
+ test("capabilities schema identity and protocol minor must agree", () => {
127
+ const base = fixture<JsonObject>("capabilities-v2.2.captured.json");
128
+ const wrongMinor = clone(base);
129
+ (wrongMinor.protocol as JsonObject).minor = 1;
130
+ assert.throws(() => decodeReviewCapabilitiesV2(wrongMinor, capturedDigest), /protocol/);
131
+ const wrongSchema = clone(base);
132
+ wrongSchema.schema = "gentle-ai.review-integration.capabilities/v2.3";
133
+ assert.throws(() => decodeReviewCapabilitiesV2(wrongSchema, capturedDigest), /schema/);
134
+ // The old identity never inherits the new advertisements: a v2 envelope
135
+ // advertising the v2.2 schema surface is missing its own required floor.
136
+ const crossed = clone(base);
137
+ crossed.schema = "gentle-ai.review-integration.capabilities/v2";
138
+ (crossed.protocol as JsonObject).minor = 0;
139
+ assert.throws(() => decodeReviewCapabilitiesV2(crossed, capturedDigest), /schemas/);
140
+ });
141
+
142
+ test("the captured status/v5 payload decodes with its forecast and base-ref collect input", () => {
143
+ const status = decodeReviewStatusV3(fixture("status-v5.captured.json"));
144
+ assert.equal(status.applicability, "unrelated");
145
+ assert.equal(status.action, "start");
146
+ assert.equal(status.forecast?.horizon, "partial");
147
+ assert.deepEqual(status.forecast?.steps, [{ step: 1, kind: "collect", reasonCode: "empty_candidate_base_ref_required", description: "empty candidate base ref required" }]);
148
+ assert.equal(status.nextTransition?.kind, "collect");
149
+ assert.equal(status.nextTransition?.collect?.inputs[0]?.captureOperation, "external.select_base_ref");
150
+ });
151
+
152
+ test("the pinned status/v3 fixture still decodes unchanged", () => {
153
+ const status = decodeReviewStatusV3(v2Fixture("status.fixture.json"));
154
+ assert.equal(status.contract, "gentle-ai.review-integration/v2");
155
+ });
156
+
157
+ test("the captured status/v5 payload decodes its top-level repository context reference", () => {
158
+ const status = decodeReviewStatusV3(fixture("status-v5-repository-context.captured.json"));
159
+ assert.equal(status.authority?.state, "reviewing");
160
+ assert.equal(status.repositoryContext?.capability, "review.opaque_repository_context");
161
+ assert.match(status.repositoryContext?.handle ?? "", /^rctx1_[0-9a-f]{64}$/);
162
+ assert.equal(status.repositoryContext?.revision, status.authority?.revision);
163
+ assert.equal(status.repositoryContext?.targetIdentity, status.targetIdentity);
164
+ assert.equal(status.repositoryContext?.outcome, "applied");
165
+ });
166
+
167
+ test("a status/v3 payload never carries a top-level repository context", () => {
168
+ const v5 = fixture<JsonObject>("status-v5-repository-context.captured.json");
169
+ const downgraded = clone(v5);
170
+ downgraded.schema = "gentle-ai.review-integration.status/v3";
171
+ delete downgraded.forecast; // forecast is rejected first; isolate the field under test
172
+ assert.throws(() => decodeReviewStatusV3(downgraded), /repository_context/);
173
+ });
174
+
175
+ test("a status/v3 payload never carries v5 fields", () => {
176
+ const v5 = fixture<JsonObject>("status-v5.captured.json");
177
+ const downgraded = clone(v5);
178
+ downgraded.schema = "gentle-ai.review-integration.status/v3";
179
+ // forecast is a v5 field; the v3 identity must keep rejecting it.
180
+ assert.throws(() => decodeReviewStatusV3(downgraded), /forecast/);
181
+ const withoutForecast = clone(downgraded);
182
+ delete withoutForecast.forecast;
183
+ const status = decodeReviewStatusV3(withoutForecast);
184
+ assert.equal(status.forecast, undefined);
185
+ });
186
+
187
+ test("a v5 forecast requires the next transition and a coherent horizon", () => {
188
+ const base = fixture<JsonObject>("status-v5.captured.json");
189
+ const orphanForecast = clone(base);
190
+ delete orphanForecast.next_transition;
191
+ assert.throws(() => decodeReviewStatusV3(orphanForecast), /forecast/);
192
+ const wrongHorizon = clone(base);
193
+ (wrongHorizon.forecast as JsonObject).horizon = "terminal";
194
+ assert.throws(() => decodeReviewStatusV3(wrongHorizon), /horizon/);
195
+ const badStep = clone(base);
196
+ ((badStep.forecast as JsonObject).steps as JsonObject[])[0]!.step = 2;
197
+ assert.throws(() => decodeReviewStatusV3(badStep), /step/);
198
+ const extraKey = clone(base);
199
+ (extraKey.forecast as JsonObject).unadvertised = true;
200
+ assert.throws(() => decodeReviewStatusV3(extraKey), /not allowed/);
201
+ });
202
+
203
+ // --- v5-only next_transition surfaces, constructed per the vendored schemas ---
204
+
205
+ function v5StatusWith(nextTransition: JsonObject, forecast?: JsonObject): JsonObject {
206
+ const base = fixture<JsonObject>("status-v5.captured.json");
207
+ base.next_transition = nextTransition;
208
+ if (forecast === undefined) delete base.forecast;
209
+ else base.forecast = forecast;
210
+ return base;
211
+ }
212
+
213
+ const correctionPlanRequest: JsonObject = {
214
+ schema: "gentle-ai.review-correction-plan-request/v1",
215
+ request_hash: sha("a"),
216
+ lineage_id: "review-1d5aadacc600e167",
217
+ expected_revision: sha("b"),
218
+ target_identity: sha("c"),
219
+ correction_budget: 25,
220
+ fix_finding_ids: ["risk-1"],
221
+ findings: [{
222
+ id: "risk-1",
223
+ lens: "risk",
224
+ location: "app.ts:10",
225
+ severity: "BLOCKER",
226
+ claim: "candidate-introduced credential exposure",
227
+ proof_refs: ["app.ts:10-14"],
228
+ evidence: "the candidate logs the raw token",
229
+ evidence_class: "deterministic",
230
+ causal_disposition: "introduced",
231
+ }],
232
+ };
233
+
234
+ test("a v5 correction_plan_required transition carries its correction request and plan submission", () => {
235
+ const transition: JsonObject = {
236
+ kind: "collect",
237
+ reason_code: "correction_plan_required",
238
+ correction_request: correctionPlanRequest,
239
+ collect: {
240
+ inputs: [{
241
+ name: "correction_plan",
242
+ schema: "gentle-ai.review-correction-plan/v1",
243
+ capture_operation: "external.plan_correction",
244
+ arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
245
+ submission: {
246
+ operation_token: "finalize",
247
+ argument_tokens: [
248
+ "--contract=gentle-ai.review-integration/v2",
249
+ "--lineage=review-1d5aadacc600e167",
250
+ `--expected-revision=${sha("b")}`,
251
+ `--target=${sha("c")}`,
252
+ `--request-hash=${sha("a")}`,
253
+ `--repository-context=rctx1_${"0".repeat(64)}`,
254
+ "--correction-lines={{value}}",
255
+ ],
256
+ value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 25, substitution_location: 6 },
257
+ },
258
+ }],
259
+ },
260
+ };
261
+ const status = decodeReviewStatusV3(v5StatusWith(transition));
262
+ assert.equal(status.nextTransition?.reasonCode, "correction_plan_required");
263
+ assert.equal(status.nextTransition?.correctionRequest?.correctionBudget, 25);
264
+ assert.equal(status.nextTransition?.collect?.inputs[0]?.submissionDescriptor?.operationToken, "finalize");
265
+ // The same reason code without its correction request is incomplete.
266
+ const missing = clone(transition);
267
+ delete missing.correction_request;
268
+ assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /correction_request/);
269
+ // And correction_request never rides an unrelated reason code.
270
+ const unrelated = clone(transition);
271
+ unrelated.reason_code = "verification_evidence_required";
272
+ assert.throws(() => decodeReviewStatusV3(v5StatusWith(unrelated)), /correction_request/);
273
+ });
274
+
275
+ test("a v5 capture-evidence transition decodes its evidence submission descriptor", () => {
276
+ const transition: JsonObject = {
277
+ kind: "collect",
278
+ reason_code: "verification_evidence_required",
279
+ collect: {
280
+ inputs: [{
281
+ name: "verification_evidence",
282
+ schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1",
283
+ capture_operation: "review.capture-evidence",
284
+ arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
285
+ submission: {
286
+ operation_token: "capture-evidence",
287
+ argument_tokens: [
288
+ "--lineage=review-1d5aadacc600e167",
289
+ `--expected-revision=${sha("b")}`,
290
+ `--target=${sha("c")}`,
291
+ `--repository-context=rctx1_${"0".repeat(64)}`,
292
+ "--outcome={{outcome}}",
293
+ "--input={{input}}",
294
+ ],
295
+ values: [
296
+ { slot: "outcome", domain: "verification_outcome", allowed_values: ["passed", "verification_failed", "procedural_tooling_failed"], substitution_location: 4 },
297
+ { slot: "input", domain: "artifact_path_or_stdin", schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1", substitution_location: 5 },
298
+ ],
299
+ },
300
+ }],
301
+ },
302
+ };
303
+ const status = decodeReviewStatusV3(v5StatusWith(transition));
304
+ const input = status.nextTransition?.collect?.inputs[0];
305
+ assert.equal(input?.submissionDescriptor?.operationToken, "capture-evidence");
306
+ assert.equal(input?.submission, undefined);
307
+ // Missing its required submission descriptor is incomplete.
308
+ const missing = clone(transition);
309
+ delete ((missing.collect as JsonObject).inputs as JsonObject[])[0]!.submission;
310
+ assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /submission/);
311
+ });
312
+
313
+ test("a v5 provider role task input decodes and is confined to external.run_provider_role", () => {
314
+ const providerTask = { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" };
315
+ const transition: JsonObject = {
316
+ kind: "collect",
317
+ reason_code: "refuter_batch_required",
318
+ collect: {
319
+ inputs: [{
320
+ name: "refuter_batch",
321
+ schema: "https://gentle-ai.dev/schema/review/refuter/v1",
322
+ capture_operation: "external.run_provider_role",
323
+ arguments: [{ name: "lineage", value: "review-1d5aadacc600e167" }],
324
+ provider_task: providerTask,
325
+ }],
326
+ },
327
+ };
328
+ const status = decodeReviewStatusV3(v5StatusWith(transition));
329
+ assert.deepEqual(status.nextTransition?.collect?.inputs[0]?.providerTask, { agent: "review-refuter", role: "refuter", prompt: "GENTLE_AI_REVIEW_BINDING {}" });
330
+ // The task is required on its own vector and forbidden anywhere else.
331
+ const missing = clone(transition);
332
+ delete ((missing.collect as JsonObject).inputs as JsonObject[])[0]!.provider_task;
333
+ assert.throws(() => decodeReviewStatusV3(v5StatusWith(missing)), /provider_task/);
334
+ const misplaced = fixture<JsonObject>("status-v5.captured.json");
335
+ (((misplaced.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!.provider_task = providerTask;
336
+ assert.throws(() => decodeReviewStatusV3(misplaced), /provider_task/);
337
+ });
338
+
339
+ // --- v5 pi materialize capture-result submission: the singular `value` form ---
340
+
341
+ test("the captured v5 materialize capture-result submission decodes its singular value form", () => {
342
+ const status = decodeReviewStatusV3(fixture("status-v5-capture-result-submission.captured.json"));
343
+ assert.equal(status.authority?.state, "reviewing");
344
+ assert.equal(status.nextTransition?.reasonCode, "reviewer_results_required");
345
+ const input = status.nextTransition?.collect?.inputs[0];
346
+ assert.equal(input?.captureOperation, "review.capture-result");
347
+ assert.equal(input?.submissionDescriptor, undefined);
348
+ assert.equal(input?.submission?.operationToken, "capture-result");
349
+ // The singular wire `value` normalizes into the typed one-entry values
350
+ // array the host relay already consumes.
351
+ assert.deepEqual(input?.submission?.values, [{
352
+ slot: "reviewer_result",
353
+ domain: "artifact_path_or_stdin",
354
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
355
+ substitutionLocation: 7,
356
+ }]);
357
+ });
358
+
359
+ test("the v5 singular capture-result value form is closed to its captured shape", () => {
360
+ const base = fixture<JsonObject>("status-v5-capture-result-submission.captured.json");
361
+ const withSubmission = (mutate: (submission: JsonObject) => void): JsonObject => {
362
+ const body = clone(base);
363
+ mutate((((body.next_transition as JsonObject).collect as JsonObject).inputs as JsonObject[])[0]!.submission as JsonObject);
364
+ return body;
365
+ };
366
+ // Carrying both wire forms at once matches no captured shape.
367
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
368
+ submission.values = [submission.value];
369
+ })), /value/);
370
+ // The singular form owns no numeric or enumerated domain: minimum,
371
+ // maximum, and allowed_values belong to the finalize/capture-evidence
372
+ // descriptor forms only.
373
+ for (const [key, smuggled] of [["minimum", 1], ["maximum", 2], ["allowed_values", ["x"]]] as const) {
374
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
375
+ (submission.value as JsonObject)[key] = smuggled;
376
+ })), /not allowed/);
377
+ }
378
+ // The captured form always names its reviewer schema and slot.
379
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
380
+ delete (submission.value as JsonObject).schema;
381
+ })), /schema/);
382
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
383
+ (submission.value as JsonObject).slot = "validation";
384
+ })), /slot/);
385
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
386
+ submission.operation_token = "finalize-relay";
387
+ })), /operation_token/);
388
+ // The legacy values-array rows stay exactly as they were: they never
389
+ // learned the schema key the singular form carries.
390
+ assert.throws(() => decodeReviewStatusV3(withSubmission((submission) => {
391
+ const row = submission.value as JsonObject;
392
+ delete submission.value;
393
+ submission.values = [row];
394
+ })), /not allowed/);
395
+ });
396
+
397
+ test("the v3 identity keeps rejecting the singular capture-result value form", () => {
398
+ const body = clone(fixture<JsonObject>("status-v5-capture-result-submission.captured.json"));
399
+ body.schema = "gentle-ai.review-integration.status/v3";
400
+ delete body.forecast;
401
+ delete body.repository_context;
402
+ assert.throws(() => decodeReviewStatusV3(body), /values is required/);
403
+ });
404
+
405
+ test("the v3 next transition keeps rejecting every v5-only surface", () => {
406
+ const v5 = fixture<JsonObject>("status-v5.captured.json");
407
+ // Re-identify the captured envelope as v3 (drop the v5-only forecast) and
408
+ // then try to smuggle each v5 surface through the old identity.
409
+ const asV3 = (mutate: (transition: JsonObject) => void): JsonObject => {
410
+ const body = clone(v5);
411
+ body.schema = "gentle-ai.review-integration.status/v3";
412
+ delete body.forecast;
413
+ mutate(body.next_transition as JsonObject);
414
+ return body;
415
+ };
416
+ assert.throws(() => decodeReviewStatusV3(asV3((transition) => { transition.correction_request = correctionPlanRequest; })), /not allowed|correction_request/);
417
+ assert.throws(() => decodeReviewStatusV3(asV3((transition) => {
418
+ (((transition.collect as JsonObject).inputs as JsonObject[])[0]!).provider_task = { agent: "review-refuter", role: "refuter", prompt: "x" };
419
+ })), /not allowed|provider_task/);
420
+ assert.throws(() => decodeReviewStatusV3(asV3((transition) => {
421
+ (((transition.collect as JsonObject).inputs as JsonObject[])[0]!).submission = { operation_token: "finalize", argument_tokens: ["--correction-lines={{value}}"], value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 25, substitution_location: 6 } };
422
+ })), /submission|values/);
423
+ });
424
+
425
+ // --- consent/v3 — the negotiated v2.1+ consent question (adds `agent`) ---
426
+
427
+ test("the captured consent/v3 envelope decodes with its fixed agent binding", () => {
428
+ const consent = decodeReviewConsentV3(fixture("consent-v3.captured.json"));
429
+ assert.equal(consent.schema, "gentle-ai.review-integration.consent/v3");
430
+ assert.equal(consent.agent, "claude-code");
431
+ assert.equal(consent.action, "consent_required");
432
+ assert.equal(consent.blocking, true);
433
+ assert.equal(consent.riskLevel, "high");
434
+ assert.equal(consent.changedFiles, 2);
435
+ assert.equal(consent.changedLines, 12);
436
+ assert.equal(consent.choices[0].answer, "granted");
437
+ assert.equal(consent.choices[1].answer, "declined");
438
+ for (const choice of consent.choices) {
439
+ assert.ok(choice.invocation.includes(` --target ${consent.targetIdentity} `));
440
+ }
441
+ assert.equal(consent.offPath.command, "gentle-ai review mode disable");
442
+ });
443
+
444
+ test("consent identities never cross-decode", () => {
445
+ const v3 = fixture<JsonObject>("consent-v3.captured.json");
446
+ const v2 = v2Fixture<JsonObject>("consent.fixture.json");
447
+ // The old identity never accepts the new surface, and vice versa.
448
+ assert.throws(() => decodeReviewConsentV2(clone(v3)), /agent|schema/);
449
+ assert.throws(() => decodeReviewConsentV3(clone(v2)), /agent|schema/);
450
+ // A schema-swapped v3 body keeps its agent key, which the v2 identity
451
+ // still rejects as an unadvertised surface.
452
+ const downgraded = clone(v3);
453
+ downgraded.schema = "gentle-ai.review-integration.consent/v2";
454
+ assert.throws(() => decodeReviewConsentV2(downgraded), /agent/);
455
+ // A schema-swapped v2 body is missing the agent key v3 requires.
456
+ const upgraded = clone(v2);
457
+ upgraded.schema = "gentle-ai.review-integration.consent/v3";
458
+ assert.throws(() => decodeReviewConsentV3(upgraded), /agent/);
459
+ });
460
+
461
+ test("consent/v3 keeps every v2 semantic guard and pins its agent constant", () => {
462
+ const base = fixture<JsonObject>("consent-v3.captured.json");
463
+ const wrongAgent = clone(base);
464
+ wrongAgent.agent = "opencode";
465
+ assert.throws(() => decodeReviewConsentV3(wrongAgent), /agent/);
466
+ const swapped = clone(base);
467
+ swapped.choices = [...(swapped.choices as JsonObject[])].reverse();
468
+ assert.throws(() => decodeReviewConsentV3(swapped), /answer/);
469
+ const badInvocation = clone(base);
470
+ ((badInvocation.choices as JsonObject[])[0]!).invocation = "gentle-ai review finalize --consent granted";
471
+ assert.throws(() => decodeReviewConsentV3(badInvocation), /invocation/);
472
+ const differentTarget = clone(base);
473
+ differentTarget.target_identity = sha("d");
474
+ assert.throws(() => decodeReviewConsentV3(differentTarget), /target|invocation/);
475
+ const notBlocking = clone(base);
476
+ notBlocking.blocking = false;
477
+ assert.throws(() => decodeReviewConsentV3(notBlocking), /blocking/);
478
+ const extraKey = clone(base);
479
+ extraKey.unadvertised = true;
480
+ assert.throws(() => decodeReviewConsentV3(extraKey), /not allowed/);
481
+ });
482
+
483
+ test("the pinned consent/v2 fixture still decodes unchanged", () => {
484
+ const consent = decodeReviewConsentV2(v2Fixture("consent.fixture.json"));
485
+ assert.equal(consent.schema, "gentle-ai.review-integration.consent/v2");
486
+ assert.equal(consent.action, "consent_required");
487
+ });
488
+
489
+ // --- start/v3 — main extends repository_context with event_id/outcome ---
490
+
491
+ test("the captured granted start/v3 decodes with its repository context event binding", () => {
492
+ const start = decodeReviewStartV3(fixture("start-v3-consent-granted.captured.json"));
493
+ assert.equal(start.lineageId, "review-377c60e10b852cfc");
494
+ assert.equal(start.state, "reviewing");
495
+ assert.equal(start.riskLevel, "high");
496
+ assert.equal(start.selectedLenses.length, 4);
497
+ assert.equal(start.correctionBudget, 6);
498
+ assert.equal(start.repositoryContext?.outcome, "applied");
499
+ assert.match(start.repositoryContext?.eventId ?? "", /^sha256:[0-9a-f]{64}$/);
500
+ });
501
+
502
+ test("start/v3 repository context event fields stay optional and exact", () => {
503
+ // The pinned fixture carries neither field and still decodes unchanged.
504
+ const pinned = decodeReviewStartV3(v2Fixture("start.fixture.json"));
505
+ assert.equal(pinned.repositoryContext?.eventId, undefined);
506
+ assert.equal(pinned.repositoryContext?.outcome, undefined);
507
+ const base = fixture<JsonObject>("start-v3-consent-granted.captured.json");
508
+ const badEvent = clone(base);
509
+ (badEvent.repository_context as JsonObject).event_id = "not-a-digest";
510
+ assert.throws(() => decodeReviewStartV3(badEvent), /event_id/);
511
+ const badOutcome = clone(base);
512
+ (badOutcome.repository_context as JsonObject).outcome = "unheard-of";
513
+ assert.throws(() => decodeReviewStartV3(badOutcome), /outcome/);
514
+ const extraKey = clone(base);
515
+ (extraKey.repository_context as JsonObject).unadvertised = true;
516
+ assert.throws(() => decodeReviewStartV3(extraKey), /not allowed/);
517
+ });
518
+
519
+ // --- result-artifact/v2: the `review capture-result` admission envelope ---
520
+
521
+ test("the captured reference-form result artifact decodes with its rart1 locator", () => {
522
+ const artifact = decodeReviewResultArtifactV2(fixture("result-artifact-v2.captured.json"));
523
+ assert.equal(artifact.schema, "gentle-ai.review-result-artifact/v2");
524
+ assert.equal(artifact.capability, "review.native_result_artifact");
525
+ assert.match(artifact.reference ?? "", /^rart1_[0-9a-f]{64}$/);
526
+ assert.equal(artifact.path, undefined);
527
+ assert.equal(artifact.lineageId, "review-ceeb2b862bd39709");
528
+ assert.equal(artifact.lens, "review-reliability");
529
+ assert.equal(artifact.selectedOrder, 0);
530
+ assert.equal(artifact.admissionDecision, "completed");
531
+ assert.match(artifact.sha256, /^sha256:[0-9a-f]{64}$/);
532
+ assert.match(artifact.subjectHash, /^sha256:[0-9a-f]{64}$/);
533
+ assert.match(artifact.targetIdentity, /^sha256:[0-9a-f]{64}$/);
534
+ });
535
+
536
+ test("the captured path-form result artifact decodes with its provider-owned path", () => {
537
+ const artifact = decodeReviewResultArtifactV2(fixture("result-artifact-v2-path.captured.json"));
538
+ assert.equal(artifact.reference, undefined);
539
+ assert.match(artifact.path ?? "", /reviewer-results\/00-review-reliability\.json$/);
540
+ // Same admitted slot through both locator forms: every binding agrees.
541
+ const reference = decodeReviewResultArtifactV2(fixture("result-artifact-v2.captured.json"));
542
+ assert.equal(artifact.sha256, reference.sha256);
543
+ assert.equal(artifact.subjectHash, reference.subjectHash);
544
+ assert.equal(artifact.lineageId, reference.lineageId);
545
+ assert.equal(artifact.targetIdentity, reference.targetIdentity);
546
+ });
547
+
548
+ test("a result artifact carries exactly one locator", () => {
549
+ const base = fixture<JsonObject>("result-artifact-v2.captured.json");
550
+ const both = clone(base);
551
+ both.path = "/store/reviewer-results/00-review-reliability.json";
552
+ assert.throws(() => decodeReviewResultArtifactV2(both), /exactly one/);
553
+ const neither = clone(base);
554
+ delete neither.reference;
555
+ assert.throws(() => decodeReviewResultArtifactV2(neither), /exactly one/);
556
+ });
557
+
558
+ test("a result artifact rejects unknown keys and weakened bindings", () => {
559
+ const base = fixture<JsonObject>("result-artifact-v2.captured.json");
560
+ const extra = clone(base);
561
+ extra.unadvertised = true;
562
+ assert.throws(() => decodeReviewResultArtifactV2(extra), /not allowed/);
563
+ const wrongCapability = clone(base);
564
+ wrongCapability.capability = "review.result_artifact";
565
+ assert.throws(() => decodeReviewResultArtifactV2(wrongCapability), /capability/);
566
+ const unadmitted = clone(base);
567
+ unadmitted.admission_decision = "quarantined";
568
+ assert.throws(() => decodeReviewResultArtifactV2(unadmitted), /admission_decision/);
569
+ const foreignLocator = clone(base);
570
+ foreignLocator.reference = `rref1_${"b".repeat(64)}`;
571
+ assert.throws(() => decodeReviewResultArtifactV2(foreignLocator), /reference/);
572
+ const outOfRange = clone(base);
573
+ outOfRange.selected_order = 4;
574
+ assert.throws(() => decodeReviewResultArtifactV2(outOfRange), /selected_order/);
575
+ });
576
+
577
+ test("result artifact identities never cross-decode", () => {
578
+ // A prior identity rejects the new surface (it carries no negotiated
579
+ // contract/operation identity pair at all)...
580
+ assert.throws(() => decodeReviewOperationV2(fixture("result-artifact-v2.captured.json")), /contract|schema/);
581
+ // ...the new decoder pins its own exact identity...
582
+ const downgraded = clone(fixture<JsonObject>("result-artifact-v2.captured.json"));
583
+ downgraded.schema = "gentle-ai.review-result-artifact/v1";
584
+ assert.throws(() => decodeReviewResultArtifactV2(downgraded), /schema/);
585
+ // ...and rejects prior surfaces wholesale.
586
+ assert.throws(() => decodeReviewResultArtifactV2({
587
+ schema: "gentle-ai.review-provider-role-capture/v1",
588
+ lineage_id: "review-1d5aadacc600e167",
589
+ target_identity: `sha256:${"9".repeat(64)}`,
590
+ role: "targeted-validator",
591
+ captured: true,
592
+ }));
593
+ assert.throws(() => decodeReviewResultArtifactV2(v2Fixture("status.fixture.json")));
594
+ });
595
+
596
+ // Fixture provenance:
597
+ // - failure-v2-capture-evidence.captured.json was captured 2026-08-16 from a
598
+ // binary built at gentle-ai commit a2d57117 (branch
599
+ // fix/capture-evidence-typed-refusal, `go build ./cmd/gentle-ai`, version
600
+ // banner "gentle-ai dev"), in a scratch git repository driven to the
601
+ // correction evidence-pending state (medium start with consent granted,
602
+ // one deterministic BLOCKER capture-result, rendered finalize transition,
603
+ // rendered plan-correction submission, bounded fix edit), then a
604
+ // deliberately misbound capture:
605
+ // gentle-ai review capture-evidence --cwd <scratch> --lineage <lineage> \
606
+ // --target <live workspace identity> --expected-revision <revision> \
607
+ // --outcome passed --input <evidence file>
608
+ // The live workspace identity instead of the slot's fix-diff --target is
609
+ // the exact fambig misbinding shape; the branch answers it with the typed
610
+ // failure/v2 envelope on stdout (operation review.capture-evidence, code
611
+ // verification_evidence_binding_mismatch, mutation_outcome not_started)
612
+ // instead of a bare stderr line.
613
+ // - The other three capture-verb operations are constructed from the same
614
+ // captured envelope, grounded in that branch's
615
+ // contracts/review-integration/v2/schemas/failure.schema.json 12-value
616
+ // operation enum; no cheap live flow provokes them individually.
617
+ test("failure/v2 decodes the four collect-capture operations the typed-refusal branch emits", () => {
618
+ const captured = JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "failure-v2-capture-evidence.captured.json"), "utf8")) as Record<string, unknown>;
619
+ const decoded = decodeReviewFailureV2(captured);
620
+ assert.equal(decoded.operation, "review.capture-evidence");
621
+ assert.equal(decoded.code, "verification_evidence_binding_mismatch");
622
+ assert.equal(decoded.mutationOutcome, "not_started");
623
+ assert.equal(decoded.nextAction, "review.status");
624
+ for (const operation of ["review.capture-result", "review.capture-refuter", "review.capture-validation"]) {
625
+ assert.equal(decodeReviewFailureV2({ ...captured, operation }).operation, operation, operation);
626
+ }
627
+ // Cross-pinned rejection preserved: an operation outside the published
628
+ // 12-value enum never decodes.
629
+ assert.throws(() => decodeReviewFailureV2({ ...captured, operation: "review.capture-bogus" }), /failure\.operation/);
630
+ assert.throws(() => decodeReviewFailureV2({ ...captured, operation: "review.recover" }), /failure\.operation/);
631
+ });