gentle-pi 1.2.0 → 2.0.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 (107) hide show
  1. package/README.md +40 -9
  2. package/assets/agents/review-readability.md +0 -1
  3. package/assets/agents/review-reliability.md +0 -1
  4. package/assets/agents/review-resilience.md +0 -1
  5. package/assets/agents/review-risk.md +0 -1
  6. package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
  7. package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
  8. package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
  9. package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
  10. package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
  11. package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
  12. package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
  13. package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
  14. package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
  15. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
  16. package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
  17. package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
  18. package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
  19. package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
  20. package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
  21. package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
  22. package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
  23. package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
  24. package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
  25. package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
  26. package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
  27. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
  28. package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
  29. package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
  30. package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
  31. package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
  32. package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
  33. package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
  34. package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
  35. package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
  36. package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
  37. package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
  38. package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
  39. package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
  40. package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
  41. package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
  42. package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
  43. package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
  44. package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
  45. package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
  46. package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
  47. package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
  48. package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
  49. package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
  50. package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
  51. package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
  52. package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
  53. package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
  54. package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
  55. package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
  56. package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
  57. package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
  58. package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
  59. package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
  60. package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
  61. package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
  62. package/docs/native-authority-architecture.md +18 -3
  63. package/docs/review-integration.md +175 -27
  64. package/extensions/gentle-ai.ts +414 -19
  65. package/lib/gentle-ai-binary.ts +1 -1
  66. package/lib/native-review-cli.ts +679 -57
  67. package/lib/review-candidate-view.ts +131 -1
  68. package/lib/review-compact-contract.ts +12 -3
  69. package/lib/review-consent-latch.ts +51 -0
  70. package/lib/review-correction-lifecycle.ts +170 -0
  71. package/lib/review-integration-v2.ts +1563 -0
  72. package/package.json +15 -11
  73. package/runtime/gentle-ai-binary.mjs +1 -1
  74. package/runtime/native-review-cli.mjs +663 -41
  75. package/runtime/review-integration-v2.mjs +1564 -0
  76. package/scripts/build-git-commit-transaction-runner.mjs +1 -1
  77. package/scripts/gentle-ai-installer.mjs +16 -11
  78. package/scripts/install-gentle-ai.mjs +7 -3
  79. package/scripts/test-packed-runner.mjs +2 -2
  80. package/scripts/verify-package-files.mjs +216 -64
  81. package/skills/release/SKILL.md +38 -11
  82. package/tests/devbinary/native-review-parity.devtest.ts +348 -0
  83. package/tests/gentle-ai-binary.test.ts +8 -1
  84. package/tests/gentle-ai-installer.test.ts +24 -21
  85. package/tests/native-binary-gate.test.ts +145 -0
  86. package/tests/native-review-capability-contract.test.ts +87 -0
  87. package/tests/native-review-cli.test.ts +44 -1
  88. package/tests/native-review-parity-runtime.test.ts +70 -23
  89. package/tests/native-review-parity.test.ts +673 -0
  90. package/tests/native-risk-evidence-parity.test.ts +91 -0
  91. package/tests/package-manifest.test.ts +72 -7
  92. package/tests/review-actor-tool-deny.test.ts +20 -5
  93. package/tests/review-candidate-view.test.ts +331 -1
  94. package/tests/review-compact-contract.test.ts +5 -0
  95. package/tests/review-consent-latch.test.ts +67 -0
  96. package/tests/review-controller-native-recovery.test.ts +180 -5
  97. package/tests/review-controller-native-routing.test.ts +385 -12
  98. package/tests/review-controller-workspace-root.test.ts +26 -6
  99. package/tests/review-correction-lifecycle.test.ts +174 -0
  100. package/tests/review-integration-v2.test.ts +539 -0
  101. package/tests/runtime-harness.mjs +187 -0
  102. package/tests/support/native-binary-gate.ts +86 -0
  103. package/tests/verify-package-files.test.ts +119 -0
  104. package/lib/review-integration-v1.ts +0 -795
  105. package/runtime/review-integration-v1.mjs +0 -796
  106. package/tests/native-review-integration-v1.test.ts +0 -102
  107. package/tests/review-integration-v1.test.ts +0 -530
@@ -6,19 +6,23 @@ import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, isAbsolute, join, posix, win32 } from "node:path";
8
8
  import { promisify } from "node:util";
9
- import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
9
+ import { GENTLE_AI_VERSION, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.mjs";
10
10
  import {
11
11
  REVIEW_INTEGRATION_CONTRACT,
12
- decodeReviewCapabilitiesV1,
13
- decodeReviewFailureV1,
14
- decodeReviewOperationV1,
15
- decodeReviewStartV1,
16
- decodeReviewStatusV1,
12
+ decodeReviewCapabilitiesV2,
13
+ decodeReviewConsentV2,
14
+ decodeReviewFailureV2,
15
+ decodeReviewOperationV2,
16
+ decodeReviewRepairV2,
17
+ decodeReviewStartV3,
18
+ decodeReviewStatusV3,
17
19
 
18
20
 
19
21
 
20
22
 
21
- } from "./review-integration-v1.mjs";
23
+
24
+
25
+ } from "./review-integration-v2.mjs";
22
26
 
23
27
  const execFileAsync = promisify(execFile);
24
28
 
@@ -36,6 +40,10 @@ export const NATIVE_REVIEW_OPERATION = {
36
40
  QUARANTINE_LEGACY: "review/quarantine-legacy",
37
41
  RECONCILE_AUTHORITY: "review/reconcile-authority",
38
42
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
43
+ MODE: "review/mode",
44
+ REPAIR: "review/repair",
45
+ CAPTURE_EVIDENCE: "review/capture-evidence",
46
+ CAPTURE_RESULT: "review/capture-result",
39
47
  } ;
40
48
 
41
49
 
@@ -53,6 +61,7 @@ export const NATIVE_REVIEW_ERROR_CODE = {
53
61
  VERSION_INCOMPATIBLE: "version-incompatible",
54
62
  CANCELLED: "cancelled",
55
63
  PACKAGE_BINARY_MISSING: "package-local-binary-missing",
64
+ UNSUPPORTED_TRANSITION_OPERATION: "unsupported-transition-operation",
56
65
  } ;
57
66
 
58
67
 
