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
@@ -6,14 +6,17 @@ import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, isAbsolute, join, posix, win32 } from "node:path";
8
8
  import { promisify } from "node:util";
9
- import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
9
+ import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
10
+ import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
10
11
  import {
11
12
  REVIEW_INTEGRATION_CONTRACT,
12
13
  decodeReviewCapabilitiesV2,
13
14
  decodeReviewConsentV2,
15
+ decodeReviewConsentV3,
14
16
  decodeReviewFailureV2,
15
17
  decodeReviewOperationV2,
16
18
  decodeReviewRepairV2,
19
+ decodeReviewResultArtifactV2,
17
20
  decodeReviewStartV3,
18
21
  decodeReviewStatusV3,
19
22
 
@@ -26,6 +29,23 @@ import {
26
29
 
27
30
  const execFileAsync = promisify(execFile);
28
31
 
32
+ // Negotiated review/status responses can carry a complete authority inventory.
33
+ // Keep the production default large enough for that payload while retaining a
34
+ // hard 64 MiB ceiling even when GENTLE_PI_REVIEW_MAX_BUFFER_BYTES is set.
35
+ export const NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
36
+ const NATIVE_REVIEW_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
37
+ const NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV = "GENTLE_PI_REVIEW_MAX_BUFFER_BYTES";
38
+ const NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT = "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.";
39
+
40
+ function resolveNativeReviewMaxBufferBytes(environment = process.env) {
41
+ const value = environment[NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV];
42
+ if (value === undefined || !/^[1-9]\d*$/.test(value)) return NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
43
+ const parsed = Number(value);
44
+ return Number.isSafeInteger(parsed) && parsed <= NATIVE_REVIEW_MAX_BUFFER_BYTES
45
+ ? parsed
46
+ : NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
47
+ }
48
+
29
49
  export const NATIVE_REVIEW_OPERATION = {
30
50
  VERSION: "version",
31
51
  START: "review/start",
@@ -44,6 +64,7 @@ export const NATIVE_REVIEW_OPERATION = {
44
64
  REPAIR: "review/repair",
45
65
  CAPTURE_EVIDENCE: "review/capture-evidence",
46
66
  CAPTURE_RESULT: "review/capture-result",
67
+ CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
47
68
  } ;
48
69
 
49
70
 
@@ -104,6 +125,22 @@ export const NATIVE_SDD_ARTIFACT_STATE = {
104
125
 
105
126
 
106
127
 
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
107
144
 
108
145
 
109
146
 
@@ -176,18 +213,24 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
176
213
 
177
214
  // Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
178
215
  // capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
179
- // against gentle-ai's headless notice (internal/cli/review_mode.go
180
- // reviewConsentSkippedNotice) written when the switch is on but no interactive
181
- // terminal answered the one-time consent question — which is always true when
182
- // Pi spawns gentle-ai without a TTY. Any other text still fails closed as
183
- // UNEXPECTED_STDERR.
184
- // Each entry is one whole line gentle-ai may write to the console stream while
185
- // still succeeding. The provenance line rides with the skip notice whenever the
186
- // resolved mode source is `default`, so a headless START legitimately emits two
187
- // lines; they are separate Fprintln calls, never one joined string.
216
+ // against gentle-ai's headless notices (internal/cli/review_mode.go
217
+ // reviewConsentSkippedNotice and its siblings) written when the switch is on
218
+ // but no interactive terminal answered the one-time consent question — which
219
+ // is always true when Pi spawns gentle-ai without a TTY. Any other text still
220
+ // fails closed as UNEXPECTED_STDERR.
221
+ //
222
+ // Each entry is one whole line the PINNED gentle-ai may write to the console
223
+ // stream while still succeeding. That last qualifier is what keeps this list
224
+ // honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
225
+ // on by default; this was never explicitly chosen. ..."), which used to ride
226
+ // with the skip notice whenever the resolved mode source was `default`. Under
227
+ // opt-in receipt-driven development a default-source clone is refused long
228
+ // before the consent ceremony runs, so the pinned binary can no longer emit
229
+ // that line and it is removed here rather than left as dead tolerance. Multi-
230
+ // line stderr is still expected in principle — these are separate Fprintln
231
+ // calls, never one joined string — which is why membership is per line.
188
232
  export const REVIEW_CONSENT_NOTICES = Object.freeze([
189
233
  "Gentle AI reviewed this change without asking, because this session has no terminal to answer on. Run 'gentle-ai review mode disable' to turn reviews off, or 'gentle-ai review mode status' to see the current setting.",
190
- "Reviews are on by default; this was never explicitly chosen. Run 'gentle-ai review mode enable' to make reviews an explicit choice, or 'gentle-ai review mode disable' to turn them off.",
191
234
  "Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
192
235
  "Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
193
236
  "Review skipped for this candidate at your request. It will be offered again on the next change.",
@@ -202,6 +245,22 @@ function stderrIsTolerated(stderr , tolerated )
202
245
  return lines.length > 0 && lines.every((line) => tolerated.includes(line));
203
246
  }
204
247
 
248
+ // gentle-ai main narrates the negotiated STATUS forecast head to a human on
249
+ // stderr while keeping the machine envelope on stdout (reviewNarrateForecast
250
+ // in internal/cli/review_narration.go, ground-truthed live against a main-line
251
+ // dev build). The pinned release does not emit it, so this narration is
252
+ // tolerated only for negotiated STATUS while the dev-binary override is
253
+ // configured; any other stderr keeps failing closed.
254
+ const FORECAST_NARRATION_LINES = Object.freeze([
255
+ /^Forecast horizon: (?:partial|terminal)$/,
256
+ /^step [0-9]+: (?:execute|collect|stop); reason_code=[a-z0-9_]+; description=.+$/,
257
+ /^Re-query STATUS after completing this partial head\.$/,
258
+ ]);
259
+ function stderrIsForecastNarration(stderr ) {
260
+ const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
261
+ return lines.length > 0 && lines.every((line) => FORECAST_NARRATION_LINES.some((pattern) => pattern.test(line)));
262
+ }
263
+
205
264
  export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] ;
206
265
 
207
266
 
@@ -286,6 +345,9 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
286
345
 
287
346
 
288
347
 
348
+
349
+
350
+
289
351
 
290
352
 
291
353
 
@@ -346,6 +408,54 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
346
408
 
347
409
 
348
410
 
411
+ // gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
412
+ // provider renders a SELF-CONTAINED authority-advancing vector
413
+ // (`review.capture-refuter` / `review.capture-validation` with binding tokens
414
+ // plus `--agent=pi --execute=true`, no submission descriptor); Pi executes the
415
+ // exact rendered invocation verbatim, in the foreground, and Go materializes
416
+ // the role prompt, spawns its own locked-down pi subprocess, and admits the
417
+ // raw verdict. Nothing here authors, parses, or transports role output.
418
+ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
419
+
420
+
421
+
422
+
423
+
424
+
425
+
426
+
427
+
428
+
429
+
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+
439
+
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+ // Field defect (fambig, 2026-08-16): the evidence collect slot renders the
449
+ // exact submission tokens native admits — fix-diff `--target`,
450
+ // `--expected-revision`, and an opaque cwd-independent `--repository-context` —
451
+ // with `{{outcome}}`/`{{input}}` substitution slots. Satisfying the slot means
452
+ // executing those tokens verbatim with only the two slot substitutions, the
453
+ // same discipline captureResult uses; identities are never reconstructed from
454
+ // top-level status fields.
455
+
456
+
457
+
458
+
349
459
 
350
460
 
351
461
 
@@ -394,6 +504,50 @@ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "dec
394
504
 
395
505
 
396
506
 
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+ // gentle-pi#311 P5: the provider-driven FINALIZE. `argumentTokens` are the
518
+ // exact rendered tokens of one provider-returned `review.finalize` execute
519
+ // transition (e.g. `--lineage=<id> --captured-results=true`), passed through
520
+ // verbatim and never synthesized or reordered by the host.
521
+
522
+
523
+
524
+
525
+
526
+
527
+ // The provider-rendered `finalize` submission descriptor forms (status/v5):
528
+ // the correction PLAN slot substitutes a positive line-count literal into its
529
+ // {{value}} token; the TARGETED VALIDATION slot substitutes a staged validator
530
+ // artifact path. The tokens are self-contained and execute verbatim.
531
+
532
+
533
+
534
+
535
+
536
+
537
+
538
+
539
+
540
+
541
+
542
+
543
+
544
+
545
+
546
+
547
+
548
+
549
+
550
+
397
551
 
398
552
 
399
553
 
@@ -667,6 +821,20 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
667
821
  // advertises capabilities/v1.5 and the negotiated start envelope is still
668
822
  // the closed `start/v2`, so riskEvidence and hint still cannot arrive.
669
823
  "2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
824
+ // Ground-truthed against the released v2.2.3 binary: the v2 lane remains
825
+ // protocol 2.0 with the same operation set and closed START fields consumed
826
+ // by Pi, so the existing capability columns are unchanged.
827
+ "2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
828
+ // Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
829
+ // capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
830
+ // existing decoders already read, and the START envelope Pi consumes is
831
+ // still `start/v3` carrying `risk_reasons` with no `risk_evidence` and no
832
+ // `hint`, so the existing capability columns are unchanged. v2.4.0 also
833
+ // made receipt-driven development opt-in, which changes what the mode
834
+ // envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
835
+ // while Pi stayed on 2.2.3; they were never pinned or probed, so they get
836
+ // no row.
837
+ "2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
670
838
  });
671
839
 
672
840
 
@@ -683,6 +851,23 @@ function resolvedNativeCliContract(version )
683
851
  return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS )[version];
684
852
  }
