gentle-pi 2.1.2 → 2.3.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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -1,20 +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, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
7
+ import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
8
+ import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.mjs";
10
9
  import {
11
10
  REVIEW_INTEGRATION_CONTRACT,
12
- decodeReviewCapabilitiesV2,
11
+ decodeReviewAcknowledgedV1,
13
12
  decodeReviewConsentV2,
13
+ decodeReviewConsentV3,
14
14
  decodeReviewFailureV2,
15
- decodeReviewOperationV2,
15
+ decodeReviewLastEventClosureV1,
16
16
  decodeReviewRepairV2,
17
+ decodeReviewResultArtifactV2,
17
18
  decodeReviewStartV3,
19
+ decodeReviewStartV4,
18
20
  decodeReviewStatusV3,
19
21
 
20
22
 
@@ -22,17 +24,34 @@ import {
22
24
 
23
25
 
24
26
 
27
+
28
+
29
+
30
+
31
+
25
32
  } from "./review-integration-v2.mjs";
26
33
 
27
34
  const execFileAsync = promisify(execFile);
28
35
 
36
+ // Negotiated review/status responses can carry a complete authority inventory.
37
+ // Keep the production default large enough for that payload while retaining a
38
+ // hard 64 MiB ceiling even when GENTLE_PI_REVIEW_MAX_BUFFER_BYTES is set.
39
+ export const NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
40
+ const NATIVE_REVIEW_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
41
+ const NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV = "GENTLE_PI_REVIEW_MAX_BUFFER_BYTES";
42
+ const NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT = "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.";
43
+
44
+ function resolveNativeReviewMaxBufferBytes(environment = process.env) {
45
+ const value = environment[NATIVE_REVIEW_MAX_BUFFER_BYTES_ENV];
46
+ if (value === undefined || !/^[1-9]\d*$/.test(value)) return NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
47
+ const parsed = Number(value);
48
+ return Number.isSafeInteger(parsed) && parsed <= NATIVE_REVIEW_MAX_BUFFER_BYTES
49
+ ? parsed
50
+ : NATIVE_REVIEW_DEFAULT_MAX_BUFFER_BYTES;
51
+ }
52
+
29
53
  export const NATIVE_REVIEW_OPERATION = {
30
- VERSION: "version",
31
54
  START: "review/start",
32
- FINALIZE: "review/finalize",
33
- VALIDATE: "review/validate",
34
- BIND_SDD: "review/bind-sdd",
35
- SDD_STATUS: "sdd-status",
36
55
  STATUS: "review/status",
37
56
  RECLAIM: "review/reclaim",
38
57
  RECOVER: "review/recover",
@@ -42,8 +61,10 @@ export const NATIVE_REVIEW_OPERATION = {
42
61
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
43
62
  MODE: "review/mode",
44
63
  REPAIR: "review/repair",
45
- CAPTURE_EVIDENCE: "review/capture-evidence",
46
64
  CAPTURE_RESULT: "review/capture-result",
65
+ CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
66
+ CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
67
+ ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
47
68
  } ;
48
69
 
49
70
 
@@ -65,41 +86,6 @@ export const NATIVE_REVIEW_ERROR_CODE = {
65
86
  } ;
66
87
 
67
88
 
68
-
69
-
70
-
71
-
72
- export const NATIVE_SDD_ARTIFACT_STORE = {
73
- OPENSPEC: "openspec",
74
- ENGRAM: "engram",
75
- NONE: "none",
76
- } ;
77
-
78
-
79
- export const NATIVE_SDD_ARTIFACT_STATE = {
80
- MISSING: "missing",
81
- DONE: "done",
82
- PARTIAL: "partial",
83
- } ;
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
89
 
104
90
 
105
91
 
@@ -147,6 +133,12 @@ export const NATIVE_REVIEW_MODE_SOURCE = {
147
133
  } ;
148
134
 
149
135
 
136
+ export const NATIVE_REVIEW_MODE_REACH = {
137
+ MACHINE: "machine",
138
+ THIS_BUILD: "this_build",
139
+ } ;
140
+
141
+
150
142
  export const NATIVE_REVIEW_MODE_SCOPE = {
151
143
  GLOBAL: "global",
152
144
  CLONE: "clone",
@@ -172,22 +164,29 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
172
164
 
173
165
 
174
166
 
167
+
175
168
 
176
169
 
177
170
  // Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
178
171
  // capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
179
- // against gentle-ai's headless notice (internal/cli/review_mode.go
180
- // reviewConsentSkippedNotice) written when the switch is on but no interactive
181
- // terminal answered the one-time consent question — which is always true when
182
- // Pi spawns gentle-ai without a TTY. Any other text still fails closed as
183
- // UNEXPECTED_STDERR.
184
- // Each entry is one whole line gentle-ai may write to the console stream while
185
- // still succeeding. The provenance line rides with the skip notice whenever the
186
- // resolved mode source is `default`, so a headless START legitimately emits two
187
- // lines; they are separate Fprintln calls, never one joined string.
172
+ // against gentle-ai's headless notices (internal/cli/review_mode.go
173
+ // reviewConsentSkippedNotice and its siblings) written when the switch is on
174
+ // but no interactive terminal answered the one-time consent question — which
175
+ // is always true when Pi spawns gentle-ai without a TTY. Any other text still
176
+ // fails closed as UNEXPECTED_STDERR.
177
+ //
178
+ // Each entry is one whole line the PINNED gentle-ai may write to the console
179
+ // stream while still succeeding. That last qualifier is what keeps this list
180
+ // honest: v2.4.0 deleted reviewConsentSkippedDefaultProvenance ("Reviews are
181
+ // on by default; this was never explicitly chosen. ..."), which used to ride
182
+ // with the skip notice whenever the resolved mode source was `default`. Under
183
+ // opt-in receipt-driven development a default-source clone is refused long
184
+ // before the consent ceremony runs, so the pinned binary can no longer emit
185
+ // that line and it is removed here rather than left as dead tolerance. Multi-
186
+ // line stderr is still expected in principle — these are separate Fprintln
187
+ // calls, never one joined string — which is why membership is per line.
188
188
  export const REVIEW_CONSENT_NOTICES = Object.freeze([
189
189
  "Gentle AI reviewed this change without asking, because this session has no terminal to answer on. Run 'gentle-ai review mode disable' to turn reviews off, or 'gentle-ai review mode status' to see the current setting.",
190
- "Reviews are on by default; this was never explicitly chosen. Run 'gentle-ai review mode enable' to make reviews an explicit choice, or 'gentle-ai review mode disable' to turn them off.",
191
190
  "Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
192
191
  "Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
193
192
  "Review skipped for this candidate at your request. It will be offered again on the next change.",
@@ -202,6 +201,22 @@ function stderrIsTolerated(stderr , tolerated )
202
201
  return lines.length > 0 && lines.every((line) => tolerated.includes(line));
203
202
  }
204
203
 
204
+ // gentle-ai main narrates the negotiated STATUS forecast head to a human on
205
+ // stderr while keeping the machine envelope on stdout (reviewNarrateForecast
206
+ // in internal/cli/review_narration.go, ground-truthed live against a main-line
207
+ // dev build). The pinned release does not emit it, so this narration is
208
+ // tolerated only for negotiated STATUS while the dev-binary override is
209
+ // configured; any other stderr keeps failing closed.
210
+ const FORECAST_NARRATION_LINES = Object.freeze([
211
+ /^Forecast horizon: (?:partial|terminal)$/,
212
+ /^step [0-9]+: (?:execute|collect|stop); reason_code=[a-z0-9_]+; description=.+$/,
213
+ /^Re-query STATUS after completing this partial head\.$/,
214
+ ]);
215
+ function stderrIsForecastNarration(stderr ) {
216
+ const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
217
+ return lines.length > 0 && lines.every((line) => FORECAST_NARRATION_LINES.some((pattern) => pattern.test(line)));
218
+ }
219
+
205
220
  export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] ;
206
221
 
207
222
 
@@ -286,6 +301,9 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
286
301
 
287
302
 
288
303
 
304
+
305
+
306
+
289
307
 
290
308
 
291
309
 
@@ -306,23 +324,12 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
306
324
 
307
325
 
308
326
 
309
- // `review capture-evidence` argv and response shape are pinned to a real
310
- // v2.2.2 review run (lineage review-b39d803b68a90767): exactly
311
- // --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
312
- // --contract, and the `gentle-ai.review-verification-evidence/v2` record
313
- // returned directly (not wrapped in an operation/v2 envelope).
314
- export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] ;
315
-
316
-
317
327
  // `capture-result` is an additive headless command, NOT a negotiated
318
328
  // repository operation: it accepts no --contract, and the provider's own
319
329
  // transition tokens already carry the repository context -- it takes that or
320
330
  // --cwd, never both. So Pi passes the tokens through verbatim and adds only
321
331
  // --input. Reconstructing them would mean re-deriving a lineage, revision,
322
332
  // target, lens slot, and subject hash the provider already issued.
323
- // Added for the v2 finalize transport. `capturedResults` asks the provider to
324
- // discover every manifest it already admitted; `resultArtifactFiles` hands them
325
- // over explicitly in lens order. Both replace the retired `--result`.
326
333
 
327
334
 
328
335
 
@@ -341,8 +348,16 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
341
348
 
342
349
 
343
350
 
351
+ /** A non-final reviewer capture acknowledges an admitted artifact; final captures close natively. */
344
352
 
345
353
 
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.
346
361
 
347
362
 
348
363
 
@@ -350,6 +365,7 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
350
365
 
351
366
 
352
367
 
368
+ /** `undefined` is the pinned silent burn (every release up to v2.5.0-rc.3); an envelope is the #3947 typed burn. */
353
369
 
354
370
 
355
371
 
@@ -359,9 +375,27 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
359
375
 
360
376
 
361
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";
362
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
+ } ;
363
394
 
364
395
 
396
+ function isNativeReviewProviderRoleCaptureOperation(operation ) {
397
+ return Object.hasOwn(NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION, operation);
398
+ }
365
399
 
366
400
 
367
401
 
@@ -373,7 +407,27 @@ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "
373
407
 
374
408
 
375
409
 
376
- export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } ;
410
+
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+ /** Refuter and validator captures close only when they are the native last event. */
419
+
420
+
421
+ export const NATIVE_UNTRACKED_SCOPE = {
422
+ EXCLUDE: "exclude",
423
+ SELECT: "select",
424
+ } ;
425
+
426
+
427
+
428
+
429
+
430
+
377
431
 
378
432
 
379
433
 
@@ -391,6 +445,20 @@ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "dec
391
445
 
392
446
 
393
447
 
448
+
449
+ export const NATIVE_REVIEW_CONSENT_ANSWER = { GRANTED: "granted", DECLINED: "declined" } ;
450
+
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+
394
462
 
395
463
 
396
464
 