@@ -105,6 +114,92 @@ export const NATIVE_SDD_ARTIFACT_STATE = {
105
114
 
106
115
 
107
116
 
117
+
118
+
119
+
120
+
121
+
122
+
123
+  
124
+
125
+
126
+
127
+
128
+ export const NATIVE_REVIEW_MODE_OPERATION = {
129
+ STATUS: "status",
130
+ ENABLE: "enable",
131
+ DISABLE: "disable",
132
+ } ;
133
+
134
+
135
+ export const NATIVE_REVIEW_MODE_VALUE = {
136
+ UNSET: "",
137
+ ON: "on",
138
+ OFF: "off",
139
+ } ;
140
+
141
+
142
+ export const NATIVE_REVIEW_MODE_SOURCE = {
143
+ DEFAULT: "default",
144
+ GLOBAL: "global",
145
+ CLONE_LOCAL: "clone_local",
146
+ } ;
147
+
148
+
149
+ export const NATIVE_REVIEW_MODE_SCOPE = {
150
+ GLOBAL: "global",
151
+ CLONE: "clone",
152
+ BOTH: "both",
153
+ } ;
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+ // Exact-match tolerated-stderr allowlist for START only, gated on the `mode`
177
+ // capability being true (Design Decision #6, organic-rdd-parity). Byte-exact
178
+ // against gentle-ai's headless notice (internal/cli/review_mode.go
179
+ // reviewConsentSkippedNotice) written when the switch is on but no interactive
180
+ // terminal answered the one-time consent question — which is always true when
181
+ // Pi spawns gentle-ai without a TTY. Any other text still fails closed as
182
+ // UNEXPECTED_STDERR.
183
+ // Each entry is one whole line gentle-ai may write to the console stream while
184
+ // still succeeding. The provenance line rides with the skip notice whenever the
185
+ // resolved mode source is `default`, so a headless START legitimately emits two
186
+ // lines; they are separate Fprintln calls, never one joined string.
187
+ export const REVIEW_CONSENT_NOTICES = Object.freeze([
188
+ "Gentle AI reviewed this change without asking, because this session has no terminal to answer on. Run 'gentle-ai review mode disable' to turn reviews off, or 'gentle-ai review mode status' to see the current setting.",
189
+ "Reviews are on by default; this was never explicitly chosen. Run 'gentle-ai review mode enable' to make reviews an explicit choice, or 'gentle-ai review mode disable' to turn them off.",
190
+ "Gentle AI could not read an answer, so it reviewed this change and will ask again next time.",
191
+ "Gentle AI did not recognize that answer, so it reviewed this change and will ask again next time.",
192
+ "Review skipped for this candidate at your request. It will be offered again on the next change.",
193
+ ]);
194
+
195
+ // Every non-empty line must be allowlisted. Matching the whole stderr blob as
196
+ // one string only worked while exactly one notice could appear; the moment a
197
+ // second legitimate line joined it, an otherwise successful START was reported
198
+ // as unexpected-stderr.
199
+ function stderrIsTolerated(stderr , tolerated ) {
200
+ const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
201
+ return lines.length > 0 && lines.every((line) => tolerated.includes(line));
202
+ }
108
203
 
109
204
  export const NATIVE_REVIEW_RECOVER_DISPOSITION = ["scope_changed", "invalidated", "escalated"] ;
110
205
 
@@ -197,6 +292,85 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
197
292
  /** Raw audited native record; Pi relays it verbatim and never reinterprets it. */
198
293
 
199
294
 
295
+ // Net-new negotiated `review.repair` (contract v2). `repair(request)` always
296
+ // runs a `--mode preflight` first; only an eligible assessment is ever
297
+ // executed, using the exact provider_inputs that assessment published — Pi's
298
+ // own NATIVE_REVIEW_LEGACY_ALIAS_REPAIR constants are never a source, only a
299
+ // disagreement check (Design Decision #6, migrate-review-integration-v2).
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+ // `review capture-evidence` argv and response shape are pinned to a real
309
+ // v2.2.2 review run (lineage review-b39d803b68a90767): exactly
310
+ // --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
311
+ // --contract, and the `gentle-ai.review-verification-evidence/v2` record
312
+ // returned directly (not wrapped in an operation/v2 envelope).
313
+ export const NATIVE_REVIEW_CAPTURE_OUTCOME = ["passed", "verification_failed", "procedural_tooling_failed"] ;
314
+
315
+
316
+ // `capture-result` is an additive headless command, NOT a negotiated
317
+ // repository operation: it accepts no --contract, and the provider's own
318
+ // transition tokens already carry the repository context -- it takes that or
319
+ // --cwd, never both. So Pi passes the tokens through verbatim and adds only
320
+ // --input. Reconstructing them would mean re-deriving a lineage, revision,
321
+ // target, lens slot, and subject hash the provider already issued.
322
+ // Added for the v2 finalize transport. `capturedResults` asks the provider to
323
+ // discover every manifest it already admitted; `resultArtifactFiles` hands them
324
+ // over explicitly in lens order. Both replace the retired `--result`.
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+
365
+
366
+
367
+
368
+
369
+
370
+
371
+
372
+
373
+
200
374
 
201
375
 
202
376
 
@@ -343,6 +517,76 @@ export function isCanonicalProcessString(value ) {
343
517
  }
344
518
 
345
519
  const NATIVE_RISK_LEVEL = ["low", "medium", "high"] ;
520
+
521
+ // gentle-ai's negotiated `start/v2` envelope is a closed schema
522
+ // (`additionalProperties: false`), so the two projections its plain sibling
523
+ // carries cannot be added to it without a new contract version. Both are
524
+ // projections of facts `start/v2` already reports as required fields, so a
525
+ // negotiated caller reconstructs them here instead of ending up with a worse
526
+ // recovery story than a plain one. These are Pi-side renderings of native
527
+ // facts, never a claim that the CLI sent them: the `riskEvidence`/`hint`
528
+ // capability rows stay dark for every version whose envelope omits them.
529
+ //
530
+ // Byte-for-byte mirrors of internal/cli/review_mode.go and review_facade.go.
531
+ // `reviewConsentEvidencePhrases` is documented there as the single phrasing
532
+ // source so its surfaces cannot drift, which makes this a second surface: an
533
+ // unrecognized reason code therefore renders nothing rather than guessing, and
534
+ // nativeRiskEvidencePhrases is pinned against a gentle-ai fixture in
535
+ // tests/native-review-parity.test.ts so a vocabulary change fails loudly.
536
+ const REVIEW_EMPTY_CANDIDATE_HINT =
537
+ "the candidate has no pending changes; already-committed work can be reviewed by rerunning review start with --base-ref <commit> naming the base to compare against";
538
+ const REVIEW_MEDIUM_RISK_REASON = "this change is not purely passive documentation, so it gets one consolidated review.";
539
+ const REVIEW_EMPTY_CONTENT_CODE = "empty_content";
540
+ const REVIEW_RISK_SUBJECT_BY_CODE = Object.freeze({
541
+ service_token: "service credentials",
542
+ shell_source: "shell scripting",
543
+ process_boundary: "code that starts other processes",
544
+ process_scan_limit: "code that starts other processes",
545
+ executable_mode: "an executable permission change",
546
+ executable_change: "an executable change",
547
+ configuration_change: "a configuration change",
548
+ });
549
+ const REVIEW_RISK_SUBJECT_BY_SIGNAL = Object.freeze({
550
+ auth: "authentication",
551
+ update: "the update path",
552
+ security: "security",
553
+ payments: "payments",
554
+ data_exposure: "data exposure",
555
+ data_loss: "data loss",
556
+ permissions: "permissions",
557
+ shell_process: "shell or process execution",
558
+ });
559
+ // The Go signal switch has no empty default: every hot_path reason speaks, and
560
+ // an unmapped signal degrades to this rather than dropping the path entirely.
561
+ const REVIEW_RISK_UNKNOWN_SIGNAL_SUBJECT = "a sensitive area";
562
+
563
+ function nativeRiskEvidenceSubject(reason ) {
564
+ const code = typeof reason.code === "string" ? reason.code : "";
565
+ if (code === "hot_path") {
566
+ const signal = typeof reason.signal === "string" ? reason.signal : "";
567
+ return REVIEW_RISK_SUBJECT_BY_SIGNAL[signal] ?? REVIEW_RISK_UNKNOWN_SIGNAL_SUBJECT;
568
+ }
569
+ return REVIEW_RISK_SUBJECT_BY_CODE[code] ?? "";
570
+ }
571
+
572
+ function nativeRiskEvidencePhrase(reason ) {
573
+ const path = typeof reason.path === "string" ? reason.path.trim() : "";
574
+ // An empty file is named first and described second. Every other subject
575
+ // reads "<what changed> in <path>", which for a file with no bytes would
576
+ // assert something about content that is not there.
577
+ if (reason.code === REVIEW_EMPTY_CONTENT_CODE) {
578
+ return path === "" ? "" : `${path}, an empty file whose type cannot be determined from its content`;
579
+ }
580
+ const subject = nativeRiskEvidenceSubject(reason);
581
+ if (subject === "" || path === "") return subject;
582
+ return `${subject} in ${path}`;
583
+ }
584
+
585
+ export function nativeRiskEvidencePhrases(riskLevel , reasons ) {
586
+ if (riskLevel !== "high" && riskLevel !== "medium") return [];
587
+ const phrases = reasons.map((reason) => nativeRiskEvidencePhrase(reason)).filter((phrase) => phrase !== "");
588
+ return riskLevel === "medium" ? [REVIEW_MEDIUM_RISK_REASON, ...phrases] : phrases;
589
+ }
346
590
  const NATIVE_REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] ;