685
853
 
854
+ // The latest known contract row, used as the capability floor for the
855
+ // explicit dev-binary override (unpinned field-test mode): a maintainer
856
+ // dev build is at least as capable as the newest release Pi knows. The table
857
+ // is declared in ascending release order, so its last key is the latest.
858
+ const LATEST_NATIVE_CLI_CONTRACT_VERSION = Object.keys(NATIVE_CLI_CONTRACTS).at(-1) ;
859
+
860
+ // Dev-binary override version discipline: with the override configured, a
861
+ // version reported outside the pinned table resolves to the latest known row;
862
+ // with the override absent, behavior is byte-identical to the pinned gate.
863
+ function effectiveNativeCliContract(version ) {
864
+ const pinned = resolvedNativeCliContract(version);
865
+ if (pinned !== undefined) return pinned;
866
+ return gentleAiDevBinaryOverrideConfigured() ? NATIVE_CLI_CONTRACTS[LATEST_NATIVE_CLI_CONTRACT_VERSION] : undefined;
867
+ }
868
+
869
+
870
+
686
871
 
687
872
 
688
873
 
@@ -725,15 +910,25 @@ export class NativeReviewCliError extends Error {
725
910
  }
726
911
  }
727
912
 
913
+ // The one central runner for every gentle-ai CLI invocation the extension
914
+ // makes. It declares the Pi host relay handshake on each spawn: gentle-ai
915
+ // refuses pi admission pre-authority without it (gentle-pi#311 P4), and a
916
+ // single injection point keeps the declaration impossible to forget on any
917
+ // individual operation.
918
+ export function gentleAiProcessEnvironment(base = process.env) {
919
+ return { ...base, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
920
+ }
921
+
728
922
  export function createNodeExecFileAdapter() {
729
923
  return async (request) => {
730
924
  try {
731
- const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal });
925
+ const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal, env: gentleAiProcessEnvironment() });
732
926
  return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