@@ -508,18 +576,12 @@ export const NATIVE_REVIEW_RECOVERY_DISPOSITION = {
508
576
 
509
577
 
510
578
 
511
- export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REUSE_RECEIPT: "reuse-receipt", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } ;
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
579
+ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REPLAYED: "replayed", CLOSED: "closed", BLOCKED_SCOPE_ACTION: "blocked-scope-action" } ;
521
580
 
522
581
 
582
+ export function isCanonicalProcessString(value ) {
583
+ return typeof value === "string" && value.length > 0 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
584
+ }
523
585
 
524
586
 
525
587
 
@@ -527,10 +589,47 @@ export const NATIVE_START_ACTION = { CREATED: "created", RESUMED: "resumed", REU
527
589
 
528
590
 
529
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
+ }
530
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
+ }
531
623
 
532
- export function isCanonicalProcessString(value ) {
533
- 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
+ ];
534
633
  }
535
634
 
536
635
  const NATIVE_RISK_LEVEL = ["low", "medium", "high"] ;
@@ -611,28 +710,21 @@ export function nativeRiskEvidencePhrases(riskLevel , reasons
611
710
  return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
612
711
  }
613
712
  const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
614
- const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] ;
615
713
  const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
616
- const NATIVE_GATE_RESULT = ["allow", "scope-changed", "invalidated", "escalated"] ;
617
- const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] ;
618
- const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] ;
619
- const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] ;
620
-
621
- // Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
622
- // every shipped row, including "2.1.11". PI-2 adds the first capability-true
623
- // row (and bumps the triple pin) in one dedicated commit — never one without
624
- // 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.
625
717
  const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } ;
626
718
 
627
719
  export const NATIVE_CLI_CONTRACTS = Object.freeze({
628
- "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 }),
629
- "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 }),
630
- "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 }),
631
- "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 }),
632
- "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 }),
633
- "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 }),
634
- "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 }),
635
- "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 }),
636
728
  // First capability-true row, paired with the triple pin bump in this same
637
729
  // commit as Design Decision #1 requires.
638
730
  //
@@ -653,7 +745,7 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
653
745
  // needs the negotiated start envelope extended upstream, which moves a
654
746
  // byte-pinned fixture and therefore belongs to a gentle-ai release, not to
655
747
  // a Pi capability flip.
656
- "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 }),
657
749
  // 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
658
750
  // v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
659
751
  // review-integration/v1, but the negotiated start envelope is still the
@@ -661,36 +753,46 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
661
753
  // they are dark on 2.2.0. The release does publish a second contract,
662
754
  // review-integration/v2, whose `start/v3` carries base/candidate trees --
663
755
  // but Pi does not negotiate it yet, and a row must describe the lane in use.
664
- "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 }),
665
757
  // 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
666
758
  // v2.2.2 binary rather than assumed: on review-integration/v1 it still
667
759
  // advertises capabilities/v1.5 and the negotiated start envelope is still
668
760
  // the closed `start/v2`, so riskEvidence and hint still cannot arrive.
669
- "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 }),
762
+ // Ground-truthed against the released v2.2.3 binary: the v2 lane remains
763
+ // protocol 2.0 with the same operation set and closed START fields consumed
764
+ // by Pi, so the existing capability columns are unchanged.
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 }),
766
+ // Ground-truthed against the released v2.4.0 binary: the v2 lane advertises
767
+ // capabilities/v2.2 and answers status/v5 and consent/v3, all of which the
768
+ // existing decoders already read, and the START envelope Pi consumes is
769
+ // still `start/v3` carrying `risk_reasons` with no `risk_evidence` and no
770
+ // `hint`, so the existing capability columns are unchanged. v2.4.0 also
771
+ // made receipt-driven development opt-in, which changes what the mode
772
+ // envelope reports, not whether it reports it. v2.2.4 and v2.3.0 shipped
773
+ // while Pi stayed on 2.2.3; they were never pinned or probed, so they get
774
+ // no row.
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 }),
670
793
  });
671
794
 
672
795
 
673
- // Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
674
- // delivery decode paths under a synthetic version key without ever making a
675
- // shipped row (including "2.1.11") capability-true. Production code never
676
- // calls the setter, so this is inert outside test files.
677
- const nativeCliContractsTestingOverlay = new Map ();
678
- export function setNativeCliContractForTesting(version , contract ) {
679
- if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
680
- else nativeCliContractsTestingOverlay.set(version, contract);
681
- }
682
- function resolvedNativeCliContract(version ) {
683
- return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS )[version];
684
- }
685
-
686
-
687
-
688
-
689
-
690
-
691
-
692
-
693
-
694
796
 
695
797
 
696
798
 
@@ -725,15 +827,25 @@ export class NativeReviewCliError extends Error {
725
827
  }
726
828
  }
727
829
 
830
+ // The one central runner for every gentle-ai CLI invocation the extension
831
+ // makes. It declares the Pi host relay handshake on each spawn: gentle-ai
832
+ // refuses pi admission pre-authority without it (gentle-pi#311 P4), and a
833
+ // single injection point keeps the declaration impossible to forget on any
834
+ // individual operation.
835
+ export function gentleAiProcessEnvironment(base = process.env) {
836
+ return { ...base, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
837
+ }
838
+
728
839
  export function createNodeExecFileAdapter() {
729
840
  return async (request) => {
730
841
  try {
731
- const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal });
842
+ const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal, env: gentleAiProcessEnvironment() });
732
843
  return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
733
844
  } catch (error) {
734
845
  const detail = error ;
735
846
  if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
736
- return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: detail.killed === true, outputLimitExceeded: detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" };
847
+ const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
848
+ return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && detail.killed === true, outputLimitExceeded };
737
849
  }
738
850
  };
739
851
  }
@@ -761,7 +873,6 @@ function decodeSelectedLenses(value , riskLevel , lensesRequired
761
873
  }
762
874
  function enumString(value , allowed ) { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
763
875
  const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
764
- const NATIVE_REVIEW_DENIAL_TEXT_LIMIT = 1_024;
765
876
 
766
877
  function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT) {
767
878
  const normalized = value
@@ -776,70 +887,46 @@ function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_T
776
887
  return normalized.length <= limit ? normalized : `${normalized.slice(0, limit - 14)}…[truncated]`;
777
888
  }
778
889
 
779
- function parseStructuredNativeDenial(stdout ) {
780
- if (Buffer.byteLength(stdout, "utf8") > NATIVE_DIAGNOSTIC_TEXT_LIMIT * 4) return undefined;
781
- try {
782
- const value = exactObject(JSON.parse(stdout), ["schema", "result", "allowed", "action", "reason", "context"]);
783
- const result = enumString(value.result, ["scope-changed", "invalidated", "escalated"] ) ;
784
- const action = sanitizeNativeDiagnosticText(requiredString(value.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
785
- const reason = sanitizeNativeDiagnosticText(requiredString(value.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
786
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
787
- if (
788
- value.schema !== "gentle-ai.review-gate-result/v1" ||
789
- value.allowed !== false ||
790
- action !== expectedAction ||
791
- !isCanonicalProcessString(action) ||
792
- !isCanonicalProcessString(reason)
793
- ) return undefined;
794
- const context = decodeGateContext(value.context).raw;
795
- const rawDenial = context.denial;
796
- const denial = rawDenial === undefined
797
- ? undefined
798
- : (() => {
799
- const parsed = exactObject(rawDenial, ["stage", "code"]);
800
- const stage = sanitizeNativeDiagnosticText(requiredString(parsed.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
801
- const code = sanitizeNativeDiagnosticText(requiredString(parsed.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
802
- if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence");
803
- return { stage, code };
804
- })();
805
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
806
- } catch { return undefined; }
807
- }
808
-
809
890
  // Rebuild diagnostics from a duplicated module instance before facade output.
810
891
  export function sanitizeForeignNativeReviewDiagnostics(value ) {
811
892
  try {
812
- const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "stderr", "denial"]);
893
+ const raw = exactObject(value, ["operation", "error_code", "timed_out", "output_limit_exceeded"], ["exit_code", "signal", "max_buffer_bytes", "configuration_hint", "stderr"]);
813
894
  const operation = enumString(raw.operation, Object.values(NATIVE_REVIEW_OPERATION)) ;
814
895
  const errorCode = enumString(raw.error_code, Object.values(NATIVE_REVIEW_ERROR_CODE)) ;
815
896
  const signal = raw.signal === undefined ? undefined : requiredString(raw.signal);
816
- if (signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) return undefined;
817
- return { operation, error_code: errorCode, ...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }), ...(signal === undefined ? {} : { signal: signal }), timed_out: booleanValue(raw.timed_out), output_limit_exceeded: booleanValue(raw.output_limit_exceeded), ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }), ...(raw.denial === undefined ? {} : { denial: sanitizeForeignStructuredDenial(raw.denial) }) };
897
+ const maxBufferBytes = raw.max_buffer_bytes === undefined ? undefined : positiveInteger(raw.max_buffer_bytes);
898
+ const configurationHint = raw.configuration_hint === undefined ? undefined : stringValue(raw.configuration_hint);
899
+ if (
900
+ (signal !== undefined && !/^SIG[A-Z0-9]{1,12}$/.test(signal)) ||
901
+ (maxBufferBytes === undefined) !== (configurationHint === undefined) ||
902
+ (configurationHint !== undefined && (errorCode !== NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT || configurationHint !== NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT))
903
+ ) return undefined;
904
+ return {
905
+ operation,
906
+ error_code: errorCode,
907
+ ...(raw.exit_code === undefined ? {} : { exit_code: nonNegativeInteger(raw.exit_code) }),
908
+ ...(signal === undefined ? {} : { signal: signal }),
909
+ timed_out: booleanValue(raw.timed_out),
910
+ output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
911
+ ...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
912
+ ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
913
+ };
818
914
  } catch { return undefined; }
819
915
  }
820
916
 
821
- function sanitizeForeignStructuredDenial(value ) {
822
- const parsed = exactObject(value, ["schema", "result", "action", "reason"], ["denial"]);
823
- if (parsed.schema !== "gentle-ai.review-gate-result/v1") throw new Error("invalid denial schema");
824
- const result = enumString(parsed.result, ["scope-changed", "invalidated", "escalated"] ) ;
825
- const action = sanitizeNativeDiagnosticText(requiredString(parsed.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
826
- const reason = sanitizeNativeDiagnosticText(requiredString(parsed.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
827
- const expectedAction = { "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[result];
828
- if (action !== expectedAction || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("non-canonical denial evidence");
829
- 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 }; })();
830
- return { schema: "gentle-ai.review-gate-result/v1", result, action, reason, ...(denial === undefined ? {} : { denial }) };
831
- }
832
-
833
- function nativeProcessDiagnostics(operation , code , result ) {
917
+ function nativeProcessDiagnostics(operation , code , result , maxBufferBytes ) {
918
+ const outputLimitExceeded = result?.outputLimitExceeded === true;
834
919
  return {
835
920
  operation,
836
921
  error_code: code,
837
922
  ...(result === undefined ? {} : { exit_code: result.exitCode }),
838
923
  ...(result?.signal === null || result?.signal === undefined ? {} : { signal: result.signal }),
839
- timed_out: result?.timedOut === true,
840
- output_limit_exceeded: result?.outputLimitExceeded === true,
924
+ timed_out: !outputLimitExceeded && result?.timedOut === true,
925
+ output_limit_exceeded: outputLimitExceeded,
926
+ ...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
927
+ ? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
928
+ : {}),
841
929
  ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
842
- ...(result === undefined ? {} : { denial: parseStructuredNativeDenial(result.stdout) }),
843
930
  };
844
931
  }
845
932
 
@@ -858,39 +945,13 @@ function decodeLegacyReconcileAudit(value )
858
945
  for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
859
946
  return { record };
860
947
  }
861
- function decodeNativeReviewVerificationEvidence(value ) {
862
- 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"]);
863
- if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
864
- return {
865
- schema: "gentle-ai.review-verification-evidence/v2",
866
- // verification-evidence.schema.json pins `version` to {"const": 2} -- the
867
- // NUMBER two, not the string. It was written as a string from a handoff
868
- // that listed the observed field NAMES without their types.
869
- version: (() => { if (body.version !== 2) throw new Error(`native verification evidence version must be the number 2, received ${JSON.stringify(body.version)}`); return 2 ; })(),
870
- lineageId: requiredString(body.lineage_id),
871
- authorityRevision: requiredString(body.authority_revision),
872
- targetIdentity: requiredString(body.target_identity),
873
- candidateTree: requiredString(body.candidate_tree),
874
- pathsDigest: requiredString(body.paths_digest),
875
- paths: stringArray(body.paths),
876
- // The schema requires ledger_ids to be an array, but v2.2.2 emits null when
877
- // there are none. Tolerated deliberately: refusing here would reject a
878
- // response the provider actually sends, and the provider violating its own
879
- // published schema is its defect to fix, not a reason to break the client.
880
- ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
881
- rawPayloadSha256: requiredString(body.raw_payload_sha256),
882
- rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
883
- outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) ,
884
- recordDigest: requiredString(body.record_digest),
885
- };
886
- }
887
948
  // Unimplemented next_transition.execute.operation values must never reach
888
949
  // argv synthesis. Checked against the raw pre-decode body so an operation
889
950
  // gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
890
951
  // named, typed refusal instead of a generic schema-incompatible error, and
891
952
  // before any client ever tries to build an invocation for it (Design
892
953
  // Decision #6, migrate-review-integration-v2).
893
- const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"]);
954
+ const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.recover", "review.repair", "review.acknowledge-approved"]);
894
955
  function assertSupportedNextTransitionOperation(body ) {
895
956
  const nextTransition = body.next_transition;
896
957
  if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
@@ -904,11 +965,23 @@ function assertSupportedNextTransitionOperation(body )
904
965
  function decode (operation , mutating , callback , diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)) {
905
966
  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 }); }
906
967
  }