347
591
  const NATIVE_FINALIZE_STATE = ["reviewing", "correction_required", "validating", "approved", "escalated"] ;
348
592
  const NATIVE_START_ACTION_VALUES = Object.values(NATIVE_START_ACTION);
@@ -351,18 +595,71 @@ const NATIVE_GATE = ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"
351
595
  const NATIVE_SDD_NEXT_ACTION = ["apply", "verify", "remediate", "archive", "review", "resolve-review", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] ;
352
596
  const NATIVE_SDD_POST_REVIEW_ACTION = ["verify", "archive"] ;
353
597
 
598
+ // Organic-parity columns (mode, riskEvidence, hint, delivery) stay false on
599
+ // every shipped row, including "2.1.11". PI-2 adds the first capability-true
600
+ // row (and bumps the triple pin) in one dedicated commit — never one without
601
+ // the other. See Design Decision #1 (organic-rdd-parity).
602
+ const ORGANIC_PARITY_DARK = { mode: false, riskEvidence: false, hint: false, delivery: false } ;
603
+
354
604
  export const NATIVE_CLI_CONTRACTS = Object.freeze({
355
- "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 }),
356
- "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 }),
357
- "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 }),
358
- "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 }),
359
- "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 }),
360
- "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 }),
361
- "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 }),
362
- "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 }),
605
+ "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 }),
606
+ "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 }),
607
+ "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 }),
608
+ "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 }),
609
+ "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 }),
610
+ "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 }),
611
+ "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 }),
612
+ "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 }),
613
+ // First capability-true row, paired with the triple pin bump in this same
614
+ // commit as Design Decision #1 requires.
615
+ //
616
+ // Only two of the four organic-parity columns are lit, because a capability
617
+ // row is a promise and these are the two whose data was proven to reach the
618
+ // negotiated path Pi actually consumes:
619
+ //
620
+ // mode `gentle-ai review mode status` answers the review-mode/v1
621
+ // envelope directly.
622
+ // delivery the gate result carries `delivery` ("disabled/unmanaged" when
623
+ // the kill switch is off), verified against v2.2.0.
624
+ //
625
+ // riskEvidence and hint stay dark deliberately. Both exist in gentle-ai
626
+ // v2.2.0 but only on the PLAIN start envelope; the negotiated
627
+ // `review-integration.start/v2` that NativeReviewCliV216 decodes carries
628
+ // `risk_reasons` instead of `risk_evidence` and omits `hint` entirely.
629
+ // Lighting them would advertise data that cannot arrive. Closing that gap
630
+ // needs the negotiated start envelope extended upstream, which moves a
631
+ // byte-pinned fixture and therefore belongs to a gentle-ai release, not to
632
+ // a Pi capability flip.
633
+ "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 }),
634
+ // 2.2.1 repeats 2.2.0 because the wire did not move for the lane Pi speaks.
635
+ // v2.2.1 advertises capabilities/v1.5 (protocol minor 5) on
636
+ // review-integration/v1, but the negotiated start envelope is still the
637
+ // closed `start/v2`, so riskEvidence and hint stay dark for the same reason
638
+ // they are dark on 2.2.0. The release does publish a second contract,
639
+ // review-integration/v2, whose `start/v3` carries base/candidate trees --
640
+ // but Pi does not negotiate it yet, and a row must describe the lane in use.
641
+ "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 }),
642
+ // 2.2.2 repeats 2.2.1 for the same reason, confirmed against the released
643
+ // v2.2.2 binary rather than assumed: on review-integration/v1 it still
644
+ // advertises capabilities/v1.5 and the negotiated start envelope is still
645
+ // the closed `start/v2`, so riskEvidence and hint still cannot arrive.
646
+ "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 }),
363
647
  });
364
648
 
365
649
 
650
+ // Testing-only capability overlay: lets tests exercise mode/riskEvidence/hint/
651
+ // delivery decode paths under a synthetic version key without ever making a
652
+ // shipped row (including "2.1.11") capability-true. Production code never
653
+ // calls the setter, so this is inert outside test files.
654
+ const nativeCliContractsTestingOverlay = new Map ();
655
+ export function setNativeCliContractForTesting(version , contract ) {
656
+ if (contract === undefined) nativeCliContractsTestingOverlay.delete(version);
657
+ else nativeCliContractsTestingOverlay.set(version, contract);
658
+ }
659
+ function resolvedNativeCliContract(version ) {
660
+ return nativeCliContractsTestingOverlay.get(version) ?? (NATIVE_CLI_CONTRACTS )[version];
661
+ }
662
+
366
663
 
367
664
 
368
665
 
@@ -537,6 +834,49 @@ function decodeLegacyReconcileAudit(value )
537
834
  for (const field of ["predecessor_lineage", "successor_lineage", "outcome"]) requiredString(record[field]);
538
835
  return { record };
539
836
  }
