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,23 +1,22 @@
1
- // Generated by scripts/build-git-commit-transaction-runner.mjs. Do not edit.
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
2
  import { execFile } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
- import { readFileSync, statSync } from "node:fs";
5
3
  import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
6
4
  import { tmpdir } from "node:os";
7
5
  import { dirname, isAbsolute, join, posix, win32 } from "node:path";
8
6
  import { promisify } from "node:util";
9
- import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, gentleAiDevBinaryOverrideConfigured, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
7
+ import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
10
8
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
11
9
  import {
12
10
  REVIEW_INTEGRATION_CONTRACT,
13
- decodeReviewCapabilitiesV2,
11
+ decodeReviewAcknowledgedV1,
14
12
  decodeReviewConsentV2,
15
13
  decodeReviewConsentV3,
16
14
  decodeReviewFailureV2,
17
- decodeReviewOperationV2,
15
+ decodeReviewLastEventClosureV1,
18
16
  decodeReviewRepairV2,
19
17
  decodeReviewResultArtifactV2,
20
18
  decodeReviewStartV3,
19
+ decodeReviewStartV4,
21
20
  decodeReviewStatusV3,
22
21
 
23
22
 
@@ -25,6 +24,11 @@ import {
25
24
 
26
25
 
27
26
 
27
+
28
+
29
+
30
+
31
+
28
32
  } from "./review-integration-v2.mjs";
29
33
 
30
34
  const execFileAsync = promisify(execFile);
@@ -47,12 +51,7 @@ function resolveNativeReviewMaxBufferBytes(environment = proc
47
51
  }
48
52
 
49
53
  export const NATIVE_REVIEW_OPERATION = {
50
- VERSION: "version",
51
54
  START: "review/start",
52
- FINALIZE: "review/finalize",
53
- VALIDATE: "review/validate",
54
- BIND_SDD: "review/bind-sdd",
55
- SDD_STATUS: "sdd-status",
56
55
  STATUS: "review/status",
57
56
  RECLAIM: "review/reclaim",
58
57
  RECOVER: "review/recover",
@@ -62,9 +61,10 @@ export const NATIVE_REVIEW_OPERATION = {
62
61
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
63
62
  MODE: "review/mode",
64
63
  REPAIR: "review/repair",
65
- CAPTURE_EVIDENCE: "review/capture-evidence",
66
64
  CAPTURE_RESULT: "review/capture-result",
65
+ CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
67
66
  CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
67
+ ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
68
68
  } ;
69
69
 
70
70
 
@@ -86,57 +86,6 @@ export const NATIVE_REVIEW_ERROR_CODE = {
86
86
  } ;
87
87
 
88
88
 
89
-
90
-
91
-
92
-
93
- export const NATIVE_SDD_ARTIFACT_STORE = {
94
- OPENSPEC: "openspec",
95
- ENGRAM: "engram",
96
- NONE: "none",
97
- } ;
98
-
99
-
100
- export const NATIVE_SDD_ARTIFACT_STATE = {
101
- MISSING: "missing",
102
- DONE: "done",
103
- PARTIAL: "partial",
104
- } ;
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
89
 
141
90
 
142
91
 
@@ -184,6 +133,12 @@ export const NATIVE_REVIEW_MODE_SOURCE = {
184
133
  } ;
185
134
 
186
135
 
136
+ export const NATIVE_REVIEW_MODE_REACH = {
137
+ MACHINE: "machine",
138
+ THIS_BUILD: "this_build",
139
+ } ;
140
+
141
+
187
142
  export const NATIVE_REVIEW_MODE_SCOPE = {
188
143
  GLOBAL: "global",
189
144
  CLONE: "clone",
@@ -209,6 +164,7 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
209
164
 
210
165
 
211
166
 
167
+
212
168
 
213
169
 
214
170
  // Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
@@ -368,26 +324,12 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
368
324
 
369
325
 
370
326
 
371
- // `review capture-evidence` argv and response shape are pinned to a real
372
- // v2.2.2 review run (lineage review-b39d803b68a90767): exactly
373
- // --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
374
- // --contract, and the `gentle-ai.review-verification-evidence/v2` record
375
- // returned directly (not wrapped in an operation/v2 envelope).
376
- export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] ;
377
-
378
-
379
327
  // `capture-result` is an additive headless command, NOT a negotiated
380
328
  // repository operation: it accepts no --contract, and the provider's own
381
329
  // transition tokens already carry the repository context -- it takes that or
382
330
  // --cwd, never both. So Pi passes the tokens through verbatim and adds only
383
331
  // --input. Reconstructing them would mean re-deriving a lineage, revision,
384
332
  // target, lens slot, and subject hash the provider already issued.
385
- // Added for the v2 finalize transport. `capturedResults` asks the provider to
386
- // discover every manifest it already admitted; `resultArtifactFiles` hands them
387
- // over explicitly in lens order. Both replace the retired `--result`.
388
-
389
-
390
-
391
333
 
392
334
 
393
335
 
@@ -406,60 +348,16 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
406
348
 
407
349
 
408
350
 
351
+ /** A non-final reviewer capture acknowledges an admitted artifact; final captures close natively. */
409
352
 
410
353
 
411
- // gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
412
- // provider renders a SELF-CONTAINED authority-advancing vector
413
- // (`review.capture-refuter` / `review.capture-validation` with binding tokens
414
- // plus `--agent=pi --execute=true`, no submission descriptor); Pi executes the
415
- // exact rendered invocation verbatim, in the foreground, and Go materializes
416
- // the role prompt, spawns its own locked-down pi subprocess, and admits the
417
- // raw verdict. Nothing here authors, parses, or transports role output.
418
- export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
- // Field defect (fambig, 2026-08-16): the evidence collect slot renders the
449
- // exact submission tokens native admits — fix-diff `--target`,
450
- // `--expected-revision`, and an opaque cwd-independent `--repository-context` —
451
- // with `{{outcome}}`/`{{input}}` substitution slots. Satisfying the slot means
452
- // executing those tokens verbatim with only the two slot substitutions, the
453
- // same discipline captureResult uses; identities are never reconstructed from
454
- // top-level status fields.
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
354
+ // The one continuation that burns approved authority. Its tokens are rendered
355
+ // by the provider in a closed order and are relayed verbatim: Pi never builds,
356
+ // reorders, or substitutes one, because a synthesized acknowledgement would be
357
+ // Pi deciding that a review is over. `binding` is the lineage, target, and
358
+ // revision the caller already holds from STATUS: when the provider answers the
359
+ // burn with a review-acknowledged/v1 envelope (gentle-ai #3947), that envelope
360
+ // must name exactly this burn.
463
361
 
464
362
 
465
363
 
@@ -467,6 +365,7 @@ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-prov
467
365
 
468
366
 
469
367
 
368
+ /** `undefined` is the pinned silent burn (every release up to v2.5.0-rc.3); an envelope is the #3947 typed burn. */
470
369
 
471
370
 
472
371
 
@@ -476,14 +375,31 @@ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-prov
476
375
 
477
376
 
478
377
 
378
+ // gentle-pi#311 P4-roles: one Go-owned non-lens provider role capture. The
379
+ // provider renders a SELF-CONTAINED authority-advancing vector
380
+ // (`review.capture-refuter` / `review.capture-validation` with binding tokens
381
+ // plus `--agent=pi --execute=true`, no submission descriptor); Pi executes the
382
+ // exact rendered invocation verbatim, in the foreground, and Go materializes
383
+ // the role prompt, spawns its own locked-down pi subprocess, and admits the
384
+ // raw verdict. Nothing here authors, parses, or transports role output.
385
+ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA = "gentle-ai.review-provider-role-capture/v1";
479
386
 
387
+ // Capture operations and their terminal closure operations are independently
388
+ // named upstream. Keep this closed mapping explicit: capture-validation is the
389
+ // intentionally nonuniform `review/capture-validation` closure operation.
390
+ export const NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION = {
391
+ "review.capture-refuter": "review.capture-refuter",
392
+ "review.capture-validation": "review/capture-validation",
393
+ } ;
480
394
 
481
395
 
396
+ function isNativeReviewProviderRoleCaptureOperation(operation ) {
397
+ return Object.hasOwn(NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION, operation);
398
+ }
482
399
 
483
400
 
484
401
 
485
402
 
486
- export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } ;
487
403
 
488
404
 
489
405
 
@@ -499,8 +415,13 @@ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "dec
499
415
 
500
416
 
501
417
 
418
+ /** Refuter and validator captures close only when they are the native last event. */
502
419
 
503
420
 
421
+ export const NATIVE_UNTRACKED_SCOPE = {
422
+ EXCLUDE: "exclude",
423
+ SELECT: "select",
424
+ } ;
504
425
 
505
426
 
506
427
 
@@ -514,25 +435,18 @@ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "dec
514
435
 
515
436
 
516
437
 
517
- // gentle-pi#311 P5: the provider-driven FINALIZE. `argumentTokens` are the
518
- // exact rendered tokens of one provider-returned `review.finalize` execute
519
- // transition (e.g. `--lineage=<id> --captured-results=true`), passed through
520
- // verbatim and never synthesized or reordered by the host.
521
438
 
522
439
 
523
440
 
524
441
 
525
442
 
526
443
 
527
- // The provider-rendered `finalize` submission descriptor forms (status/v5):
528
- // the correction PLAN slot substitutes a positive line-count literal into its
529
- // {{value}} token; the TARGETED VALIDATION slot substitutes a staged validator
530
- // artifact path. The tokens are self-contained and execute verbatim.
531
444
 
532
445
 
533
446
 
534
447
 
535
448
 
449
+ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } ;
536
450
 
537
451
 
538
452
 
@@ -662,18 +576,12 @@ export const NATIVE_REVIEW_RECOVERY_DISPOSITION = {
662
576
 
663
577
 
664
578
 
665
- export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } ;
666
-
667
-
668
-
669
-
670
-
671
-
672
-
673
-
674
-
579
+ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REPLAYED: "replayed", CLOSED: "closed", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } ;
675
580
 
676
581
 