733
927
  } catch (error) {
734
928
  const detail = error ;
735
929
  if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
736
- return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: detail.killed === true, outputLimitExceeded: detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" };
930
+ const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
931
+ return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && detail.killed === true, outputLimitExceeded };
737
932
  }
738
933
  };
739
934
  }
@@ -809,12 +1004,28 @@ function parseStructuredNativeDenial(stdout )
809
1004
  // Rebuild diagnostics from a duplicated module instance before facade output.
810
1005
  export function sanitizeForeignNativeReviewDiagnostics(value ) {
811
1006
  try {
812
- const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "stderr", "denial"]);
1007
+ const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr", "denial"]);
813
1008
  const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) ;
814
1009
  const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) ;
815
1010
  const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
816
- if (signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) return undefined;
817
- return { operation, error_code: errorCode, ...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }), ...(signal === undefined ? {} : { signal: signal }), timed_out: booleanValue(raw.timed_out), output_limit_exceeded: booleanValue(raw.output_limit_exceeded), ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }), ...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }) };
1011
+ const maxBufferBytes = raw.max_buffer_bytes === undefined ? undefined : positiveInteger(raw.max_buffer_bytes);
1012
+ const configurationHint = raw.configuration_hint === undefined ? undefined : stringValue(raw.configuration_hint);
1013
+ if (
1014
+ (signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) ||
1015
+ (maxBufferBytes === undefined) !== (configurationHint === undefined) ||
1016
+ (configurationHint !== undefined && (errorCode !== NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT || configurationHint !== NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT))
1017
+ ) return undefined;
1018
+ return {
1019
+ operation,
1020
+ error_code: errorCode,
1021
+ ...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }),
1022
+ ...(signal === undefined ? {} : { signal: signal }),
1023
+ timed_out: booleanValue(raw.timed_out),
1024
+ output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
1025
+ ...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
1026
+ ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
1027
+ ...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
1028
+ };
818
1029
  } catch { return undefined; }
819
1030
  }
820
1031
 
@@ -830,14 +1041,18 @@ function sanitizeForeignStructuredDenial(value )
830
1041
  return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
831
1042
  }
832
1043
 
833
- function nativeProcessDiagnostics(operation , code , result ) {
1044
+ function nativeProcessDiagnostics(operation , code , result , maxBufferBytes ) {
1045
+ const outputLimitExceeded = result?.outputLimitExceeded === true;
834
1046
  return {
835
1047
  operation,
836
1048
  error_code: code,
837
1049
  ...(result === undefined ? {} : { exit_code: result.exitCode }),
838
1050
  ...(result?.signal === null || result?.signal === undefined ? {} : { signal: result.signal }),
839
- timed_out: result?.timedOut === true,
840
- output_limit_exceeded: result?.outputLimitExceeded === true,
1051
+ timed_out: !outputLimitExceeded && result?.timedOut === true,
1052
+ output_limit_exceeded: outputLimitExceeded,
1053
+ ...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
1054
+ ? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
1055
+ : {}),
841
1056
  ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
842
1057
  ...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
843
1058
  };
@@ -1155,8 +1370,8 @@ function hasValidLensesRequired(action , state , riskLe
1155
1370
  return !lensesRequired;
1156
1371
  }
1157
1372
 
1158
- function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord ) {
1159
- return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result), auditRecord);
1373
+ function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord , maxBufferBytes ) {
1374
+ return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result, maxBufferBytes), auditRecord);
1160
1375
  }
1161
1376
 
1162
1377
 