837
+ function decodeNativeReviewVerificationEvidence(value ) {
838
+ 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"]);
839
+ if (body.schema !== "gentle-ai.review-verification-evidence/v2") throw new Error("wrong verification evidence schema");
840
+ return {
841
+ schema: "gentle-ai.review-verification-evidence/v2",
842
+ // verification-evidence.schema.json pins `version` to {"const": 2} -- the
843
+ // NUMBER two, not the string. It was written as a string from a handoff
844
+ // that listed the observed field NAMES without their types.
845
+ version: (() => { if (body.version !== 2) throw new Error(`native verification evidence version must be the number 2, received ${JSON.stringify(body.version)}`); return 2 ; })(),
846
+ lineageId: requiredString(body.lineage_id),
847
+ authorityRevision: requiredString(body.authority_revision),
848
+ targetIdentity: requiredString(body.target_identity),
849
+ candidateTree: requiredString(body.candidate_tree),
850
+ pathsDigest: requiredString(body.paths_digest),
851
+ paths: stringArray(body.paths),
852
+ // The schema requires ledger_ids to be an array, but v2.2.2 emits null when
853
+ // there are none. Tolerated deliberately: refusing here would reject a
854
+ // response the provider actually sends, and the provider violating its own
855
+ // published schema is its defect to fix, not a reason to break the client.
856
+ ledgerIds: body.ledger_ids === null || body.ledger_ids === undefined ? [] : stringArray(body.ledger_ids),
857
+ rawPayloadSha256: requiredString(body.raw_payload_sha256),
858
+ rawPayloadBytes: nonNegativeInteger(body.raw_payload_bytes),
859
+ outcome: enumString(body.outcome, NATIVE_REVIEW_CAPTURE_OUTCOME) ,
860
+ recordDigest: requiredString(body.record_digest),
861
+ };
862
+ }
863
+ // Unimplemented next_transition.execute.operation values must never reach
864
+ // argv synthesis. Checked against the raw pre-decode body so an operation
865
+ // gentle-pi does not implement (e.g. a future `dispose-result`) fails with a
866
+ // named, typed refusal instead of a generic schema-incompatible error, and
867
+ // before any client ever tries to build an invocation for it (Design
868
+ // Decision #6, migrate-review-integration-v2).
869
+ const NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS = new Set(["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"]);
870
+ function assertSupportedNextTransitionOperation(body ) {
871
+ const nextTransition = body.next_transition;
872
+ if (typeof nextTransition !== "object" || nextTransition === null || Array.isArray(nextTransition)) return;
873
+ const execute = (nextTransition ).execute;
874
+ if (typeof execute !== "object" || execute === null || Array.isArray(execute)) return;
875
+ const operation = (execute ).operation;
876
+ if (typeof operation === "string" && !NATIVE_REVIEW_SUPPORTED_TRANSITION_OPERATIONS.has(operation)) {
877
+ throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNSUPPORTED_TRANSITION_OPERATION, NATIVE_REVIEW_OPERATION.STATUS, false, `unsupported-transition-operation: gentle-pi does not implement the next_transition operation "${operation}"; refusing rather than synthesizing an invocation for it`);
878
+ }
879
+ }
540
880
  function decode (operation , mutating , callback , diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)) {
541
881
  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 }); }
542
882
  }
@@ -635,6 +975,28 @@ function decodeNativeReviewStatusDiagnostic(value )
635
975
  const diagnostic = exactObject(value, ["path", "problem"]);
636
976
  return { path: requiredString(diagnostic.path), problem: requiredString(diagnostic.problem) };
637
977
  }
978
+ function decodeNativeReviewModeStatus(value ) {
979
+ const status = exactObject(value, ["schema", "global", "clone_local", "effective", "source"], ["revision"]);
980
+ if (status.schema !== "gentle-ai.rdd-mode-status/v1") throw new Error("wrong review mode status schema");
981
+ return {
982
+ global: enumString(status.global, Object.values(NATIVE_REVIEW_MODE_VALUE)) ,
983
+ cloneLocal: enumString(status.clone_local, Object.values(NATIVE_REVIEW_MODE_VALUE)) ,
984
+ effective: enumString(status.effective, ["on", "off"]) ,
985
+ source: enumString(status.source, Object.values(NATIVE_REVIEW_MODE_SOURCE)) ,
986
+ ...(status.revision === undefined ? {} : { revision: requiredString(status.revision) }),
987
+ };
988
+ }
989
+
990
+ function decodeNativeReviewMode(value , expectedOperation ) {
991
+ const body = exactObject(value, ["schema", "operation", "scope", "status"]);
992
+ if (body.schema !== "gentle-ai.review-mode/v1" || body.operation !== expectedOperation) throw new Error("wrong review mode discriminator");
993
+ return {
994
+ operation: expectedOperation,
995
+ scope: enumString(body.scope, Object.values(NATIVE_REVIEW_MODE_SCOPE)) ,
996
+ status: decodeNativeReviewModeStatus(body.status),
997
+ };
998
+ }
999
+
638
1000
  function decodeNativeReviewStatus(value ) {
639
1001
  const body = exactObject(value, ["schema", "operation", "repository", "complete", "authoritative", "status", "entries", "locks", "diagnostics"]);
640
1002
  if (body.schema !== "gentle-ai.review-authority-status/v1" || body.operation !== "review/status") throw new Error("wrong review status discriminator");
@@ -791,7 +1153,12 @@ export class NativeReviewCliV214 {
791
1153
  }
792
1154
  }
793
1155
 