582
+ export function isCanonicalProcessString(value ) {
583
+ return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
584
+ }
677
585
 
678
586
 
679
587
 
@@ -681,10 +589,47 @@ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REU
681
589
 
682
590
 
683
591
 
592
+ function isNativeUntrackedPath(value ) {
593
+ return isCanonicalProcessString(value)
594
+ && !posix.isAbsolute(value)
595
+ && !win32.isAbsolute(value)
596
+ && !value.includes("\\")
597
+ && value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
598
+ }
684
599
 
600
+ function nativeUntrackedSelection(request ) {
601
+ const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
602
+ const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
603
+ if (!declared) return {};
604
+ if (
605
+ (untrackedScope !== NATIVE_UNTRACKED_SCOPE.EXCLUDE && untrackedScope !== NATIVE_UNTRACKED_SCOPE.SELECT) ||
606
+ !isCanonicalProcessString(expectedUntrackedInventory) ||
607
+ (intendedUntracked !== undefined && (!Array.isArray(intendedUntracked) || intendedUntracked.some((path) => !isNativeUntrackedPath(path) || intendedUntracked.indexOf(path) !== intendedUntracked.lastIndexOf(path))))
608
+ ) {
609
+ throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
610
+ }
611
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (intendedUntracked?.length ?? 0) > 0) {
612
+ throw new TypeError("Native exclude untracked selection cannot include paths");
613
+ }
614
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (intendedUntracked?.length ?? 0) === 0) {
615
+ throw new TypeError("Native select untracked selection requires at least one path");
616
+ }
617
+ return {
618
+ untrackedScope,
619
+ expectedUntrackedInventory,
620
+ intendedUntracked: intendedUntracked === undefined ? undefined : [...intendedUntracked],
621
+ };
622
+ }
685
623
 
686
- export function isCanonicalProcessString(value ) {
687
- return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
624
+ function nativeUntrackedSelectionArguments(selection ) {
625
+ if (selection.untrackedScope === undefined) return [];
626
+ return [
627
+ `--untracked-scope=${selection.untrackedScope}`,
628
+ `--expected-untracked-inventory=${selection.expectedUntrackedInventory }`,
629
+ ...(selection.untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT
630
+ ? selection.intendedUntracked .map((path) => `--intended-untracked=${path}`)
631
+ : []),
632
+ ];
688
633
  }
689
634
 
690
635
  const NATIVE_RISK_LEVEL = ["low", "medium", "high"] ;
@@ -765,28 +710,21 @@ export function nativeRiskEvidencePhrases(riskLevel , reasons
765
710
  return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
766
711
  }
767
712
  const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
768
- const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] ;
769
713
  const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
770
- const NATIVE_GATE_RESULT = ["allow", "scope-changed", "invalidated", "escalated"] ;
771
- const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] ;
772
- const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] ;
773
- const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] ;
774
-
775
- // Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
776
- // every shipped row, including "2.1.11". PI-2 adds the first capability-true
777
- // row (and bumps the triple pin) in one dedicated commit — never one without
778
- // the other. See Design Decision #1 (organic-rdd-parity).
714
+ // The pin table is intentionally preserved unchanged while #3587 is
715
+ // unpublished. Last-event capture support is exercised through the explicit
716
+ // dev-binary override; no source-level pin or contract-row mutation is allowed.
779
717
  const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } ;
780
718
 
781
719
  export const NATIVE_CLI_CONTRACTS = Object.freeze({
782
- "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 }),
783
- "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 }),
784
- "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 }),
785
- "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 }),
786
- "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 }),
787
- "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 }),
788
- "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 }),
789
- "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 }),
720
+ "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 }),
721
+ "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 }),
722
+ "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 }),
723
+ "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 }),
724
+ "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 }),
725
+ "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 }),
726
+ "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 }),
727
+ "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 }),
790
728
  // First capability-true row, paired with the triple pin bump in this same
791
729
  // commit as Design Decision #1 requires.
792
730
  //
@@ -807,7 +745,7 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
807
745
  // needs the negotiated start envelope extended upstream, which moves a
808
746
  // byte-pinned fixture and therefore belongs to a gentle-ai release, not to
809
747
  // a Pi capability flip.
810
- "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 }),
748
+ "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 }),
811
749
  // 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
812
750
  // v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
813
751
  // review-integration/v1, but the negotiated start envelope is still the
@@ -815,16 +753,16 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
815
753
  // they are dark on 2.2.0. The release does publish a second contract,
816
754
  // review-integration/v2, whose `start/v3` carries base/candidate trees --
817
755
  // but Pi does not negotiate it yet, and a row must describe the lane in use.
818
- "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 }),
756
+ "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 }),
819
757
  // 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
820
758
  // v2.2.2 binary rather than assumed: on review-integration/v1 it still
821
759
  // advertises capabilities/v1.5 and the negotiated start envelope is still
822
760
  // the closed `start/v2`, so riskEvidence and hint still cannot arrive.
823
- "2.2.2": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
761
+ "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 }),
824
762
  // Ground-truthed against the released v2.2.3 binary: the v2 lane remains
825
763
  // protocol 2.0 with the same operation set and closed START fields consumed
826
764
  // by Pi, so the existing capability columns are unchanged.
827
- "2.2.3": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
765
+ "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 }),
828
766
  // Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
829
767
  // capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
830
768
  // existing decoders already read, and the START envelope Pi consumes is
@@ -834,48 +772,42 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
834
772
  // envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
835
773
  // while Pi stayed on 2.2.3; they were never pinned or probed, so they get
836
774
  // no row.
837
- "2.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
775
+ "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 }),
776
+ // Ground-truthed by driving the exact v2.5.0-rc.3 tagged build through the
777
+ // gentle-ai-bench driven journey corpus (exit 0), which exercises the
778
+ // start/status/capture/validate/mode/delivery lifecycles Pi consumes. The
779
+ // v2 lane advertises capabilities/v2.3 and its reviewing START is the
780
+ // `start/v4` continuation envelope that #499 already decodes; the closed
781
+ // fields Pi consumes are unchanged, so the columns match the 2.4.0 row.
782
+ // riskEvidence and hint stay dark: still not proven to reach the
783
+ // negotiated path Pi reads.
784
+ "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 }),
785
+ // Ground-truthed against the published v2.5.0 binary installed from its
786
+ // signed archive: `review capabilities` on the v2 lane advertises
787
+ // capabilities/v2.4 (protocol minor 4) and answers status/v6, consent/v3,
788
+ // and the `start/v4` continuation, all of which the decoders already read.
789
+ // The closed fields Pi consumes did not change between rc.3 and stable, so
790
+ // the columns match the 2.5.0-rc.3 row. riskEvidence and hint stay dark:
791
+ // still not proven to reach the negotiated START path Pi reads.
792
+ "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 }),
793
+ // Ground-truthed against the published v2.6.0 binary installed from its
794
+ // signed archive: `review capabilities` on the v2 lane advertises
795
+ // capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
796
+ // and the `start/v4` continuation, all of which the decoders already read.
797
+ // `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
798
+ // and `review validate --gate pre-commit` still answers
799
+ // `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
800
+ // against the binary. Declining the consent/v3 prompt during `review start`
801
+ // still returns `risk_evidence` only on that blocking envelope, never on
802
+ // the negotiated `start/v4` continuation, so riskEvidence and hint stay
803
+ // dark for the same reason they have on every row since 2.2.0: still not
804
+ // proven to reach the negotiated START path Pi reads. The closed fields Pi
805
+ // consumes did not change between 2.5.0 and 2.6.0, so the columns match
806
+ // the 2.5.0 row.
807
+ "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 }),
838
808
  });
839
809
 
840
810
 
841
- // Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
842
- // delivery decode paths under a synthetic version key without ever making a
843
- // shipped row (including "2.1.11") capability-true. Production code never
844
- // calls the setter, so this is inert outside test files.
845
- const nativeCliContractsTestingOverlay = new Map ();
846
- export function setNativeCliContractForTesting(version , contract ) {
847
- if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
848
- else nativeCliContractsTestingOverlay.set(version, contract);
849
- }
850
- function resolvedNativeCliContract(version ) {
851
- return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS )[version];
852
- }
853
-
854
- // The latest known contract row, used as the capability floor for the
855
- // explicit dev-binary override (unpinned field-test mode): a maintainer
856
- // dev build is at least as capable as the newest release Pi knows. The table
857
- // is declared in ascending release order, so its last key is the latest.
858
- const LATEST_NATIVE_CLI_CONTRACT_VERSION = Object.keys(NATIVE_CLI_CONTRACTS).at(-1) ;
859
-
860
- // Dev-binary override version discipline: with the override configured, a
861
- // version reported outside the pinned table resolves to the latest known row;
862
- // with the override absent, behavior is byte-identical to the pinned gate.
863
- function effectiveNativeCliContract(version ) {
864
- const pinned = resolvedNativeCliContract(version);
865
- if (pinned !== undefined) return pinned;
866
- return gentleAiDevBinaryOverrideConfigured() ? NATIVE_CLI_CONTRACTS[LATEST_NATIVE_CLI_CONTRACT_VERSION] : undefined;
867
- }
868
-
869
-
870
-
871
-
872
-
873
-
874
-
875
-
876
-
877
-
878
-
879
811
 
880
812
 
881
813
 
@@ -956,7 +888,6 @@ function decodeSelectedLenses(value , riskLevel , lensesRequired
956
888
  }
957
889
  function enumString(value , allowed ) { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
958
890
  const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
959
- const NATIVE_REVIEW_DENIAL_TEXT_LIMIT = 1_024;
960
891
 
961
892
  function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT) {
962
893
  const normalized = value
@@ -971,40 +902,10 @@ function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_T
971
902
  return normalized.length <= limit ? normalized : `${normalized.slice(0, limit - 14)}…[truncated]`;
972
903
  }
973
904
 