968
+ function decodeReviewStartResponse(value ) {
969
+ const body = object(value);
970
+ return body.schema === "gentle-ai.review-integration.start/v4"
971
+ ? decodeReviewStartV4(body)
972
+ : decodeReviewStartV3(body);
973
+ }
907
974
  function decodeReleaseEvidence(value ) {
908
975
  const release = exactObject(value, ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "publication_state", "evidence_freshness_hash", "evidence_freshness_state"]);
909
976
  for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
910
977
  if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
911
978
  }
979
+ function decodeNonDecidingGateContext(value , expectedGate ) {
980
+ const context = exactObject(value, ["gate"]);
981
+ const gate = enumString(context.gate, NATIVE_GATE);
982
+ if (gate !== expectedGate) throw new Error("native non-deciding gate context does not match the requested gate");
983
+ return { lineageId: "", storeRevision: "", raw: context };
984
+ }
912
985
  function decodeGateContext(value ) {
913
986
  const context = exactObject(
914
987
  value,
@@ -1001,7 +1074,7 @@ function decodeNativeReviewStatusDiagnostic(value )
1001
1074
  return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
1002
1075
  }
1003
1076
  function decodeNativeReviewModeStatus(value ) {
1004
- const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
1077
+ const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision", "reach"]);
1005
1078
  if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
1006
1079
  return {
1007
1080
  global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) ,
@@ -1009,6 +1082,7 @@ function decodeNativeReviewModeStatus(value ) {
1009
1082
  effective: enumString(status.effective, ["on", "off"]) ,
1010
1083
  source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) ,
1011
1084
  ...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
1085
+ ...(status.reach === undefined ? {} : { reach: enumString(status.reach, Object.values(NATIVE_REVIEW_MODE_REACH)) }),
1012
1086
  };
1013
1087
  }
1014
1088
 
@@ -1151,12 +1225,11 @@ function hasValidLensesRequired(action , state , riskLe
1151
1225
  if (riskLevel === "low") return !lensesRequired;
1152
1226
  if (action === NATIVE_START_ACTION.CREATED) return state === "reviewing" && lensesRequired;
1153
1227
  if (action === NATIVE_START_ACTION.RESUMED) return !lensesRequired || state === "reviewing";
1154
- if (action === NATIVE_START_ACTION.REUSE_RECEIPT) return state === "approved" && !lensesRequired;
1155
1228
  return !lensesRequired;
1156
1229
  }
1157
1230
 
1158
- function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord ) {
1159
- return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result), auditRecord);
1231
+ function nativeError(code , operation , mutating , message , result , launchAttempted = true, auditRecord , maxBufferBytes ) {
1232
+ return new NativeReviewCliError(code, operation, launchAttempted, mutating, message, nativeProcessDiagnostics(operation, code, result, maxBufferBytes), auditRecord);
1160
1233
  }
1161
1234
 
1162
1235
 
@@ -1165,13 +1238,13 @@ function nativeError(code , operation
1165
1238
 
1166
1239
 
1167
1240
 
1168
- export class NativeReviewCliV214 {
1241
+ class NativeReviewPlainCli {
1169
1242
  adapter ;
1170
1243
  executable ;
1171
1244
  timeoutMs ;
1172
1245
  maxBufferBytes ;
1173
1246
  cleanupDirectory ;
1174
- constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = 1024 * 1024, cleanupDirectory = (directory ) => rm(directory, { recursive: true, force: true })) {
1247
+ constructor(adapter , executable = resolveGentleAiBinary, timeoutMs = 30_000, maxBufferBytes = resolveNativeReviewMaxBufferBytes(), cleanupDirectory = (directory ) => rm(directory, { recursive: true, force: true })) {
1175
1248
  if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
1176
1249
  this.adapter = adapter;
1177
1250
  this.executable = executable;
@@ -1208,165 +1281,17 @@ export class NativeReviewCliV214 {
1208
1281
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
1209
1282
  }
1210
1283
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1284
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1211
1285
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1212
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
1213
1286
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1214
- const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
1215
1287
  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;
1216
1288
  const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
1217
- if (result.exitCode !== 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1218
- if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1289
+ if (result.exitCode !== 0 && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1290
+ if (result.stderr.trim().length > 0 && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1219
1291
  return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
1220
1292
  }
1221
1293
 
1222
- async verifyVersion(cwd , signal , capabilities ) {
1223
- let result ;
1224
- try { result = await this.adapter({ file: this.executablePath(NATIVE_REVIEW_OPERATION.VERSION, false), arguments: ["version"], cwd, timeoutMs: this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
1225
- catch (error) {
1226
- if (error instanceof NativeReviewCliError) throw error;
1227
- if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was cancelled");
1228
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, NATIVE_REVIEW_OPERATION.VERSION, false, "gentle-ai is unavailable");
1229
- }
1230
- if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process timed out", result);
1231
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, NATIVE_REVIEW_OPERATION.VERSION, false, "version process output exceeded limit", result);
1232
- if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
1233
- if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
1234
- const version = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(result.stdout.replace(/\r\n$/, "\n"))?.[1];
1235
- const contract = version === undefined ? undefined : resolvedNativeCliContract(version);
1236
- if (result.stderr.trim().length > 0 || contract === undefined || capabilities.some((capability) => !contract[capability])) throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `native gentle-ai lacks required capabilities: expected v${GENTLE_AI_VERSION}, found v${version ?? "unparseable"}`);
1237
- return version ;
1238
- }
1239
-
1240
- async start(request ) {
1241
- if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1242
- if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
1243
- if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1244
- if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1245
- const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
1246
- const toleratedStderr = resolvedNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
1247
- const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", "--cwd", request.cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...(request.lineageId ? ["--lineage", request.lineageId] : []), ...(request.policyPath ? ["--policy", request.policyPath] : []), ...(request.focus ? ["--focus", request.focus] : [])], true, request.signal, toleratedStderr);
1248
- return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1249
- // `target_identity` and `lens_bindings` are real, unconditionally-present
1250
- // fields on gentle-ai's plain (non-negotiated) `review start` JSON output
1251
- // (confirmed against the pinned v2.1.11 Go source and live against a dev
1252
- // build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
1253
- // tolerated here but not consumed: Pi's negotiated-contract client
1254
- // (NativeReviewCliV216, the production default) carries lineage/target
1255
- // identity through its own `review-integration/v1` decoder instead.
1256
- 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"]);
1257
- if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE ).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
1258
- const lineageId = requiredString(body.lineage_id);
1259
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
1260
- const riskLevel = requiredString(body.risk_level);
1261
- const action = enumString(body.action, NATIVE_START_ACTION_VALUES) ;
1262
- const lensesRequired = booleanValue(body.lenses_required);
1263
- const selectedLenses = decodeSelectedLenses(body.selected_lenses, riskLevel, lensesRequired);
1264
- if (
1265
- !(NATIVE_RISK_LEVEL ).includes(riskLevel) ||
1266
- selectedLenses.some((lens) => !(NATIVE_REVIEW_LENS ).includes(lens)) ||
1267
- !hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
1268
- !hasValidLensesRequired(action, body.state , riskLevel, lensesRequired)
1269
- ) throw new Error("unknown or contradictory start enum");
1270
- return {
1271
- lineageId, state: body.state , riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired,
1272
- ...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
1273
- ...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
1274
- };
1275
- });
1276
- }
1277
-
1278
- async stageDocument(directory , name , document ) {
1279
- const path = join(directory, `${name}.json`);
1280
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
1281
- await chmod(path, 0o600);
1282
- return path;
1283
- }
1284
- async stageEvidence(directory , evidence ) {
1285
- const path = join(directory, "evidence.txt");
1286
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
1287
- await chmod(path, 0o600);
1288
- return path;
1289
- }
1290
-
1291
- async finalize(request ) {
1292
- if (request.evidenceDocument !== undefined && (typeof request.evidenceDocument !== "string" || request.evidenceDocument.length === 0)) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
1293
- await this.verifyVersion(request.cwd, request.signal, ["finalize"]);
1294
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
1295
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
1296
- try {
1297
- if (directory) await chmod(directory, 0o700);
1298
- const resultFiles = directory && request.lensResults ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
1299
- const refuterFile = directory && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
1300
- const validationFile = directory && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
1301
- const evidenceFile = directory && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
1302
- 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);
1303
- return decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => {
1304
- const body = exactObject(result, ["operation", "lineage_id", "state", "action", "store_revision"], ["receipt_path"]);
1305
- if (body.operation !== "review/finalize") throw new Error("wrong finalize discriminator");
1306
- const lineageId = requiredString(body.lineage_id);
1307
- if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
1308
- const state = requiredString(body.state);
1309
- if (!(NATIVE_FINALIZE_STATE ).includes(state)) throw new Error("unknown finalize state");
1310
- return { lineageId, state, action: requiredString(body.action), storeRevision: requiredString(body.store_revision), ...(body.receipt_path === undefined ? {} : { receiptPath: requiredString(body.receipt_path) }) };
1311
- });
1312
- } finally { if (directory) await this.cleanupDirectory(directory).catch(() => undefined); }
1313
- }
1314
-
1315
- async validate(request ) {
1316
- await this.verifyVersion(request.cwd, request.signal, ["validate"]);
1317
- 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);
1318
- return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
1319
- const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1320
- const gateResult = enumString(body.result, NATIVE_GATE_RESULT) ;
1321
- const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1322
- const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1323
- // `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
1324
- // discriminator: when present it must be the single literal
1325
- // "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
1326
- // kill switch is off and no receipt governs the candidate), paired
1327
- // exactly with result:invalidated, allowed:false, action:repository-policy,
1328
- // and exit 0 — never the strict continue/create-new-lineage/
1329
- // explicit-maintainer-action/stop pairing used when delivery is absent.
1330
- const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) );
1331
- if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
1332
- if (delivery !== undefined) {
1333
- if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
1334
- } else {
1335
- const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
1336
- const expectedExitCode = gateResult === "allow" ? 0 : 1;
1337
- if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
1338
- }
1339
- const gateContext = decodeGateContext(body.context);
1340
- const returnedGate = gateContext.raw.gate;
1341
- if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
1342
- 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");
1343
- return { allowed: body.allowed , result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
1344
- });
1345
- }
1346
-
1347
- async bindSdd(request ) {
1348
- await this.verifyVersion(request.cwd, request.signal, ["bindSdd"]);
1349
- 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);
1350
- return decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => {
1351
- const body = exactObject(result, ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1352
- 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");
1353
- const receiptHash = requiredString(body.receipt_hash);
1354
- const gateContext = decodeGateContext(body.gate_context);
1355
- const authorityRevision = requiredString(body.authority_revision);
1356
- 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");
1357
- return {
1358
- revision: requiredString(body.revision),
1359
- change: requiredString(body.change),
1360
- lineage: requiredString(body.lineage),
1361
- authorityRevision,
1362
- receiptHash,
1363
- gateContext,
1364
- };
1365
- });
1366
- }
1367
-
1368
1294
  async reviewStatus(request ) {
1369
- await this.verifyVersion(request.cwd, request.signal, ["status", "inventory"]);
1370
1295
  const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, ["review", "status", "--cwd", request.cwd], false, request.signal);