794
- async execute(operation , cwd , arguments_ , mutating , signal ) {
1156
+ // toleratedStderr is an exact-match, operation-scoped allowlist (Design
1157
+ // Decision #6, organic-rdd-parity): only START passes a non-empty list, and
1158
+ // only when the negotiated version's `mode` capability is true. A near-miss,
1159
+ // prefixed, or multi-line stderr is never tolerated — only byte-exact
1160
+ // membership in the frozen set.
1161
+ async execute(operation , cwd , arguments_ , mutating , signal , toleratedStderr = []) {
795
1162
  let result ;
796
1163
  try { result = await this.adapter({ file: this.executablePath(operation, mutating), arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
797
1164
  catch (error) {
@@ -805,8 +1172,9 @@ export class NativeReviewCliV214 {
805
1172
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
806
1173
  const structuredValidateDenial = operation === NATIVE_REVIEW_OPERATION.VALIDATE && result.exitCode === 1;
807
1174
  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;
1175
+ const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
808
1176
  if (result.exitCode !== 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
809
- if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1177
+ if (result.stderr.trim().length > 0 && !structuredValidateDenial && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
810
1178
  return { body: parseJson(result.stdout, operation, mutating, diagnostics), exitCode: result.exitCode, process: result };
811
1179
  }
812
1180
 
@@ -823,8 +1191,8 @@ export class NativeReviewCliV214 {
823
1191
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, NATIVE_REVIEW_OPERATION.VERSION, false, "version process was signalled", result);
824
1192
  if (result.exitCode !== 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.VERSION, false, "version process failed", result);
825
1193
  const version = /^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/.exec(result.stdout.replace(/\r\n$/, "\n"))?.[1];
826
- const contract = version === undefined ? undefined : NATIVE_CLI_CONTRACTS[version ];
827
- 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");
1194
+ const contract = version === undefined ? undefined : resolvedNativeCliContract(version);
1195
+ 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"}`);
828
1196
  return version ;
829
1197
  }
830
1198
 
@@ -833,10 +1201,18 @@ export class NativeReviewCliV214 {
833
1201
  if (request.committedOnly !== undefined && typeof request.committedOnly !== "boolean") throw new TypeError("Native START committedOnly must be a boolean when supplied");
834
1202
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
835
1203
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
836
- await this.verifyVersion(request.cwd, request.signal, ["start"]);
837
- 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);
1204
+ const version = await this.verifyVersion(request.cwd, request.signal, ["start"]);
1205
+ const toleratedStderr = resolvedNativeCliContract(version)?.mode === true ? REVIEW_CONSENT_NOTICES : [];
1206
+ 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);
838
1207
  return decode(NATIVE_REVIEW_OPERATION.START, true, () => {
839
- const body = exactObject(result, ["operation", "lineage_id", "state", "risk_level", "selected_lenses", "changed_files", "changed_lines", "correction_budget", "action", "lenses_required", "projection"]);
1208
+ // `target_identity` and `lens_bindings` are real, unconditionally-present
1209
+ // fields on gentle-ai's plain (non-negotiated) `review start` JSON output
1210
+ // (confirmed against the pinned v2.1.11 Go source and live against a dev
1211
+ // build during Phase 6 dev-binary ground-truthing, organic-rdd-parity) —
1212
+ // tolerated here but not consumed: Pi's negotiated-contract client
1213
+ // (NativeReviewCliV216, the production default) carries lineage/target
1214
+ // identity through its own `review-integration/v1` decoder instead.
1215
+ 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"]);
840
1216
  if (body.operation !== "review/start" || body.projection !== "workspace" || !(NATIVE_FINALIZE_STATE ).includes(stringValue(body.state))) throw new Error("wrong start discriminator");
841
1217
  const lineageId = requiredString(body.lineage_id);
842
1218
  if (request.lineageId && lineageId !== request.lineageId) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.START, true, "native start lineage mismatch");
@@ -850,7 +1226,11 @@ export class NativeReviewCliV214 {
850
1226
  !hasCanonicalSelectedLenses(riskLevel, selectedLenses) ||
851
1227
  !hasValidLensesRequired(action, body.state , riskLevel, lensesRequired)
852
1228
  ) throw new Error("unknown or contradictory start enum");
853
- return { lineageId, state: body.state , riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired };
1229
+ return {
1230
+ lineageId, state: body.state , riskLevel, selectedLenses, changedFiles: nonNegativeInteger(body.changed_files), changedLines: nonNegativeInteger(body.changed_lines), correctionBudget: nonNegativeInteger(body.correction_budget), action, lensesRequired,
1231
+ ...(body.risk_evidence === undefined ? {} : { riskEvidence: stringArray(body.risk_evidence) }),
1232
+ ...(body.hint === undefined ? {} : { hint: requiredString(body.hint) }),
1233
+ };
854
1234
  });
855
1235
  }
856
1236
 
@@ -895,18 +1275,31 @@ export class NativeReviewCliV214 {
895
1275
  await this.verifyVersion(request.cwd, request.signal, ["validate"]);
896
1276
  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);
897
1277
  return decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => {
898
- const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"]);
1278
+ const body = exactObject(execution.body, ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
899
1279
  const gateResult = enumString(body.result, NATIVE_GATE_RESULT) ;
900
1280
  const action = sanitizeNativeDiagnosticText(requiredString(body.action), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
901
1281
  const reason = sanitizeNativeDiagnosticText(requiredString(body.reason), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
902
- const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
903
- const expectedExitCode = gateResult === "allow" ? 0 : 1;
904
- if (body.schema !== "gentle-ai.review-gate-result/v1" || typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason) || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
1282
+ // `delivery` (Design Decision #9, organic-rdd-parity) is an alternate
1283
+ // discriminator: when present it must be the single literal
1284
+ // "disabled/unmanaged" (gentle-ai's RDDDeliveryDisabledUnmanaged — the
1285
+ // kill switch is off and no receipt governs the candidate), paired
1286
+ // exactly with result:invalidated, allowed:false, action:repository-policy,
1287
+ // and exit 0 — never the strict continue/create-new-lineage/
1288
+ // explicit-maintainer-action/stop pairing used when delivery is absent.
1289
+ const delivery = body.delivery === undefined ? undefined : (enumString(body.delivery, ["disabled/unmanaged"]) );
1290
+ if (body.schema !== "gentle-ai.review-gate-result/v1" || !isCanonicalProcessString(action) || !isCanonicalProcessString(reason)) throw new Error("wrong validate discriminator");
1291
+ if (delivery !== undefined) {
1292
+ if (gateResult !== "invalidated" || body.allowed !== false || action !== "repository-policy" || execution.exitCode !== 0) throw new Error("wrong validate delivery discriminator");
1293
+ } else {
1294
+ const expectedAction = { allow: "continue", "scope-changed": "create-new-lineage", invalidated: "explicit-maintainer-action", escalated: "stop" }[gateResult];
1295
+ const expectedExitCode = gateResult === "allow" ? 0 : 1;
1296
+ if (typeof body.allowed !== "boolean" || body.allowed !== (gateResult === "allow") || action !== expectedAction || execution.exitCode !== expectedExitCode) throw new Error("wrong validate discriminator");
1297
+ }
905
1298
  const gateContext = decodeGateContext(body.context);
906
1299
  const returnedGate = gateContext.raw.gate;
907
1300
  if (returnedGate !== request.gate && (gateResult === "allow" || returnedGate !== "")) throw new Error("native gate context does not match the requested gate");
908
1301
  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");
909
- return { allowed: body.allowed, result: gateResult, action, reason, gateContext };
1302
+ return { allowed: body.allowed , result: gateResult, action, reason, gateContext, ...(delivery === undefined ? {} : { delivery }) };
910
1303
  });
911
1304
  }
912
1305
 
@@ -939,6 +1332,24 @@ export class NativeReviewCliV214 {
939
1332
  return status;
940
1333
  }
941
1334
 
1335
+ // Dark until the negotiated version reports `mode: true` (Design Decision
1336
+ // #7, organic-rdd-parity). `status` is read-only (no timeout suppression,
1337
+ // exact fixed argv per the design's data flow); `enable`/`disable` mutate
1338
+ // and always pass `--scope clone` so Pi's own kill-switch command surface
1339
+ // never mutates the operator's global gentle-ai state across other clones.
1340
+ async reviewMode(request ) {
1341
+ await this.verifyVersion(request.cwd, request.signal, ["mode"]);
1342
+ const mutating = request.operation !== NATIVE_REVIEW_MODE_OPERATION.STATUS;
1343
+ const { body } = await this.execute(
1344
+ NATIVE_REVIEW_OPERATION.MODE,
1345
+ request.cwd,
1346
+ ["review", "mode", request.operation, "--cwd", request.cwd, ...(mutating ? ["--scope", "clone"] : []), "--json"],
1347
+ mutating,
1348
+ request.signal,
1349
+ );
1350
+ return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(body, request.operation));
1351
+ }
1352
+
942
1353
  async sddStatus(request ) {
943
1354
  await this.verifyVersion(request.cwd, request.signal, ["sddStatus"]);
944
1355
  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);
@@ -967,9 +1378,16 @@ export class NativeReviewCliV214 {
967
1378
  nonNegativeInteger(remediation.generation); nonNegativeInteger(remediation.fixBatch);
968
1379
  if (remediation.correctionBudget !== undefined) nonNegativeInteger(remediation.correctionBudget);
969
1380
  let reviewGateResult ;
1381
+ let reviewGateDelivery ;
970
1382
  if (body.reviewGate !== undefined) {
971
- const gate = exactObject(body.reviewGate, ["result", "reason"]);
1383
+ // `delivery` (present since 09e4b14c, gentle-ai v2.2.0+) is emitted
1384
+ // when the kill switch is off: gentle-ai's status struct carries
1385
+ // `Delivery` with `json:"delivery,omitempty"`, so an exact-key decode
1386
+ // without it rejects that payload outright. Pre-existing bug fix
1387
+ // (Phase 13.13, migrate-review-integration-v2).
1388
+ const gate = exactObject(body.reviewGate, ["result", "reason"], ["delivery"]);
972
1389
  reviewGateResult = enumString(gate.result, NATIVE_GATE_RESULT); requiredString(gate.reason);
1390
+ if (gate.delivery !== undefined) reviewGateDelivery = enumString(gate.delivery, ["disabled/unmanaged"]);
973
1391
  }
974
1392
  if (body.reviewTransaction !== undefined) decodeReviewTransaction(body.reviewTransaction);
975
1393
  if (body.phaseInstructions !== undefined) {
@@ -987,7 +1405,13 @@ export class NativeReviewCliV214 {
987
1405
  ready:
988
1406
  (NATIVE_SDD_POST_REVIEW_ACTION ).includes(nextRecommended) &&
989
1407
  blockedReasons.length === 0 &&
990
- reviewGateResult === "allow",
1408
+ // With the kill switch off, the gate result can never become
1409
+ // "allow" (delivery follows ordinary repository policy instead) —
1410
+ // gated only on `result === "allow"` this deadlocked `ready`
1411
+ // forever, the same deadlock gentle-ai fixed upstream in 2c18fa10.
1412
+ // `disabled/unmanaged` delivery is the legitimate non-blocking
1413
+ // terminal state and must also unblock readiness.
1414
+ (reviewGateResult === "allow" || reviewGateDelivery === "disabled/unmanaged"),
991
1415
  };
992
1416
  });
993
1417
  }
@@ -1193,6 +1617,20 @@ export class NativeReviewIntegrationError extends Error {
1193
1617
  }
1194
1618
  }
1195
1619
 
1620
+ // Raised when negotiated START answers `consent/v2` (action:
1621
+ // "consent_required") instead of `start/v3`. Pi stays headless and never
1622
+ // sends `--consent relay`, so this is a blocking question surfaced to the
1623
+ // caller rather than answered on its behalf.
1624
+ export class NativeReviewConsentRequiredError extends Error {
1625
+ consent ;
1626
+ launchAttempted = true;
1627
+ constructor(consent ) {
1628
+ super(consent.headline);
1629
+ this.name = "NativeReviewConsentRequiredError";
1630
+ this.consent = consent;
1631
+ }
1632
+ }
1633
+
1196
1634
 
1197
1635
  const nativeCapabilitiesByDigest = new Map ();
1198
1636
 
@@ -1218,6 +1656,31 @@ function defaultExecutableDigest(path ) {
1218
1656
 
1219
1657
 
1220
1658
 
1659
+ function decodeNativeAdmittedResultManifest(value ) {
1660
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native capture-result manifest must be an object");
1661
+ const body = value ;
1662
+ const text = (key ) => {
1663
+ const found = body[key];
1664
+ 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`);
1665
+ return found;
1666
+ };
1667
+ const schema = text("schema");
1668
+ 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}`);
1669
+ const admission = text("admission_decision");
1670
+ if (admission !== "completed") throw new TypeError(`native capture-result manifest admission_decision must be completed, received ${admission}`);
1671
+ // Exactly one locator: a provider-owned path OR an opaque reference. Both or
1672
+ // neither means the manifest cannot be handed to FINALIZE.
1673
+ const hasPath = body.path !== undefined, hasReference = body.reference !== undefined;
1674
+ if (hasPath === hasReference) throw new TypeError("native capture-result manifest must carry exactly one of path or reference");
1675
+ return Object.freeze({
1676
+ schema,
1677
+ subjectHash: text("subject_hash"),
1678
+ admissionDecision: admission,
1679
+ ...(body.lens === undefined ? {} : { lens: text("lens") }),
1680
+ ...(hasPath ? { path: text("path") } : { reference: text("reference") }),
1681
+ });
1682
+ }
1683
+
1221
1684
  export class NativeReviewCliV216 {
1222
1685
  legacy ;
1223
1686
  adapter ;
@@ -1271,6 +1734,7 @@ export class NativeReviewCliV216 {
1271
1734
  mutating ,
1272
1735
  signal ,
1273
1736
  path ,
1737
+ toleratedStderr = [],
1274
1738
  ) {
1275
1739
  let result ;
1276
1740
  try {
@@ -1286,13 +1750,13 @@ export class NativeReviewCliV216 {
1286
1750
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
1287
1751
  if (result.exitCode !== 0) {
1288
1752
  try {
1289
- throw new NativeReviewIntegrationError(decodeReviewFailureV1(body));
1753
+ throw new NativeReviewIntegrationError(decodeReviewFailureV2(body));
1290
1754
  } catch (error) {
1291
1755
  if (error instanceof NativeReviewIntegrationError) throw error;
1292
1756
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
1293
1757
  }
1294
1758
  }
1295
- if (result.stderr.trim().length > 0) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
1759
+ 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);
1296
1760
  return { body, exitCode: result.exitCode };
1297
1761
  }
1298
1762
 
@@ -1309,7 +1773,22 @@ export class NativeReviewCliV216 {
1309
1773
  request.signal,
1310
1774
  executable.path,
1311
1775
  );
1312
- return decode(NATIVE_REVIEW_OPERATION.VERSION, false, () => decodeReviewCapabilitiesV1(execution.body, executable.digest));
1776
+ // Two distinct half-upgraded-install failures (Design Decision #7,
1777
+ // migrate-review-integration-v2): a decode failure means the installed
1778
+ // runtime cannot even answer this shape (an older `.gentle-ai/` payload
1779
+ // that still speaks `unsupported_contract` for v2); a successful decode
1780
+ // whose package.version disagrees with the pin names both versions.
1781
+ let capabilities ;
1782
+ try {
1783
+ capabilities = decodeReviewCapabilitiesV2(execution.body, executable.digest);
1784
+ } catch (error) {
1785
+ if (error instanceof NativeReviewCliError) throw error;
1786
+ 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`);
1787
+ }
1788
+ if (capabilities.packageVersion !== GENTLE_AI_VERSION) {
1789
+ 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}`);
1790
+ }
1791
+ return capabilities;
1313
1792
  })();
1314
1793
  nativeCapabilitiesByDigest.set(executable.digest, negotiation);
1315
1794
  try {
@@ -1326,6 +1805,7 @@ export class NativeReviewCliV216 {
1326
1805
  arguments_ ,
1327
1806
  mutating ,
1328
1807
  signal ,
1808
+ toleratedStderr = [],
1329
1809
  ) {
1330
1810
  const executable = this.verifiedExecutable(operation, mutating);
1331
1811
  await this.capabilities({ cwd, ...(signal === undefined ? {} : { signal }) });
@@ -1333,21 +1813,47 @@ export class NativeReviewCliV216 {
1333
1813
  if (afterNegotiation.path !== executable.path || afterNegotiation.digest !== executable.digest) {
1334
1814
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, operation, mutating, "native executable was replaced after capability negotiation", undefined, false);
1335
1815
  }
1336
- return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path);
1816
+ return this.invoke(operation, cwd, arguments_, mutating, signal, executable.path, toleratedStderr);
1337
1817
  }
1338
1818
 
1339
1819
  async start(request ) {
1340
1820
  if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1341
1821
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
1342
1822
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native START committedOnly requires an explicit baseRef");
1823
+ // A negotiated START requires exactly one --contract, --target, and
1824
+ // --projection. The target is resolved here, from the same root START is
1825
+ // about to run in, rather than threaded in by the caller: the caller's
1826
+ // root and START's root differ whenever a candidate view is in play, and
1827
+ // a target frozen from the wrong root would name a snapshot this START
1828
+ // never inspects. targetStatus is read-only, so this costs one extra
1829
+ // read and cannot create authority.
1830
+ const projection = request.projection ?? "workspace";
1831
+ const target = await this.targetStatus({
1832
+ cwd: request.cwd,
1833
+ projection,
1834
+ ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef }),
1835
+ ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }),
1836
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
1837
+ });
1343
1838
  const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.START, request.cwd, [
1344
1839
  "review", "start", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
1840
+ "--target", target.targetIdentity, "--projection", projection,
1345
1841
  ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
1346
1842
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1347
1843
  ...(request.policyPath === undefined ? [] : ["--policy", request.policyPath]),
1348
1844
  ...(request.focus === undefined ? [] : ["--focus", request.focus]),
1349
- ], true, request.signal);
1350
- const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV1(execution.body));
1845
+ // A headless START that reviews without asking still succeeds; its
1846
+ // consent notices are console output, never a failure signal.
1847
+ ], true, request.signal, REVIEW_CONSENT_NOTICES);
1848
+ // A negotiated v2 START may answer `consent/v2` (action:
1849
+ // "consent_required") instead of `start/v3` when the provider needs an
1850
+ // explicit answer it cannot infer. Discriminated BEFORE decode: the two
1851
+ // envelopes share no required shape, and Pi never sends `--consent
1852
+ // relay`, so this is always surfaced to the caller, never answered here.
1853
+ if (execution.body.action === "consent_required") {
1854
+ throw new NativeReviewConsentRequiredError(decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewConsentV2(execution.body)));
1855
+ }
1856
+ const result = decode(NATIVE_REVIEW_OPERATION.START, true, () => decodeReviewStartV3(execution.body));
1351
1857
  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");