974
- function parseStructuredNativeDenial(stdout ) {
975
- if (Buffer.byteLength(stdout, "utf8") > NATIVE_DIAGNOSTIC_TEXT_LIMIT * 4) return undefined;
976
- try {
977
- const value = exactObject(JSON.parse(stdout), ["schema", "result", "allowed", "action", "reason", "context"]);
978
- const result = enumString(value.result, ["scope-changed", "invalidated", "escalated"] ) ;
979
- const action = sanitizeNativeDiagnosticText(requiredString(value.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
980
- const reason = sanitizeNativeDiagnosticText(requiredString(value.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
981
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
982
- if (
983
- value.schema !== "gentle-ai.review-gate-result/v1" ||
984
- value.allowed !== false ||
985
- action !== expectedAction ||
986
- !isCanonicalProcessString(action) ||
987
- !isCanonicalProcessString(reason)
988
- ) return undefined;
989
- const context = decodeGateContext(value.context).raw;
990
- const rawDenial = context.denial;
991
- const denial = rawDenial === undefined
992
- ? undefined
993
- : (() => {
994
- const parsed = exactObject(rawDenial, ["stage", "code"]);
995
- const stage = sanitizeNativeDiagnosticText(requiredString(parsed.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
996
- const code = sanitizeNativeDiagnosticText(requiredString(parsed.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
997
- if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence");
998
- return { stage, code };
999
- })();
1000
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
1001
- } catch { return undefined; }
1002
- }
1003
-
1004
905
  // Rebuild diagnostics from a duplicated module instance before facade output.
1005
906
  export function sanitizeForeignNativeReviewDiagnostics(value ) {
1006
907
  try {
1007
- const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr", "denial"]);
908
+ const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr"]);
1008
909
  const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) ;
1009
910
  const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) ;
1010
911
  const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
@@ -1024,23 +925,10 @@ export function sanitizeForeignNativeReviewDiagnostics(value )
1024
925
  output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
1025
926
  ...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
1026
927
  ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
1027
- ...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }),
1028
928
  };
1029
929
  } catch { return undefined; }
1030
930
  }
1031
931
 
1032
- function sanitizeForeignStructuredDenial(value ) {
1033
- const parsed = exactObject(value, ["schema", "result", "action", "reason"], ["denial"]);
1034
- if (parsed.schema !== "gentle-ai.review-gate-result/v1") throw new Error("invalid denial schema");
1035
- const result = enumString(parsed.result, ["scope-changed", "invalidated", "escalated"] ) ;
1036
- const action = sanitizeNativeDiagnosticText(requiredString(parsed.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1037
- const reason = sanitizeNativeDiagnosticText(requiredString(parsed.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1038
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
1039
- if (action !== expectedAction || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("non-canonical denial evidence");
1040
- 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 }; })();
1041
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
1042
- }
1043
-
1044
932
  function nativeProcessDiagnostics(operation , code , result , maxBufferBytes ) {
1045
933
  const outputLimitExceeded = result?.outputLimitExceeded === true;
1046
934
  return {
@@ -1054,7 +942,6 @@ function nativeProcessDiagnostics(operation , code
1054
942
  ? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
1055
943
  : {}),
1056
944
  ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
1057
- ...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
1058
945
  };
1059
946
  }
1060
947
 
@@ -1073,39 +960,13 @@ function decodeLegacyReconcileAudit(value )
1073
960
  for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
1074
961
  return { record };
1075
962
  }
1076
- function decodeNativeReviewVerificationEvidence(value ) {
1077
- 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"]);
1078
- if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
1079
- return {
1080
- schema: "gentle-ai.review-verification-evidence/v2",
1081
- // verification-evidence.schema.json pins `version` to {"const": 2} -- the
1082
- // NUMBER two, not the string. It was written as a string from a handoff
1083
- // that listed the observed field NAMES without their types.
1084
- version: (() => { if (body.version !== 2) throw new Error(`native verification evidence version must be the number 2, received ${JSON.stringify(body.version)}`); return 2 ; })(),
1085
- lineageId: requiredString(body.lineage_id),
1086
- authorityRevision: requiredString(body.authority_revision),
1087
- targetIdentity: requiredString(body.target_identity),
1088
- candidateTree: requiredString(body.candidate_tree),
1089
- pathsDigest: requiredString(body.paths_digest),
1090
- paths: stringArray(body.paths),
1091
- // The schema requires ledger_ids to be an array, but v2.2.2 emits null when
1092
- // there are none. Tolerated deliberately: refusing here would reject a
1093
- // response the provider actually sends, and the provider violating its own
1094
- // published schema is its defect to fix, not a reason to break the client.
1095
- ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
1096
- rawPayloadSha256: requiredString(body.raw_payload_sha256),
1097
- rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
1098
- outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) ,
1099
- recordDigest: requiredString(body.record_digest),
1100
- };
1101
- }
1102
963
  // Unimplemented next_transition.execute.operation values must never reach
1103
964
  // argv synthesis. Checked against the raw pre-decode body so an operation
1104
965
  // gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
1105
966
  // named, typed refusal instead of a generic schema-incompatible error, and
1106
967
  // before any client ever tries to build an invocation for it (Design
1107
968
  // Decision #6, migrate-review-integration-v2).
1108
- const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"]);
969
+ const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.recover", "review.repair", "review.acknowledge-approved"]);
1109
970
  function assertSupportedNextTransitionOperation(body ) {
1110
971
  const nextTransition = body.next_transition;
1111
972
  if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
@@ -1119,11 +980,23 @@ function assertSupportedNextTransitionOperation(body )
1119
980
  function decode (operation , mutating , callback , diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)) {
1120
981
  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 }); }
1121
982
  }
983
+ function decodeReviewStartResponse(value ) {
984
+ const body = object(value);
985
+ return body.schema === "gentle-ai.review-integration.start/v4"
986
+ ? decodeReviewStartV4(body)
987
+ : decodeReviewStartV3(body);
988
+ }
1122
989
  function decodeReleaseEvidence(value ) {
1123
990
  const release = exactObject(value, ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "publication_state", "evidence_freshness_hash", "evidence_freshness_state"]);
1124
991
  for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
1125
992
  if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
1126
993
  }
994
+ function decodeNonDecidingGateContext(value , expectedGate ) {
995
+ const context = exactObject(value, ["gate"]);
996
+ const gate = enumString(context.gate, NATIVE_GATE);
997
+ if (gate !== expectedGate) throw new Error("native non-deciding gate context does not match the requested gate");
998
+ return { lineageId: "", storeRevision: "", raw: context };
999
+ }
1127
1000
  function decodeGateContext(value ) {
1128
1001
  const context = exactObject(
1129
1002
  value,
@@ -1216,7 +1089,7 @@ function decodeNativeReviewStatusDiagnostic(value )
1216
1089
  return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
1217
1090
  }
1218
1091
  function decodeNativeReviewModeStatus(value ) {
1219
- const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
1092
+ const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision", "reach"]);
1220
1093
  if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
1221
1094
  return {
1222
1095
  global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) ,
@@ -1224,6 +1097,7 @@ function decodeNativeReviewModeStatus(value ) {
1224
1097
  effective: enumString(status.effective, ["on", "off"]) ,
1225
1098
  source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) ,
1226
1099
  ...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
1100
+ ...(status.reach === undefined ? {} : { reach: enumString(status.reach, Object.values(NATIVE_REVIEW_MODE_REACH)) }),
1227
1101
  };
1228
1102
  }
1229
1103
 
@@ -1366,7 +1240,6 @@ function hasValidLensesRequired(action , state , riskLe
1366
1240
  if (riskLevel === "low") return !lensesRequired;
1367
1241
  if (action === NATIVE_START_ACTION.CREATED) return state === "reviewing" && lensesRequired;
1368
1242
  if (action === NATIVE_START_ACTION.RESUMED) return !lensesRequired || state === "reviewing";
1369
- if (action === NATIVE_START_ACTION.REUSE_RECEIPT) return state === "approved" && !lensesRequired;
1370
1243
  return !lensesRequired;
1371
1244
  }
1372
1245
 