@@ -1171,7 +1386,7 @@ export class NativeReviewCliV214 {
1171
1386
  timeoutMs ;
1172
1387
  maxBufferBytes ;
1173
1388
  cleanupDirectory ;
1174
- constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = 1024 * 1024, cleanupDirectory = (directory ) => rm(directory, { recursive: true, force: true })) {
1389
+ constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = resolveNativeReviewMaxBufferBytes(), cleanupDirectory = (directory ) => rm(directory, { recursive: true, force: true })) {
1175
1390
  if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
1176
1391
  this.adapter = adapter;
1177
1392
  this.executable = executable;
@@ -1208,8 +1423,8 @@ export class NativeReviewCliV214 {
1208
1423
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
1209
1424
  }
1210
1425
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1426
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1211
1427
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1212
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
1213
1428
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1214
1429
  const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
1215
1430
  const maintenancePartialFailure = [NATIVE_REVIEW_OPERATION.ABANDON, NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS].includes(operation) && result.exitCode !== 0;
@@ -1227,12 +1442,18 @@ export class NativeReviewCliV214 {
1227
1442
  if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
1228
1443
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
1229
1444
  }
1445
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1230
1446
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
1231
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result);
1232
1447
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
1233
1448
  if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
1234
- const version = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(result.stdout.replace(/\r\n$/, "\n"))?.[1];
1235
- const contract = version === undefined ? undefined : resolvedNativeCliContract(version);
1449
+ const normalized = result.stdout.replace(/\r\n$/, "\n");
1450
+ const pinnedVersion = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(normalized)?.[1];
1451
+ // Dev-binary override (unpinned field-test mode): accept the binary's
1452
+ // reported version even when it is not a pinned three-part release
1453
+ // version (a main-line dev build reports e.g. "2.4.0-rc.8+fix...").
1454
+ // The banner shape itself stays strict. Pinned mode is byte-identical.
1455
+ const version = pinnedVersion ?? (gentleAiDevBinaryOverrideConfigured() ? /^gentle-ai (\S+)\n$/.exec(normalized)?.[1] : undefined);
1456
+ const contract = version === undefined ? undefined : effectiveNativeCliContract(version);
1236
1457
  if (result.stderr.trim().length > 0 || contract === undefined || capabilities.some((capability) => !contract[capability])) throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `native gentle-ai lacks required capabilities: expected v${GENTLE_AI_VERSION}, found v${version ?? "unparseable"}`);
1237
1458
  return version ;
1238
1459
  }
@@ -1243,7 +1464,7 @@ export class NativeReviewCliV214 {
1243
1464
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1244
1465
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1245
1466
  const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
1246
- const toleratedStderr = resolvedNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
1467
+ const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
1247
1468
  const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", "--cwd", request.cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.policyPath ? ["--policy", request.policyPath] : []), ...(request.focus ? ["--focus", request.focus] : [])], true, request.signal, toleratedStderr);
1248
1469
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1249
1470
  // `target_identity` and `lens_bindings` are real, unconditionally-present
@@ -1501,7 +1722,11 @@ export class NativeReviewCliV214 {
1501
1722
  for (const [name, value] of [["lineage", request.lineage], ["expectedRevision", request.expectedRevision], ["snapshotIdentity", request.snapshotIdentity], ["actor", request.actor], ["reason", request.reason]] ) {
1502
1723
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
1503
1724
  }
1504
- if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, actor, and reason binding");
1725
+ if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
1726
+ for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] ) {
1727
+ if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1728
+ }
1729
+ if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1505
1730
  await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
1506
1731
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1507
1732
  "review", "abandon", "--cwd", request.cwd,
@@ -1595,14 +1820,17 @@ export class NativeReviewCliV214 {
1595
1820
  }
1596
1821
  }
1597
1822
 
1598
- export function nativeReviewAbandonAuthorization(request ) {
1823
+ export function nativeReviewAbandonAuthorization(request ) {
1599
1824
  return [
1600
- "gentle-ai.review-abandon-authorization/v1",
1825
+ "gentle-ai.review-abandon-authorization/v2",
1601
1826
  `lineage=${request.lineage}`,
1602
1827
  `revision=${request.expectedRevision}`,
1603
1828
  `snapshot_identity=${request.snapshotIdentity}`,
1604
- `actor=${request.actor}`,
1605
1829
  `reason=${request.reason}`,
1830
+ `captured_lens_results=${request.capturedLensResults.join(",")}`,
1831
+ `findings_present=${request.findingsPresent}`,
1832
+ `evidence_records_present=${request.evidenceRecordsPresent}`,
1833
+ `actor=${request.actor.trim()}`,
1606
1834
  ].join("\n");
1607
1835
  }
1608
1836
 
@@ -1619,6 +1847,32 @@ export function nativeReviewLegacyQuarantineAuthorization(request
1619
1847
  ].join("\n");
1620
1848
  }
1621
1849
 
