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