@@ -1380,7 +1253,7 @@ function nativeError(code , operation
1380
1253
 
1381
1254
 
1382
1255
 
1383
- export class NativeReviewCliV214 {
1256
+ class NativeReviewPlainCli {
1384
1257
  adapter ;
1385
1258
  executable ;
1386
1259
  timeoutMs ;
@@ -1426,168 +1299,14 @@ export class NativeReviewCliV214 {
1426
1299
  if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1427
1300
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1428
1301
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1429
- const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
1430
1302
  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;
1431
1303
  const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
1432
- if (result.exitCode !== 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1433
- if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1304
+ if (result.exitCode !== 0 && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1305
+ if (result.stderr.trim().length > 0 && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1434
1306
  return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
1435
1307
  }
1436
1308
 
1437
- async verifyVersion(cwd , signal , capabilities ) {
1438
- let result ;
1439
- try { result = await this.adapter({ file: this.executablePath(NATIVE_REVIEW_OPERATION.VERSION, false), arguments: ["version"], cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
1440
- catch (error) {
1441
- if (error instanceof NativeReviewCliError) throw error;
1442
- if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
1443
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
1444
- }
1445
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1446
- if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
1447
- if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
1448
- if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
1449
- const normalized = result.stdout.replace(/\r\n$/, "\n");
1450
- const pinnedVersion = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(normalized)?.[1];
1451
- // Dev-binary override (unpinned field-test mode): accept the binary's
1452
- // reported version even when it is not a pinned three-part release
1453
- // version (a main-line dev build reports e.g. "2.4.0-rc.8+fix...").
1454
- // The banner shape itself stays strict. Pinned mode is byte-identical.
1455
- const version = pinnedVersion ?? (gentleAiDevBinaryOverrideConfigured() ? /^gentle-ai (\S+)\n$/.exec(normalized)?.[1] : undefined);
1456
- const contract = version === undefined ? undefined : effectiveNativeCliContract(version);
1457
- if (result.stderr.trim().length > 0 || contract === undefined || capabilities.some((capability) => !contract[capability])) throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `native gentle-ai lacks required capabilities: expected v${GENTLE_AI_VERSION}, found v${version ?? "unparseable"}`);
1458
- return version ;
1459
- }
1460
-
1461
- async start(request ) {
1462
- if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1463
- if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
1464
- if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1465
- if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1466
- const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
1467
- const toleratedStderr = effectiveNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
1468
- const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", "--cwd", request.cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.policyPath ? ["--policy", request.policyPath] : []), ...(request.focus ? ["--focus", request.focus] : [])], true, request.signal, toleratedStderr);
1469
- return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1470
- // `target_identity` and `lens_bindings` are real, unconditionally-present
1471
- // fields on gentle-ai's plain (non-negotiated) `review start` JSON output
1472
- // (confirmed against the pinned v2.1.11 Go source and live against a dev
1473
- // build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
1474
- // tolerated here but not consumed: Pi's negotiated-contract client
1475
- // (NativeReviewCliV216, the production default) carries lineage/target
1476
- // identity through its own `review-integration/v1` decoder instead.
1477
- 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"]);
1478
- if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE ).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
1479
- const lineageId = requiredString(body.lineage_id);
1480
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
1481
- const riskLevel = requiredString(body.risk_level);
1482
- const action = enumString(body.action, NATIVE_START_ACTION_VALUES) ;
1483
- const lensesRequired = booleanValue(body.lenses_required);
1484
- const selectedLenses = decodeSelectedLenses(body.selected_lenses, riskLevel, lensesRequired);
1485
- if (
1486
- !(NATIVE_RISK_LEVEL ).includes(riskLevel) ||
1487
- selectedLenses.some((lens) => !(NATIVE_REVIEW_LENS ).includes(lens)) ||
1488
- !hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
1489
- !hasValidLensesRequired(action, body.state , riskLevel, lensesRequired)
1490
- ) throw new Error("unknown or contradictory start enum");
1491
- return {
1492
- lineageId, state: body.state , riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired,
1493
- ...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
1494
- ...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
1495
- };
1496
- });
1497
- }
1498
-
1499
- async stageDocument(directory , name , document ) {
1500
- const path = join(directory, `${name}.json`);
1501
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
1502
- await chmod(path, 0o600);
1503
- return path;
1504
- }
1505
- async stageEvidence(directory , evidence ) {
1506
- const path = join(directory, "evidence.txt");
1507
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
1508
- await chmod(path, 0o600);
1509
- return path;
1510
- }
1511
-
1512
- async finalize(request ) {
1513
- if (request.evidenceDocument !== undefined && (typeof request.evidenceDocument !== "string" || request.evidenceDocument.length === 0)) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
1514
- await this.verifyVersion(request.cwd, request.signal, ["finalize"]);
1515
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
1516
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
1517
- try {
1518
- if (directory) await chmod(directory, 0o700);
1519
- const resultFiles = directory && request.lensResults ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
1520
- const refuterFile = directory && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
1521
- const validationFile = directory && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
1522
- const evidenceFile = directory && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
1523
- 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);
1524
- return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
1525
- const body = exactObject(result, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path"]);
1526
- if (body.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
1527
- const lineageId = requiredString(body.lineage_id);
1528
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
1529
- const state = requiredString(body.state);
1530
- if (!(NATIVE_FINALIZE_STATE ).includes(state)) throw new Error("unknown finalize state");
1531
- return { lineageId, state, action: requiredString(body.action), storeRevision: requiredString(body.store_revision), ...(body.receipt_path === undefined ? {} : { receiptPath: requiredString(body.receipt_path) }) };
1532
- });
1533
- } finally { if (directory) await this.cleanupDirectory(directory).catch(() => undefined); }
1534
- }
1535
-
1536
- async validate(request ) {
1537
- await this.verifyVersion(request.cwd, request.signal, ["validate"]);
1538
- 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);
1539
- return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
1540
- const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1541
- const gateResult = enumString(body.result, NATIVE_GATE_RESULT) ;
1542
- const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1543
- const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1544
- // `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
1545
- // discriminator: when present it must be the single literal
1546
- // "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
1547
- // kill switch is off and no receipt governs the candidate), paired
1548
- // exactly with result:invalidated, allowed:false, action:repository-policy,
1549
- // and exit 0 — never the strict continue/create-new-lineage/
1550
- // explicit-maintainer-action/stop pairing used when delivery is absent.
1551
- const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) );
1552
- if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
1553
- if (delivery !== undefined) {
1554
- if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
1555
- } else {
1556
- const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
1557
- const expectedExitCode = gateResult === "allow" ? 0 : 1;
1558
- if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
1559
- }
1560
- const gateContext = decodeGateContext(body.context);
1561
- const returnedGate = gateContext.raw.gate;
1562
- if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
1563
- 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");
1564
- return { allowed: body.allowed , result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
1565
- });
1566
- }
1567
-
1568
- async bindSdd(request ) {
1569
- await this.verifyVersion(request.cwd, request.signal, ["bindSdd"]);
1570
- 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);
1571
- return decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => {
1572
- const body = exactObject(result, ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1573
- 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");
1574
- const receiptHash = requiredString(body.receipt_hash);
1575
- const gateContext = decodeGateContext(body.gate_context);
1576
- const authorityRevision = requiredString(body.authority_revision);
1577
- 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");
1578
- return {
1579
- revision: requiredString(body.revision),
1580
- change: requiredString(body.change),
1581
- lineage: requiredString(body.lineage),
1582
- authorityRevision,
1583
- receiptHash,
1584
- gateContext,
1585
- };
1586
- });
1587
- }
1588
-
1589
1309
  async reviewStatus(request ) {
1590
- await this.verifyVersion(request.cwd, request.signal, ["status", "inventory"]);
1591
1310
  const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, ["review", "status", "--cwd", request.cwd], false, request.signal);
1592
1311
  const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(result));
1593
1312
  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");
