gentle-pi 2.2.0 → 2.4.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 (160) hide show
  1. package/README.md +76 -79
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -1,29 +1,33 @@
1
1
  import { execFile } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { readFileSync, statSync } from "node:fs";
4
2
  import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
5
3
  import { tmpdir } from "node:os";
6
4
  import { dirname, isAbsolute, join, posix, win32 } from "node:path";
7
5
  import { promisify } from "node:util";
8
- import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
6
+ import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
9
7
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
10
8
  import {
11
9
  REVIEW_INTEGRATION_CONTRACT,
12
- decodeReviewCapabilitiesV2,
10
+ decodeReviewAcknowledgedV1,
13
11
  decodeReviewConsentV2,
14
12
  decodeReviewConsentV3,
15
13
  decodeReviewFailureV2,
16
- decodeReviewOperationV2,
14
+ decodeReviewLastEventClosureV1,
17
15
  decodeReviewRepairV2,
18
16
  decodeReviewResultArtifactV2,
19
17
  decodeReviewStartV3,
18
+ decodeReviewStartV4,
20
19
  decodeReviewStatusV3,
21
- type ReviewCapabilitiesV2,
20
+ type ReviewAcknowledgedExpectationV1,
21
+ type ReviewAcknowledgedV1,
22
22
  type ReviewConsentEnvelope,
23
23
  type ReviewFailureV2,
24
24
  type ReviewRepairV2,
25
+ type ReviewNextTransitionV3,
25
26
  type ReviewStartState,
27
+ type ReviewStartV3,
28
+ type ReviewStartV4,
26
29
  type ReviewStatusV3,
30
+ type ReviewLastEventClosureV1,
27
31
  } from "./review-integration-v2.ts";
28
32
 
29
33
  const execFileAsync = promisify(execFile);
@@ -46,12 +50,7 @@ function resolveNativeReviewMaxBufferBytes(environment: NodeJS.ProcessEnv = proc
46
50
  }
47
51
 