1371
1296
  const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(result));
1372
1297
  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");
@@ -1380,7 +1305,6 @@ export class NativeReviewCliV214 {
1380
1305
  // never mutates the operator's global gentle-ai state across other clones.
1381
1306
  async reviewMode(request ) {
1382
1307
  const cwd = await canonicalNativeReviewCwd(request.cwd);
1383
- await this.verifyVersion(cwd, request.signal, ["mode"]);
1384
1308
  const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
1385
1309
  const { body } = await this.execute(
1386
1310
  NATIVE_REVIEW_OPERATION.MODE,
@@ -1392,77 +1316,10 @@ export class NativeReviewCliV214 {
1392
1316
  return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
1393
1317
  }
1394
1318
 
1395
- async sddStatus(request ) {
1396
- await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
1397
- 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);
1398
- return decode(NATIVE_REVIEW_OPERATION.SDD_STATUS, false, () => {
1399
- const body = exactObject(result, ["schemaName", "schemaVersion", "changeName", "artifactStore", "planningHome", "changeRoot", "artifactPaths", "contextFiles", "artifacts", "taskProgress", "dependencies", "applyState", "actionContext", "relationships", "remediationState", "nextRecommended", "blockedReasons"], ["reviewGate", "reviewTransaction", "phaseInstructions"]);
1400
- 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");
1401
- const paths = ["proposal", "specs", "design", "tasks", "applyProgress", "verifyReport", "reviewPolicy", "reviewLedger", "reviewReceipt", "reviewBundle", "reviewContext", "reviewState"];
1402
- const pathMap = (value ) => { const parsed = exactObject(value, paths); for (const path of paths) stringArray(parsed[path]); };
1403
- const planningHome = exactObject(body.planningHome, ["mode", "path"]);
1404
- if (planningHome.mode !== "repo-local") throw new Error("invalid planning home");
1405
- requiredString(planningHome.path); requiredString(body.changeRoot); pathMap(body.artifactPaths); pathMap(body.contextFiles);
1406
- const artifactStates = paths.filter((path) => path !== "reviewPolicy" || body.artifactStore === NATIVE_SDD_ARTIFACT_STORE.ENGRAM);
1407
- const artifacts = exactObject(body.artifacts, artifactStates);
1408
- for (const path of artifactStates) if (!Object.values(NATIVE_SDD_ARTIFACT_STATE).includes(artifacts[path] )) throw new Error("invalid artifact state");
1409
- const taskProgress = exactObject(body.taskProgress, ["total", "completed", "pending", "allComplete"]);
1410
- const total = nonNegativeInteger(taskProgress.total), completed = nonNegativeInteger(taskProgress.completed), pending = nonNegativeInteger(taskProgress.pending);
1411
- if (typeof taskProgress.allComplete !== "boolean" || completed + pending !== total || taskProgress.allComplete !== (pending === 0)) throw new Error("invalid task progress");
1412
- const dependencies = exactObject(body.dependencies, ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"]);
1413
- 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");
1414
- const actionContext = exactObject(body.actionContext, ["mode", "workspaceRoot", "allowedEditRoots"]);
1415
- if (actionContext.mode !== "repo-local" || requiredString(actionContext.workspaceRoot).length === 0 || stringArray(actionContext.allowedEditRoots).length === 0) throw new Error("invalid action context");
1416
- const relationships = exactObject(body.relationships, ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]);
1417
- for (const field of ["dependsOn", "supersedes", "amends", "conflictsWith", "sameDomainActiveChanges"]) stringArray(relationships[field]);
1418
- const remediation = exactObject(body.remediationState, ["required", "complete", "failedEvidenceRevision", "lineageId", "generation", "fixBatch", "reason"], ["correctionBudget"]);
1419
- if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || ["failedEvidenceRevision", "lineageId", "reason"].some((field) => typeof remediation[field] !== "string")) throw new Error("invalid remediation state");
1420
- nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
1421
- if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
1422
- let reviewGateResult ;
1423
- let reviewGateDelivery ;
1424
- if (body.reviewGate !== undefined) {
1425
- // `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
1426
- // when the kill switch is off: gentle-ai's status struct carries
1427
- // `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
1428
- // without it rejects that payload outright. Pre-existing bug fix
1429
- // (Phase 13.13, migrate-review-integration-v2).
1430
- const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
1431
- reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
1432
- if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
1433
- }
1434
- if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
1435
- if (body.phaseInstructions !== undefined) {
1436
- const instructions = exactObject(body.phaseInstructions, ["apply", "verify", "remediate", "archive"]);
1437
- for (const phase of ["apply", "verify", "remediate", "archive"]) stringArray(instructions[phase]);
1438
- }
1439
- const nextRecommended = requiredString(body.nextRecommended);
1440
- if (!(NATIVE_SDD_NEXT_ACTION ).includes(nextRecommended)) throw new Error("unknown SDD next action");
1441
- const blockedReasons = stringArray(body.blockedReasons);
1442
- return {
1443
- ...body,
1444
- artifactStore: body.artifactStore ,
1445
- artifacts: artifacts ,
1446
- nextRecommended,
1447
- ready:
1448
- (NATIVE_SDD_POST_REVIEW_ACTION ).includes(nextRecommended) &&
1449
- blockedReasons.length === 0 &&
1450
- // With the kill switch off, the gate result can never become
1451
- // "allow" (delivery follows ordinary repository policy instead) —
1452
- // gated only on `result === "allow"` this deadlocked `ready`
1453
- // forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
1454
- // `disabled/unmanaged` delivery is the legitimate non-blocking
1455
- // terminal state and must also unblock readiness.
1456
- (reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
1457
- };
1458
- });
1459
- }
1460
-
1461
1319
  async reclaim(request ) {
1462
1320
  for (const [name, value] of [["lineage", request.lineage], ["actor", request.actor], ["reason", request.reason]] ) {
1463
1321
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native RECLAIM ${name} must be a non-empty, trimmed, NUL-free string`);
1464
1322
  }
1465
- await this.verifyVersion(request.cwd, request.signal, ["reclaim"]);
1466
1323
  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);
1467
1324
  return { record: body };
1468
1325
  }
@@ -1483,7 +1340,6 @@ export class NativeReviewCliV214 {
1483
1340
  throw new TypeError("Native RECOVER maintainerAuthorization must be a non-empty LF-only binding");
1484
1341
  }
1485
1342
  if (!(NATIVE_REVIEW_RECOVER_DISPOSITION ).includes(request.disposition)) throw new TypeError("Native RECOVER disposition must be scope_changed, invalidated, or escalated");
1486
- await this.verifyVersion(request.cwd, request.signal, ["recover"]);
1487
1343
  const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECOVER, request.cwd, [
1488
1344
  "review", "recover", "--cwd", request.cwd,
1489
1345
  "--predecessor-lineage", request.predecessorLineage,
@@ -1501,8 +1357,11 @@ export class NativeReviewCliV214 {
1501
1357
  for (const [name, value] of [["lineage", request.lineage], ["expectedRevision", request.expectedRevision], ["snapshotIdentity", request.snapshotIdentity], ["actor", request.actor], ["reason", request.reason]] ) {
1502
1358
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
1503
1359
  }
1504
- if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, actor, and reason binding");
1505
- await this.verifyVersion(request.cwd, request.signal, ["abandon"]);
1360
+ if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
1361
+ for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] ) {
1362
+ if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1363
+ }
1364
+ if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1506
1365
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1507
1366
  "review", "abandon", "--cwd", request.cwd,
1508
1367
  "--lineage", request.lineage,
@@ -1522,7 +1381,6 @@ export class NativeReviewCliV214 {
1522
1381
  }
1523
1382
  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");
1524
1383
  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");
1525
- await this.verifyVersion(request.cwd, request.signal, ["quarantineLegacy"]);
1526
1384
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, request.cwd, [
1527
1385
  "review", "quarantine-legacy", "--cwd", request.cwd,
1528
1386
  "--lineage", request.lineage,
@@ -1554,7 +1412,6 @@ export class NativeReviewCliV214 {
1554
1412
  if (request.maintainerAuthorization !== expectedAuthorization) {
1555
1413
  throw new TypeError("Native RECONCILE_AUTHORITY maintainerAuthorization must match the exact target and revision binding");
1556
1414
  }
1557
- const version = await this.verifyVersion(request.cwd, request.signal, ["reconcileAuthority"]);
1558
1415
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, request.cwd, [
1559
1416
  "review", "reconcile-authority", "--cwd", request.cwd,
1560
1417
  "--predecessor-lineage", request.predecessorLineage,
@@ -1565,9 +1422,7 @@ export class NativeReviewCliV214 {
1565
1422
  "--reason", request.reason,
1566
1423
  "--maintainer-authorization", request.maintainerAuthorization,
1567
1424
  ], true, request.signal);
1568
- const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => version === "2.1.8"
1569
- ? decodeLegacyReconcileAudit(execution.body)
1570
- : decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1425
+ const result = decode(NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, true, () => decodeNativeMaintenanceResult(execution.body, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY));
1571
1426
  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);
1572
1427
  return result;
1573
1428
  }
@@ -1578,7 +1433,6 @@ export class NativeReviewCliV214 {
1578
1433
  }
1579
1434
  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");
1580
1435
  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");
1581
- await this.verifyVersion(request.cwd, request.signal, ["repairLegacyAlias"]);
1582
1436
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS, request.cwd, [
1583
1437
  "review", "repair-legacy-alias", "--cwd", request.cwd,
1584
1438
  "--lineage", request.lineage,
@@ -1595,14 +1449,17 @@ export class NativeReviewCliV214 {
1595
1449
  }
1596
1450
  }
1597
1451
 
1598
- export function nativeReviewAbandonAuthorization(request ) {
1452
+ export function nativeReviewAbandonAuthorization(request ) {
1599
1453
  return [
1600
- "gentle-ai.review-abandon-authorization/v1",
1454
+ "gentle-ai.review-abandon-authorization/v2",
1601
1455
  `lineage=${request.lineage}`,
1602
1456
  `revision=${request.expectedRevision}`,
1603
1457
  `snapshot_identity=${request.snapshotIdentity}`,
1604
- `actor=${request.actor}`,
1605
1458
  `reason=${request.reason}`,
1459
+ `captured_lens_results=${request.capturedLensResults.join(",")}`,
1460
+ `findings_present=${request.findingsPresent}`,
1461
+ `evidence_records_present=${request.evidenceRecordsPresent}`,
1462
+ `actor=${request.actor.trim()}`,
1606
1463
  ].join("\n");
1607
1464
  }
1608
1465
 
@@ -1619,6 +1476,32 @@ export function nativeReviewLegacyQuarantineAuthorization(request
1619
1476
  ].join("\n");
1620
1477
  }
1621
1478
 
1479
+ /**
1480
+ * The exact native `gentle-ai.review-recovery-authorization/v1` binding for one
1481
+ * recovery edge.
1482
+ *
1483
+ * Native `review recover` accepts a caller-supplied authorization only when it
1484
+ * reproduces this binding byte for byte, because it is copied verbatim into the
1485
+ * recovery provenance and read afterwards as a maintainer attestation. Pi
1486
+ * therefore derives it from freshly read native target status and never
1487
+ * forwards a caller-supplied one: a wrong binding is worse than an absent one,
1488
+ * since an absent field cannot lie about who approved what.
1489
+ *
1490
+ * `targetIdentity` is the live target identity the provider itself publishes in
1491
+ * the `review.recover` eligibility binding (`status.target_identity`), which is
1492
+ * the identity the successor's initial snapshot takes.
1493
+ */
1494
+ export function nativeReviewRecoverAuthorization(request ) {
1495
+ return [
1496
+ "gentle-ai.review-recovery-authorization/v1",
1497
+ `predecessor_lineage=${request.predecessorLineage}`,
1498
+ `predecessor_revision=${request.expectedPredecessorRevision}`,
1499
+ `target_identity=${request.targetIdentity}`,
1500
+ `actor=${request.actor.trim()}`,
1501
+ `reason=${request.reason.trim()}`,
1502
+ ].join("\n");
1503
+ }
1504
+
1622
1505
  export function nativeReviewReconcileAuthorization(request ) {
1623
1506
  return [
1624
1507
  "gentle-ai.review-reconcile-authorization/v1",
@@ -1659,15 +1542,16 @@ export class NativeReviewIntegrationError extends Error {
1659
1542
  }
1660
1543
  }
1661
1544
 
1662
- // Raised when negotiated START answers `consent/v2` (action:
1545
+ // Raised when negotiated START answers a consent question (`consent/v2` from
1546
+ // the pinned line, `consent/v3` from gentle-ai >= 2.3.0; action:
1663
1547
  // "consent_required") instead of `start/v3`. The provider has frozen no
1664
1548
  // authority yet: Pi must relay this complete candidate-scoped question and may
1665
1549
  // answer only through one of the exact invocations carried by the envelope.
1666
1550
  export class NativeReviewConsentRequiredError extends Error {
1667
- consent ;
1551
+ consent ;
1668
1552
  launchAttempted = true;
1669
1553
  mutationOutcome = "none";
1670
- constructor(consent ) {
1554
+ constructor(consent ) {
1671
1555
  super(consent.headline);
1672
1556
  this.name = "NativeReviewConsentRequiredError";
1673
1557
  this.consent = consent;
@@ -1751,7 +1635,44 @@ function exactConsentOption(arguments_ , name )
1751
1635
  return values[0] ;
1752
1636
  }
1753
1637
 
1638
+ // A Pi consent envelope is bound to Pi only when every exact provider replay
1639
+ // path carries exactly one `--agent pi` option. The outer envelope agent alone
1640
+ // cannot bind an omitted, embedded, duplicated, or conflicting invocation.
1641
+ function validatePiConsentChoiceAgentBindings(consent ) {
1642
+ if (!("agent" in consent) || consent.agent !== "pi") return;
1643
+ for (const choice of consent.choices) {
1644
+ const arguments_ = splitNativeConsentInvocation(choice.invocation).slice(1);
1645
+ if (exactConsentOption(arguments_, "--agent") !== "pi") {
1646
+ throw new NativeReviewConsentBindingError("consent-invocation-agent-changed", "Native Pi consent invocation agent binding changed");
1647
+ }
1648
+ }
1649
+ }
1650
+
1651
+ function optionalConsentLineageOption(arguments_ ) {
1652
+ const values = [];
1653
+ for (let index = 0; index < arguments_.length; index += 1) {
1654
+ const token = arguments_[index] ;
1655
+ if (token === "--lineage") {
1656
+ const value = arguments_[index + 1];
1657
+ if (value === undefined || value.startsWith("--")) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is missing its value");
1658
+ values.push(value);
1659
+ index += 1;
1660
+ } else if (token.startsWith("--lineage=")) values.push(token.slice("--lineage=".length));
1661
+ }
1662
+ if (values.length > 1) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation permits at most one --lineage");
1663
+ if (values.length === 0) return undefined;
1664
+ const lineageId = values[0] ;
1665
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(lineageId)) throw new NativeReviewConsentBindingError("consent-invocation-option-invalid", "Native consent invocation --lineage is malformed");
1666
+ return lineageId;
1667
+ }
1668
+
1669
+
1670
+
1671
+
1672
+
1673
+
1754
1674
  function consentInvocationArguments(request ) {
1675
+ validatePiConsentChoiceAgentBindings(request.consent);
1755
1676
  const choice = request.consent.choices.find((candidate) => candidate.answer === request.answer);
1756
1677
  if (choice === undefined) throw new NativeReviewConsentBindingError("consent-answer-unknown", "Native consent answer must be granted or declined");
1757
1678
  const words = splitNativeConsentInvocation(choice.invocation);
@@ -1761,8 +1682,9 @@ function consentInvocationArguments(request )
1761
1682
  if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1762
1683
  if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
1763
1684
  if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
1685
+ const lineageId = optionalConsentLineageOption(arguments_);
1764
1686
  if (exactConsentOption(arguments_, "--consent") !== request.answer || arguments_.at(-1) !== request.answer) throw new NativeReviewConsentBindingError("consent-invocation-answer-changed", "Native consent invocation answer binding changed");
1765
- return arguments_;
1687
+ return { arguments_, ...(lineageId === undefined ? {} : { lineageId }) };
1766
1688
  }
1767
1689
 
1768
1690
  function decodeDeclinedConsentStart(value , expected ) {
@@ -1786,24 +1708,7 @@ function decodeDeclinedConsentStart(value , expected
1786
1708
  }
1787
1709
 
1788
1710
 
1789
- const nativeCapabilitiesByDigest = new Map ();
1790
1711
 
1791
- export function clearNativeReviewCapabilitiesCacheForTesting() {
1792
- nativeCapabilitiesByDigest.clear();
1793
- }
1794
-
1795
- function defaultExecutableDigest(path ) {
1796
- const before = statSync(path);
1797
- const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
1798
- const after = statSync(path);
1799
- if (
1800
- before.dev !== after.dev ||
1801
- before.ino !== after.ino ||
1802
- before.size !== after.size ||
1803
- before.mtimeMs !== after.mtimeMs
1804
- ) throw new Error("native review executable changed during capability verification");
1805
- return digest;
1806
- }
1807
1712
 
1808
1713
 
1809
1714
 
@@ -1811,45 +1716,62 @@ function defaultExecutableDigest(path ) {
1811
1716
 
1812
1717
 
1813
1718
  function decodeNativeAdmittedResultManifest(value ) {
1814
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native capture-result manifest must be an object");
1719
+ // The admission answer routes through the exact-identity forward decoder
1720
+ // (decoder-freshness discipline): the complete live envelope — identity
1721
+ // constants, binding fields, and the exactly-one-locator rule — is
1722
+ // validated before anything is handed to FINALIZE, and an unknown field
1723
+ // grown by gentle-ai main is rejected instead of silently dropped.
1724
+ const artifact = decodeReviewResultArtifactV2(value);
1725
+ return Object.freeze({
1726
+ schema: artifact.schema,
1727
+ subjectHash: artifact.subjectHash,
1728
+ admissionDecision: artifact.admissionDecision,
1729
+ lens: artifact.lens,
1730
+ ...(artifact.path === undefined ? {} : { path: artifact.path }),
1731
+ ...(artifact.reference === undefined ? {} : { reference: artifact.reference }),
1732
+ });
1733
+ }
1734
+
1735
+ // gentle-pi#311 P4-roles: the strict acknowledgement for one executed
1736
+ // provider role vector. The immutable verdict bytes live in the Go-owned
1737
+ // compact store slot; this envelope only names the binding the capture
1738
+ // proved, so anything beyond that exact shape is refused.
1739
+ function decodeNativeProviderRoleCaptureArtifact(value ) {
1740
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native provider role capture artifact must be an object");
1815
1741
  const body = value ;
1742
+ const allowed = new Set(["schema", "lineage_id", "target_identity", "role", "captured"]);
1743
+ for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native provider role capture artifact carries unexpected key ${key}`);
1816
1744
  const text = (key ) => {
1817
1745
  const found = body[key];
1818
- if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native capture-result manifest ${key} must be a non-empty trimmed string`);
1746
+ if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native provider role capture artifact ${key} must be a non-empty trimmed string`);
1819
1747
  return found;
1820
1748
  };
1821
- const schema = text("schema");
1822
- if (schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`native capture-result manifest schema must be gentle-ai.review-result-artifact/v2, received ${schema}`);
1823
- const admission = text("admission_decision");
1824
- if (admission !== "completed") throw new TypeError(`native capture-result manifest admission_decision must be completed, received ${admission}`);
1825
- // Exactly one locator: a provider-owned path OR an opaque reference. Both or
1826
- // neither means the manifest cannot be handed to FINALIZE.
1827
- const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
1828
- if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
1749
+ if (text("schema") !== NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA) throw new TypeError(`native provider role capture artifact schema must be ${NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA}`);
1750
+ const role = text("role");
1751
+ if (role !== "refuter" && role !== "targeted-validator") throw new TypeError(`native provider role capture artifact role must be refuter or targeted-validator, received ${role}`);
1752
+ if (body.captured !== true) throw new TypeError("native provider role capture artifact must report captured: true");
1829
1753
  return Object.freeze({
1830
- schema,
1831
- subjectHash: text("subject_hash"),
1832
- admissionDecision: admission,
1833
- ...(body.lens === undefined ? {} : { lens: text("lens") }),
1834
- ...(hasPath ? { path: text("path") } : { reference: text("reference") }),
1754
+ schema: NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_SCHEMA,
1755
+ lineageId: text("lineage_id"),
1756
+ targetIdentity: text("target_identity"),
1757
+ role,
1758
+ captured: true,
1835
1759
  });
1836
1760
  }
1837
1761
 
1838
1762
  export class NativeReviewCliV216 {
1839
- legacy ;
1763
+ plain ;
1840
1764
  adapter ;
1841
1765
  executable ;
1842
1766
  timeoutMs ;
1843
1767
  maxBufferBytes ;
1844
1768
  cleanupDirectory ;
1845
- executableDigest ;
1846
1769
  constructor(
1847
1770
  adapter ,
1848
1771
  executable = resolveGentleAiBinary,
1849
1772
  timeoutMs = 30_000,
1850
- maxBufferBytes = 1024 * 1024,
1773
+ maxBufferBytes = resolveNativeReviewMaxBufferBytes(),
1851
1774
  cleanupDirectory = (directory) => rm(directory, { recursive: true, force: true }),
1852
- executableDigest = defaultExecutableDigest,
1853
1775
  ) {
1854
1776
  if (typeof executable === "string" && (!isAbsolute(executable) || executable === "gentle-ai")) throw new TypeError("Native review requires an absolute package-local executable");
1855
1777
  this.adapter = adapter;
@@ -1857,8 +1779,7 @@ export class NativeReviewCliV216 {
1857
1779
  this.timeoutMs = timeoutMs;
1858
1780
  this.maxBufferBytes = maxBufferBytes;
1859
1781
  this.cleanupDirectory = cleanupDirectory;
1860
- this.executableDigest = executableDigest;
1861
- this.legacy = new NativeReviewCliV214(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
1782
+ this.plain = new NativeReviewPlainCli(adapter, executable, timeoutMs, maxBufferBytes, cleanupDirectory);
1862
1783
  }
1863
1784
 
1864
1785
  executablePath(operation , mutating ) {
@@ -1872,15 +1793,6 @@ export class NativeReviewCliV216 {
1872
1793
  }
1873
1794
  }
1874
1795
 
1875
- verifiedExecutable(operation , mutating ) {
1876
- const path = this.executablePath(operation, mutating);
1877
- try {
1878
- return { path, digest: this.executableDigest(path) };
1879
- } catch {
1880
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "package-local native executable identity could not be verified", undefined, false);
1881
- }
1882
- }
1883
-
1884
1796
  async invoke(
1885
1797
  operation ,
1886
1798
  cwd ,
@@ -1889,6 +1801,14 @@ export class NativeReviewCliV216 {
1889
1801
  signal ,
1890
1802
  path ,
1891
1803
  toleratedStderr = [],
1804
+ // A successful acknowledgement burns its authority and, on every
1805
+ // published release up to v2.5.0-rc.3, prints nothing: there is no
1806
+ // receipt left to describe. Only that shape opts out of the body, and
1807
+ // only for a zero exit. When such an operation does print, the bytes
1808
+ // are handed back as a body for the caller to decode against the one
1809
+ // identity it accepts (gentle-ai #3947); a non-zero exit still has to
1810
+ // produce its typed failure envelope like every other operation.
1811
+ expectsBody = true,
1892
1812
  ) {
1893
1813
  let result ;
1894
1814
  try {
@@ -1897,10 +1817,14 @@ export class NativeReviewCliV216 {
1897
1817
  if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
1898
1818
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
1899
1819
  }
1820
+ if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1900
1821
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1901
- if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result);
1902
1822
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1903
1823
  const diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.NON_ZERO, result);
1824
+ if (!expectsBody && result.exitCode === 0 && result.stdout.trim().length === 0) {
1825
+ 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);
1826
+ return { body: {}, exitCode: result.exitCode, silent: true };
1827
+ }
1904
1828
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
1905
1829
  if (result.exitCode !== 0) {
1906
1830
  try {
@@ -1910,49 +1834,11 @@ export class NativeReviewCliV216 {
1910
1834
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
1911
1835
  }
1912
1836
  }
1913
- if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1837
+ const forecastNarrationTolerated = operation === NATIVE_REVIEW_OPERATION.STATUS && stderrIsForecastNarration(result.stderr);
1838
+ if (result.stderr.trim().length > 0 && !stderrIsTolerated(result.stderr, toleratedStderr) && !forecastNarrationTolerated) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1914
1839
  return { body, exitCode: result.exitCode };
1915
1840
  }
1916
1841
 
1917
- async capabilities(request = {}) {
1918
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.VERSION, false);
1919
- const cached = nativeCapabilitiesByDigest.get(executable.digest);
1920
- if (cached !== undefined) return cached;
1921
- const negotiation = (async () => {
1922
- const execution = await this.invoke(
1923
- NATIVE_REVIEW_OPERATION.VERSION,
1924
- request.cwd ?? dirname(executable.path),
1925
- ["review", "capabilities", "--contract", REVIEW_INTEGRATION_CONTRACT],
1926
- false,
1927
- request.signal,
1928
- executable.path,
1929
- );
1930
- // Two distinct half-upgraded-install failures (Design Decision #7,
1931
- // migrate-review-integration-v2): a decode failure means the installed
1932
- // runtime cannot even answer this shape (an older `.gentle-ai/` payload
1933
- // that still speaks `unsupported_contract` for v2); a successful decode
1934
- // whose package.version disagrees with the pin names both versions.
1935
- let capabilities ;
1936
- try {
1937
- capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
1938
- } catch (error) {
1939
- if (error instanceof NativeReviewCliError) throw error;
1940
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}; the installed runtime is incompatible — reinstall gentle-pi`);
1941
- }
1942
- if (capabilities.packageVersion !== GENTLE_AI_VERSION) {
1943
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, NATIVE_REVIEW_OPERATION.VERSION, false, `expected gentle-ai v${GENTLE_AI_VERSION}, provider reported v${capabilities.packageVersion}`);
1944
- }
1945
- return capabilities;
1946
- })();
1947
- nativeCapabilitiesByDigest.set(executable.digest, negotiation);
1948
- try {
1949
- return await negotiation;
1950
- } catch (error) {
1951
- nativeCapabilitiesByDigest.delete(executable.digest);
1952
- throw error;
1953
- }
1954
- }
1955
-
1956
1842
  async negotiated(
1957
1843
  operation ,
1958
1844
  cwd ,
@@ -1961,13 +1847,7 @@ export class NativeReviewCliV216 {
1961
1847
  signal ,
1962
1848
  toleratedStderr = [],
1963
1849
  ) {
1964
- const executable = this.verifiedExecutable(operation, mutating);
1965
- await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
1966
- const afterNegotiation = this.verifiedExecutable(operation, mutating);
1967
- if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
1968
- throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
1969
- }
1970
- return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
1850
+ return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
1971
1851
  }
1972
1852
 
1973
1853
  async start(request ) {
@@ -1975,36 +1855,54 @@ export class NativeReviewCliV216 {
1975
1855
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1976
1856
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1977
1857
  if (request.targetIdentity !== undefined && !/^sha256:[0-9a-f]{64}$/.test(request.targetIdentity)) throw new TypeError("Native START targetIdentity must be a canonical sha256 identity");
1978
- // The controller supplies the target it already projected from the
1979
- // authority workspace after proving its immutable actor view is identical.
1980
- // Direct adapter callers may omit it and retain the same-root projection.
1858
+ // STATUS owns the candidate binding and renders the only executable START
1859
+ // vector. Callers may supply a previously observed identity only to detect
1860
+ // drift; Pi never rebuilds that vector from request fields.
1981
1861
  const projection = request.projection ?? "workspace";
1982
- const targetIdentity = request.targetIdentity ?? (await this.targetStatus({
1862
+ const selection = nativeUntrackedSelection(request);
1863
+ const status = await this.targetStatus({
1983
1864
  cwd: request.cwd,
1984
1865
  projection,
1985
- ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
1866
+ ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
1986
1867
  ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
1868
+ ...selection,
1869
+ ...(request.intendedUntrackedSelection === undefined ? {} : { intendedUntrackedSelection: request.intendedUntrackedSelection }),
1870
+ agent: "pi",
1987
1871
  ...(request.signal === undefined ? {} : { signal: request.signal }),
1988
- })).targetIdentity;
1989
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
1990
- "review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
1991
- "--target", targetIdentity, "--projection", projection,
1992
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
1993
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1994
- ...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
1995
- ...(request.focus === undefined ? [] : ["--focus", request.focus]),
1996
- "--consent", "relay",
1997
- ], true, request.signal);
1998
- // A negotiated v2 START may answer `consent/v2` (action:
1872
+ });
1873
+ const transition = status.nextTransition?.kind === "execute" && status.nextTransition.execute?.operation === "review.start"
1874
+ ? status.nextTransition.execute
1875
+ : undefined;
1876
+ 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);
1877
+ if (status.projection.projection !== projection || transition.binding.targetIdentity !== status.targetIdentity || (request.targetIdentity !== undefined && request.targetIdentity !== status.targetIdentity)) {
1878
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, false, "native START transition target binding mismatch", undefined, false);
1879
+ }
1880
+ 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);
1881
+ const transitionTokens = transition.arguments.map((argument) => {
1882
+ 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);
1883
+ return argument.token;
1884
+ });
1885
+ const targetIdentity = status.targetIdentity;
1886
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, ["review", "start", ...transitionTokens], true, request.signal);
1887
+ // A negotiated v2 START may answer a consent question (action:
1999
1888
  // "consent_required") instead of `start/v3` when the provider needs an
2000
1889
  // explicit answer it cannot infer. Discriminate before decode and surface
2001
- // the complete envelope; only the caller can map a human answer.
1890
+ // the complete envelope; only the caller can map a human answer. The
1891
+ // body's own schema string selects the identity-exact decoder: the
1892
+ // pinned 2.2.x line emits consent/v2, gentle-ai >= 2.3.0 (capabilities
1893
+ // v2.1+) emits consent/v3, and any other identity fails closed inside
1894
+ // the v3 decoder's exact identity gate.
2002
1895
  if (execution.body.action === "consent_required") {
2003
- const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body));
1896
+ const consent = decode(NATIVE_REVIEW_OPERATION.START, true, () => {
1897
+ if (execution.body.schema === "gentle-ai.review-integration.consent/v2") return decodeReviewConsentV2(execution.body);
1898
+ const decoded = decodeReviewConsentV3(execution.body, "pi");
1899
+ validatePiConsentChoiceAgentBindings(decoded);
1900
+ return decoded;
1901
+ });
2004
1902
  if (consent.targetIdentity !== targetIdentity || consent.projection !== projection) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent target binding mismatch");
2005
1903
  throw new NativeReviewConsentRequiredError(consent);
2006
1904
  }
2007
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1905
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2008
1906
  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");
2009
1907
  const resultTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2010
1908
  if (resultTarget !== undefined && resultTarget !== targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start target mismatch");
@@ -2019,6 +1917,7 @@ export class NativeReviewCliV216 {
2019
1917
  action: result.action ,
2020
1918
  lensesRequired: result.lensesRequired,
2021
1919
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1920
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2022
1921
  // Derived, not received. `risk_reasons` is a required start/v2 field
2023
1922
  // already recomputed against the authoritative frozen snapshot, so
2024
1923
  // these phrases describe the same candidate the lenses will review.
@@ -2037,16 +1936,15 @@ export class NativeReviewCliV216 {
2037
1936
  }
2038
1937
 
2039
1938
  async answerConsent(request ) {
2040
- const arguments_ = consentInvocationArguments(request);
2041
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, arguments_, true, request.signal);
1939
+ const invocation = consentInvocationArguments(request);
1940
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, invocation.arguments_, true, request.signal);
2042
1941
  if (request.answer === NATIVE_REVIEW_CONSENT_ANSWER.DECLINED) {
2043
1942
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeDeclinedConsentStart(execution.body, request));
2044
1943
  }
2045
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1944
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartResponse(execution.body));
2046
1945
  const answeredTarget = result.targetIdentity ?? result.repositoryContext?.targetIdentity;
2047
1946
  if (answeredTarget !== request.consent.targetIdentity) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer target mismatch");
2048
- const lineageId = exactConsentOption(arguments_, "--lineage");
2049
- if (result.lineageId !== lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
1947
+ if (invocation.lineageId !== undefined && result.lineageId !== invocation.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native consent answer lineage mismatch");
2050
1948
  return { kind: "started", start: {
2051
1949
  lineageId: result.lineageId,
2052
1950
  state: result.state ,
@@ -2058,6 +1956,7 @@ export class NativeReviewCliV216 {
2058
1956
  action: result.action ,
2059
1957
  lensesRequired: result.lensesRequired,
2060
1958
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1959
+ ...("nextTransition" in result && result.nextTransition !== undefined ? { nextTransition: result.nextTransition } : {}),
2061
1960
  ...(() => {
2062
1961
  const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
2063
1962
  return evidence.length === 0 ? {} : { riskEvidence: evidence };
@@ -2066,119 +1965,23 @@ export class NativeReviewCliV216 {
2066
1965
  } };
2067
1966
  }
2068
1967
 
2069
- async stageDocument(directory , name , document ) {
2070
- const path = join(directory, `${name}.json`);
2071
- await writeFile(path, JSON.stringify(document), { encoding: "utf8", mode: 0o600 });
2072
- await chmod(path, 0o600);
2073
- return path;
2074
- }
2075
-
2076
- async stageEvidence(directory , evidence ) {
2077
- const path = join(directory, "evidence.txt");
2078
- await writeFile(path, evidence, { encoding: "utf8", mode: 0o600 });
2079
- await chmod(path, 0o600);
2080
- return path;
2081
- }
2082
-
2083
- async finalize(request ) {
2084
- if (request.evidenceDocument !== undefined && request.evidenceDocument.length === 0) throw new TypeError("Native FINALIZE evidence must contain at least one byte");
2085
- const needsStaging = request.lensResults !== undefined || request.refuterDocument !== undefined || request.validationDocument !== undefined || request.evidenceDocument !== undefined;
2086
- const directory = needsStaging ? await mkdtemp(join(tmpdir(), "gentle-ai-finalize-")) : undefined;
2087
- try {
2088
- if (directory !== undefined) await chmod(directory, 0o700);
2089
- const resultFiles = directory !== undefined && request.lensResults !== undefined ? await Promise.all(request.lensResults.map((entry, index) => this.stageDocument(directory, `result-${index}`, entry.document))) : request.resultFiles ?? [];
2090
- const refuterFile = directory !== undefined && request.refuterDocument !== undefined ? await this.stageDocument(directory, "refuter", request.refuterDocument) : request.refuterFile;
2091
- const validationFile = directory !== undefined && request.validationDocument !== undefined ? await this.stageDocument(directory, "validation", request.validationDocument) : request.validationFile;
2092
- const evidenceFile = directory !== undefined && request.evidenceDocument !== undefined ? await this.stageEvidence(directory, request.evidenceDocument) : request.evidenceFile;
2093
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
2094
- "review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2095
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2096
- // `--result` is RETIRED. A reviewer result supplied that way carries
2097
- // no provider-owned admission, so it cannot prove the lens inspected
2098
- // the frozen candidate. Results reach authority through
2099
- // `review capture-result`, and FINALIZE either discovers them
2100
- // (`--captured-results`) or is handed each manifest in lens order.
2101
- ...(request.capturedResults === true ? ["--captured-results=true"] : []),
2102
- ...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
2103
- ...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
2104
- ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
2105
- ...(validationFile === undefined ? [] : ["--validation", validationFile]),
2106
- ...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
2107
- ...(request.failed === true ? ["--failed"] : []),
2108
- ], true, request.signal);
2109
- const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
2110
- if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
2111
- const body = envelope.result;
2112
- const lineageId = requiredString(body.lineage_id);
2113
- if (request.lineageId !== undefined && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.FINALIZE, true, "native finalize lineage mismatch");
2114
- return {
2115
- lineageId,
2116
- state: requiredString(body.state),
2117
- action: requiredString(body.action),
2118
- storeRevision: requiredString(body.store_revision),
2119
- // Present only when state is correction_required (validated by
2120
- // decodeReviewOperationV2); carried through as the raw validated
2121
- // shape rather than re-decoded, since the typed decoder for this
2122
- // nested object is private to lib/review-integration-v2.ts.
2123
- ...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request }),
2124
- ...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
2125
- };
2126
- } finally {
2127
- if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
2128
- }
2129
- }
2130
-
2131
- async validate(request ) {
2132
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.VALIDATE, request.cwd, [
2133
- "review", "validate", "--contract", REVIEW_INTEGRATION_CONTRACT, "--gate", request.gate, "--cwd", request.cwd,
2134
- ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2135
- ...(request.flags ?? []),
2136
- ], false, request.signal);
2137
- const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
2138
- if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
2139
- const body = envelope.result;
2140
- const gateContext = decodeGateContext(body.context);
2141
- return {
2142
- allowed: booleanValue(body.allowed),
2143
- result: enumString(body.result, NATIVE_GATE_RESULT) ,
2144
- action: requiredString(body.action),
2145
- reason: requiredString(body.reason),
2146
- gateContext,
2147
- ...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) }),
2148
- };
2149
- }
2150
-
2151
- async bindSdd(request ) {
2152
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.BIND_SDD, request.cwd, [
2153
- "review", "bind-sdd", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2154
- "--change", request.change, "--lineage", request.lineage,
2155
- `--expected-binding-revision=${request.expectedBindingRevision}`,
2156
- ], true, request.signal);
2157
- const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
2158
- if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
2159
- const body = envelope.result;
2160
- const gateContext = decodeGateContext(body.gate_context);
2161
- const lineage = requiredString(body.lineage);
2162
- const change = requiredString(body.change);
2163
- 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");
2164
- return {
2165
- revision: requiredString(body.revision),
2166
- change,
2167
- lineage,
2168
- authorityRevision: requiredString(body.authority_revision),
2169
- receiptHash: requiredString(body.receipt_hash),
2170
- gateContext,
2171
- };
2172
- }
2173
-
2174
1968
  async targetStatus(request ) {
2175
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, [
1969
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native STATUS baseRef must be a non-empty, trimmed, NUL-free string");
1970
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native STATUS baseRef requires explicit committedOnly acknowledgement");
1971
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native STATUS committedOnly requires an explicit baseRef");
1972
+ const selection = nativeUntrackedSelection(request);
1973
+ const submitted = request.intendedUntrackedSelection;
1974
+ 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");
1975
+ const statusArguments = submitted === undefined ? [
2176
1976
  "review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
2177
1977
  "--projection", request.projection ?? "workspace",
2178
- ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
1978
+ ...nativeUntrackedSelectionArguments(selection),
1979
+ ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2179
1980
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1981
+ ...(request.agent === undefined ? [] : ["--agent", request.agent]),
2180
1982
  "--next-transition",
2181
- ], false, request.signal);
1983
+ ] : ["review", "status", "--cwd", request.cwd, ...submitted.argumentTokens.map((token) => token.replaceAll("{{value}}", submitted.value))];
1984
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
2182
1985
  assertSupportedNextTransitionOperation(execution.body);
2183
1986
  return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
2184
1987
  }
@@ -2218,7 +2021,7 @@ export class NativeReviewCliV216 {
2218
2021
  // --contract, and the verification-evidence/v2 record returned DIRECTLY —
2219
2022
  // not wrapped in an operation/v2 envelope. Evidence is staged through the
2220
2023
  // same 0o600 tmpfile discipline FINALIZE uses.
2221
- async captureResult(request ) {
2024
+ async captureResult(request ) {
2222
2025
  if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
2223
2026
  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");
2224
2027
  if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
@@ -2230,87 +2033,179 @@ export class NativeReviewCliV216 {
2230
2033
  const resultFile = join(directory, "result.json");
2231
2034
  await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
2232
2035
  await chmod(resultFile, 0o600);
2233
- const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2036
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2234
2037
  const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
2235
2038
  "review", "capture-result",
2236
2039
  ...request.argumentTokens,
2237
2040
  ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2238
2041
  "--input", resultFile,
2239
- ], true, request.signal, executable.path);
2240
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => decodeNativeAdmittedResultManifest(execution.body));
2042
+ ], true, request.signal, executable);
2043
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => {
2044
+ const body = object(execution.body);
2045
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2046
+ const closure = decodeReviewLastEventClosureV1(body);
2047
+ if (closure.operation !== "review/capture-result") throw new TypeError("capture-result returned a closure for another operation");
2048
+ return closure;
2049
+ }
2050
+ return decodeNativeAdmittedResultManifest(body);
2051
+ });
2241
2052
  } finally {
2242
2053
  await this.cleanupDirectory(directory).catch(() => undefined);
2243
2054
  }
2244
2055
  }
2245
2056
 
2246
- async captureEvidence(request ) {
2247
- if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2248
- if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2249
- const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2250
- try {
2251
- await chmod(directory, 0o700);
2252
- const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2253
- const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
2254
- "review", "capture-evidence", "--cwd", request.cwd,
2255
- "--lineage", request.lineageId,
2256
- "--target", request.targetIdentity,
2257
- "--expected-revision", request.expectedRevision,
2258
- "--outcome", request.outcome,
2259
- "--input", evidenceFile,
2260
- ], true, request.signal);
2261
- return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2262
- } finally {
2263
- await this.cleanupDirectory(directory).catch(() => undefined);
2264
- }
2057
+ // Relays the provider-issued acknowledgement exactly as rendered. A success
2058
+ // burns the authority and its artifacts; a refusal still arrives as the
2059
+ // ordinary typed failure envelope. Two success shapes exist and both are
2060
+ // the same burn: every published release up to v2.5.0-rc.3 prints nothing
2061
+ // (resolved as `undefined`, byte-for-byte the pinned behaviour), and
2062
+ // gentle-ai #3947 prints one `gentle-ai.review-acknowledged/v1` envelope
2063
+ // naming the burned lineage, target, and consumed revision. Any other
2064
+ // output on a zero exit is schema-incompatible with the mutation outcome
2065
+ // unknown, exactly like a malformed terminal closure: the caller
2066
+ // reconciles through STATUS and never infers the burn from prose.
2067
+ async acknowledgeApproved(request ) {
2068
+ if (request.argumentTokens.length === 0) throw new TypeError("Native ACKNOWLEDGE_APPROVED requires the provider-issued argument tokens");
2069
+ 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");
2070
+ if (request.argumentTokens.some((token) => token.includes("{{value}}"))) throw new TypeError("Native ACKNOWLEDGE_APPROVED argument tokens carry no caller-substituted value");
2071
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true);
2072
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, request.cwd, [
2073
+ "review", "acknowledge-approved", ...request.argumentTokens,
2074
+ ], true, request.signal, executable, [], false);
2075
+ if (execution.silent) return undefined;
2076
+ return decode(NATIVE_REVIEW_OPERATION.ACKNOWLEDGE_APPROVED, true, () => decodeReviewAcknowledgedV1(execution.body, request.binding ?? {}));
2265
2077
  }
2266
2078
 
2267
- reviewStatus(request ) {
2268
- return this.legacy.reviewStatus(request);
2079
+ async captureCorrectionPlan(request ) {
2080
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_CORRECTION_PLAN requires the provider-issued argument tokens");
2081
+ 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");
2082
+ 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");
2083
+ const valueIndex = request.argumentTokens.findIndex((token) => token.includes("{{value}}"));
2084
+ 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");
2085
+ const argumentTokens = request.argumentTokens.map((token, index) => index === valueIndex ? token.replaceAll("{{value}}", String(request.correctionLines)) : token);
2086
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true);
2087
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, request.cwd, [
2088
+ "review", "capture-correction-plan", ...argumentTokens,
2089
+ ], true, request.signal, executable);
2090
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_CORRECTION_PLAN, true, () => {
2091
+ const closure = decodeReviewLastEventClosureV1(execution.body);
2092
+ if (closure.operation !== "review.capture-correction-plan") throw new TypeError("capture-correction-plan returned a closure for another operation");
2093
+ return closure;
2094
+ });
2095
+ }
2096
+
2097
+ // gentle-pi#311 P4-roles: executes one provider-rendered self-contained
2098
+ // role capture vector exactly as rendered — one CLI invocation, verbatim
2099
+ // tokens in provider order, in the foreground. Go materializes the role
2100
+ // prompt, spawns its own locked-down pi subprocess, and admits the raw
2101
+ // verdict; Pi never adds, removes, or reorders a single token (not even
2102
+ // --cwd: the vector's --repository-context is authoritative and mutually
2103
+ // exclusive with a path). The central adapter injects the relay handshake
2104
+ // environment on this spawn like on every other gentle-ai invocation.
2105
+ async captureProviderRole(request ) {
2106
+ 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)}`);
2107
+ const verb = request.captureOperation.slice("review.".length);
2108
+ const expectedClosureOperation = NATIVE_REVIEW_PROVIDER_ROLE_CAPTURE_CLOSURE_OPERATION[request.captureOperation];
2109
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_PROVIDER_ROLE requires the provider-rendered argument tokens");
2110
+ 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");
2111
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true);
2112
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, request.cwd, [
2113
+ "review", verb,
2114
+ ...request.argumentTokens,
2115
+ ], true, request.signal, executable);
2116
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, () => {
2117
+ const body = object(execution.body);
2118
+ if (body.schema === "gentle-ai.review-last-event-closure/v1") {
2119
+ const closure = decodeReviewLastEventClosureV1(body);
2120
+ if (closure.operation !== expectedClosureOperation) throw new TypeError("provider role capture returned a closure for another operation");
2121
+ return closure;
2122
+ }
2123
+ return decodeNativeProviderRoleCaptureArtifact(body);
2124
+ });
2269
2125
  }