@@ -1601,7 +1320,6 @@ export class NativeReviewCliV214 {
1601
1320
  // never mutates the operator's global gentle-ai state across other clones.
1602
1321
  async reviewMode(request ) {
1603
1322
  const cwd = await canonicalNativeReviewCwd(request.cwd);
1604
- await this.verifyVersion(cwd, request.signal, ["mode"]);
1605
1323
  const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
1606
1324
  const { body } = await this.execute(
1607
1325
  NATIVE_REVIEW_OPERATION.MODE,
@@ -1613,77 +1331,10 @@ export class NativeReviewCliV214 {
1613
1331
  return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
1614
1332
  }
1615
1333
 
1616
- async sddStatus(request ) {
1617
- await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
1618
- 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);
1619
- return decode(NATIVE_REVIEW_OPERATION.SDD_STATUS, false, () => {
1620
- const body = exactObject(result, ["schemaName", "schemaVersion", "changeName", "artifactStore", "planningHome", "changeRoot", "artifactPaths", "contextFiles", "artifacts", "taskProgress", "dependencies", "applyState", "actionContext", "relationships", "remediationState", "nextRecommended", "blockedReasons"], ["reviewGate", "reviewTransaction", "phaseInstructions"]);
1621
- if (body.schemaName !== "gentle-ai.sdd-status" || body.schemaVersion !== 1 || body.changeName !== request.change || !["openspec", "engram", "none"].includes(body.artifactStore ) || !["blocked", "all_done", "ready"].includes(body.applyState )) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.SDD_STATUS, false, "native status identity mismatch");
1622
- const paths = ["proposal", "specs", "design", "tasks", "applyProgress", "verifyReport", "reviewPolicy", "reviewLedger", "reviewReceipt", "reviewBundle", "reviewContext", "reviewState"];
1623
- const pathMap = (value ) => { const parsed = exactObject(value, paths); for (const path of paths) stringArray(parsed[path]); };
1624
- const planningHome = exactObject(body.planningHome, ["mode", "path"]);
1625
- if (planningHome.mode !== "repo-local") throw new Error("invalid planning home");
1626
- requiredString(planningHome.path); requiredString(body.changeRoot); pathMap(body.artifactPaths); pathMap(body.contextFiles);
1627
- const artifactStates = paths.filter((path) => path !== "reviewPolicy" || body.artifactStore === NATIVE_SDD_ARTIFACT_STORE.ENGRAM);
1628
- const artifacts = exactObject(body.artifacts, artifactStates);
1629
- for (const path of artifactStates) if (!Object.values(NATIVE_SDD_ARTIFACT_STATE).includes(artifacts[path] )) throw new Error("invalid artifact state");
1630
- const taskProgress = exactObject(body.taskProgress, ["total", "completed", "pending", "allComplete"]);
1631
- const total = nonNegativeInteger(taskProgress.total), completed = nonNegativeInteger(taskProgress.completed), pending = nonNegativeInteger(taskProgress.pending);
1632
- if (typeof taskProgress.allComplete !== "boolean" || completed + pending !== total || taskProgress.allComplete !== (pending === 0)) throw new Error("invalid task progress");
1633
- const dependencies = exactObject(body.dependencies, ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]);
1634
- for (const phase of ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]) if (!["blocked", "ready", "all_done"].includes(dependencies[phase] )) throw new Error("invalid dependency state");
1635
- const actionContext = exactObject(body.actionContext, ["mode", "workspaceRoot", "allowedEditRoots"]);
1636
- if (actionContext.mode !== "repo-local" || requiredString(actionContext.workspaceRoot).length === 0 || stringArray(actionContext.allowedEditRoots).length === 0) throw new Error("invalid action context");
1637
- const relationships = exactObject(body.relationships, ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]);
1638
- for (const field of ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]) stringArray(relationships[field]);
1639
- const remediation = exactObject(body.remediationState, ["required", "complete", "failedEvidenceRevision", "lineageId", "generation", "fixBatch", "reason"], ["correctionBudget"]);
1640
- if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || ["failedEvidenceRevision", "lineageId", "reason"].some((field) => typeof remediation[field] !== "string")) throw new Error("invalid remediation state");
1641
- nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
1642
- if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
1643
- let reviewGateResult ;
1644
- let reviewGateDelivery ;
1645
- if (body.reviewGate !== undefined) {
1646
- // `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
1647
- // when the kill switch is off: gentle-ai's status struct carries
1648
- // `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
1649
- // without it rejects that payload outright. Pre-existing bug fix
1650
- // (Phase 13.13, migrate-review-integration-v2).
1651
- const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
1652
- reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
1653
- if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
1654
- }
1655
- if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
1656
- if (body.phaseInstructions !== undefined) {
1657
- const instructions = exactObject(body.phaseInstructions, ["apply", "verify", "remediate", "archive"]);
1658
- for (const phase of ["apply", "verify", "remediate", "archive"]) stringArray(instructions[phase]);
1659
- }
1660
- const nextRecommended = requiredString(body.nextRecommended);
1661
- if (!(NATIVE_SDD_NEXT_ACTION ).includes(nextRecommended)) throw new Error("unknown SDD next action");
1662
- const blockedReasons = stringArray(body.blockedReasons);
1663
- return {
1664
- ...body,
1665
- artifactStore: body.artifactStore ,
1666
- artifacts: artifacts ,
1667
- nextRecommended,
1668
- ready:
1669
- (NATIVE_SDD_POST_REVIEW_ACTION ).includes(nextRecommended) &&
1670
- blockedReasons.length === 0 &&
1671
- // With the kill switch off, the gate result can never become
1672
- // "allow" (delivery follows ordinary repository policy instead) —
1673
- // gated only on `result === "allow"` this deadlocked `ready`
1674
- // forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
1675
- // `disabled/unmanaged` delivery is the legitimate non-blocking
1676
- // terminal state and must also unblock readiness.
1677
- (reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
1678
- };
1679
- });
1680
- }
1681
-
1682
1334
  async reclaim(request ) {
1683
1335
  for (const [name, value] of [["lineage", request.lineage], ["actor", request.actor], ["reason", request.reason]] ) {
1684
1336
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native RECLAIM ${name} must be a non-empty, trimmed, NUL-free string`);
1685
1337
  }
1686
- await this.verifyVersion(request.cwd, request.signal, ["reclaim"]);
1687
1338
  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);
1688
1339
  return { record: body };
1689
1340
  }
@@ -1704,7 +1355,6 @@ export class NativeReviewCliV214 {
1704
1355
  throw new TypeError("Native RECOVER maintainerAuthorization must be a non-empty LF-only binding");
1705
1356
  }
1706
1357
  if (!(NATIVE_REVIEW_RECOVER_DISPOSITION ).includes(request.disposition)) throw new TypeError("Native RECOVER disposition must be scope_changed, invalidated, or escalated");
1707
- await this.verifyVersion(request.cwd, request.signal, ["recover"]);
1708
1358
  const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECOVER, request.cwd, [
1709
1359
  "review", "recover", "--cwd", request.cwd,
1710
1360
  "--predecessor-lineage", request.predecessorLineage,
@@ -1727,7 +1377,6 @@ export class NativeReviewCliV214 {
1727
1377
  if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1728
1378
  }
1729
1379
  if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1730
- await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
1731
1380
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1732
1381
  "review", "abandon", "--cwd", request.cwd,
1733
1382
  "--lineage", request.lineage,
@@ -1747,7 +1396,6 @@ export class NativeReviewCliV214 {
1747
1396
  }
1748
1397
  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");
1749
1398
  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");
1750
- await this.verifyVersion(request.cwd, request.signal, ["quarantineLegacy"]);
1751
1399
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, request.cwd, [
1752
1400
  "review", "quarantine-legacy", "--cwd", request.cwd,
1753
1401
  "--lineage", request.lineage,
@@ -1779,7 +1427,6 @@ export class NativeReviewCliV214 {
1779
1427
  if (request.maintainerAuthorization !== expectedAuthorization) {
1780
1428
  throw new TypeError("Native RECONCILE_AUTHORITY maintainerAuthorization must match the exact target and revision binding");
1781
1429
  }
1782
- const version = await this.verifyVersion(request.cwd, request.signal, ["reconcileAuthority"]);
1783
1430
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, request.cwd, [
1784
1431
  "review", "reconcile-authority", "--cwd", request.cwd,
1785
1432
  "--predecessor-lineage", request.predecessorLineage,
@@ -1790,9 +1437,7 @@ export class NativeReviewCliV214 {
1790
1437
  "--reason", request.reason,
1791
1438
  "--maintainer-authorization", request.maintainerAuthorization,
1792
1439
  ], true, request.signal);
1793
- const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => version === "2.1.8"
1794
- ? decodeLegacyReconcileAudit(execution.body)
1795
- : decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1440
+ const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1796
1441
  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);
1797
1442
  return result;
1798
1443
  }
@@ -1803,7 +1448,6 @@ export class NativeReviewCliV214 {
1803
1448
  }
1804
1449
  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");
1805
1450
  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");
1806
- await this.verifyVersion(request.cwd, request.signal, ["repairLegacyAlias"]);
1807
1451
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS, request.cwd, [
1808
1452
  "review", "repair-legacy-alias", "--cwd", request.cwd,
1809
1453
  "--lineage", request.lineage,
@@ -2006,6 +1650,19 @@ function exactConsentOption(arguments_ , name )
2006
1650
  return values[0] ;
2007
1651
  }
2008
1652
 
1653
+ // A Pi consent envelope is bound to Pi only when every exact provider replay
1654
+ // path carries exactly one `--agent pi` option. The outer envelope agent alone
1655
+ // cannot bind an omitted, embedded, duplicated, or conflicting invocation.
1656
+ function validatePiConsentChoiceAgentBindings(consent ) {
1657
+ if (!("agent" in consent) || consent.agent !== "pi") return;
1658
+ for (const choice of consent.choices) {
1659
+ const arguments_ = splitNativeConsentInvocation(choice.invocation).slice(1);
1660
+ if (exactConsentOption(arguments_, "--agent") !== "pi") {
1661
+ throw new NativeReviewConsentBindingError("consent-invocation-agent-changed", "Native Pi consent invocation agent binding changed");
1662
+ }
1663
+ }
1664
+ }
1665
+
2009
1666
  function optionalConsentLineageOption(arguments_ ) {
2010
1667
  const values = [];
2011
1668
  for (let index = 0; index < arguments_.length; index += 1) {
@@ -2030,6 +1687,7 @@ function optionalConsentLineageOption(arguments_ )
2030
1687
 
2031
1688
 
2032
1689
  function consentInvocationArguments(request ) {
1690
+ validatePiConsentChoiceAgentBindings(request.consent);
2033
1691
  const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
2034
1692
  if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
2035
1693
  const words = splitNativeConsentInvocation(choice.invocation);
@@ -2065,24 +1723,7 @@ function decodeDeclinedConsentStart(value , expected
2065
1723
  }
2066
1724
 
2067
1725
 
2068
- const nativeCapabilitiesByDigest = new Map ();
2069
1726
 
2070
- export function clearNativeReviewCapabilitiesCacheForTesting() {
2071
- nativeCapabilitiesByDigest.clear();
2072
- }
2073
-
2074
- function defaultExecutableDigest(path ) {
2075
- const before = statSync(path);
2076
- const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
2077
- const after = statSync(path);
2078
- if (
2079
- before.dev !== after.dev ||
2080
- before.ino !== after.ino ||
2081
- before.size !== after.size ||
2082
- before.mtimeMs !== after.mtimeMs
2083
- ) throw new Error("native review executable changed during capability verification");
2084
- return digest;
2085
- }
2086
1727
 
2087
1728
 
2088
1729
 
@@ -2134,20 +1775,18 @@ function decodeNativeProviderRoleCaptureArtifact(value )
2134
1775
  }
2135
1776
 
2136
1777
  export class NativeReviewCliV216 {
2137
- legacy ;
1778
+ plain ;
2138
1779
  adapter ;
2139
1780
  executable ;
2140
1781
  timeoutMs ;
2141
1782
  maxBufferBytes ;
2142
1783
  cleanupDirectory ;
2143
- executableDigest ;
2144
1784
  constructor(
2145
1785
  adapter ,
2146
1786
  executable = resolveGentleAiBinary,
2147
1787
  timeoutMs = 30_000,
2148
1788
  maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
2149
1789
  cleanupDirectory = (directory) => rm(directory, { recursive: true, force: true }),
2150
- executableDigest = defaultExecutableDigest,
2151
1790
  ) {
2152
1791
  if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
2153
1792
  this.adapter = adapter;
@@ -2155,8 +1794,7 @@ export class NativeReviewCliV216 {
2155
1794
  this.timeoutMs = timeoutMs;
2156
1795
  this.maxBufferBytes = maxBufferBytes;
2157
1796
  this.cleanupDirectory = cleanupDirectory;
2158
- this.executableDigest = executableDigest;
2159
- this.legacy = new NativeReviewCliV214(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
1797
+ this.plain = new NativeReviewPlainCli(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
2160
1798
  }
2161
1799
 
2162
1800
  executablePath(operation , mutating ) {
@@ -2170,15 +1808,6 @@ export class NativeReviewCliV216 {
2170
1808
  }
2171
1809
  }
2172
1810
 
2173
- verifiedExecutable(operation , mutating ) {
2174
- const path = this.executablePath(operation, mutating);
2175
- try {
2176
- return { path, digest: this.executableDigest(path) };
2177
- } catch {
2178
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "package-local native executable identity could not be verified", undefined, false);
2179
- }
2180
- }
2181
-
2182
1811
  async invoke(
2183
1812
  operation ,
2184
1813
  cwd ,
@@ -2187,6 +1816,14 @@ export class NativeReviewCliV216 {
2187
1816
  signal ,
2188
1817
  path ,
2189
1818
  toleratedStderr = [],
1819
+ // A successful acknowledgement burns its authority and, on every
1820
+ // published release up to v2.5.0-rc.3, prints nothing: there is no
1821
+ // receipt left to describe. Only that shape opts out of the body, and
1822
+ // only for a zero exit. When such an operation does print, the bytes
1823
+ // are handed back as a body for the caller to decode against the one
1824
+ // identity it accepts (gentle-ai #3947); a non-zero exit still has to
1825
+ // produce its typed failure envelope like every other operation.
1826
+ expectsBody = true,
2190
1827
  ) {
2191
1828
  let result ;
2192
1829
  try {
@@ -2199,6 +1836,10 @@ export class NativeReviewCliV216 {
2199
1836
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
2200
1837
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
2201
1838
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1839
+ if (!expectsBody && result.exitCode === 0 && result.stdout.trim().length === 0) {
1840
+ 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);
1841
+ return { body: {}, exitCode: result.exitCode, silent: true };
1842
+ }
2202
1843
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
2203
1844
  if (result.exitCode !== 0) {
2204
1845
  try {
@@ -2208,55 +1849,11 @@ export class NativeReviewCliV216 {
2208
1849
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
2209
1850
  }
2210
1851
  }
2211
- const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && gentleAiDevBinaryOverrideConfigured() && stderrIsForecastNarration(result.stderr);
1852
+ const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && stderrIsForecastNarration(result.stderr);
2212
1853
  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);
2213
1854
  return { body, exitCode: result.exitCode };
2214
1855
  }
2215
1856
 
2216
- async capabilities(request = {}) {
2217
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.VERSION, false);
2218
- const cached = nativeCapabilitiesByDigest.get(executable.digest);
2219
- if (cached !== undefined) return cached;
2220
- const negotiation = (async () => {
2221
- const execution = await this.invoke(
2222
- NATIVE_REVIEW_OPERATION.VERSION,
2223
- request.cwd ?? dirname(executable.path),
2224
- ["review", "capabilities", "--contract", REVIEW_INTEGRATION_CONTRACT],
2225
- false,
2226
- request.signal,
2227
- executable.path,
2228
- );
2229
- // Two distinct half-upgraded-install failures (Design Decision #7,
2230
- // migrate-review-integration-v2): a decode failure means the installed
2231
- // runtime cannot even answer this shape (an older `.gentle-ai/` payload
2232
- // that still speaks `unsupported_contract` for v2); a successful decode
2233
- // whose package.version disagrees with the pin names both versions.
2234
- let capabilities ;
2235
- try {
2236
- capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
2237
- } catch (error) {
2238
- if (error instanceof NativeReviewCliError) throw error;
2239
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}; the installed runtime is incompatible — reinstall gentle-pi`);
2240
- }
2241
- // Dev-binary override (unpinned field-test mode): the session accepts
2242
- // the binary's self-reported package version instead of the exact pin.
2243
- // The full capabilities decode above still applies unchanged, so an
2244
- // incompatible envelope is refused either way. Pinned mode keeps the
2245
- // exact equality byte-identical.
2246
- if (capabilities.packageVersion !== GENTLE_AI_VERSION && !gentleAiDevBinaryOverrideConfigured()) {
2247
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}, provider reported v${capabilities.packageVersion}`);
2248
- }
2249
- return capabilities;
2250
- })();
2251
- nativeCapabilitiesByDigest.set(executable.digest, negotiation);
2252
- try {
2253
- return await negotiation;
2254
- } catch (error) {
2255
- nativeCapabilitiesByDigest.delete(executable.digest);
2256
- throw error;
2257
- }
2258
- }
2259
-
2260
1857
  async negotiated(
2261
1858
  operation ,
2262
1859
  cwd ,
@@ -2265,13 +1862,7 @@ export class NativeReviewCliV216 {
2265
1862
  signal ,
2266
1863
  toleratedStderr = [],
2267
1864
  ) {
2268
- const executable = this.verifiedExecutable(operation, mutating);
2269
- await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
2270
- const afterNegotiation = this.verifiedExecutable(operation, mutating);
2271
- if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
2272
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
2273
- }
2274
- return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
1865
+ return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
2275
1866
  }
2276
1867
 
2277
1868
  async start(request ) {
@@ -2279,26 +1870,35 @@ export class NativeReviewCliV216 {
2279
1870
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
2280
1871
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
2281
1872
  if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
2282
- // The controller supplies the target it already projected from the
2283
- // authority workspace after proving its immutable actor view is identical.
2284
- // Direct adapter callers may omit it and retain the same-root projection.
1873
+ // STATUS owns the candidate binding and renders the only executable START
1874
+ // vector. Callers may supply a previously observed identity only to detect
1875
+ // drift; Pi never rebuilds that vector from request fields.
2285
1876
  const projection = request.projection ?? "workspace";
2286
- const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
1877
+ const selection = nativeUntrackedSelection(request);
1878
+ const status = await this.targetStatus({
2287
1879
  cwd: request.cwd,
2288
1880
  projection,
2289
- ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
1881
+ ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
2290
1882
  ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
1883
+ ...selection,
1884
+ ...(request.intendedUntrackedSelection === undefined ? {} : { intendedUntrackedSelection: request.intendedUntrackedSelection }),
1885
+ agent: "pi",
2291
1886
  ...(request.signal === undefined ? {} : { signal: request.signal }),
2292
- })).targetIdentity;
2293
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
2294
- "review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2295
- "--target", targetIdentity, "--projection", projection,
2296
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2297
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2298
- ...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
2299
- ...(request.focus === undefined ? [] : ["--focus", request.focus]),
2300
- "--consent", "relay",
2301
- ], true, request.signal);
1887
+ });
1888
+ const transition = status.nextTransition?.kind === "execute" && status.nextTransition.execute?.operation === "review.start"
1889
+ ? status.nextTransition.execute
1890
+ : undefined;
1891
+ 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);
1892
+ if (status.projection.projection !== projection || transition.binding.targetIdentity !== status.targetIdentity || (request.targetIdentity !== undefined && request.targetIdentity !== status.targetIdentity)) {
1893
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition target binding mismatch", undefined, false);
1894
+ }
1895
+ 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);
1896
+ const transitionTokens = transition.arguments.map((argument) => {
1897
+ 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);
1898
+ return argument.token;
1899
+ });
1900
+ const targetIdentity = status.targetIdentity;
1901
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", ...transitionTokens], true, request.signal);
2302
1902
  // A negotiated v2 START may answer a consent question (action:
2303
1903
  // "consent_required") instead of `start/v3` when the provider needs an
2304
1904
  // explicit answer it cannot infer. Discriminate before decode and surface
@@ -2308,15 +1908,16 @@ export class NativeReviewCliV216 {
2308
1908
  // v2.1+) emits consent/v3, and any other identity fails closed inside
2309
1909
  // the v3 decoder's exact identity gate.
2310
1910
  if (execution.body.action === "consent_required") {
2311
- const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => (
2312
- execution.body.schema === "gentle-ai.review-integration.consent/v2"
2313
- ? decodeReviewConsentV2(execution.body)
2314
- : decodeReviewConsentV3(execution.body)
2315
- ));
1911
+ const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1912
+ if (execution.body.schema === "gentle-ai.review-integration.consent/v2") return decodeReviewConsentV2(execution.body);
1913
+ const decoded = decodeReviewConsentV3(execution.body, "pi");
1914
+ validatePiConsentChoiceAgentBindings(decoded);
1915
+ return decoded;
1916
+ });
2316
1917
  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");
2317
1918
  throw new NativeReviewConsentRequiredError(consent);
2318
1919
  }
2319
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1920
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2320
1921
  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");
2321
1922
  const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2322
1923
  if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
@@ -2331,6 +1932,7 @@ export class NativeReviewCliV216 {
2331
1932
  action: result.action ,
2332
1933
  lensesRequired: result.lensesRequired,
2333
1934
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1935
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2334
1936
  // Derived, not received. `risk_reasons` is a required start/v2 field
2335
1937
  // already recomputed against the authoritative frozen snapshot, so
2336
1938
  // these phrases describe the same candidate the lenses will review.
@@ -2354,7 +1956,7 @@ export class NativeReviewCliV216 {
2354
1956
  if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
2355
1957
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
2356
1958
  }
2357
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1959
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2358
1960
  const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2359
1961
  if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
2360
1962
  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");
@@ -2369,6 +1971,7 @@ export class NativeReviewCliV216 {
2369
1971
  action: result.action ,
2370
1972
  lensesRequired: result.lensesRequired,
2371
1973
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1974
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2372
1975
  ...(() => {
2373
1976
  const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
2374
1977
  return evidence.length === 0 ? {} : { riskEvidence: evidence };
@@ -2377,120 +1980,23 @@ export class NativeReviewCliV216 {
2377
1980
  } };
2378
1981
  }
2379
1982
 
2380
- async stageDocument(directory , name , document ) {
2381
- const path = join(directory, `${name}.json`);
2382
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
2383
- await chmod(path, 0o600);
2384
- return path;
2385
- }
2386
-
2387
- async stageEvidence(directory , evidence ) {
2388
- const path = join(directory, "evidence.txt");
2389
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
2390
- await chmod(path, 0o600);
2391
- return path;
2392
- }
2393
-
2394
- async finalize(request ) {
2395
- if (request.evidenceDocument !== undefined && request.evidenceDocument.length === 0) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
2396
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
2397
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
2398
- try {
2399
- if (directory !== undefined) await chmod(directory, 0o700);
2400
- const resultFiles = directory !== undefined && request.lensResults !== undefined ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
2401
- const refuterFile = directory !== undefined && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
2402
- const validationFile = directory !== undefined && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
2403
- const evidenceFile = directory !== undefined && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
2404
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2405
- "review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2406
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2407
- // `--result` is RETIRED. A reviewer result supplied that way carries
2408
- // no provider-owned admission, so it cannot prove the lens inspected
2409
- // the frozen candidate. Results reach authority through
2410
- // `review capture-result`, and FINALIZE either discovers them
2411
- // (`--captured-results`) or is handed each manifest in lens order.
2412
- ...(request.capturedResults === true ? ["--captured-results=true"] : []),
2413
- ...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
2414
- ...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
2415
- ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
2416
- ...(validationFile === undefined ? [] : ["--validation", validationFile]),
2417
- ...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
2418
- ...(request.failed === true ? ["--failed"] : []),
2419
- ], true, request.signal);
2420
- const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
2421
- if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
2422
- const body = envelope.result;
2423
- const lineageId = requiredString(body.lineage_id);
2424
- if (request.lineageId !== undefined && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
2425
- return {
2426
- lineageId,
2427
- state: requiredString(body.state),
2428
- action: requiredString(body.action),
2429
- storeRevision: requiredString(body.store_revision),
2430
- // Present only when state is correction_required (validated by
2431
- // decodeReviewOperationV2); carried through as the raw validated
2432
- // shape rather than re-decoded, since the typed decoder for this
2433
- // nested object is private to lib/review-integration-v2.ts.
2434
- ...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request }),
2435
- ...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
2436
- };
2437
- } finally {
2438
- if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
2439
- }
2440
- }
2441
-
2442
- async validate(request ) {
2443
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, [
2444
- "review", "validate", "--contract", REVIEW_INTEGRATION_CONTRACT, "--gate", request.gate, "--cwd", request.cwd,
2445
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2446
- ...(request.flags ?? []),
2447
- ], false, request.signal);
2448
- const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
2449
- if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
2450
- const body = envelope.result;
2451
- const gateContext = decodeGateContext(body.context);
2452
- return {
2453
- allowed: booleanValue(body.allowed),
2454
- result: enumString(body.result, NATIVE_GATE_RESULT) ,
2455
- action: requiredString(body.action),
2456
- reason: requiredString(body.reason),
2457
- gateContext,
2458
- ...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) }),
2459
- };
2460
- }
2461
-
2462
- async bindSdd(request ) {
2463
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, [
2464
- "review", "bind-sdd", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2465
- "--change", request.change, "--lineage", request.lineage,
2466
- `--expected-binding-revision=${request.expectedBindingRevision}`,
2467
- ], true, request.signal);
2468
- const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
2469
- if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
2470
- const body = envelope.result;
2471
- const gateContext = decodeGateContext(body.gate_context);
2472
- const lineage = requiredString(body.lineage);
2473
- const change = requiredString(body.change);
2474
- 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");
2475
- return {
2476
- revision: requiredString(body.revision),
2477
- change,
2478
- lineage,
2479
- authorityRevision: requiredString(body.authority_revision),
2480
- receiptHash: requiredString(body.receipt_hash),
2481
- gateContext,
2482
- };
2483
- }
2484
-
2485
1983
  async targetStatus(request ) {
2486
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, [
1984
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native STATUS baseRef must be a non-empty, trimmed, NUL-free string");
1985
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native STATUS baseRef requires explicit committedOnly acknowledgement");
1986
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native STATUS committedOnly requires an explicit baseRef");
1987
+ const selection = nativeUntrackedSelection(request);
1988
+ const submitted = request.intendedUntrackedSelection;
1989
+ 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");
1990
+ const statusArguments = submitted === undefined ? [
2487
1991
  "review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2488
1992
  "--projection", request.projection ?? "workspace",
2489
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
1993
+ ...nativeUntrackedSelectionArguments(selection),
1994
+ ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2490
1995
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2491
1996
  ...(request.agent === undefined ? [] : ["--agent", request.agent]),
2492
1997
  "--next-transition",
2493
- ], false, request.signal);
1998
+ ] : ["review", "status", "--cwd", request.cwd, ...submitted.argumentTokens.map((token) => token.replaceAll("{{value}}", submitted.value))];
1999
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
2494
2000
  assertSupportedNextTransitionOperation(execution.body);
2495
2001
  return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
2496
2002
  }
@@ -2530,7 +2036,7 @@ export class NativeReviewCliV216 {
2530
2036
  // --contract, and the verification-evidence/v2 record returned DIRECTLY —
2531
2037
  // not wrapped in an operation/v2 envelope. Evidence is staged through the
2532
2038
  // same 0o600 tmpfile discipline FINALIZE uses.
2533
- async captureResult(request ) {
2039
+ async captureResult(request ) {
2534
2040
  if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
2535
2041
  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");
2536
2042
  if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
@@ -2542,19 +2048,67 @@ export class NativeReviewCliV216 {
2542
2048
  const resultFile = join(directory, "result.json");
2543
2049
  await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
2544
2050
  await chmod(resultFile, 0o600);
2545
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2051
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2546
2052
  const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
2547
2053
  "review", "capture-result",
2548
2054
  ...request.argumentTokens,
2549
2055
  ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2550
2056
  "--input", resultFile,
2551
- ], true, request.signal, executable.path);
2552
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => decodeNativeAdmittedResultManifest(execution.body));
2057
+ ], true, request.signal, executable);
2058
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => {
2059
+ const body = object(execution.body);
2060
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2061
+ const closure = decodeReviewLastEventClosureV1(body);
2062
+ if (closure.operation !== "review/capture-result") throw new TypeError("capture-result returned a closure for another operation");
2063
+ return closure;
2064
+ }
2065
+ return decodeNativeAdmittedResultManifest(body);
2066
+ });
2553
2067
  } finally {
2554
2068
  await this.cleanupDirectory(directory).catch(() => undefined);
2555
2069
  }
2556
2070
  }
2557
2071
 
2072
+ // Relays the provider-issued acknowledgement exactly as rendered. A success
2073
+ // burns the authority and its artifacts; a refusal still arrives as the
2074
+ // ordinary typed failure envelope. Two success shapes exist and both are
2075
+ // the same burn: every published release up to v2.5.0-rc.3 prints nothing
2076
+ // (resolved as `undefined`, byte-for-byte the pinned behaviour), and
2077
+ // gentle-ai #3947 prints one `gentle-ai.review-acknowledged/v1` envelope
2078
+ // naming the burned lineage, target, and consumed revision. Any other
2079
+ // output on a zero exit is schema-incompatible with the mutation outcome
2080
+ // unknown, exactly like a malformed terminal closure: the caller
2081
+ // reconciles through STATUS and never infers the burn from prose.
2082
+ async acknowledgeApproved(request ) {
2083
+ if (request.argumentTokens.length === 0) throw new TypeError("Native ACKNOWLEDGE_APPROVED requires the provider-issued argument tokens");
2084
+ 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");
2085
+ if (request.argumentTokens.some((token) => token.includes("{{value}}"))) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens carry no caller-substituted value");
2086
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true);
2087
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, request.cwd, [
2088
+ "review", "acknowledge-approved", ...request.argumentTokens,
2089
+ ], true, request.signal, executable, [], false);
2090
+ if (execution.silent) return undefined;
2091
+ return decode(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true, () => decodeReviewAcknowledgedV1(execution.body, request.binding ?? {}));
2092
+ }
2093
+
2094
+ async captureCorrectionPlan(request ) {
2095
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires the provider-issued argument tokens");
2096
+ 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");
2097
+ 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");
2098
+ const valueIndex = request.argumentTokens.findIndex((token) => token.includes("{{value}}"));
2099
+ 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");
2100
+ const argumentTokens = request.argumentTokens.map((token, index) => index === valueIndex ? token.replaceAll("{{value}}", String(request.correctionLines)) : token);
2101
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true);
2102
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, request.cwd, [
2103
+ "review", "capture-correction-plan", ...argumentTokens,
2104
+ ], true, request.signal, executable);
2105
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true, () => {
2106
+ const closure = decodeReviewLastEventClosureV1(execution.body);
2107
+ if (closure.operation !== "review.capture-correction-plan") throw new TypeError("capture-correction-plan returned a closure for another operation");
2108
+ return closure;
2109
+ });
2110
+ }
2111
+
2558
2112
  // gentle-pi#311 P4-roles: executes one provider-rendered self-contained
2559
2113
  // role capture vector exactly as rendered — one CLI invocation, verbatim
2560
2114
  // tokens in provider order, in the foreground. Go materializes the role
@@ -2563,17 +2117,26 @@ export class NativeReviewCliV216 {
2563
2117
  // --cwd: the vector's --repository-context is authoritative and mutually
2564
2118
  // exclusive with a path). The central adapter injects the relay handshake
2565
2119
  // environment on this spawn like on every other gentle-ai invocation.
2566
- async captureProviderRole(request ) {
2567
- const verb = request.captureOperation.startsWith("review.") ? request.captureOperation.slice("review.".length) : "";
2568
- if (verb !== "capture-refuter" && verb !== "capture-validation") throw new TypeError(`Native CAPTURE_PROVIDER_ROLE supports only review.capture-refuter and review.capture-validation, received ${JSON.stringify(request.captureOperation)}`);
2120
+ async captureProviderRole(request ) {
2121
+ 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)}`);
2122
+ const verb = request.captureOperation.slice("review.".length);
2123
+ const expectedClosureOperation = NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION[request.captureOperation];
2569
2124
  if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