1352
1858
  return {
1353
1859
  lineageId: result.lineageId,
@@ -1360,6 +1866,19 @@ export class NativeReviewCliV216 {
1360
1866
  action: result.action ,
1361
1867
  lensesRequired: result.lensesRequired,
1362
1868
  riskReasons: result.riskReasons.map((reason) => ({ ...reason })),
1869
+ // Derived, not received. `risk_reasons` is a required start/v2 field
1870
+ // already recomputed against the authoritative frozen snapshot, so
1871
+ // these phrases describe the same candidate the lenses will review.
1872
+ ...(() => {
1873
+ const evidence = nativeRiskEvidencePhrases(result.riskLevel, result.riskReasons);
1874
+ return evidence.length === 0 ? {} : { riskEvidence: evidence };
1875
+ })(),
1876
+ // Only the empty-candidate recovery is reconstructed. Its sibling
1877
+ // tells a plain caller to rerun under the negotiated contract, which
1878
+ // this client already did, so relaying it would name a step that
1879
+ // changes nothing. A committed-only start (baseRef) is already the
1880
+ // recovery, and reporting zero changes there is a real answer.
1881
+ ...(result.changedFiles === 0 && request.baseRef === undefined ? { hint: REVIEW_EMPTY_CANDIDATE_HINT } : {}),
1363
1882
  raw: result.raw,
1364
1883
  };
1365
1884
  }