1850
+ /**
1851
+ * The exact native `gentle-ai.review-recovery-authorization/v1` binding for one
1852
+ * recovery edge.
1853
+ *
1854
+ * Native `review recover` accepts a caller-supplied authorization only when it
1855
+ * reproduces this binding byte for byte, because it is copied verbatim into the
1856
+ * recovery provenance and read afterwards as a maintainer attestation. Pi
1857
+ * therefore derives it from freshly read native target status and never
1858
+ * forwards a caller-supplied one: a wrong binding is worse than an absent one,
1859
+ * since an absent field cannot lie about who approved what.
1860
+ *
1861
+ * `targetIdentity` is the live target identity the provider itself publishes in
1862
+ * the `review.recover` eligibility binding (`status.target_identity`), which is
1863
+ * the identity the successor's initial snapshot takes.
1864
+ */
1865
+ export function nativeReviewRecoverAuthorization(request ) {
1866
+ return [
1867
+ "gentle-ai.review-recovery-authorization/v1",
1868
+ `predecessor_lineage=${request.predecessorLineage}`,
1869
+ `predecessor_revision=${request.expectedPredecessorRevision}`,
1870
+ `target_identity=${request.targetIdentity}`,
1871
+ `actor=${request.actor.trim()}`,
1872
+ `reason=${request.reason.trim()}`,
1873
+ ].join("\n");
1874
+ }
1875
+
1622
1876
  export function nativeReviewReconcileAuthorization(request ) {
1623
1877
  return [
1624
1878
  "gentle-ai.review-reconcile-authorization/v1",
@@ -1659,15 +1913,16 @@ export class NativeReviewIntegrationError extends Error {
1659
1913
  }
1660
1914
  }
1661
1915
 
1662
- // Raised when negotiated START answers `consent/v2` (action:
1916
+ // Raised when negotiated START answers a consent question (`consent/v2` from
1917
+ // the pinned line, `consent/v3` from gentle-ai >= 2.3.0; action:
1663
1918
  // "consent_required") instead of `start/v3`. The provider has frozen no
1664
1919
  // authority yet: Pi must relay this complete candidate-scoped question and may
1665
1920
  // answer only through one of the exact invocations carried by the envelope.
1666
1921
  export class NativeReviewConsentRequiredError extends Error {
1667
- consent ;
1922
+ consent ;
1668
1923
  launchAttempted = true;
1669
1924
  mutationOutcome = "none";
1670
- constructor(consent ) {
1925
+ constructor(consent ) {
1671
1926
  super(consent.headline);
1672
1927
  this.name = "NativeReviewConsentRequiredError";
1673
1928
  this.consent = consent;
@@ -1751,6 +2006,29 @@ function exactConsentOption(arguments_ , name )
1751
2006
  return values[0] ;
1752
2007
  }
1753
2008
 
2009
+ function optionalConsentLineageOption(arguments_ ) {
2010
+ const values = [];
2011
+ for (let index = 0; index < arguments_.length; index += 1) {
2012
+ const token = arguments_[index] ;
2013
+ if (token === "--lineage") {
2014
+ const value = arguments_[index + 1];
2015
+ if (value === undefined || value.startsWith("--")) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is missing its value");
2016
+ values.push(value);
2017
+ index += 1;
2018
+ } else if (token.startsWith("--lineage=")) values.push(token.slice("--lineage=".length));
2019
+ }
2020
+ if (values.length > 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation permits at most one --lineage");
2021
+ if (values.length === 0) return undefined;
2022
+ const lineageId = values[0] ;
2023
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(lineageId)) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is malformed");
2024
+ return lineageId;
2025
+ }
2026
+
2027
+
2028
+
2029
+
2030
+
2031
+
1754
2032
  function consentInvocationArguments(request ) {
1755
2033
  const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
1756
2034
  if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
@@ -1761,8 +2039,9 @@ function consentInvocationArguments(request )
1761
2039
  if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1762
2040
  if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
1763
2041
  if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
2042
+ const lineageId = optionalConsentLineageOption(arguments_);
1764
2043
  if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
1765
- return arguments_;
2044
+ return { arguments_, ...(lineageId === undefined ? {} : { lineageId }) };
1766
2045
  }
1767
2046
 
1768
2047
  function decodeDeclinedConsentStart(value , expected ) {
@@ -1811,27 +2090,46 @@ function defaultExecutableDigest(path ) {
1811
2090
 
1812
2091
 
1813
2092
  function decodeNativeAdmittedResultManifest(value ) {
1814
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native capture-result manifest must be an object");
2093
+ // The admission answer routes through the exact-identity forward decoder
2094
+ // (decoder-freshness discipline): the complete live envelope — identity
2095
+ // constants, binding fields, and the exactly-one-locator rule — is
2096
+ // validated before anything is handed to FINALIZE, and an unknown field
2097
+ // grown by gentle-ai main is rejected instead of silently dropped.
2098
+ const artifact = decodeReviewResultArtifactV2(value);
2099
+ return Object.freeze({
2100
+ schema: artifact.schema,
2101
+ subjectHash: artifact.subjectHash,
2102
+ admissionDecision: artifact.admissionDecision,
2103
+ lens: artifact.lens,
2104
+ ...(artifact.path === undefined ? {} : { path: artifact.path }),
2105
+ ...(artifact.reference === undefined ? {} : { reference: artifact.reference }),
2106
+ });
2107
+ }
2108
+
2109
+ // gentle-pi#311 P4-roles: the strict acknowledgement for one executed
2110
+ // provider role vector. The immutable verdict bytes live in the Go-owned
2111
+ // compact store slot; this envelope only names the binding the capture
2112
+ // proved, so anything beyond that exact shape is refused.
2113
+ function decodeNativeProviderRoleCaptureArtifact(value ) {
2114
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native provider role capture artifact must be an object");
1815
2115
  const body = value ;
2116
+ const allowed = new Set(["schema", "lineage_id", "target_identity", "role", "captured"]);
2117
+ for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native provider role capture artifact carries unexpected key ${key}`);
1816
2118
  const text = (key ) => {
1817
2119
  const found = body[key];
1818
- if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native capture-result manifest ${key} must be a non-empty trimmed string`);
2120
+ if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native provider role capture artifact ${key} must be a non-empty trimmed string`);
1819
2121
  return found;
1820
2122
  };
1821
- const schema = text("schema");
1822
- if (schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`native capture-result manifest schema must be gentle-ai.review-result-artifact/v2, received ${schema}`);
1823
- const admission = text("admission_decision");
1824
- if (admission !== "completed") throw new TypeError(`native capture-result manifest admission_decision must be completed, received ${admission}`);
1825
- // Exactly one locator: a provider-owned path OR an opaque reference. Both or
1826
- // neither means the manifest cannot be handed to FINALIZE.
1827
- const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
1828
- if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
2123
+ if (text("schema") !== NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA) throw new TypeError(`native provider role capture artifact schema must be ${NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA}`);
2124
+ const role = text("role");
2125
+ if (role !== "refuter" && role !== "targeted-validator") throw new TypeError(`native provider role capture artifact role must be refuter or targeted-validator, received ${role}`);
2126
+ if (body.captured !== true) throw new TypeError("native provider role capture artifact must report captured: true");
1829
2127
  return Object.freeze({
1830
- schema,
1831
- subjectHash: text("subject_hash"),
1832
- admissionDecision: admission,
1833
- ...(body.lens === undefined ? {} : { lens: text("lens") }),
1834
- ...(hasPath ? { path: text("path") } : { reference: text("reference") }),
2128
+ schema: NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA,
2129
+ lineageId: text("lineage_id"),
2130
+ targetIdentity: text("target_identity"),
2131
+ role,
2132
+ captured: true,
1835
2133
  });
1836
2134
  }
1837
2135
 
@@ -1847,7 +2145,7 @@ export class NativeReviewCliV216 {
1847
2145
  adapter ,
1848
2146
  executable = resolveGentleAiBinary,
1849
2147
  timeoutMs = 30_000,
1850
- maxBufferBytes = 1024 * 1024,
2148
+ maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
1851
2149
  cleanupDirectory = (directory) => rm(directory, { recursive: true, force: true }),
1852
2150
  executableDigest = defaultExecutableDigest,
1853
2151
  ) {
@@ -1897,8 +2195,8 @@ export class NativeReviewCliV216 {
1897
2195
  if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
1898
2196
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
1899
2197
  }
2198
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1900
2199
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1901
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
1902
2200
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1903
2201
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1904
2202
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
@@ -1910,7 +2208,8 @@ export class NativeReviewCliV216 {
1910
2208
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
1911
2209
  }
1912
2210
  }
1913
- if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
2211
+ const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && gentleAiDevBinaryOverrideConfigured() && stderrIsForecastNarration(result.stderr);
2212
+ if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr) && !forecastNarrationTolerated) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1914
2213
  return { body, exitCode: result.exitCode };
1915
2214
  }
1916
2215
 
@@ -1939,7 +2238,12 @@ export class NativeReviewCliV216 {
1939
2238
  if (error instanceof NativeReviewCliError) throw error;
1940
2239
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}; the installed runtime is incompatible — reinstall gentle-pi`);
1941
2240
  }
1942
- if (capabilities.packageVersion !== GENTLE_AI_VERSION) {
2241
+ // Dev-binary override (unpinned field-test mode): the session accepts
2242
+ // the binary's self-reported package version instead of the exact pin.
2243
+ // The full capabilities decode above still applies unchanged, so an
2244
+ // incompatible envelope is refused either way. Pinned mode keeps the
2245
+ // exact equality byte-identical.
2246
+ if (capabilities.packageVersion !== GENTLE_AI_VERSION && !gentleAiDevBinaryOverrideConfigured()) {
1943
2247
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}, provider reported v${capabilities.packageVersion}`);
1944
2248
  }
1945
2249
  return capabilities;
@@ -1995,12 +2299,20 @@ export class NativeReviewCliV216 {
1995
2299
  ...(request.focus === undefined ? [] : ["--focus", request.focus]),
1996
2300
  "--consent", "relay",
1997
2301
  ], true, request.signal);
1998
- // A negotiated v2 START may answer `consent/v2` (action:
2302
+ // A negotiated v2 START may answer a consent question (action:
1999
2303
  // "consent_required") instead of `start/v3` when the provider needs an
2000
2304
  // explicit answer it cannot infer. Discriminate before decode and surface
2001
- // the complete envelope; only the caller can map a human answer.
2305
+ // the complete envelope; only the caller can map a human answer. The
2306
+ // body's own schema string selects the identity-exact decoder: the
2307
+ // pinned 2.2.x line emits consent/v2, gentle-ai >= 2.3.0 (capabilities
2308
+ // v2.1+) emits consent/v3, and any other identity fails closed inside
2309
+ // the v3 decoder's exact identity gate.
2002
2310
  if (execution.body.action === "consent_required") {
2003
- const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body));
2311
+ const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => (
2312
+ execution.body.schema === "gentle-ai.review-integration.consent/v2"
2313
+ ? decodeReviewConsentV2(execution.body)
2314
+ : decodeReviewConsentV3(execution.body)
2315
+ ));
2004
2316
  if (consent.targetIdentity !== targetIdentity || consent.projection !== projection) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent target binding mismatch");
2005
2317
  throw new NativeReviewConsentRequiredError(consent);
2006
2318
  }
@@ -2037,16 +2349,15 @@ export class NativeReviewCliV216 {
2037
2349
  }
2038
2350
 
2039
2351
  async answerConsent(request ) {
2040
- const arguments_ = consentInvocationArguments(request);
2041
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
2352
+ const invocation = consentInvocationArguments(request);
2353
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, invocation.arguments_, true, request.signal);
2042
2354
  if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
2043
2355
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
2044
2356
  }
2045
2357
  const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
2046
2358
  const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2047
2359
  if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
2048
- const lineageId = exactConsentOption(arguments_, "--lineage");
2049
- if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
2360
+ if (invocation.lineageId !== undefined && result.lineageId !== invocation.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
2050
2361
  return { kind: "started", start: {
2051
2362
  lineageId: result.lineageId,
2052
2363
  state: result.state ,
@@ -2177,6 +2488,7 @@ export class NativeReviewCliV216 {
2177
2488
  "--projection", request.projection ?? "workspace",
2178
2489
  ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
2179
2490
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2491
+ ...(request.agent === undefined ? [] : ["--agent", request.agent]),
2180
2492
  "--next-transition",
2181
2493
  ], false, request.signal);
2182
2494
  assertSupportedNextTransitionOperation(execution.body);
@@ -2243,6 +2555,118 @@ export class NativeReviewCliV216 {
2243
2555
  }
2244
2556
  }
2245
2557
 
2558
+ // gentle-pi#311 P4-roles: executes one provider-rendered self-contained
2559
+ // role capture vector exactly as rendered — one CLI invocation, verbatim
2560
+ // tokens in provider order, in the foreground. Go materializes the role
2561
+ // prompt, spawns its own locked-down pi subprocess, and admits the raw
2562
+ // verdict; Pi never adds, removes, or reorders a single token (not even
2563
+ // --cwd: the vector's --repository-context is authoritative and mutually
2564
+ // exclusive with a path). The central adapter injects the relay handshake
2565
+ // environment on this spawn like on every other gentle-ai invocation.
2566
+ async captureProviderRole(request ) {
2567
+ const verb = request.captureOperation.startsWith("review.") ? request.captureOperation.slice("review.".length) : "";
2568
+ if (verb !== "capture-refuter" && verb !== "capture-validation") throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
2569
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
2570
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_PROVIDER_ROLE argument tokens must all be non-empty strings");
2571
+ const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
2572
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
2573
+ "review", verb,
2574
+ ...request.argumentTokens,
2575
+ ], true, request.signal, executable.path);
2576
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => decodeNativeProviderRoleCaptureArtifact(execution.body));
2577
+ }
2578
+
2579
+ // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
2580
+ // execute transition exactly as rendered. The tokens come verbatim from
2581
+ // the negotiated next_transition (e.g. `--lineage=<id>
2582
+ // --captured-results=true`); Pi assembles no reviewer, refuter, or
2583
+ // validator documents for this lane — the provider discovers its own
2584
+ // admitted role and lens slots.
2585
+ async finalizeTransition(request ) {
2586
+ if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE transition requires the provider-rendered argument tokens");
2587
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE transition argument tokens must all be non-empty strings");
2588
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2589
+ "review", "finalize",
2590
+ ...request.argumentTokens,
2591
+ ], true, request.signal);
2592
+ return this.decodeFinalizeTransitionExecution(execution);
2593
+ }
2594
+
2595
+ // Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
2596
+ // correction PLAN and TARGETED VALIDATION collect slots render `finalize`
2597
+ // submission descriptors whose tokens are self-contained (--contract,
2598
+ // --lineage, --expected-revision, --target, --request-hash,
2599
+ // --repository-context) plus exactly one {{value}} slot. Executing anything
2600
+ // other than those rendered tokens fails the live emitter's committed-
2601
+ // intent reconciliation, so the tokens pass through verbatim with only the
2602
+ // {{value}} substitution: a literal for correction_lines, a staged 0o600
2603
+ // artifact path for a validation document.
2604
+ async finalizeSubmission(request ) {
2605
+ if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE submission requires the provider-rendered argument tokens");
2606
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE submission argument tokens must all be non-empty strings");
2607
+ if ((request.valueLiteral === undefined) === (request.valueDocument === undefined)) throw new TypeError("Native FINALIZE submission takes exactly one of valueLiteral or valueDocument");
2608
+ const valueToken = request.argumentTokens[request.valueSubstitutionLocation];
2609
+ if (valueToken === undefined || !valueToken.includes("{{value}}")) throw new TypeError("Native FINALIZE submission must render exactly one {{value}} slot token");
2610
+ if (request.valueDocument !== undefined && request.valueDocument.length === 0) throw new TypeError("Native FINALIZE submission value document must contain at least one byte");
2611
+ const directory = request.valueDocument === undefined ? undefined : await mkdtemp(join(tmpdir(), "gentle-ai-finalize-submission-"));
2612
+ try {
2613
+ let substituted ;
2614
+ if (directory !== undefined) {
2615
+ await chmod(directory, 0o700);
2616
+ const valueFile = join(directory, "value.json");
2617
+ await writeFile(valueFile, request.valueDocument , { encoding: "utf8", mode: 0o600 });
2618
+ await chmod(valueFile, 0o600);
2619
+ substituted = valueFile;
2620
+ } else {
2621
+ substituted = request.valueLiteral ;
2622
+ }
2623
+ const resolved = request.argumentTokens.map((token, index) => index === request.valueSubstitutionLocation ? token.replaceAll("{{value}}", substituted) : token);
2624
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2625
+ "review", "finalize",
2626
+ ...resolved,
2627
+ ], true, request.signal);
2628
+ return this.decodeFinalizeTransitionExecution(execution);
2629
+ } finally {
2630
+ if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
2631
+ }
2632
+ }
2633
+
2634
+ decodeFinalizeTransitionExecution(execution ) {
2635
+ return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
2636
+ const body = object(execution.body);
2637
+ // A transition rendered with `--contract` answers with the negotiated
2638
+ // operation envelope; one rendered without (the pi runtime form)
2639
+ // answers with the plain `review/finalize` shape. Both are decoded
2640
+ // strictly; nothing else is accepted.
2641
+ if (typeof body.schema === "string") {
2642
+ const envelope = decodeReviewOperationV2(body);
2643
+ if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
2644
+ const result = envelope.result;
2645
+ return {
2646
+ lineageId: requiredString(result.lineage_id),
2647
+ state: requiredString(result.state),
2648
+ action: requiredString(result.action),
2649
+ storeRevision: requiredString(result.store_revision),
2650
+ ...(result.validation_request === undefined ? {} : { validationRequest: result.validation_request }),
2651
+ ...(result.escalation === undefined ? {} : { escalation: requiredString(result.escalation) }),
2652
+ };
2653
+ }
2654
+ const plain = exactObject(body, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path", "validation_request", "escalation"]);
2655
+ if (plain.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
2656
+ const state = requiredString(plain.state);
2657
+ if (!(NATIVE_FINALIZE_STATE ).includes(state)) throw new Error("unknown finalize state");
2658
+ return {
2659
+ lineageId: requiredString(plain.lineage_id),
2660
+ state,
2661
+ action: requiredString(plain.action),
2662
+ storeRevision: requiredString(plain.store_revision),
2663
+ ...(plain.receipt_path === undefined ? {} : { receiptPath: requiredString(plain.receipt_path) }),
2664
+ ...(plain.validation_request === undefined ? {} : { validationRequest: plain.validation_request }),
2665
+ ...(plain.escalation === undefined ? {} : { escalation: requiredString(plain.escalation) }),
2666
+ };
2667
+ });
2668
+ }
2669
+
2246
2670
  async captureEvidence(request ) {
2247
2671
  if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2248
2672
  if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
@@ -2264,6 +2688,45 @@ export class NativeReviewCliV216 {
2264
2688
  }
2265
2689
  }