2570
2125
  if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_PROVIDER_ROLE argument tokens must all be non-empty strings");
2571
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
2126
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
2572
2127
  const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
2573
2128
  "review", verb,
2574
2129
  ...request.argumentTokens,
2575
- ], true, request.signal, executable.path);
2576
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => decodeNativeProviderRoleCaptureArtifact(execution.body));
2130
+ ], true, request.signal, executable);
2131
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => {
2132
+ const body = object(execution.body);
2133
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2134
+ const closure = decodeReviewLastEventClosureV1(body);
2135
+ if (closure.operation !== expectedClosureOperation) throw new TypeError("provider role capture returned a closure for another operation");
2136
+ return closure;
2137
+ }
2138
+ return decodeNativeProviderRoleCaptureArtifact(body);
2139
+ });
2577
2140
  }
2578
2141
 
2579
2142
  // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
@@ -2582,15 +2145,6 @@ export class NativeReviewCliV216 {
2582
2145
  // --captured-results=true`); Pi assembles no reviewer, refuter, or
2583
2146
  // validator documents for this lane — the provider discovers its own
2584
2147
  // admitted role and lens slots.
2585
- async finalizeTransition(request ) {
2586
- if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE transition requires the provider-rendered argument tokens");
2587
- if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE transition argument tokens must all be non-empty strings");
2588
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2589
- "review", "finalize",
2590
- ...request.argumentTokens,
2591
- ], true, request.signal);
2592
- return this.decodeFinalizeTransitionExecution(execution);
2593
- }
2594
2148
 