2270
2126
 
2271
- sddStatus(request ) {
2272
- return this.legacy.sddStatus(request);
2127
+ // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
2128
+ // execute transition exactly as rendered. The tokens come verbatim from
2129
+ // the negotiated next_transition (e.g. `--lineage=<id>
2130
+ // --captured-results=true`); Pi assembles no reviewer, refuter, or
2131
+ // validator documents for this lane — the provider discovers its own
2132
+ // admitted role and lens slots.
2133
+
2134
+ // Same misbinding class as capture-evidence (live smoke, 2026-08-16): the
2135
+ // correction PLAN and TARGETED VALIDATION collect slots render `finalize`
2136
+ // submission descriptors whose tokens are self-contained (--contract,
2137
+ // --lineage, --expected-revision, --target, --request-hash,
2138
+ // --repository-context) plus exactly one {{value}} slot. Executing anything
2139
+ // other than those rendered tokens fails the live emitter's committed-
2140
+ // intent reconciliation, so the tokens pass through verbatim with only the
2141
+ // {{value}} substitution: a literal for correction_lines, a staged 0o600
2142
+ // artifact path for a validation document.
2143
+
2144
+
2145
+
2146
+ // Field defect (fambig, 2026-08-16): at every evidence-pending sub-state
2147
+ // the collect slot renders the identity native demands — for a correction
2148
+ // that is the fix-diff `--target`, not the live workspace snapshot — so the
2149
+ // slot's rendered submission tokens execute verbatim, with only the
2150
+ // {{outcome}} and {{input}} slots substituted. Same verbatim-token
2151
+ // discipline as captureResult; --repository-context is authoritative and
2152
+ // mutually exclusive with a path.
2153
+
2154
+ async reviewStatus(request ) {
2155
+ const execution = await this.invoke(
2156
+ NATIVE_REVIEW_OPERATION.STATUS,
2157
+ request.cwd,
2158
+ ["review", "status", "--cwd", request.cwd],
2159
+ false,
2160
+ request.signal,
2161
+ this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false),
2162
+ );
2163
+ const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(execution.body));
2164
+ 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");
2165
+ return status;
2273
2166
  }