@@ -1391,14 +1910,20 @@ export class NativeReviewCliV216 {
1391
1910
  const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.FINALIZE, request.cwd, [
1392
1911
  "review", "finalize", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
1393
1912
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1394
- ...resultFiles.flatMap((path) => ["--result", path]),
1913
+ // `--result` is RETIRED. A reviewer result supplied that way carries
1914
+ // no provider-owned admission, so it cannot prove the lens inspected
1915
+ // the frozen candidate. Results reach authority through
1916
+ // `review capture-result`, and FINALIZE either discovers them
1917
+ // (`--captured-results`) or is handed each manifest in lens order.
1918
+ ...(request.capturedResults === true ? ["--captured-results=true"] : []),
1919
+ ...(request.resultArtifactFiles ?? []).flatMap((path) => ["--result-artifact-file", path]),
1395
1920
  ...(refuterFile === undefined ? [] : ["--refuter", refuterFile]),
1396
1921
  ...(request.correctionLines === undefined ? [] : ["--correction-lines", String(request.correctionLines)]),
1397
1922
  ...(validationFile === undefined ? [] : ["--validation", validationFile]),
1398
1923
  ...(evidenceFile === undefined ? [] : ["--evidence", evidenceFile]),
1399
1924
  ...(request.failed === true ? ["--failed"] : []),
1400
1925
  ], true, request.signal);
1401
- const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV1(execution.body));
1926
+ const envelope = decode(NATIVE_REVIEW_OPERATION.FINALIZE, true, () => decodeReviewOperationV2(execution.body));
1402
1927
  if (envelope.operation !== "review.finalize") throw new Error("wrong finalize operation envelope");
1403
1928
  const body = envelope.result;
1404
1929
  const lineageId = requiredString(body.lineage_id);
@@ -1408,6 +1933,12 @@ export class NativeReviewCliV216 {
1408
1933
  state: requiredString(body.state),
1409
1934
  action: requiredString(body.action),
1410
1935
  storeRevision: requiredString(body.store_revision),
1936
+ // Present only when state is correction_required (validated by
1937
+ // decodeReviewOperationV2); carried through as the raw validated
1938
+ // shape rather than re-decoded, since the typed decoder for this
1939
+ // nested object is private to lib/review-integration-v2.ts.
1940
+ ...(body.validation_request === undefined ? {} : { validationRequest: body.validation_request }),
1941
+ ...(body.escalation === undefined ? {} : { escalation: requiredString(body.escalation) }),
1411
1942
  };
1412
1943
  } finally {
1413
1944
  if (directory !== undefined) await this.cleanupDirectory(directory).catch(() => undefined);
@@ -1420,7 +1951,7 @@ export class NativeReviewCliV216 {
1420
1951
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1421
1952
  ...(request.flags ?? []),
1422
1953
  ], false, request.signal);
1423
- const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV1(execution.body));
1954
+ const envelope = decode(NATIVE_REVIEW_OPERATION.VALIDATE, false, () => decodeReviewOperationV2(execution.body));
1424
1955
  if (envelope.operation !== "review.validate") throw new Error("wrong validate operation envelope");
1425
1956
  const body = envelope.result;
1426
1957
  const gateContext = decodeGateContext(body.context);
@@ -1430,6 +1961,7 @@ export class NativeReviewCliV216 {
1430
1961
  action: requiredString(body.action),
1431
1962
  reason: requiredString(body.reason),
1432
1963
  gateContext,
1964
+ ...(body.delivery === undefined ? {} : { delivery: enumString(body.delivery, ["disabled/unmanaged"]) }),
1433
1965
  };
1434
1966
  }
1435
1967
 
@@ -1439,7 +1971,7 @@ export class NativeReviewCliV216 {
1439
1971
  "--change", request.change, "--lineage", request.lineage,
1440
1972
  `--expected-binding-revision=${request.expectedBindingRevision}`,
1441
1973
  ], true, request.signal);