2595
2149
  // Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
2596
2150
  // correction PLAN and TARGETED VALIDATION collect slots render `finalize`
@@ -2601,92 +2155,8 @@ export class NativeReviewCliV216 {
2601
2155
  // intent reconciliation, so the tokens pass through verbatim with only the
2602
2156
  // {{value}} substitution: a literal for correction_lines, a staged 0o600
2603
2157
  // artifact path for a validation document.
2604
- async finalizeSubmission(request ) {
2605
- if (request.argumentTokens.length === 0) throw new TypeError("Native FINALIZE submission requires the provider-rendered argument tokens");
2606
- if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native FINALIZE submission argument tokens must all be non-empty strings");
2607
- if ((request.valueLiteral === undefined) === (request.valueDocument === undefined)) throw new TypeError("Native FINALIZE submission takes exactly one of valueLiteral or valueDocument");
2608
- const valueToken = request.argumentTokens[request.valueSubstitutionLocation];
2609
- if (valueToken === undefined || !valueToken.includes("{{value}}")) throw new TypeError("Native FINALIZE submission must render exactly one {{value}} slot token");
2610
- if (request.valueDocument !== undefined && request.valueDocument.length === 0) throw new TypeError("Native FINALIZE submission value document must contain at least one byte");
2611
- const directory = request.valueDocument === undefined ? undefined : await mkdtemp(join(tmpdir(), "gentle-ai-finalize-submission-"));
2612
- try {
2613
- let substituted ;
2614
- if (directory !== undefined) {
2615
- await chmod(directory, 0o700);
2616
- const valueFile = join(directory, "value.json");
2617
- await writeFile(valueFile, request.valueDocument , { encoding: "utf8", mode: 0o600 });
2618
- await chmod(valueFile, 0o600);
2619
- substituted = valueFile;
2620
- } else {
2621
- substituted = request.valueLiteral ;
2622
- }
2623
- const resolved = request.argumentTokens.map((token, index) => index === request.valueSubstitutionLocation ? token.replaceAll("{{value}}", substituted) : token);
2624
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2625
- "review", "finalize",
2626
- ...resolved,
2627
- ], true, request.signal);
2628
- return this.decodeFinalizeTransitionExecution(execution);
2629
- } finally {
2630
- if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
2631
- }
2632
- }
2633
2158
 