2274
2167
 
2275
- // Same plain-CLI delegation as reviewStatus/sddStatus above: `review mode`
2276
- // is dark until the negotiated version reports `mode: true` and stays
2277
- // outside the negotiated integration-v1 contract.
2278
- reviewMode(request ) {
2279
- return this.legacy.reviewMode(request);
2168
+ async reviewMode(request ) {
2169
+ const cwd = await canonicalNativeReviewCwd(request.cwd);
2170
+ const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
2171
+ const execution = await this.invoke(
2172
+ NATIVE_REVIEW_OPERATION.MODE,
2173
+ cwd,
2174
+ ["review", "mode", request.operation, "--cwd", cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
2175
+ mutating,
2176
+ request.signal,
2177
+ this.executablePath(NATIVE_REVIEW_OPERATION.MODE, mutating),
2178
+ );
2179
+ return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
2280
2180
  }
2281
2181
 
2282
2182
  // Recovery commands are version-gated plain CLI operations outside the
2283
- // negotiated integration-v1 contract, exactly like reviewStatus/sddStatus.
2183
+ // negotiated integration-v1 contract, exactly like reviewStatus.
2284
2184
  reclaim(request ) {
2285
- return this.legacy.reclaim(request);
2185
+ return this.plain.reclaim(request);
2286
2186
  }
2287
2187
 
2288
2188
  recover(request ) {
2289
- return this.legacy.recover(request);
2189
+ return this.plain.recover(request);
2290
2190
  }
2291
2191
 
2292
2192
  abandon(request ) {
2293
- return this.legacy.abandon(request);
2193
+ return this.plain.abandon(request);
2294
2194
  }
2295
2195
 
2296
2196
  quarantineLegacy(request ) {
2297
- return this.legacy.quarantineLegacy(request);
2197
+ return this.plain.quarantineLegacy(request);
2298
2198
  }
2299
2199
 
2300
2200
  reconcileAuthority(request ) {
2301
- return this.legacy.reconcileAuthority(request);
2201
+ return this.plain.reconcileAuthority(request);
2302
2202
  }
2303
2203
 
2304
2204
  repairLegacyAlias(request ) {
2305
- return this.legacy.repairLegacyAlias(request);
2205
+ return this.plain.repairLegacyAlias(request);
2306
2206
  }
2307
2207
  }
2308
2208
 
2309
2209
  export function createNativeReviewCli(adapter , executable = resolveGentleAiBinary) {
2310
- if (adapter !== undefined) return new NativeReviewCliV214(adapter, executable);
2311
- return new NativeReviewCliV216(createNodeExecFileAdapter(), executable);
2210
+ return new NativeReviewCliV216(adapter ?? createNodeExecFileAdapter(), executable);
2312
2211
  }
2313
-
2314
- // Response-schema fixtures remain v2.1.3 historical contracts; the production
2315
- // client above accepts only the v2.1.4 runtime release.
2316
- export { NativeReviewCliV214 as NativeReviewCliV213 };