2266
2690
 
2691
+ // Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
2692
+ // the collect slot renders the identity native demands — for a correction
2693
+ // that is the fix-diff `--target`, not the live workspace snapshot — so the
2694
+ // slot's rendered submission tokens execute verbatim, with only the
2695
+ // {{outcome}} and {{input}} slots substituted. Same verbatim-token
2696
+ // discipline as captureResult; --repository-context is authoritative and
2697
+ // mutually exclusive with a path.
2698
+ async captureEvidenceSubmission(request ) {
2699
+ if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2700
+ if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2701
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE submission requires the provider-rendered argument tokens");
2702
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_EVIDENCE submission argument tokens must all be non-empty strings");
2703
+ const outcomeToken = request.argumentTokens[request.outcomeSubstitutionLocation];
2704
+ const inputToken = request.argumentTokens[request.inputSubstitutionLocation];
2705
+ if (request.outcomeSubstitutionLocation === request.inputSubstitutionLocation || outcomeToken === undefined || !outcomeToken.includes("{{outcome}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{outcome}} slot token");
2706
+ if (inputToken === undefined || !inputToken.includes("{{input}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{input}} slot token");
2707
+ const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
2708
+ if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_EVIDENCE submission takes a repository context or --cwd, never both");
2709
+ const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2710
+ try {
2711
+ await chmod(directory, 0o700);
2712
+ const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2713
+ const resolved = request.argumentTokens.map((token, index) =>
2714
+ index === request.outcomeSubstitutionLocation
2715
+ ? token.replaceAll("{{outcome}}", request.outcome)
2716
+ : index === request.inputSubstitutionLocation
2717
+ ? token.replaceAll("{{input}}", evidenceFile)
2718
+ : token);
2719
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd ?? process.cwd(), [
2720
+ "review", "capture-evidence",
2721
+ ...resolved,
2722
+ ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2723
+ ], true, request.signal);
2724
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2725
+ } finally {
2726
+ await this.cleanupDirectory(directory).catch(() => undefined);
2727
+ }
2728
+ }
2729
+
2267
2730
  reviewStatus(request ) {
2268
2731
  return this.legacy.reviewStatus(request);
2269
2732
  }