2634
- decodeFinalizeTransitionExecution(execution ) {
2635
- return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
2636
- const body = object(execution.body);
2637
- // A transition rendered with `--contract` answers with the negotiated
2638
- // operation envelope; one rendered without (the pi runtime form)
2639
- // answers with the plain `review/finalize` shape. Both are decoded
2640
- // strictly; nothing else is accepted.
2641
- if (typeof body.schema === "string") {
2642
- const envelope = decodeReviewOperationV2(body);
2643
- if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
2644
- const result = envelope.result;
2645
- return {
2646
- lineageId: requiredString(result.lineage_id),
2647
- state: requiredString(result.state),
2648
- action: requiredString(result.action),
2649
- storeRevision: requiredString(result.store_revision),
2650
- ...(result.validation_request === undefined ? {} : { validationRequest: result.validation_request }),
2651
- ...(result.escalation === undefined ? {} : { escalation: requiredString(result.escalation) }),
2652
- };
2653
- }
2654
- const plain = exactObject(body, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path", "validation_request", "escalation"]);
2655
- if (plain.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
2656
- const state = requiredString(plain.state);
2657
- if (!(NATIVE_FINALIZE_STATE ).includes(state)) throw new Error("unknown finalize state");
2658
- return {
2659
- lineageId: requiredString(plain.lineage_id),
2660
- state,
2661
- action: requiredString(plain.action),
2662
- storeRevision: requiredString(plain.store_revision),
2663
- ...(plain.receipt_path === undefined ? {} : { receiptPath: requiredString(plain.receipt_path) }),
2664
- ...(plain.validation_request === undefined ? {} : { validationRequest: plain.validation_request }),
2665
- ...(plain.escalation === undefined ? {} : { escalation: requiredString(plain.escalation) }),
2666
- };
2667
- });
2668
- }
2669
2159
 
2670
- async captureEvidence(request ) {
2671
- if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2672
- if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2673
- const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2674
- try {
2675
- await chmod(directory, 0o700);
2676
- const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2677
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
2678
- "review", "capture-evidence", "--cwd", request.cwd,
2679
- "--lineage", request.lineageId,
2680
- "--target", request.targetIdentity,
2681
- "--expected-revision", request.expectedRevision,
2682
- "--outcome", request.outcome,
2683
- "--input", evidenceFile,
2684
- ], true, request.signal);
2685
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2686
- } finally {
2687
- await this.cleanupDirectory(directory).catch(() => undefined);
2688
- }
2689
- }
2690
2160
 
2691
2161
  // Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
2692
2162
  // the collect slot renders the identity native demands — for a correction
@@ -2695,85 +2165,62 @@ export class NativeReviewCliV216 {
2695
2165
  // {{outcome}} and {{input}} slots substituted. Same verbatim-token
2696
2166
  // discipline as captureResult; --repository-context is authoritative and
2697
2167
  // mutually exclusive with a path.
2698
- async captureEvidenceSubmission(request ) {
2699
- if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2700
- if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2701
- if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE submission requires the provider-rendered argument tokens");
2702
- if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_EVIDENCE submission argument tokens must all be non-empty strings");
2703
- const outcomeToken = request.argumentTokens[request.outcomeSubstitutionLocation];
2704
- const inputToken = request.argumentTokens[request.inputSubstitutionLocation];
2705
- if (request.outcomeSubstitutionLocation === request.inputSubstitutionLocation || outcomeToken === undefined || !outcomeToken.includes("{{outcome}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{outcome}} slot token");
2706
- if (inputToken === undefined || !inputToken.includes("{{input}}")) throw new TypeError("Native CAPTURE_EVIDENCE submission must render exactly one {{input}} slot token");
2707
- const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
2708
- if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_EVIDENCE submission takes a repository context or --cwd, never both");
2709
- const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2710
- try {
2711
- await chmod(directory, 0o700);
2712
- const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2713
- const resolved = request.argumentTokens.map((token, index) =>
2714
- index === request.outcomeSubstitutionLocation
2715
- ? token.replaceAll("{{outcome}}", request.outcome)
2716
- : index === request.inputSubstitutionLocation
2717
- ? token.replaceAll("{{input}}", evidenceFile)
2718
- : token);
2719
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd ?? process.cwd(), [
2720
- "review", "capture-evidence",
2721
- ...resolved,
2722
- ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2723
- ], true, request.signal);
2724
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2725
- } finally {
2726
- await this.cleanupDirectory(directory).catch(() => undefined);
2727
- }
2728
- }
2729
-
2730
- reviewStatus(request ) {
2731
- return this.legacy.reviewStatus(request);
2732
- }
2733
2168
 
2734
- sddStatus(request ) {
2735
- return this.legacy.sddStatus(request);
2169
+ async reviewStatus(request ) {
2170
+ const execution = await this.invoke(
2171
+ NATIVE_REVIEW_OPERATION.STATUS,
2172
+ request.cwd,
2173
+ ["review", "status", "--cwd", request.cwd],
2174
+ false,
2175
+ request.signal,
2176
+ this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false),
2177
+ );
2178
+ const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(execution.body));
2179
+ 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");
2180
+ return status;
2736
2181
  }
2737
2182
 
2738
- // Same plain-CLI delegation as reviewStatus/sddStatus above: `review mode`
2739
- // is dark until the negotiated version reports `mode: true` and stays
2740
- // outside the negotiated integration-v1 contract.
2741
- reviewMode(request ) {
2742
- return this.legacy.reviewMode(request);
2183
+ async reviewMode(request ) {
2184
+ const cwd = await canonicalNativeReviewCwd(request.cwd);
2185
+ const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
2186
+ const execution = await this.invoke(
2187
+ NATIVE_REVIEW_OPERATION.MODE,
2188
+ cwd,
2189
+ ["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
2190
+ mutating,
2191
+ request.signal,
2192
+ this.executablePath(NATIVE_REVIEW_OPERATION.MODE, mutating),
2193
+ );
2194
+ return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
2743
2195
  }
2744
2196
 
2745
2197
  // Recovery commands are version-gated plain CLI operations outside the
2746
- // negotiated integration-v1 contract, exactly like reviewStatus/sddStatus.
2198
+ // negotiated integration-v1 contract, exactly like reviewStatus.
2747
2199
  reclaim(request ) {
2748
- return this.legacy.reclaim(request);
2200
+ return this.plain.reclaim(request);
2749
2201
  }
2750
2202
 
2751
2203
  recover(request ) {
2752
- return this.legacy.recover(request);
2204
+ return this.plain.recover(request);
2753
2205
  }
2754
2206
 
2755
2207
  abandon(request ) {
2756
- return this.legacy.abandon(request);
2208
+ return this.plain.abandon(request);
2757
2209
  }
2758
2210
 
2759
2211
  quarantineLegacy(request ) {
2760
- return this.legacy.quarantineLegacy(request);
2212
+ return this.plain.quarantineLegacy(request);
2761
2213
  }
2762
2214
 
2763
2215
  reconcileAuthority(request ) {
2764
- return this.legacy.reconcileAuthority(request);
2216
+ return this.plain.reconcileAuthority(request);
2765
2217
  }
2766
2218
 
2767
2219
  repairLegacyAlias(request ) {
2768
- return this.legacy.repairLegacyAlias(request);
2220
+ return this.plain.repairLegacyAlias(request);
2769
2221
  }
2770
2222
  }
2771
2223
 
2772
2224
  export function createNativeReviewCli(adapter , executable = resolveGentleAiBinary) {
2773
- if (adapter !== undefined) return new NativeReviewCliV214(adapter, executable);
2774
- return new NativeReviewCliV216(createNodeExecFileAdapter(), executable);
2225
+ return new NativeReviewCliV216(adapter ?? createNodeExecFileAdapter(), executable);
2775
2226
  }
2776
-
2777
- // Response-schema fixtures remain v2.1.3 historical contracts; the production
2778
- // client above accepts only the v2.1.4 runtime release.
2779
- export { NativeReviewCliV214 as NativeReviewCliV213 };