1442
- const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV1(execution.body));
1974
+ const envelope = decode(NATIVE_REVIEW_OPERATION.BIND_SDD, true, () => decodeReviewOperationV2(execution.body));
1443
1975
  if (envelope.operation !== "review.bind_sdd") throw new Error("wrong bind-sdd operation envelope");
1444
1976
  const body = envelope.result;
1445
1977
  const gateContext = decodeGateContext(body.gate_context);
@@ -1462,8 +1994,91 @@ export class NativeReviewCliV216 {
1462
1994
  "--projection", request.projection ?? "workspace",
1463
1995
  ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef]),
1464
1996
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
1997
+ "--next-transition",
1465
1998
  ], false, request.signal);
1466
- return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV1(execution.body));
1999
+ assertSupportedNextTransitionOperation(execution.body);
2000
+ return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
2001
+ }
2002
+
2003
+ // Net-new negotiated `review.repair`: preflight first, execute only when
2004
+ // the assessment is eligible, using exactly the provider_inputs that
2005
+ // preflight published (Design Decision #6, migrate-review-integration-v2).
2006
+ // Argv shape beyond --mode is inferred from provider_inputs' own field
2007
+ // names — no repair/v2 fixture is mirrored upstream to ground-truth it
2008
+ // against (design.md Open Questions); this is a documented risk.
2009
+ async repair(request ) {
2010
+ const preflightExecution = await this.negotiated(NATIVE_REVIEW_OPERATION.REPAIR, request.cwd, [
2011
+ "review", "repair", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd, "--mode", "preflight",
2012
+ ], false, request.signal);
2013
+ const preflight = decode(NATIVE_REVIEW_OPERATION.REPAIR, false, () => decodeReviewRepairV2(preflightExecution.body));
2014
+ if (preflight.mode !== "preflight") throw new Error("wrong repair preflight discriminator");
2015
+ if (preflight.assessment.status !== "eligible" || preflight.providerInputs === undefined) return preflight;
2016
+ const providerInputs = preflight.providerInputs;
2017
+ const executeExecution = await this.negotiated(NATIVE_REVIEW_OPERATION.REPAIR, request.cwd, [
2018
+ "review", "repair", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd, "--mode", "execute",
2019
+ "--lineage", providerInputs.lineageId,
2020
+ "--expected-revision", providerInputs.expectedRevision,
2021
+ "--cause", providerInputs.cause,
2022
+ "--disposition", providerInputs.disposition,
2023
+ "--repository-binding", providerInputs.repositoryBinding,
2024
+ "--actor", request.actor,
2025
+ "--reason", request.reason,
2026
+ "--maintainer-authorization", request.maintainerAuthorization,
2027
+ ], true, request.signal);
2028
+ return decode(NATIVE_REVIEW_OPERATION.REPAIR, true, () => decodeReviewRepairV2(executeExecution.body));
2029
+ }
2030
+
2031
+ // `review capture-evidence`: the evidence-first correction lifecycle's
2032
+ // collection step. Confirmed argv and response shape against a real v2.2.2
2033
+ // review run (lineage review-b39d803b68a90767): exactly
2034
+ // --cwd/--lineage/--target/--expected-revision/--outcome/--input, no
2035
+ // --contract, and the verification-evidence/v2 record returned DIRECTLY —
2036
+ // not wrapped in an operation/v2 envelope. Evidence is staged through the
2037
+ // same 0o600 tmpfile discipline FINALIZE uses.
2038
+ async captureResult(request ) {
2039
+ if (request.argumentTokens.length === 0) throw new TypeError("Native CAPTURE_RESULT requires the provider-issued argument tokens");
2040
+ if (request.argumentTokens.some((token) => typeof token !== "string" || token.length === 0)) throw new TypeError("Native CAPTURE_RESULT argument tokens must all be non-empty strings");
2041
+ if (request.resultDocument.length === 0) throw new TypeError("Native CAPTURE_RESULT result document must contain at least one byte");
2042
+ const carriesContext = request.argumentTokens.some((token) => token === "--repository-context" || token.startsWith("--repository-context="));
2043
+ if (carriesContext && request.cwd !== undefined) throw new TypeError("Native CAPTURE_RESULT takes a repository context or --cwd, never both");
2044
+ const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-result-"));
2045
+ try {
2046
+ await chmod(directory, 0o700);
2047
+ const resultFile = join(directory, "result.json");
2048
+ await writeFile(resultFile, request.resultDocument, { encoding: "utf8", mode: 0o600 });
2049
+ await chmod(resultFile, 0o600);
2050
+ const executable = this.verifiedExecutable(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true);
2051
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, request.cwd ?? process.cwd(), [
2052
+ "review", "capture-result",
2053
+ ...request.argumentTokens,
2054
+ ...(carriesContext || request.cwd === undefined ? [] : ["--cwd", request.cwd]),
2055
+ "--input", resultFile,
2056
+ ], true, request.signal, executable.path);
2057
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_RESULT, true, () => decodeNativeAdmittedResultManifest(execution.body));
2058
+ } finally {
2059
+ await this.cleanupDirectory(directory).catch(() => undefined);
2060
+ }
2061
+ }
2062
+
2063
+ async captureEvidence(request ) {
2064
+ if (!(NATIVE_REVIEW_CAPTURE_OUTCOME ).includes(request.outcome)) throw new TypeError("Native CAPTURE_EVIDENCE outcome must be passed, verification_failed, or procedural_tooling_failed");
2065
+ if (request.evidenceDocument.length === 0) throw new TypeError("Native CAPTURE_EVIDENCE evidence must contain at least one byte");
2066
+ const directory = await mkdtemp(join(tmpdir(), "gentle-ai-capture-evidence-"));
2067
+ try {
2068
+ await chmod(directory, 0o700);
2069
+ const evidenceFile = await this.stageEvidence(directory, request.evidenceDocument);
2070
+ const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, request.cwd, [
2071
+ "review", "capture-evidence", "--cwd", request.cwd,
2072
+ "--lineage", request.lineageId,
2073
+ "--target", request.targetIdentity,
2074
+ "--expected-revision", request.expectedRevision,
2075
+ "--outcome", request.outcome,
2076
+ "--input", evidenceFile,
2077
+ ], true, request.signal);
2078
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_EVIDENCE, true, () => decodeNativeReviewVerificationEvidence(execution.body));
2079
+ } finally {
2080
+ await this.cleanupDirectory(directory).catch(() => undefined);
2081
+ }
1467
2082
  }
1468
2083
 
1469
2084
  reviewStatus(request ) {
@@ -1474,6 +2089,13 @@ export class NativeReviewCliV216 {
1474
2089
  return this.legacy.sddStatus(request);
1475
2090
  }
1476
2091
 
2092
+ // Same plain-CLI delegation as reviewStatus/sddStatus above: `review mode`
2093
+ // is dark until the negotiated version reports `mode: true` and stays
2094
+ // outside the negotiated integration-v1 contract.
2095
+ reviewMode(request ) {
2096
+ return this.legacy.reviewMode(request);
2097
+ }
2098
+
1477
2099
  // Recovery commands are version-gated plain CLI operations outside the
1478
2100
  // negotiated integration-v1 contract, exactly like reviewStatus/sddStatus.
1479
2101
  reclaim(request ) {