48
52
  export const NATIVE_REVIEW_OPERATION = {
49
- VERSION: "version",
50
53
  START: "review/start",
51
- FINALIZE: "review/finalize",
52
- VALIDATE: "review/validate",
53
- BIND_SDD: "review/bind-sdd",
54
- SDD_STATUS: "sdd-status",
55
54
  STATUS: "review/status",
56
55
  RECLAIM: "review/reclaim",
57
56
  RECOVER: "review/recover",
@@ -61,9 +60,10 @@ export const NATIVE_REVIEW_OPERATION = {
61
60
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
62
61
  MODE: "review/mode",
63
62
  REPAIR: "review/repair",
64
- CAPTURE_EVIDENCE: "review/capture-evidence",
65
63
  CAPTURE_RESULT: "review/capture-result",
64
+ CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
66
65
  CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
66
+ ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
67
67
  } as const;
68
68
  export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
69
69
 
@@ -89,43 +89,9 @@ export interface ExecFileRequest { file: string; arguments: readonly string[]; c
89
89
  export interface ExecFileResult { stdout: string; stderr: string; exitCode: number; signal: NodeJS.Signals | null; timedOut: boolean; outputLimitExceeded: boolean; }
90
90
  export type ExecFileAdapter = (request: ExecFileRequest) => Promise<ExecFileResult>;
91
91
 
92
- export const NATIVE_SDD_ARTIFACT_STORE = {
93
- OPENSPEC: "openspec",
94
- ENGRAM: "engram",
95
- NONE: "none",
96
- } as const;
97
- export type NativeSddArtifactStore = (typeof NATIVE_SDD_ARTIFACT_STORE)[keyof typeof NATIVE_SDD_ARTIFACT_STORE];
98
-
99
- export const NATIVE_SDD_ARTIFACT_STATE = {
100
- MISSING: "missing",
101
- DONE: "done",
102
- PARTIAL: "partial",
103
- } as const;
104
- export type NativeSddArtifactState = (typeof NATIVE_SDD_ARTIFACT_STATE)[keyof typeof NATIVE_SDD_ARTIFACT_STATE];
105
-
106
- export interface NativeSddArtifactStates {
107
- proposal: NativeSddArtifactState;
108
- specs: NativeSddArtifactState;
109
- design: NativeSddArtifactState;
110
- tasks: NativeSddArtifactState;
111
- applyProgress: NativeSddArtifactState;
112
- verifyReport: NativeSddArtifactState;
113
- reviewPolicy?: NativeSddArtifactState;
114
- reviewLedger: NativeSddArtifactState;
115
- reviewReceipt: NativeSddArtifactState;
116
- reviewBundle: NativeSddArtifactState;
117
- reviewContext: NativeSddArtifactState;
118
- reviewState: NativeSddArtifactState;
119
- }
120
-
121
92
  export interface NativeReviewCli {
122
93
  start(request: NativeStartRequest): Promise<NativeStartResult>;
123
- finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult>;
124
- validate(request: NativeValidateRequest): Promise<NativeValidateResult>;
125
- bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult>;
126
- sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult>;
127
94
  reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult>;
128
- capabilities?(request?: NativeCapabilitiesRequest): Promise<ReviewCapabilitiesV2>;
129
95
  targetStatus?(request: NativeTargetStatusRequest): Promise<ReviewStatusV3>;
130
96
  answerConsent?(request: NativeReviewConsentAnswerRequest): Promise<NativeReviewConsentAnswerResult>;
131
97
  reclaim?(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult>;
@@ -134,31 +100,14 @@ export interface NativeReviewCli {
134
100
  quarantineLegacy?(request: NativeReviewLegacyQuarantineRequest): Promise<NativeReviewRecoveryResult>;
135
101
  reconcileAuthority?(request: NativeReviewReconcileAuthorityRequest): Promise<NativeReviewRecoveryResult>;
136
102
  repairLegacyAlias?(request: NativeReviewLegacyAliasRepairRequest): Promise<NativeReviewRecoveryResult>;
137
- // Net-new negotiated operations (contract v2 only): `review.repair` is
138
- // advertised in the v2 operation list; `capture-evidence` is the
139
- // evidence-first correction lifecycle's collection step.
140
103
  repair?(request: NativeReviewRepairRequest): Promise<ReviewRepairV2>;
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>;
104
+ captureResult?(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome>;
105
+ captureCorrectionPlan?(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1>;
106
+ captureProviderRole?(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome>;
158
107
  // Dark until a negotiated version reports the `mode` capability true
159
108
  // (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
160
109
  // outside the negotiated review-integration protocol — same shape as
161
- // reviewStatus/sddStatus/reclaim above.
110
+ // reviewStatus/reclaim above.
162
111
  reviewMode?(request: NativeReviewModeRequest): Promise<NativeReviewModeResult>;
163
112
  }
164
113
 
@@ -183,6 +132,12 @@ export const NATIVE_REVIEW_MODE_SOURCE = {
183
132
  } as const;
184
133
  export type NativeReviewModeSource = (typeof NATIVE_REVIEW_MODE_SOURCE)[keyof typeof NATIVE_REVIEW_MODE_SOURCE];
185
134
 
135
+ export const NATIVE_REVIEW_MODE_REACH = {
136
+ MACHINE: "machine",
137
+ THIS_BUILD: "this_build",
138
+ } as const;
139
+ export type NativeReviewModeReach = (typeof NATIVE_REVIEW_MODE_REACH)[keyof typeof NATIVE_REVIEW_MODE_REACH];
140
+
186
141
  export const NATIVE_REVIEW_MODE_SCOPE = {
187
142
  GLOBAL: "global",
188
143
  CLONE: "clone",
@@ -202,6 +157,7 @@ export interface NativeReviewModeStatus {
202
157
  effective: "on" | "off";
203
158
  source: NativeReviewModeSource;
204
159
  revision?: string;
160
+ reach?: NativeReviewModeReach;
205
161
  }
206
162
 
207
163
  export interface NativeReviewModeResult {
@@ -367,28 +323,12 @@ export interface NativeReviewRepairRequest {
367
323
  signal?: AbortSignal;
368
324
  }
369
325
 
370
- // `review capture-evidence` argv and response shape are pinned to a real
371
- // v2.2.2 review run (lineage review-b39d803b68a90767): exactly
372
- // --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
373
- // --contract, and the `gentle-ai.review-verification-evidence/v2` record
374
- // returned directly (not wrapped in an operation/v2 envelope).
375
- export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] as const;
376
- export type NativeReviewCaptureOutcome = (typeof NATIVE_REVIEW_CAPTURE_OUTCOME)[number];
377
-
378
326
  // `capture-result` is an additive headless command, NOT a negotiated
379
327
  // repository operation: it accepts no --contract, and the provider's own
380
328
  // transition tokens already carry the repository context -- it takes that or
381
329
  // --cwd, never both. So Pi passes the tokens through verbatim and adds only
382
330
  // --input. Reconstructing them would mean re-deriving a lineage, revision,
383
331
  // target, lens slot, and subject hash the provider already issued.
384
- // Added for the v2 finalize transport. `capturedResults` asks the provider to
385
- // discover every manifest it already admitted; `resultArtifactFiles` hands them
386
- // over explicitly in lens order. Both replace the retired `--result`.
387
- export interface NativeReviewFinalizeCapturedResults {
388
- readonly capturedResults?: boolean;
389
- readonly resultArtifactFiles?: readonly string[];
390
- }
391
-
392
332
  export interface NativeReviewCaptureResultRequest {
393
333
  readonly argumentTokens: readonly string[];
394
334
  readonly resultDocument: string;
@@ -407,6 +347,33 @@ export interface NativeReviewAdmittedResultManifest {
407
347
  readonly reference?: string;
408
348
  }
409
349
 
350
+ /** A non-final reviewer capture acknowledges an admitted artifact; final captures close natively. */
351
+ export type NativeReviewCaptureResultOutcome = NativeReviewAdmittedResultManifest | ReviewLastEventClosureV1;
352
+
353
+ // The one continuation that burns approved authority. Its tokens are rendered
354
+ // by the provider in a closed order and are relayed verbatim: Pi never builds,
355
+ // reorders, or substitutes one, because a synthesized acknowledgement would be
356
+ // Pi deciding that a review is over. `binding` is the lineage, target, and
357
+ // revision the caller already holds from STATUS: when the provider answers the
358
+ // burn with a review-acknowledged/v1 envelope (gentle-ai #3947), that envelope
359
+ // must name exactly this burn.
360
+ export interface NativeReviewAcknowledgeApprovedRequest {
361
+ readonly argumentTokens: readonly string[];
362
+ readonly cwd: string;
363
+ readonly binding?: ReviewAcknowledgedExpectationV1;
364
+ readonly signal?: AbortSignal;
365
+ }
366
+
367
+ /** `undefined` is the pinned silent burn (every release up to v2.5.0-rc.3); an envelope is the #3947 typed burn. */
368
+ export type NativeReviewAcknowledgeApprovedOutcome = ReviewAcknowledgedV1 | undefined;
369
+
370
+ export interface NativeReviewCorrectionPlanCaptureRequest {
371
+ readonly argumentTokens: readonly string[];
372
+ readonly correctionLines: number;
373
+ readonly cwd: string;
374
+ readonly signal?: AbortSignal;
375
+ }
376
+
410
377
  // gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
411
378
  // provider renders a SELF-CONTAINED authority-advancing vector
412
379
  // (`review.capture-refuter` / `review.capture-validation` with binding tokens
@@ -416,6 +383,19 @@ export interface NativeReviewAdmittedResultManifest {
416
383
  // raw verdict. Nothing here authors, parses, or transports role output.
417
384
  export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
418
385
 
386
+ // Capture operations and their terminal closure operations are independently
387
+ // named upstream. Keep this closed mapping explicit: capture-validation is the
388
+ // intentionally nonuniform `review/capture-validation` closure operation.
389
+ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION = {
390
+ "review.capture-refuter": "review.capture-refuter",
391
+ "review.capture-validation": "review/capture-validation",
392
+ } as const;
393
+ type NativeReviewProviderRoleCaptureOperation = keyof typeof NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION;
394
+
395
+ function isNativeReviewProviderRoleCaptureOperation(operation: string): operation is NativeReviewProviderRoleCaptureOperation {
396
+ return Object.hasOwn(NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION, operation);
397
+ }
398
+
419
399
  export interface NativeReviewProviderRoleCaptureRequest {
420
400
  /** The provider-named capture operation, e.g. `review.capture-refuter`. */
421
401
  readonly captureOperation: string;
@@ -434,54 +414,37 @@ export interface NativeReviewProviderRoleCaptureArtifact {
434
414
  readonly captured: true;
435
415
  }
436
416
 
437
- export interface NativeReviewCaptureEvidenceRequest {
438
- cwd: string;
439
- lineageId: string;
440
- targetIdentity: string;
441
- expectedRevision: string;
442
- outcome: NativeReviewCaptureOutcome;
443
- evidenceDocument: string;
444
- signal?: AbortSignal;
445
- }
417
+ /** Refuter and validator captures close only when they are the native last event. */
418
+ export type NativeReviewProviderRoleCaptureOutcome = NativeReviewProviderRoleCaptureArtifact | ReviewLastEventClosureV1;
446
419
 
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
- }
420
+ export const NATIVE_UNTRACKED_SCOPE = {
421
+ EXCLUDE: "exclude",
422
+ SELECT: "select",
423
+ } as const;
424
+ export type NativeUntrackedScope = (typeof NATIVE_UNTRACKED_SCOPE)[keyof typeof NATIVE_UNTRACKED_SCOPE];
467
425
 
468
- export interface NativeReviewVerificationEvidenceV2 {
469
- schema: "gentle-ai.review-verification-evidence/v2";
470
- version: 2;
471
- lineageId: string;
472
- authorityRevision: string;
473
- targetIdentity: string;
474
- candidateTree: string;
475
- pathsDigest: string;
476
- paths: readonly string[];
477
- ledgerIds: readonly string[];
478
- rawPayloadSha256: string;
479
- rawPayloadBytes: number;
480
- outcome: NativeReviewCaptureOutcome;
481
- recordDigest: string;
426
+ export interface NativeIntendedUntrackedSelectionSubmission {
427
+ readonly argumentTokens: readonly string[];
428
+ readonly value: string;
429
+ }
430
+ interface NativeUntrackedSelectionRequest {
431
+ untrackedScope?: NativeUntrackedScope;
432
+ expectedUntrackedInventory?: string;
433
+ intendedUntracked?: readonly string[];
434
+ intendedUntrackedSelection?: NativeIntendedUntrackedSelectionSubmission;
482
435
  }
483
436
 
484
- export interface NativeStartRequest { cwd: string; baseRef?: string; committedOnly?: boolean; lineageId?: string; policyPath?: string; focus?: string; targetIdentity?: string; projection?: "workspace" | "staged"; signal?: AbortSignal; }
437
+ export interface NativeStartRequest extends NativeUntrackedSelectionRequest {
438
+ cwd: string;
439
+ baseRef?: string;
440
+ committedOnly?: boolean;
441
+ lineageId?: string;
442
+ policyPath?: string;
443
+ focus?: string;
444
+ targetIdentity?: string;
445
+ projection?: "workspace" | "staged";
446
+ signal?: AbortSignal;
447
+ }
485
448
  export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } as const;
486
449
  export type NativeReviewConsentAnswer = (typeof NATIVE_REVIEW_CONSENT_ANSWER)[keyof typeof NATIVE_REVIEW_CONSENT_ANSWER];
487
450
  export interface NativeReviewConsentAnswerRequest { cwd: string; consent: ReviewConsentEnvelope; answer: NativeReviewConsentAnswer; signal?: AbortSignal; }
@@ -497,59 +460,12 @@ export interface NativeReviewConsentDeclinedResult {
497
460
  }
498
461
  export interface NativeReviewConsentStartedResult { kind: "started"; start: NativeStartResult; }
499
462
  export type NativeReviewConsentAnswerResult = NativeReviewConsentStartedResult | NativeReviewConsentDeclinedResult;
500
- export interface NativeFinalizeLensResult { lens: string; document: unknown; }
501
- export interface NativeFinalizeRequest extends NativeReviewFinalizeCapturedResults {
502
- cwd: string;
503
- lineageId?: string;
504
- resultFiles?: readonly string[];
505
- lensResults?: readonly NativeFinalizeLensResult[];
506
- refuterFile?: string;
507
- refuterDocument?: unknown;
508
- correctionLines?: number;
509
- validationFile?: string;
510
- validationDocument?: unknown;
511
- evidenceFile?: string;
512
- evidenceDocument?: string;
513
- failed?: boolean;
514
- signal?: AbortSignal;
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
-
544
- export interface NativeValidateRequest { cwd: string; gate: string; lineageId?: string; flags?: readonly string[]; signal?: AbortSignal; }
545
- export interface NativeBindSddRequest { cwd: string; change: string; lineage: string; expectedBindingRevision: string; signal?: AbortSignal; }
546
- export interface NativeSddStatusRequest { cwd: string; change: string; signal?: AbortSignal; }
547
463
  export interface NativeReviewStatusRequest { cwd: string; signal?: AbortSignal; }
548
- export interface NativeCapabilitiesRequest { cwd?: string; signal?: AbortSignal; }
549
- export interface NativeTargetStatusRequest {
464
+ export interface NativeTargetStatusRequest extends NativeUntrackedSelectionRequest {
550
465
  cwd: string;
551
466
  lineageId?: string;
552
467
  baseRef?: string;
468
+ committedOnly?: boolean;
553
469
  projection?: "workspace" | "staged";
554
470
  /**
555
471
  * The immutable reviewer runtime this host provides. Measured against the
@@ -563,8 +479,6 @@ export interface NativeTargetStatusRequest {
563
479
  agent?: "pi";
564
480
  signal?: AbortSignal;
565
481
  }
566
- export interface NativeGateContext { lineageId: string; storeRevision: string; raw: Record<string, unknown>; }
567
-
568
482
  export const NATIVE_REVIEW_AUTHORITY_STATUS = {
569
483
  CLEAN: "clean",
570
484
  ACTIVE: "active",
@@ -661,29 +575,60 @@ export interface NativeReviewStatusResult {
661
575
  diagnostics: readonly NativeReviewAuthorityDiagnostic[];
662
576
  raw: Record<string, unknown>;
663
577
  }
664
- export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } as const;
578
+ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REPLAYED: "replayed", CLOSED: "closed", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } as const;
665
579
  export type NativeStartAction = (typeof NATIVE_START_ACTION)[keyof typeof NATIVE_START_ACTION];
666
- export interface NativeStartResult { lineageId: string; state: ReviewStartState; riskLevel: string; selectedLenses: readonly string[]; changedFiles: number; changedLines: number; correctionBudget: number; action: NativeStartAction; lensesRequired: boolean; riskReasons?: readonly Record<string, unknown>[]; raw?: Readonly<Record<string, unknown>>; riskEvidence?: readonly string[]; hint?: string; }
667
- export interface NativeValidateResult { allowed: boolean; result: "allow" | "scope-changed" | "invalidated" | "escalated"; action: string; reason: string; gateContext: NativeGateContext; delivery?: "disabled/unmanaged"; }
668
- export interface NativeFinalizeResult { lineageId: string; state: string; action: string; storeRevision: string; receiptPath?: string; validationRequest?: Readonly<Record<string, unknown>>; escalation?: string; }
669
- export interface NativeBindSddResult {
670
- revision: string;
671
- change: string;
672
- lineage: string;
673
- authorityRevision: string;
674
- receiptHash: string;
675
- gateContext: NativeGateContext;
580
+ export interface NativeStartResult { lineageId: string; state: ReviewStartState; riskLevel: string; selectedLenses: readonly string[]; changedFiles: number; changedLines: number; correctionBudget: number; action: NativeStartAction; lensesRequired: boolean; riskReasons?: readonly Record<string, unknown>[]; nextTransition?: ReviewNextTransitionV3; raw?: Readonly<Record<string, unknown>>; riskEvidence?: readonly string[]; hint?: string; }
581
+ export function isCanonicalProcessString(value: unknown): value is string {
582
+ return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
676
583
  }
677
- export interface NativeSddStatusResult {
678
- ready: boolean;
679
- artifactStore: NativeSddArtifactStore;
680
- artifacts: NativeSddArtifactStates;
681
- nextRecommended: string;
682
- [key: string]: unknown;
584
+
585
+ interface NativeUntrackedSelection {
586
+ untrackedScope?: NativeUntrackedScope;
587
+ expectedUntrackedInventory?: string;
588
+ intendedUntracked?: readonly string[];
683
589
  }
684
590
 
685
- export function isCanonicalProcessString(value: unknown): value is string {
686
- return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
591
+ function isNativeUntrackedPath(value: unknown): value is string {
592
+ return isCanonicalProcessString(value)
593
+ && !posix.isAbsolute(value)
594
+ && !win32.isAbsolute(value)
595
+ && !value.includes("\\")
596
+ && value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
597
+ }
598
+
599
+ function nativeUntrackedSelection(request: NativeUntrackedSelectionRequest): NativeUntrackedSelection {
600
+ const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
601
+ const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
602
+ if (!declared) return {};
603
+ if (
604
+ (untrackedScope !== NATIVE_UNTRACKED_SCOPE.EXCLUDE && untrackedScope !== NATIVE_UNTRACKED_SCOPE.SELECT) ||
605
+ !isCanonicalProcessString(expectedUntrackedInventory) ||
606
+ (intendedUntracked !== undefined && (!Array.isArray(intendedUntracked) || intendedUntracked.some((path) => !isNativeUntrackedPath(path) || intendedUntracked.indexOf(path) !== intendedUntracked.lastIndexOf(path))))
607
+ ) {
608
+ throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
609
+ }
610
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (intendedUntracked?.length ?? 0) > 0) {
611
+ throw new TypeError("Native exclude untracked selection cannot include paths");
612
+ }
613
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (intendedUntracked?.length ?? 0) === 0) {
614
+ throw new TypeError("Native select untracked selection requires at least one path");
615
+ }
616
+ return {
617
+ untrackedScope,
618
+ expectedUntrackedInventory,
619
+ intendedUntracked: intendedUntracked === undefined ? undefined : [...intendedUntracked],
620
+ };
621
+ }
622
+
623
+ function nativeUntrackedSelectionArguments(selection: NativeUntrackedSelection): readonly string[] {
624
+ if (selection.untrackedScope === undefined) return [];
625
+ return [
626
+ `--untracked-scope=${selection.untrackedScope}`,
627
+ `--expected-untracked-inventory=${selection.expectedUntrackedInventory!}`,
628
+ ...(selection.untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT
629
+ ? selection.intendedUntracked!.map((path) => `--intended-untracked=${path}`)
630
+ : []),
631
+ ];
687
632
  }
688
633
 
689
634
  const NATIVE_RISK_LEVEL = ["low", "medium", "high"] as const;
@@ -764,28 +709,21 @@ export function nativeRiskEvidencePhrases(riskLevel: string, reasons: readonly N
764
709
  return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
765
710
  }
766
711
  const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
767
- const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] as const;
768
712
  const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
769
- const NATIVE_GATE_RESULT = ["allow", "scope-changed", "invalidated", "escalated"] as const;
770
- const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] as const;
771
- const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
772
- const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] as const;
773
-
774
- // Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
775
- // every shipped row, including "2.1.11". PI-2 adds the first capability-true
776
- // row (and bumps the triple pin) in one dedicated commit — never one without
777
- // the other. See Design Decision #1 (organic-rdd-parity).
713
+ // The pin table is intentionally preserved unchanged while #3587 is
714
+ // unpublished. Last-event capture support is exercised through the explicit
715
+ // dev-binary override; no source-level pin or contract-row mutation is allowed.
778
716
  const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } as const;
779
717
 
780
718
  export const NATIVE_CLI_CONTRACTS = Object.freeze({
781
- "2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: false, inventory: false, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
782
- "2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
783
- "2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
784
- "2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
785
- "2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: false, quarantineLegacy: false, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
786
- "2.1.9": 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: false, ...ORGANIC_PARITY_DARK }),
787
- "2.1.10": 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, ...ORGANIC_PARITY_DARK }),
788
- "2.1.11": 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, ...ORGANIC_PARITY_DARK }),
719
+ "2.1.4": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: false, inventory: false, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
720
+ "2.1.5": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
721
+ "2.1.6": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
722
+ "2.1.7": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: false, recover: false, abandon: false, quarantineLegacy: false, reconcileAuthority: false, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
723
+ "2.1.8": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: false, quarantineLegacy: false, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
724
+ "2.1.9": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: false, ...ORGANIC_PARITY_DARK }),
725
+ "2.1.10": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, ...ORGANIC_PARITY_DARK }),
726
+ "2.1.11": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, ...ORGANIC_PARITY_DARK }),
789
727
  // First capability-true row, paired with the triple pin bump in this same
790
728
  // commit as Design Decision #1 requires.
791
729
  //
@@ -806,7 +744,7 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
806
744
  // needs the negotiated start envelope extended upstream, which moves a
807
745
  // byte-pinned fixture and therefore belongs to a gentle-ai release, not to
808
746
  // a Pi capability flip.
809
- "2.2.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 }),
747
+ "2.2.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
810
748
  // 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
811
749
  // v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
812
750
  // review-integration/v1, but the negotiated start envelope is still the
@@ -814,16 +752,16 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
814
752
  // they are dark on 2.2.0. The release does publish a second contract,
815
753
  // review-integration/v2, whose `start/v3` carries base/candidate trees --
816
754
  // but Pi does not negotiate it yet, and a row must describe the lane in use.
817
- "2.2.1": 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 }),
755
+ "2.2.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
818
756
  // 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
819
757
  // v2.2.2 binary rather than assumed: on review-integration/v1 it still
820
758
  // advertises capabilities/v1.5 and the negotiated start envelope is still
821
759
  // the closed `start/v2`, so riskEvidence and hint still cannot arrive.
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 }),
760
+ "2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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
761
  // Ground-truthed against the released v2.2.3 binary: the v2 lane remains
824
762
  // protocol 2.0 with the same operation set and closed START fields consumed
825
763
  // 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 }),
764
+ "2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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
765
  // Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
828
766
  // capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
829
767
  // existing decoders already read, and the START envelope Pi consumes is
@@ -833,45 +771,40 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
833
771
  // envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
834
772
  // while Pi stayed on 2.2.3; they were never pinned or probed, so they get
835
773
  // 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 }),
774
+ "2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
775
+ // Ground-truthed by driving the exact v2.5.0-rc.3 tagged build through the
776
+ // gentle-ai-bench driven journey corpus (exit 0), which exercises the
777
+ // start/status/capture/validate/mode/delivery lifecycles Pi consumes. The
778
+ // v2 lane advertises capabilities/v2.3 and its reviewing START is the
779
+ // `start/v4` continuation envelope that #499 already decodes; the closed
780
+ // fields Pi consumes are unchanged, so the columns match the 2.4.0 row.
781
+ // riskEvidence and hint stay dark: still not proven to reach the
782
+ // negotiated path Pi reads.
783
+ "2.5.0-rc.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
784
+ // Ground-truthed against the published v2.5.0 binary installed from its
785
+ // signed archive: `review capabilities` on the v2 lane advertises
786
+ // capabilities/v2.4 (protocol minor 4) and answers status/v6, consent/v3,
787
+ // and the `start/v4` continuation, all of which the decoders already read.
788
+ // The closed fields Pi consumes did not change between rc.3 and stable, so
789
+ // the columns match the 2.5.0-rc.3 row. riskEvidence and hint stay dark:
790
+ // still not proven to reach the negotiated START path Pi reads.
791
+ "2.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
792
+ // Ground-truthed against the published v2.6.0 binary installed from its
793
+ // signed archive: `review capabilities` on the v2 lane advertises
794
+ // capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
795
+ // and the `start/v4` continuation, all of which the decoders already read.
796
+ // `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
797
+ // and `review validate --gate pre-commit` still answers
798
+ // `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
799
+ // against the binary. Declining the consent/v3 prompt during `review start`
800
+ // still returns `risk_evidence` only on that blocking envelope, never on
801
+ // the negotiated `start/v4` continuation, so riskEvidence and hint stay
802
+ // dark for the same reason they have on every row since 2.2.0: still not
803
+ // proven to reach the negotiated START path Pi reads. The closed fields Pi
804
+ // consumes did not change between 2.5.0 and 2.6.0, so the columns match
805
+ // the 2.5.0 row.
806
+ "2.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: 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 }),
837
807
  });
838
- type NativeCliCapability = keyof (typeof NATIVE_CLI_CONTRACTS)[keyof typeof NATIVE_CLI_CONTRACTS];
839
-
840
- // Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
841
- // delivery decode paths under a synthetic version key without ever making a
842
- // shipped row (including "2.1.11") capability-true. Production code never
843
- // calls the setter, so this is inert outside test files.
844
- const nativeCliContractsTestingOverlay = new Map<string, Record<NativeCliCapability, boolean>>();
845
- export function setNativeCliContractForTesting(version: string, contract: Record<NativeCliCapability, boolean> | undefined): void {
846
- if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
847
- else nativeCliContractsTestingOverlay.set(version, contract);
848
- }
849
- function resolvedNativeCliContract(version: string): Record<NativeCliCapability, boolean> | undefined {
850
- return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS as Record<string, Record<NativeCliCapability, boolean> | undefined>)[version];
851
- }
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
-
868
- export interface NativeReviewStructuredDenial {
869
- schema: "gentle-ai.review-gate-result/v1";
870
- result: "scope-changed" | "invalidated" | "escalated";
871
- action: string;
872
- reason: string;
873
- denial?: { stage: string; code: string };
874
- }
875
808
 
876
809
  export interface NativeReviewProcessDiagnostics {
877
810
  operation: NativeReviewOperation;
@@ -883,7 +816,6 @@ export interface NativeReviewProcessDiagnostics {
883
816
  max_buffer_bytes?: number;
884
817
  configuration_hint?: string;
885
818
  stderr?: string;
886
- denial?: NativeReviewStructuredDenial;
887
819
  }
888
820
 
889
821
  export class NativeReviewCliError extends Error {
@@ -955,7 +887,6 @@ function decodeSelectedLenses(value: unknown, riskLevel: string, lensesRequired:
955
887
  }
956
888
  function enumString(value: unknown, allowed: readonly string[]): string { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
957
889
  const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
958
- const NATIVE_REVIEW_DENIAL_TEXT_LIMIT = 1_024;
959
890
 
960
891
  function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT): string {
961
892
  const normalized = value
@@ -970,40 +901,10 @@ function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_T
970
901
  return normalized.length <= limit ? normalized : `${normalized.slice(0, limit - 14)}…[truncated]`;
971
902
  }
972
903
 
973
- function parseStructuredNativeDenial(stdout: string): NativeReviewStructuredDenial | undefined {
974
- if (Buffer.byteLength(stdout, "utf8") > NATIVE_DIAGNOSTIC_TEXT_LIMIT * 4) return undefined;
975
- try {
976
- const value = exactObject(JSON.parse(stdout), ["schema", "result", "allowed", "action", "reason", "context"]);
977
- const result = enumString(value.result, ["scope-changed", "invalidated", "escalated"] as const) as NativeReviewStructuredDenial["result"];
978
- const action = sanitizeNativeDiagnosticText(requiredString(value.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
979
- const reason = sanitizeNativeDiagnosticText(requiredString(value.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
980
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
981
- if (
982
- value.schema !== "gentle-ai.review-gate-result/v1" ||
983
- value.allowed !== false ||
984
- action !== expectedAction ||
985
- !isCanonicalProcessString(action) ||
986
- !isCanonicalProcessString(reason)
987
- ) return undefined;
988
- const context = decodeGateContext(value.context).raw;
989
- const rawDenial = context.denial;
990
- const denial = rawDenial === undefined
991
- ? undefined
992
- : (() => {
993
- const parsed = exactObject(rawDenial, ["stage", "code"]);
994
- const stage = sanitizeNativeDiagnosticText(requiredString(parsed.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
995
- const code = sanitizeNativeDiagnosticText(requiredString(parsed.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
996
- if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence");
997
- return { stage, code };
998
- })();
999
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
1000
- } catch { return undefined; }
1001
- }
1002
-
1003
904
  // Rebuild diagnostics from a duplicated module instance before facade output.
1004
905
  export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeReviewProcessDiagnostics | undefined {
1005
906
  try {
1006
- const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr", "denial"]);
907
+ const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr"]);
1007
908
  const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) as NativeReviewOperation;
1008
909
  const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) as NativeReviewErrorCode;
1009
910
  const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
@@ -1023,23 +924,10 @@ export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeRe
1023
924
  output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
1024
925
  ...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint! }),
1025
926
  ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
1026
- ...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
1027
927
  };
1028
928
  } catch { return undefined; }
1029
929
  }
1030
930
 
1031
- function sanitizeForeignStructuredDenial(value: unknown): NativeReviewStructuredDenial {
1032
- const parsed = exactObject(value, ["schema", "result", "action", "reason"], ["denial"]);
1033
- if (parsed.schema !== "gentle-ai.review-gate-result/v1") throw new Error("invalid denial schema");
1034
- const result = enumString(parsed.result, ["scope-changed", "invalidated", "escalated"] as const) as NativeReviewStructuredDenial["result"];
1035
- const action = sanitizeNativeDiagnosticText(requiredString(parsed.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1036
- const reason = sanitizeNativeDiagnosticText(requiredString(parsed.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1037
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
1038
- if (action !== expectedAction || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("non-canonical denial evidence");
1039
- const denial = parsed.denial === undefined ? undefined : (() => { const nested = exactObject(parsed.denial, ["stage", "code"]); const stage = sanitizeNativeDiagnosticText(requiredString(nested.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT); const code = sanitizeNativeDiagnosticText(requiredString(nested.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT); if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence"); return { stage, code }; })();
1040
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
1041
- }
1042
-
1043
931
  function nativeProcessDiagnostics(operation: NativeReviewOperation, code: NativeReviewErrorCode, result?: ExecFileResult, maxBufferBytes?: number): NativeReviewProcessDiagnostics {
1044
932
  const outputLimitExceeded = result?.outputLimitExceeded === true;
1045
933
  return {
@@ -1053,7 +941,6 @@ function nativeProcessDiagnostics(operation: NativeReviewOperation, code: Native
1053
941
  ? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
1054
942
  : {}),
1055
943
  ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
1056
- ...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
1057
944
  };
1058
945
  }
1059
946
 
@@ -1072,39 +959,13 @@ function decodeLegacyReconcileAudit(value: unknown): NativeReviewRecoveryResult
1072
959
  for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
1073
960
  return { record };
1074
961
  }
1075
- function decodeNativeReviewVerificationEvidence(value: unknown): NativeReviewVerificationEvidenceV2 {
1076
- const body = exactObject(value, ["schema", "version", "lineage_id", "authority_revision", "target_identity", "candidate_tree", "paths_digest", "paths", "ledger_ids", "raw_payload_sha256", "raw_payload_bytes", "outcome", "record_digest"]);
1077
- if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
1078
- return {
1079
- schema: "gentle-ai.review-verification-evidence/v2",
1080
- // verification-evidence.schema.json pins `version` to {"const": 2} -- the
1081
- // NUMBER two, not the string. It was written as a string from a handoff
1082
- // that listed the observed field NAMES without their types.
1083
- version: (() => { if (body.version !== 2) throw new Error(`native verification evidence version must be the number 2, received ${JSON.stringify(body.version)}`); return 2 as const; })(),
1084
- lineageId: requiredString(body.lineage_id),
1085
- authorityRevision: requiredString(body.authority_revision),
1086
- targetIdentity: requiredString(body.target_identity),
1087
- candidateTree: requiredString(body.candidate_tree),
1088
- pathsDigest: requiredString(body.paths_digest),
1089
- paths: stringArray(body.paths),
1090
- // The schema requires ledger_ids to be an array, but v2.2.2 emits null when
1091
- // there are none. Tolerated deliberately: refusing here would reject a
1092
- // response the provider actually sends, and the provider violating its own
1093
- // published schema is its defect to fix, not a reason to break the client.
1094
- ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
1095
- rawPayloadSha256: requiredString(body.raw_payload_sha256),
1096
- rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
1097
- outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) as NativeReviewCaptureOutcome,
1098
- recordDigest: requiredString(body.record_digest),
1099
- };
1100
- }
1101
962
  // Unimplemented next_transition.execute.operation values must never reach
1102
963
  // argv synthesis. Checked against the raw pre-decode body so an operation
1103
964
  // gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
1104
965
  // named, typed refusal instead of a generic schema-incompatible error, and
1105
966
  // before any client ever tries to build an invocation for it (Design
1106
967
  // Decision #6, migrate-review-integration-v2).
1107
- const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"]);
968
+ const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.recover", "review.repair", "review.acknowledge-approved"]);
1108
969
  function assertSupportedNextTransitionOperation(body: Record<string, unknown>): void {
1109
970
  const nextTransition = body.next_transition;
1110
971
  if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
@@ -1118,11 +979,23 @@ function assertSupportedNextTransitionOperation(body: Record<string, unknown>):
1118
979
  function decode<T>(operation: NativeReviewOperation, mutating: boolean, callback: () => T, diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)): T {
1119
980
  try { return callback(); } catch (error) { if (error instanceof NativeReviewCliError) throw error; throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, operation, true, mutating, "native response is schema incompatible", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE }); }
1120
981
  }
982
+ function decodeReviewStartResponse(value: unknown): ReviewStartV3 | ReviewStartV4 {
983
+ const body = object(value);
984
+ return body.schema === "gentle-ai.review-integration.start/v4"
985
+ ? decodeReviewStartV4(body)
986
+ : decodeReviewStartV3(body);
987
+ }
1121
988
  function decodeReleaseEvidence(value: unknown): void {
1122
989
  const release = exactObject(value, ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "publication_state", "evidence_freshness_hash", "evidence_freshness_state"]);
1123
990
  for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
1124
991
  if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
1125
992
  }
993
+ function decodeNonDecidingGateContext(value: unknown, expectedGate: string): NativeGateContext {
994
+ const context = exactObject(value, ["gate"]);
995
+ const gate = enumString(context.gate, NATIVE_GATE);
996
+ if (gate !== expectedGate) throw new Error("native non-deciding gate context does not match the requested gate");
997
+ return { lineageId: "", storeRevision: "", raw: context };
998
+ }
1126
999
  function decodeGateContext(value: unknown): NativeGateContext {
1127
1000
  const context = exactObject(
1128
1001
  value,
@@ -1215,7 +1088,7 @@ function decodeNativeReviewStatusDiagnostic(value: unknown): NativeReviewAuthori
1215
1088
  return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
1216
1089
  }
1217
1090
  function decodeNativeReviewModeStatus(value: unknown): NativeReviewModeStatus {
1218
- const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
1091
+ const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision", "reach"]);
1219
1092
  if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
1220
1093
  return {
1221
1094
  global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) as NativeReviewModeValue,
@@ -1223,6 +1096,7 @@ function decodeNativeReviewModeStatus(value: unknown): NativeReviewModeStatus {
1223
1096
  effective: enumString(status.effective, ["on", "off"]) as "on" | "off",
1224
1097
  source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) as NativeReviewModeSource,
1225
1098
  ...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
1099
+ ...(status.reach === undefined ? {} : { reach: enumString(status.reach, Object.values(NATIVE_REVIEW_MODE_REACH)) as NativeReviewModeReach }),
1226
1100
  };
1227
1101
  }
1228
1102
 
@@ -1365,7 +1239,6 @@ function hasValidLensesRequired(action: NativeStartAction, state: string, riskLe
1365
1239
  if (riskLevel === "low") return !lensesRequired;
1366
1240
  if (action === NATIVE_START_ACTION.CREATED) return state === "reviewing" && lensesRequired;
1367
1241
  if (action === NATIVE_START_ACTION.RESUMED) return !lensesRequired || state === "reviewing";
1368
- if (action === NATIVE_START_ACTION.REUSE_RECEIPT) return state === "approved" && !lensesRequired;
1369
1242
  return !lensesRequired;
1370
1243
  }
1371
1244
 
@@ -1379,7 +1252,7 @@ interface NativeJsonExecution {
1379
1252
  process: ExecFileResult;
1380
1253
  }
1381
1254
 
1382
- export class NativeReviewCliV214 {
1255
+ class NativeReviewPlainCli {
1383
1256
  private readonly adapter: ExecFileAdapter;
1384
1257
  private readonly executable: string | (() => string);
1385
1258
  private readonly timeoutMs: number;
@@ -1425,168 +1298,14 @@ export class NativeReviewCliV214 {
1425
1298
  if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1426
1299
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1427
1300
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1428
- const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
1429
1301
  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;
1430
1302
  const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
1431
- if (result.exitCode !== 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1432
- if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1303
+ if (result.exitCode !== 0 && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1304
+ if (result.stderr.trim().length > 0 && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1433
1305
  return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
1434
1306
  }
1435
1307
 
1436
- private async verifyVersion(cwd: string, signal: AbortSignal | undefined, capabilities: readonly NativeCliCapability[]): Promise<keyof typeof NATIVE_CLI_CONTRACTS> {
1437
- let result: ExecFileResult;
1438
- try { result = await this.adapter({ file: this.executablePath(NATIVE_REVIEW_OPERATION.VERSION, false), arguments: ["version"], cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
1439
- catch (error) {
1440
- if (error instanceof NativeReviewCliError) throw error;
1441
- if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
1442
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
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);
1445
- if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
1446
- if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
1447
- if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
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);
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"}`);
1457
- return version as keyof typeof NATIVE_CLI_CONTRACTS;
1458
- }
1459
-
1460
- async start(request: NativeStartRequest): Promise<NativeStartResult> {
1461
- if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1462
- if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
1463
- if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1464
- if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1465
- const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
1466
- const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
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);
1468
- return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1469
- // `target_identity` and `lens_bindings` are real, unconditionally-present
1470
- // fields on gentle-ai's plain (non-negotiated) `review start` JSON output
1471
- // (confirmed against the pinned v2.1.11 Go source and live against a dev
1472
- // build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
1473
- // tolerated here but not consumed: Pi's negotiated-contract client
1474
- // (NativeReviewCliV216, the production default) carries lineage/target
1475
- // identity through its own `review-integration/v1` decoder instead.
1476
- const body = exactObject(result, ["operation", "lineage_id", "state", "risk_level", "selected_lenses", "changed_files", "changed_lines", "correction_budget", "action", "lenses_required", "projection"], ["risk_evidence", "hint", "target_identity", "lens_bindings"]);
1477
- if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE as readonly string[]).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
1478
- const lineageId = requiredString(body.lineage_id);
1479
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
1480
- const riskLevel = requiredString(body.risk_level);
1481
- const action = enumString(body.action, NATIVE_START_ACTION_VALUES) as NativeStartAction;
1482
- const lensesRequired = booleanValue(body.lenses_required);
1483
- const selectedLenses = decodeSelectedLenses(body.selected_lenses, riskLevel, lensesRequired);
1484
- if (
1485
- !(NATIVE_RISK_LEVEL as readonly string[]).includes(riskLevel) ||
1486
- selectedLenses.some((lens) => !(NATIVE_REVIEW_LENS as readonly string[]).includes(lens)) ||
1487
- !hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
1488
- !hasValidLensesRequired(action, body.state as string, riskLevel, lensesRequired)
1489
- ) throw new Error("unknown or contradictory start enum");
1490
- return {
1491
- lineageId, state: body.state as NativeStartResult["state"], riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired,
1492
- ...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
1493
- ...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
1494
- };
1495
- });
1496
- }
1497
-
1498
- private async stageDocument(directory: string, name: string, document: unknown): Promise<string> {
1499
- const path = join(directory, `${name}.json`);
1500
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
1501
- await chmod(path, 0o600);
1502
- return path;
1503
- }
1504
- private async stageEvidence(directory: string, evidence: string): Promise<string> {
1505
- const path = join(directory, "evidence.txt");
1506
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
1507
- await chmod(path, 0o600);
1508
- return path;
1509
- }
1510
-
1511
- async finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult> {
1512
- if (request.evidenceDocument !== undefined && (typeof request.evidenceDocument !== "string" || request.evidenceDocument.length === 0)) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
1513
- await this.verifyVersion(request.cwd, request.signal, ["finalize"]);
1514
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
1515
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
1516
- try {
1517
- if (directory) await chmod(directory, 0o700);
1518
- const resultFiles = directory && request.lensResults ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
1519
- const refuterFile = directory && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
1520
- const validationFile = directory && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
1521
- const evidenceFile = directory && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
1522
- const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, ["review", "finalize", "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...resultFiles.flatMap((path) => ["--result", path]), ...(refuterFile ? ["--refuter", refuterFile] : []), ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]), ...(validationFile ? ["--validation", validationFile] : []), ...(evidenceFile ? ["--evidence", evidenceFile] : []), ...(request.failed ? ["--failed"] : [])], true, request.signal);
1523
- return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
1524
- const body = exactObject(result, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path"]);
1525
- if (body.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
1526
- const lineageId = requiredString(body.lineage_id);
1527
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
1528
- const state = requiredString(body.state);
1529
- if (!(NATIVE_FINALIZE_STATE as readonly string[]).includes(state)) throw new Error("unknown finalize state");
1530
- return { lineageId, state, action: requiredString(body.action), storeRevision: requiredString(body.store_revision), ...(body.receipt_path === undefined ? {} : { receiptPath: requiredString(body.receipt_path) }) };
1531
- });
1532
- } finally { if (directory) await this.cleanupDirectory(directory).catch(() => undefined); }
1533
- }
1534
-
1535
- async validate(request: NativeValidateRequest): Promise<NativeValidateResult> {
1536
- await this.verifyVersion(request.cwd, request.signal, ["validate"]);
1537
- const execution = await this.execute(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, ["review", "validate", "--gate", request.gate, "--cwd", request.cwd, ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.flags ?? [])], false, request.signal);
1538
- return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
1539
- const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1540
- const gateResult = enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"];
1541
- const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1542
- const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1543
- // `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
1544
- // discriminator: when present it must be the single literal
1545
- // "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
1546
- // kill switch is off and no receipt governs the candidate), paired
1547
- // exactly with result:invalidated, allowed:false, action:repository-policy,
1548
- // and exit 0 — never the strict continue/create-new-lineage/
1549
- // explicit-maintainer-action/stop pairing used when delivery is absent.
1550
- const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"]);
1551
- if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
1552
- if (delivery !== undefined) {
1553
- if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
1554
- } else {
1555
- const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
1556
- const expectedExitCode = gateResult === "allow" ? 0 : 1;
1557
- if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
1558
- }
1559
- const gateContext = decodeGateContext(body.context);
1560
- const returnedGate = gateContext.raw.gate;
1561
- if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
1562
- if (request.lineageId && returnedGate !== "" && gateContext.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.VALIDATE, false, "native gate lineage mismatch");
1563
- return { allowed: body.allowed as boolean, result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
1564
- });
1565
- }
1566
-
1567
- async bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult> {
1568
- await this.verifyVersion(request.cwd, request.signal, ["bindSdd"]);
1569
- const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, ["review", "bind-sdd", "--cwd", request.cwd, "--change", request.change, "--lineage", request.lineage, `--expected-binding-revision=${request.expectedBindingRevision}`], true, request.signal);
1570
- return decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => {
1571
- const body = exactObject(result, ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1572
- if (body.schema !== "gentle-ai.sdd-review-binding/v1" || body.change !== request.change || body.lineage !== request.lineage) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding identity mismatch");
1573
- const receiptHash = requiredString(body.receipt_hash);
1574
- const gateContext = decodeGateContext(body.gate_context);
1575
- const authorityRevision = requiredString(body.authority_revision);
1576
- if (gateContext.lineageId !== request.lineage || gateContext.storeRevision !== authorityRevision || gateContext.raw.gate !== "post-apply") throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding gate mismatch");
1577
- return {
1578
- revision: requiredString(body.revision),
1579
- change: requiredString(body.change),
1580
- lineage: requiredString(body.lineage),
1581
- authorityRevision,
1582
- receiptHash,
1583
- gateContext,
1584
- };
1585
- });
1586
- }
1587
-
1588
1308
  async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
1589
- await this.verifyVersion(request.cwd, request.signal, ["status", "inventory"]);
1590
1309
  const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, ["review", "status", "--cwd", request.cwd], false, request.signal);
1591
1310
  const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(result));
1592
1311
  if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");
@@ -1600,7 +1319,6 @@ export class NativeReviewCliV214 {
1600
1319
  // never mutates the operator's global gentle-ai state across other clones.
1601
1320
  async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
1602
1321
  const cwd = await canonicalNativeReviewCwd(request.cwd);
1603
- await this.verifyVersion(cwd, request.signal, ["mode"]);
1604
1322
  const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
1605
1323
  const { body } = await this.execute(
1606
1324
  NATIVE_REVIEW_OPERATION.MODE,
@@ -1612,77 +1330,10 @@ export class NativeReviewCliV214 {
1612
1330
  return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
1613
1331
  }
1614
1332
 
1615
- async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult> {
1616
- await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
1617
- const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.SDD_STATUS, request.cwd, ["sdd-status", request.change, "--cwd", request.cwd, "--json", "--instructions"], false, request.signal);
1618
- return decode(NATIVE_REVIEW_OPERATION.SDD_STATUS, false, () => {
1619
- const body = exactObject(result, ["schemaName", "schemaVersion", "changeName", "artifactStore", "planningHome", "changeRoot", "artifactPaths", "contextFiles", "artifacts", "taskProgress", "dependencies", "applyState", "actionContext", "relationships", "remediationState", "nextRecommended", "blockedReasons"], ["reviewGate", "reviewTransaction", "phaseInstructions"]);
1620
- if (body.schemaName !== "gentle-ai.sdd-status" || body.schemaVersion !== 1 || body.changeName !== request.change || !["openspec", "engram", "none"].includes(body.artifactStore as string) || !["blocked", "all_done", "ready"].includes(body.applyState as string)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.SDD_STATUS, false, "native status identity mismatch");
1621
- const paths = ["proposal", "specs", "design", "tasks", "applyProgress", "verifyReport", "reviewPolicy", "reviewLedger", "reviewReceipt", "reviewBundle", "reviewContext", "reviewState"];
1622
- const pathMap = (value: unknown) => { const parsed = exactObject(value, paths); for (const path of paths) stringArray(parsed[path]); };
1623
- const planningHome = exactObject(body.planningHome, ["mode", "path"]);
1624
- if (planningHome.mode !== "repo-local") throw new Error("invalid planning home");
1625
- requiredString(planningHome.path); requiredString(body.changeRoot); pathMap(body.artifactPaths); pathMap(body.contextFiles);
1626
- const artifactStates = paths.filter((path) => path !== "reviewPolicy" || body.artifactStore === NATIVE_SDD_ARTIFACT_STORE.ENGRAM);
1627
- const artifacts = exactObject(body.artifacts, artifactStates);
1628
- for (const path of artifactStates) if (!Object.values(NATIVE_SDD_ARTIFACT_STATE).includes(artifacts[path] as NativeSddArtifactState)) throw new Error("invalid artifact state");
1629
- const taskProgress = exactObject(body.taskProgress, ["total", "completed", "pending", "allComplete"]);
1630
- const total = nonNegativeInteger(taskProgress.total), completed = nonNegativeInteger(taskProgress.completed), pending = nonNegativeInteger(taskProgress.pending);
1631
- if (typeof taskProgress.allComplete !== "boolean" || completed + pending !== total || taskProgress.allComplete !== (pending === 0)) throw new Error("invalid task progress");
1632
- const dependencies = exactObject(body.dependencies, ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]);
1633
- for (const phase of ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]) if (!["blocked", "ready", "all_done"].includes(dependencies[phase] as string)) throw new Error("invalid dependency state");
1634
- const actionContext = exactObject(body.actionContext, ["mode", "workspaceRoot", "allowedEditRoots"]);
1635
- if (actionContext.mode !== "repo-local" || requiredString(actionContext.workspaceRoot).length === 0 || stringArray(actionContext.allowedEditRoots).length === 0) throw new Error("invalid action context");
1636
- const relationships = exactObject(body.relationships, ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]);
1637
- for (const field of ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]) stringArray(relationships[field]);
1638
- const remediation = exactObject(body.remediationState, ["required", "complete", "failedEvidenceRevision", "lineageId", "generation", "fixBatch", "reason"], ["correctionBudget"]);
1639
- if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || ["failedEvidenceRevision", "lineageId", "reason"].some((field) => typeof remediation[field] !== "string")) throw new Error("invalid remediation state");
1640
- nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
1641
- if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
1642
- let reviewGateResult: string | undefined;
1643
- let reviewGateDelivery: string | undefined;
1644
- if (body.reviewGate !== undefined) {
1645
- // `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
1646
- // when the kill switch is off: gentle-ai's status struct carries
1647
- // `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
1648
- // without it rejects that payload outright. Pre-existing bug fix
1649
- // (Phase 13.13, migrate-review-integration-v2).
1650
- const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
1651
- reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
1652
- if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
1653
- }
1654
- if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
1655
- if (body.phaseInstructions !== undefined) {
1656
- const instructions = exactObject(body.phaseInstructions, ["apply", "verify", "remediate", "archive"]);
1657
- for (const phase of ["apply", "verify", "remediate", "archive"]) stringArray(instructions[phase]);
1658
- }
1659
- const nextRecommended = requiredString(body.nextRecommended);
1660
- if (!(NATIVE_SDD_NEXT_ACTION as readonly string[]).includes(nextRecommended)) throw new Error("unknown SDD next action");
1661
- const blockedReasons = stringArray(body.blockedReasons);
1662
- return {
1663
- ...body,
1664
- artifactStore: body.artifactStore as NativeSddArtifactStore,
1665
- artifacts: artifacts as unknown as NativeSddArtifactStates,
1666
- nextRecommended,
1667
- ready:
1668
- (NATIVE_SDD_POST_REVIEW_ACTION as readonly string[]).includes(nextRecommended) &&
1669
- blockedReasons.length === 0 &&
1670
- // With the kill switch off, the gate result can never become
1671
- // "allow" (delivery follows ordinary repository policy instead) —
1672
- // gated only on `result === "allow"` this deadlocked `ready`
1673
- // forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
1674
- // `disabled/unmanaged` delivery is the legitimate non-blocking
1675
- // terminal state and must also unblock readiness.
1676
- (reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
1677
- };
1678
- });
1679
- }
1680
-
1681
1333
  async reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
1682
1334
  for (const [name, value] of [["lineage", request.lineage], ["actor", request.actor], ["reason", request.reason]] as const) {
1683
1335
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native RECLAIM ${name} must be a non-empty, trimmed, NUL-free string`);
1684
1336
  }
1685
- await this.verifyVersion(request.cwd, request.signal, ["reclaim"]);
1686
1337
  const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECLAIM, request.cwd, ["review", "reclaim", "--cwd", request.cwd, "--lineage", request.lineage, "--actor", request.actor, "--reason", request.reason], true, request.signal);
1687
1338
  return { record: body };
1688
1339
  }
@@ -1703,7 +1354,6 @@ export class NativeReviewCliV214 {
1703
1354
  throw new TypeError("Native RECOVER maintainerAuthorization must be a non-empty LF-only binding");
1704
1355
  }
1705
1356
  if (!(NATIVE_REVIEW_RECOVER_DISPOSITION as readonly string[]).includes(request.disposition)) throw new TypeError("Native RECOVER disposition must be scope_changed, invalidated, or escalated");
1706
- await this.verifyVersion(request.cwd, request.signal, ["recover"]);
1707
1357
  const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECOVER, request.cwd, [
1708
1358
  "review", "recover", "--cwd", request.cwd,
1709
1359
  "--predecessor-lineage", request.predecessorLineage,
@@ -1726,7 +1376,6 @@ export class NativeReviewCliV214 {
1726
1376
  if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1727
1377
  }
1728
1378
  if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1729
- await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
1730
1379
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1731
1380
  "review", "abandon", "--cwd", request.cwd,
1732
1381
  "--lineage", request.lineage,
@@ -1746,7 +1395,6 @@ export class NativeReviewCliV214 {
1746
1395
  }
1747
1396
  if (request.diagnostic !== NATIVE_REVIEW_LEGACY_QUARANTINE.DIAGNOSTIC || request.disposition !== NATIVE_REVIEW_LEGACY_QUARANTINE.DISPOSITION) throw new TypeError("Native QUARANTINE_LEGACY supports only the published malformed freeze-findings diagnostic and disposition");
1748
1397
  if (request.maintainerAuthorization !== nativeReviewLegacyQuarantineAuthorization(request)) throw new TypeError("Native QUARANTINE_LEGACY maintainerAuthorization must match the exact repository, lineage, revision, diagnostic, disposition, actor, and reason binding");
1749
- await this.verifyVersion(request.cwd, request.signal, ["quarantineLegacy"]);
1750
1398
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, request.cwd, [
1751
1399
  "review", "quarantine-legacy", "--cwd", request.cwd,
1752
1400
  "--lineage", request.lineage,
@@ -1778,7 +1426,6 @@ export class NativeReviewCliV214 {
1778
1426
  if (request.maintainerAuthorization !== expectedAuthorization) {
1779
1427
  throw new TypeError("Native RECONCILE_AUTHORITY maintainerAuthorization must match the exact target and revision binding");
1780
1428
  }
1781
- const version = await this.verifyVersion(request.cwd, request.signal, ["reconcileAuthority"]);
1782
1429
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, request.cwd, [
1783
1430
  "review", "reconcile-authority", "--cwd", request.cwd,
1784
1431
  "--predecessor-lineage", request.predecessorLineage,
@@ -1789,9 +1436,7 @@ export class NativeReviewCliV214 {
1789
1436
  "--reason", request.reason,
1790
1437
  "--maintainer-authorization", request.maintainerAuthorization,
1791
1438
  ], true, request.signal);
1792
- const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => version === "2.1.8"
1793
- ? decodeLegacyReconcileAudit(execution.body)
1794
- : decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1439
+ const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1795
1440
  if (execution.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, "native authority reconciliation partially failed", execution.process, true, result.record);
1796
1441
  return result;
1797
1442
  }
@@ -1802,7 +1447,6 @@ export class NativeReviewCliV214 {
1802
1447
  }
1803
1448
  if (request.diagnostic !== NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC || request.disposition !== NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DISPOSITION) throw new TypeError("Native REPAIR_LEGACY_ALIAS supports only the published historical alias diagnostic and disposition");
1804
1449
  if (request.maintainerAuthorization !== nativeReviewLegacyAliasRepairAuthorization(request)) throw new TypeError("Native REPAIR_LEGACY_ALIAS maintainerAuthorization must match the exact repository, lineage, revision, diagnostic, disposition, actor, and reason binding");
1805
- await this.verifyVersion(request.cwd, request.signal, ["repairLegacyAlias"]);
1806
1450
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS, request.cwd, [
1807
1451
  "review", "repair-legacy-alias", "--cwd", request.cwd,
1808
1452
  "--lineage", request.lineage,
@@ -2005,6 +1649,19 @@ function exactConsentOption(arguments_: readonly string[], name: string): string
2005
1649
  return values[0]!;
2006
1650
  }
2007
1651
 
1652
+ // A Pi consent envelope is bound to Pi only when every exact provider replay
1653
+ // path carries exactly one `--agent pi` option. The outer envelope agent alone
1654
+ // cannot bind an omitted, embedded, duplicated, or conflicting invocation.
1655
+ function validatePiConsentChoiceAgentBindings(consent: ReviewConsentEnvelope): void {
1656
+ if (!("agent" in consent) || consent.agent !== "pi") return;
1657
+ for (const choice of consent.choices) {
1658
+ const arguments_ = splitNativeConsentInvocation(choice.invocation).slice(1);
1659
+ if (exactConsentOption(arguments_, "--agent") !== "pi") {
1660
+ throw new NativeReviewConsentBindingError("consent-invocation-agent-changed", "Native Pi consent invocation agent binding changed");
1661
+ }
1662
+ }
1663
+ }
1664
+
2008
1665
  function optionalConsentLineageOption(arguments_: readonly string[]): string | undefined {
2009
1666
  const values: string[] = [];
2010
1667
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -2029,6 +1686,7 @@ interface ConsentInvocation {
2029
1686
  }
2030
1687
 
2031
1688
  function consentInvocationArguments(request: NativeReviewConsentAnswerRequest): ConsentInvocation {
1689
+ validatePiConsentChoiceAgentBindings(request.consent);
2032
1690
  const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
2033
1691
  if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
2034
1692
  const words = splitNativeConsentInvocation(choice.invocation);
@@ -2063,29 +1721,12 @@ function decodeDeclinedConsentStart(value: unknown, expected: NativeReviewConsen
2063
1721
  };
2064
1722
  }
2065
1723
 
2066
- type NativeExecutableDigestResolver = (path: string) => string;
2067
- const nativeCapabilitiesByDigest = new Map<string, Promise<ReviewCapabilitiesV2>>();
2068
-
2069
- export function clearNativeReviewCapabilitiesCacheForTesting(): void {
2070
- nativeCapabilitiesByDigest.clear();
2071
- }
2072
-
2073
- function defaultExecutableDigest(path: string): string {
2074
- const before = statSync(path);
2075
- const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
2076
- const after = statSync(path);
2077
- if (
2078
- before.dev !== after.dev ||
2079
- before.ino !== after.ino ||
2080
- before.size !== after.size ||
2081
- before.mtimeMs !== after.mtimeMs
2082
- ) throw new Error("native review executable changed during capability verification");
2083
- return digest;
2084
- }
2085
-
2086
1724
  interface NegotiatedExecution {
2087
1725
  body: Record<string, unknown>;
2088
1726
  exitCode: number;
1727
+ // True only for a zero-exit bodyless operation that printed nothing: the
1728
+ // pinned silent acknowledgement burn. A body is never synthesized for it.
1729
+ silent?: true;
2089
1730
  }
2090
1731
 
2091
1732
  function decodeNativeAdmittedResultManifest(value: unknown): NativeReviewAdmittedResultManifest {
@@ -2133,20 +1774,18 @@ function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewPr
2133
1774
  }
2134
1775
 
2135
1776
  export class NativeReviewCliV216 implements NativeReviewCli {
2136
- private readonly legacy: NativeReviewCliV214;
1777
+ private readonly plain: NativeReviewPlainCli;
2137
1778
  private readonly adapter: ExecFileAdapter;
2138
1779
  private readonly executable: string | (() => string);
2139
1780
  private readonly timeoutMs: number;
2140
1781
  private readonly maxBufferBytes: number;
2141
1782
  private readonly cleanupDirectory: (directory: string) => Promise<void>;
2142
- private readonly executableDigest: NativeExecutableDigestResolver;
2143
1783
  constructor(
2144
1784
  adapter: ExecFileAdapter,
2145
1785
  executable: string | (() => string) = resolveGentleAiBinary,
2146
1786
  timeoutMs = 30_000,
2147
1787
  maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
2148
1788
  cleanupDirectory: (directory: string) => Promise<void> = (directory) => rm(directory, { recursive: true, force: true }),
2149
- executableDigest: NativeExecutableDigestResolver = defaultExecutableDigest,
2150
1789
  ) {
2151
1790
  if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
2152
1791
  this.adapter = adapter;
@@ -2154,8 +1793,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2154
1793
  this.timeoutMs = timeoutMs;
2155
1794
  this.maxBufferBytes = maxBufferBytes;
2156
1795
  this.cleanupDirectory = cleanupDirectory;
2157
- this.executableDigest = executableDigest;
2158
- this.legacy = new NativeReviewCliV214(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
1796
+ this.plain = new NativeReviewPlainCli(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
2159
1797
  }
2160
1798
 
2161
1799
  private executablePath(operation: NativeReviewOperation, mutating: boolean): string {
@@ -2169,15 +1807,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2169
1807
  }
2170
1808
  }
2171
1809
 
2172
- private verifiedExecutable(operation: NativeReviewOperation, mutating: boolean): { path: string; digest: string } {
2173
- const path = this.executablePath(operation, mutating);
2174
- try {
2175
- return { path, digest: this.executableDigest(path) };
2176
- } catch {
2177
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "package-local native executable identity could not be verified", undefined, false);
2178
- }
2179
- }
2180
-
2181
1810
  private async invoke(
2182
1811
  operation: NativeReviewOperation,
2183
1812
  cwd: string,
@@ -2186,6 +1815,14 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2186
1815
  signal: AbortSignal | undefined,
2187
1816
  path: string,
2188
1817
  toleratedStderr: readonly string[] = [],
1818
+ // A successful acknowledgement burns its authority and, on every
1819
+ // published release up to v2.5.0-rc.3, prints nothing: there is no
1820
+ // receipt left to describe. Only that shape opts out of the body, and
1821
+ // only for a zero exit. When such an operation does print, the bytes
1822
+ // are handed back as a body for the caller to decode against the one
1823
+ // identity it accepts (gentle-ai #3947); a non-zero exit still has to
1824
+ // produce its typed failure envelope like every other operation.
1825
+ expectsBody = true,
2189
1826
  ): Promise<NegotiatedExecution> {
2190
1827
  let result: ExecFileResult;
2191
1828
  try {
@@ -2198,6 +1835,10 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2198
1835
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
2199
1836
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
2200
1837
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1838
+ if (!expectsBody && result.exitCode === 0 && result.stdout.trim().length === 0) {
1839
+ 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);
1840
+ return { body: {}, exitCode: result.exitCode, silent: true };
1841
+ }
2201
1842
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
2202
1843
  if (result.exitCode !== 0) {
2203
1844
  try {
@@ -2207,55 +1848,11 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2207
1848
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
2208
1849
  }
2209
1850
  }
2210
- const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && gentleAiDevBinaryOverrideConfigured() && stderrIsForecastNarration(result.stderr);
1851
+ const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && stderrIsForecastNarration(result.stderr);
2211
1852
  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);
2212
1853
  return { body, exitCode: result.exitCode };
2213
1854
  }
2214
1855
 
2215
- async capabilities(request: NativeCapabilitiesRequest = {}): Promise<ReviewCapabilitiesV2> {
2216
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.VERSION, false);
2217
- const cached = nativeCapabilitiesByDigest.get(executable.digest);
2218
- if (cached !== undefined) return cached;
2219
- const negotiation = (async () => {
2220
- const execution = await this.invoke(
2221
- NATIVE_REVIEW_OPERATION.VERSION,
2222
- request.cwd ?? dirname(executable.path),
2223
- ["review", "capabilities", "--contract", REVIEW_INTEGRATION_CONTRACT],
2224
- false,
2225
- request.signal,
2226
- executable.path,
2227
- );
2228
- // Two distinct half-upgraded-install failures (Design Decision #7,
2229
- // migrate-review-integration-v2): a decode failure means the installed
2230
- // runtime cannot even answer this shape (an older `.gentle-ai/` payload
2231
- // that still speaks `unsupported_contract` for v2); a successful decode
2232
- // whose package.version disagrees with the pin names both versions.
2233
- let capabilities: ReviewCapabilitiesV2;
2234
- try {
2235
- capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
2236
- } catch (error) {
2237
- if (error instanceof NativeReviewCliError) throw error;
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`);
2239
- }
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()) {
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}`);
2247
- }
2248
- return capabilities;
2249
- })();
2250
- nativeCapabilitiesByDigest.set(executable.digest, negotiation);
2251
- try {
2252
- return await negotiation;
2253
- } catch (error) {
2254
- nativeCapabilitiesByDigest.delete(executable.digest);
2255
- throw error;
2256
- }
2257
- }
2258
-
2259
1856
  private async negotiated(
2260
1857
  operation: NativeReviewOperation,
2261
1858
  cwd: string,
@@ -2264,13 +1861,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2264
1861
  signal?: AbortSignal,
2265
1862
  toleratedStderr: readonly string[] = [],
2266
1863
  ): Promise<NegotiatedExecution> {
2267
- const executable = this.verifiedExecutable(operation, mutating);
2268
- await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
2269
- const afterNegotiation = this.verifiedExecutable(operation, mutating);
2270
- if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
2271
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
2272
- }
2273
- return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
1864
+ return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
2274
1865
  }
2275
1866
 
2276
1867
  async start(request: NativeStartRequest): Promise<NativeStartResult> {
@@ -2278,26 +1869,35 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2278
1869
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
2279
1870
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
2280
1871
  if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
2281
- // The controller supplies the target it already projected from the
2282
- // authority workspace after proving its immutable actor view is identical.
2283
- // Direct adapter callers may omit it and retain the same-root projection.
1872
+ // STATUS owns the candidate binding and renders the only executable START
1873
+ // vector. Callers may supply a previously observed identity only to detect
1874
+ // drift; Pi never rebuilds that vector from request fields.
2284
1875
  const projection = request.projection ?? "workspace";
2285
- const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
1876
+ const selection = nativeUntrackedSelection(request);
1877
+ const status = await this.targetStatus({
2286
1878
  cwd: request.cwd,
2287
1879
  projection,
2288
- ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
1880
+ ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
2289
1881
  ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
1882
+ ...selection,
1883
+ ...(request.intendedUntrackedSelection === undefined ? {} : { intendedUntrackedSelection: request.intendedUntrackedSelection }),
1884
+ agent: "pi",
2290
1885
  ...(request.signal === undefined ? {} : { signal: request.signal }),
2291
- })).targetIdentity;
2292
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
2293
- "review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2294
- "--target", targetIdentity, "--projection", projection,
2295
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2296
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2297
- ...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
2298
- ...(request.focus === undefined ? [] : ["--focus", request.focus]),
2299
- "--consent", "relay",
2300
- ], true, request.signal);
1886
+ });
1887
+ const transition = status.nextTransition?.kind === "execute" && status.nextTransition.execute?.operation === "review.start"
1888
+ ? status.nextTransition.execute
1889
+ : undefined;
1890
+ if (transition === undefined) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.START, false, "native STATUS did not offer an executable review.start transition", undefined, false);
1891
+ if (status.projection.projection !== projection || transition.binding.targetIdentity !== status.targetIdentity || (request.targetIdentity !== undefined && request.targetIdentity !== status.targetIdentity)) {
1892
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition target binding mismatch", undefined, false);
1893
+ }
1894
+ if (request.lineageId !== undefined && transition.binding.lineageId !== undefined && transition.binding.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition lineage binding mismatch", undefined, false);
1895
+ const transitionTokens = transition.arguments.map((argument) => {
1896
+ if (argument.token === undefined) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.START, false, "native START transition omitted an ordered argument token", undefined, false);
1897
+ return argument.token;
1898
+ });
1899
+ const targetIdentity = status.targetIdentity;
1900
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", ...transitionTokens], true, request.signal);
2301
1901
  // A negotiated v2 START may answer a consent question (action:
2302
1902
  // "consent_required") instead of `start/v3` when the provider needs an
2303
1903
  // explicit answer it cannot infer. Discriminate before decode and surface
@@ -2307,15 +1907,16 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2307
1907
  // v2.1+) emits consent/v3, and any other identity fails closed inside
2308
1908
  // the v3 decoder's exact identity gate.
2309
1909
  if (execution.body.action === "consent_required") {
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
- ));
1910
+ const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1911
+ if (execution.body.schema === "gentle-ai.review-integration.consent/v2") return decodeReviewConsentV2(execution.body);
1912
+ const decoded = decodeReviewConsentV3(execution.body, "pi");
1913
+ validatePiConsentChoiceAgentBindings(decoded);
1914
+ return decoded;
1915
+ });
2315
1916
  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");
2316
1917
  throw new NativeReviewConsentRequiredError(consent);
2317
1918
  }
2318
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1919
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2319
1920
  if (request.lineageId !== undefined && result.lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
2320
1921
  const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2321
1922
  if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
@@ -2330,6 +1931,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2330
1931
  action: result.action as NativeStartAction,
2331
1932
  lensesRequired: result.lensesRequired,
2332
1933
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1934
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2333
1935
  // Derived, not received. `risk_reasons` is a required start/v2 field
2334
1936
  // already recomputed against the authoritative frozen snapshot, so
2335
1937
  // these phrases describe the same candidate the lenses will review.
@@ -2353,7 +1955,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2353
1955
  if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
2354
1956
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
2355
1957
  }
2356
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1958
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2357
1959
  const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2358
1960
  if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
2359
1961
  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");
@@ -2368,6 +1970,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2368
1970
  action: result.action as NativeStartAction,
2369
1971
  lensesRequired: result.lensesRequired,
2370
1972
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1973
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2371
1974
  ...(() => {
2372
1975
  const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
2373
1976
  return evidence.length === 0 ? {} : { riskEvidence: evidence };
@@ -2376,120 +1979,23 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2376
1979
  } };
2377
1980
  }
2378
1981
 
2379
- private async stageDocument(directory: string, name: string, document: unknown): Promise<string> {
2380
- const path = join(directory, `${name}.json`);
2381
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
2382
- await chmod(path, 0o600);
2383
- return path;
2384
- }
2385
-
2386
- private async stageEvidence(directory: string, evidence: string): Promise<string> {
2387
- const path = join(directory, "evidence.txt");
2388
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
2389
- await chmod(path, 0o600);
2390
- return path;
2391
- }
2392
-
2393
- async finalize(request: NativeFinalizeRequest): Promise<NativeFinalizeResult> {
2394
- if (request.evidenceDocument !== undefined && request.evidenceDocument.length === 0) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
2395
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
2396
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
2397
- try {
2398
- if (directory !== undefined) await chmod(directory, 0o700);
2399
- const resultFiles = directory !== undefined && request.lensResults !== undefined ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
2400
- const refuterFile = directory !== undefined && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
2401
- const validationFile = directory !== undefined && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
2402
- const evidenceFile = directory !== undefined && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
2403
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2404
- "review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2405
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2406
- // `--result` is RETIRED. A reviewer result supplied that way carries
2407
- // no provider-owned admission, so it cannot prove the lens inspected
2408
- // the frozen candidate. Results reach authority through
2409
- // `review capture-result`, and FINALIZE either discovers them
2410
- // (`--captured-results`) or is handed each manifest in lens order.
2411
- ...(request.capturedResults === true ? ["--captured-results=true"] : []),
2412
- ...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
2413
- ...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
2414
- ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
2415
- ...(validationFile === undefined ? [] : ["--validation", validationFile]),
2416
- ...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
2417
- ...(request.failed === true ? ["--failed"] : []),
2418
- ], true, request.signal);
2419
- const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
2420
- if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
2421
- const body = envelope.result;
2422
- const lineageId = requiredString(body.lineage_id);
2423
- if (request.lineageId !== undefined && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
2424
- return {
2425
- lineageId,
2426
- state: requiredString(body.state),
2427
- action: requiredString(body.action),
2428
- storeRevision: requiredString(body.store_revision),
2429
- // Present only when state is correction_required (validated by
2430
- // decodeReviewOperationV2); carried through as the raw validated
2431
- // shape rather than re-decoded, since the typed decoder for this
2432
- // nested object is private to lib/review-integration-v2.ts.
2433
- ...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request as Readonly<Record<string, unknown>> }),
2434
- ...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
2435
- };
2436
- } finally {
2437
- if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
2438
- }
2439
- }
2440
-
2441
- async validate(request: NativeValidateRequest): Promise<NativeValidateResult> {
2442
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, [
2443
- "review", "validate", "--contract", REVIEW_INTEGRATION_CONTRACT, "--gate", request.gate, "--cwd", request.cwd,
2444
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2445
- ...(request.flags ?? []),
2446
- ], false, request.signal);
2447
- const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
2448
- if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
2449
- const body = envelope.result;
2450
- const gateContext = decodeGateContext(body.context);
2451
- return {
2452
- allowed: booleanValue(body.allowed),
2453
- result: enumString(body.result, NATIVE_GATE_RESULT) as NativeValidateResult["result"],
2454
- action: requiredString(body.action),
2455
- reason: requiredString(body.reason),
2456
- gateContext,
2457
- ...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) as NativeValidateResult["delivery"] }),
2458
- };
2459
- }
2460
-
2461
- async bindSdd(request: NativeBindSddRequest): Promise<NativeBindSddResult> {
2462
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, [
2463
- "review", "bind-sdd", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2464
- "--change", request.change, "--lineage", request.lineage,
2465
- `--expected-binding-revision=${request.expectedBindingRevision}`,
2466
- ], true, request.signal);
2467
- const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
2468
- if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
2469
- const body = envelope.result;
2470
- const gateContext = decodeGateContext(body.gate_context);
2471
- const lineage = requiredString(body.lineage);
2472
- const change = requiredString(body.change);
2473
- if (lineage !== request.lineage || change !== request.change || gateContext.raw.gate !== "post-apply") throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.BIND_SDD, true, "native binding identity mismatch");
2474
- return {
2475
- revision: requiredString(body.revision),
2476
- change,
2477
- lineage,
2478
- authorityRevision: requiredString(body.authority_revision),
2479
- receiptHash: requiredString(body.receipt_hash),
2480
- gateContext,
2481
- };
2482
- }
2483
-
2484
1982
  async targetStatus(request: NativeTargetStatusRequest): Promise<ReviewStatusV3> {
2485
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, [
1983
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native STATUS baseRef must be a non-empty, trimmed, NUL-free string");
1984
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native STATUS baseRef requires explicit committedOnly acknowledgement");
1985
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native STATUS committedOnly requires an explicit baseRef");
1986
+ const selection = nativeUntrackedSelection(request);
1987
+ const submitted = request.intendedUntrackedSelection;
1988
+ if (submitted !== undefined && submitted.argumentTokens.reduce((count, token) => count + token.split("{{value}}").length - 1, 0) !== 1) throw new TypeError("Native intended-untracked selection requires exactly one provider-issued {{value}} token");
1989
+ const statusArguments = submitted === undefined ? [
2486
1990
  "review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2487
1991
  "--projection", request.projection ?? "workspace",
2488
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
1992
+ ...nativeUntrackedSelectionArguments(selection),
1993
+ ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2489
1994
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2490
1995
  ...(request.agent === undefined ? [] : ["--agent", request.agent]),
2491
1996
  "--next-transition",
2492
- ], false, request.signal);
1997
+ ] : ["review", "status", "--cwd", request.cwd, ...submitted.argumentTokens.map((token) => token.replaceAll("{{value}}", submitted.value))];
1998
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
2493
1999
  assertSupportedNextTransitionOperation(execution.body);
2494
2000
  return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
2495
2001
  }
@@ -2529,7 +2035,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2529
2035
  // --contract, and the verification-evidence/v2 record returned DIRECTLY —
2530
2036
  // not wrapped in an operation/v2 envelope. Evidence is staged through the
2531
2037
  // same 0o600 tmpfile discipline FINALIZE uses.
2532
- async captureResult(request: NativeReviewCaptureResultRequest): Promise<NativeReviewAdmittedResultManifest> {
2038
+ async captureResult(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome> {
2533
2039
  if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
2534
2040
  if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_RESULT argument tokens must all be non-empty strings");
2535
2041
  if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
@@ -2541,19 +2047,67 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2541
2047
  const resultFile = join(directory, "result.json");
2542
2048
  await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
2543
2049
  await chmod(resultFile, 0o600);
2544
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2050
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2545
2051
  const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
2546
2052
  "review", "capture-result",
2547
2053
  ...request.argumentTokens,
2548
2054
  ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2549
2055
  "--input", resultFile,
2550
- ], true, request.signal, executable.path);
2551
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => decodeNativeAdmittedResultManifest(execution.body));
2056
+ ], true, request.signal, executable);
2057
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => {
2058
+ const body = object(execution.body);
2059
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2060
+ const closure = decodeReviewLastEventClosureV1(body);
2061
+ if (closure.operation !== "review/capture-result") throw new TypeError("capture-result returned a closure for another operation");
2062
+ return closure;
2063
+ }
2064
+ return decodeNativeAdmittedResultManifest(body);
2065
+ });
2552
2066
  } finally {
2553
2067
  await this.cleanupDirectory(directory).catch(() => undefined);
2554
2068
  }
2555
2069
  }
2556
2070
 
2071
+ // Relays the provider-issued acknowledgement exactly as rendered. A success
2072
+ // burns the authority and its artifacts; a refusal still arrives as the
2073
+ // ordinary typed failure envelope. Two success shapes exist and both are
2074
+ // the same burn: every published release up to v2.5.0-rc.3 prints nothing
2075
+ // (resolved as `undefined`, byte-for-byte the pinned behaviour), and
2076
+ // gentle-ai #3947 prints one `gentle-ai.review-acknowledged/v1` envelope
2077
+ // naming the burned lineage, target, and consumed revision. Any other
2078
+ // output on a zero exit is schema-incompatible with the mutation outcome
2079
+ // unknown, exactly like a malformed terminal closure: the caller
2080
+ // reconciles through STATUS and never infers the burn from prose.
2081
+ async acknowledgeApproved(request: NativeReviewAcknowledgeApprovedRequest): Promise<NativeReviewAcknowledgeApprovedOutcome> {
2082
+ if (request.argumentTokens.length === 0) throw new TypeError("Native ACKNOWLEDGE_APPROVED requires the provider-issued argument tokens");
2083
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens must all be non-empty strings");
2084
+ if (request.argumentTokens.some((token) => token.includes("{{value}}"))) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens carry no caller-substituted value");
2085
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true);
2086
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, request.cwd, [
2087
+ "review", "acknowledge-approved", ...request.argumentTokens,
2088
+ ], true, request.signal, executable, [], false);
2089
+ if (execution.silent) return undefined;
2090
+ return decode(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true, () => decodeReviewAcknowledgedV1(execution.body, request.binding ?? {}));
2091
+ }
2092
+
2093
+ async captureCorrectionPlan(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1> {
2094
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires the provider-issued argument tokens");
2095
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_CORRECTION_PLAN argument tokens must all be non-empty strings");
2096
+ if (!Number.isSafeInteger(request.correctionLines) || request.correctionLines < 1 || request.correctionLines > 200) throw new TypeError("Native CAPTURE_CORRECTION_PLAN correction lines must be a positive integer up to 200");
2097
+ const valueIndex = request.argumentTokens.findIndex((token) => token.includes("{{value}}"));
2098
+ if (valueIndex < 0 || request.argumentTokens.filter((token) => token.includes("{{value}}")).length !== 1) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires exactly one provider-issued {{value}} token");
2099
+ const argumentTokens = request.argumentTokens.map((token, index) => index === valueIndex ? token.replaceAll("{{value}}", String(request.correctionLines)) : token);
2100
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true);
2101
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, request.cwd, [
2102
+ "review", "capture-correction-plan", ...argumentTokens,
2103
+ ], true, request.signal, executable);
2104
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true, () => {
2105
+ const closure = decodeReviewLastEventClosureV1(execution.body);
2106
+ if (closure.operation !== "review.capture-correction-plan") throw new TypeError("capture-correction-plan returned a closure for another operation");
2107
+ return closure;
2108
+ });
2109
+ }
2110
+
2557
2111
  // gentle-pi#311 P4-roles: executes one provider-rendered self-contained
2558
2112
  // role capture vector exactly as rendered — one CLI invocation, verbatim
2559
2113
  // tokens in provider order, in the foreground. Go materializes the role
@@ -2562,17 +2116,26 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2562
2116
  // --cwd: the vector's --repository-context is authoritative and mutually
2563
2117
  // exclusive with a path). The central adapter injects the relay handshake
2564
2118
  // 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)}`);
2119
+ async captureProviderRole(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome> {
2120
+ if (!isNativeReviewProviderRoleCaptureOperation(request.captureOperation)) throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
2121
+ const verb = request.captureOperation.slice("review.".length);
2122
+ const expectedClosureOperation = NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION[request.captureOperation];
2568
2123
  if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
2569
2124
  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);
2125
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
2571
2126
  const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
2572
2127
  "review", verb,
2573
2128
  ...request.argumentTokens,
2574
- ], true, request.signal, executable.path);
2575
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => decodeNativeProviderRoleCaptureArtifact(execution.body));
2129
+ ], true, request.signal, executable);
2130
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => {
2131
+ const body = object(execution.body);
2132
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2133
+ const closure = decodeReviewLastEventClosureV1(body);
2134
+ if (closure.operation !== expectedClosureOperation) throw new TypeError("provider role capture returned a closure for another operation");
2135
+ return closure;
2136
+ }
2137
+ return decodeNativeProviderRoleCaptureArtifact(body);
2138
+ });
2576
2139
  }
2577
2140
 
2578
2141
  // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
@@ -2581,15 +2144,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2581
2144
  // --captured-results=true`); Pi assembles no reviewer, refuter, or
2582
2145
  // validator documents for this lane — the provider discovers its own
2583
2146
  // 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
2147
 
2594
2148
  // Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
2595
2149
  // correction PLAN and TARGETED VALIDATION collect slots render `finalize`
@@ -2600,92 +2154,8 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2600
2154
  // intent reconciliation, so the tokens pass through verbatim with only the
2601
2155
  // {{value}} substitution: a literal for correction_lines, a staged 0o600
2602
2156
  // 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
2157
 
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
2158
 
2669
- async captureEvidence(request: NativeReviewCaptureEvidenceRequest): Promise<NativeReviewVerificationEvidenceV2> {
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");
2671
- if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2672
- const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2673
- try {
2674
- await chmod(directory, 0o700);
2675
- const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2676
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
2677
- "review", "capture-evidence", "--cwd", request.cwd,
2678
- "--lineage", request.lineageId,
2679
- "--target", request.targetIdentity,
2680
- "--expected-revision", request.expectedRevision,
2681
- "--outcome", request.outcome,
2682
- "--input", evidenceFile,
2683
- ], true, request.signal);
2684
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2685
- } finally {
2686
- await this.cleanupDirectory(directory).catch(() => undefined);
2687
- }
2688
- }
2689
2159
 
2690
2160
  // Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
2691
2161
  // the collect slot renders the identity native demands — for a correction
@@ -2694,85 +2164,62 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2694
2164
  // {{outcome}} and {{input}} slots substituted. Same verbatim-token
2695
2165
  // discipline as captureResult; --repository-context is authoritative and
2696
2166
  // 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
2167
 
2729
- reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
2730
- return this.legacy.reviewStatus(request);
2731
- }
2732
-
2733
- sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusResult> {
2734
- return this.legacy.sddStatus(request);
2168
+ async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
2169
+ const execution = await this.invoke(
2170
+ NATIVE_REVIEW_OPERATION.STATUS,
2171
+ request.cwd,
2172
+ ["review", "status", "--cwd", request.cwd],
2173
+ false,
2174
+ request.signal,
2175
+ this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false),
2176
+ );
2177
+ const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(execution.body));
2178
+ if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");
2179
+ return status;
2735
2180
  }
2736
2181
 
2737
- // Same plain-CLI delegation as reviewStatus/sddStatus above: `review mode`
2738
- // is dark until the negotiated version reports `mode: true` and stays
2739
- // outside the negotiated integration-v1 contract.
2740
- reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
2741
- return this.legacy.reviewMode(request);
2182
+ async reviewMode(request: NativeReviewModeRequest): Promise<NativeReviewModeResult> {
2183
+ const cwd = await canonicalNativeReviewCwd(request.cwd);
2184
+ const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
2185
+ const execution = await this.invoke(
2186
+ NATIVE_REVIEW_OPERATION.MODE,
2187
+ cwd,
2188
+ ["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
2189
+ mutating,
2190
+ request.signal,
2191
+ this.executablePath(NATIVE_REVIEW_OPERATION.MODE, mutating),
2192
+ );
2193
+ return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
2742
2194
  }
2743
2195
 
2744
2196
  // Recovery commands are version-gated plain CLI operations outside the
2745
- // negotiated integration-v1 contract, exactly like reviewStatus/sddStatus.
2197
+ // negotiated integration-v1 contract, exactly like reviewStatus.
2746
2198
  reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {
2747
- return this.legacy.reclaim(request);
2199
+ return this.plain.reclaim(request);
2748
2200
  }
2749
2201
 
2750
2202
  recover(request: NativeReviewRecoverRequest): Promise<NativeReviewRecoveryResult> {
2751
- return this.legacy.recover(request);
2203
+ return this.plain.recover(request);
2752
2204
  }
2753
2205
 
2754
2206
  abandon(request: NativeReviewAbandonRequest): Promise<NativeReviewRecoveryResult> {
2755
- return this.legacy.abandon(request);
2207
+ return this.plain.abandon(request);
2756
2208
  }
2757
2209
 
2758
2210
  quarantineLegacy(request: NativeReviewLegacyQuarantineRequest): Promise<NativeReviewRecoveryResult> {
2759
- return this.legacy.quarantineLegacy(request);
2211
+ return this.plain.quarantineLegacy(request);
2760
2212
  }
2761
2213
 
2762
2214
  reconcileAuthority(request: NativeReviewReconcileAuthorityRequest): Promise<NativeReviewRecoveryResult> {
2763
- return this.legacy.reconcileAuthority(request);
2215
+ return this.plain.reconcileAuthority(request);
2764
2216
  }
2765
2217
 
2766
2218
  repairLegacyAlias(request: NativeReviewLegacyAliasRepairRequest): Promise<NativeReviewRecoveryResult> {
2767
- return this.legacy.repairLegacyAlias(request);
2219
+ return this.plain.repairLegacyAlias(request);
2768
2220
  }
2769
2221
  }
2770
2222
 
2771
2223
  export function createNativeReviewCli(adapter?: ExecFileAdapter, executable: string | (() => string) = resolveGentleAiBinary): NativeReviewCli {
2772
- if (adapter !== undefined) return new NativeReviewCliV214(adapter, executable);
2773
- return new NativeReviewCliV216(createNodeExecFileAdapter(), executable);
2224
+ return new NativeReviewCliV216(adapter ?? createNodeExecFileAdapter(), executable);
2774
2225
  }
2775
-
2776
- // Response-schema fixtures remain v2.1.3 historical contracts; the production
2777
- // client above accepts only the v2.1.4 runtime release.
2778
- export { NativeReviewCliV214 as NativeReviewCliV213 };