gentle-pi 2.2.0 → 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 (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. package/tests/git-commit-transaction.test.ts +0 -530
@@ -1,13 +1,9 @@
1
1
  export const REVIEW_INTEGRATION_CONTRACT = "gentle-ai.review-integration/v2";
2
2
 
3
3
  export const REVIEW_INTEGRATION_OPERATION = {
4
- BIND_SDD: "review.bind_sdd",
5
- CAPABILITIES: "review.capabilities",
6
- FINALIZE: "review.finalize",
7
4
  REPAIR: "review.repair",
8
5
  START: "review.start",
9
6
  STATUS: "review.status",
10
- VALIDATE: "review.validate",
11
7
  } as const;
12
8
  export type ReviewIntegrationOperation = (typeof REVIEW_INTEGRATION_OPERATION)[keyof typeof REVIEW_INTEGRATION_OPERATION];
13
9
 
@@ -78,30 +74,27 @@ export const REVIEW_START_STATE = {
78
74
  } as const;
79
75
  export type ReviewStartState = (typeof REVIEW_START_STATE)[keyof typeof REVIEW_START_STATE];
80
76
 
81
- const START_ACTIONS = ["created", "resumed", "reuse-receipt", "blocked-scope-action"] as const;
77
+ const START_ACTIONS = ["created", "resumed", "closed", "blocked-scope-action"] as const;
82
78
  const RISK_LEVELS = ["low", "medium", "high"] as const;
83
79
  const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
84
80
  const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
85
81
  const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
86
- const STATUS_ACTIONS = ["start", "finalize", "validate", "recover", "retry_final_verification", "maintainer_action", "select_lineage", "repair_authority", "reconcile_finalize", "stop"] as const;
82
+ const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop"] as const;
83
+ const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
84
+ type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
87
85
  export const REVIEW_STATUS_ACTION_DISPOSITION = {
88
86
  SCOPE_CHANGED: "scope_changed",
89
87
  INVALIDATED: "invalidated",
90
88
  ESCALATED: "escalated",
91
- FINAL_VERIFICATION_RETRY: "final_verification_retry",
92
89
  } as const;
93
90
  export type ReviewStatusActionDisposition = (typeof REVIEW_STATUS_ACTION_DISPOSITION)[keyof typeof REVIEW_STATUS_ACTION_DISPOSITION];
94
- const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
95
91
  const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
96
- // failure/v2 operations: the capability floor plus the four collect-capture
97
- // verbs that emit typed refusals on the gentle-ai main line (commit a2d57117,
98
- // fix/capture-evidence-typed-refusal; exact strings from that branch's
99
- // published failure.schema.json 12-value operation enum). Additive forward
100
- // surface: every operation outside the published enum still rejects.
92
+ // Typed failures are accepted only for the supported start, status, repair,
93
+ // and last-event capture operations. Retired lifecycle verbs fail closed.
101
94
  const FAILURE_OPERATIONS = Object.freeze([
102
95
  ...REQUIRED_OPERATIONS,
103
96
  "review.capture-result",
104
- "review.capture-evidence",
97
+ "review.capture-correction-plan",
105
98
  "review.capture-refuter",
106
99
  "review.capture-validation",
107
100
  ] as const);
@@ -124,7 +117,6 @@ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
124
117
  "gentle-ai.review-integration.operation/v2",
125
118
  "gentle-ai.review-integration.projection/v1",
126
119
  "gentle-ai.review-integration.repair/v2",
127
- "gentle-ai.review-integration.start/v3",
128
120
  "gentle-ai.review-receipt/v1",
129
121
  "gentle-ai.review-receipt/v2",
130
122
  "gentle-ai.review-result-artifact/v2",
@@ -134,18 +126,62 @@ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
134
126
  "https://gentle-ai.dev/schema/review/reviewer/v1",
135
127
  "https://gentle-ai.dev/schema/review/validator/v1",
136
128
  ] as const);
137
- const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: number; requiredSchemas: readonly string[] }>> = Object.freeze({
129
+ // The v2.3 provider contract (first advertised by the pinned v2.5.0-rc.3
130
+ // runtime) retired exact_receipt_replay, five_delivery_gates, and
131
+ // sdd_receipt_binding from the mandatory feature set and
132
+ // exact_gate_receipt_discovery, one_shot_final_verification_retry, and
133
+ // outcome_bound_verification_evidence from the optional set; none of them is
134
+ // consumed by Pi's negotiated v2 lane. Earlier minors keep their frozen
135
+ // requirement sets unchanged.
136
+ const REQUIRED_MANDATORY_FEATURES_V23 = Object.freeze([
137
+ "compact_v2_authority",
138
+ "immutable_snapshot",
139
+ "legacy_v1_target_scoped_read_only",
140
+ "repository_independent_capabilities",
141
+ "restart_safe_projection",
142
+ "target_scoped_status",
143
+ "uniform_failure_envelope",
144
+ ] as const);
145
+ // v2.3 (first advertised by the pinned v2.5.0-rc.3 provider) retired three
146
+ // v1-era identities from its advertisement. Pi never decodes those envelopes
147
+ // on the negotiated v2 lane, so the v2.3 requirement list is the exact set the
148
+ // v2.3 provider contract defines; earlier minors keep the full common list.
149
+ // The load-time length assertion keeps this filter honest: if the common-list
150
+ // identifier form ever drifts away from these bare identifiers, the module
151
+ // fails loudly instead of silently requiring schemas the provider retired.
152
+ const RETIRED_SCHEMAS_V23: readonly string[] = Object.freeze([
153
+ "gentle-ai.review-final-verification-incident/v1",
154
+ "gentle-ai.review-receipt/v2",
155
+ "gentle-ai.review-verification-evidence/v2",
156
+ ]);
157
+ const REQUIRED_SCHEMAS_COMMON_V23 = Object.freeze(REQUIRED_SCHEMAS_COMMON.filter((schema) => !RETIRED_SCHEMAS_V23.includes(schema)));
158
+ if (REQUIRED_SCHEMAS_COMMON_V23.length !== REQUIRED_SCHEMAS_COMMON.length - RETIRED_SCHEMAS_V23.length) {
159
+ throw new TypeError("v2.3 retired-schema filter must remove exactly the retired identities from the common schema list");
160
+ }
161
+ const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: number; requiredSchemas: readonly string[]; requiredMandatoryFeatures?: readonly string[]; optionalFeatureFloor?: number }>> = Object.freeze({
138
162
  "gentle-ai.review-integration.capabilities/v2": Object.freeze({
139
163
  protocolMinor: 0,
140
- requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.status/v3"]),
164
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v3"]),
141
165
  }),
142
166
  "gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
143
167
  protocolMinor: 1,
144
- requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v3"]),
168
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v3"]),
145
169
  }),
146
170
  "gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
147
171
  protocolMinor: 2,
148
- requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v5"]),
172
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v3", "gentle-ai.review-integration.status/v5"]),
173
+ }),
174
+ "gentle-ai.review-integration.capabilities/v2.3": Object.freeze({
175
+ protocolMinor: 3,
176
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.3", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v5"]),
177
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
178
+ optionalFeatureFloor: 14,
179
+ }),
180
+ "gentle-ai.review-integration.capabilities/v2.4": Object.freeze({
181
+ protocolMinor: 4,
182
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.4", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
183
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
184
+ optionalFeatureFloor: 14,
149
185
  }),
150
186
  });
151
187
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
@@ -204,7 +240,6 @@ type ReviewLens = (typeof REVIEW_LENSES)[number];
204
240
  type RiskReasonCode = (typeof RISK_REASON_CODES)[number];
205
241
  type RiskSignal = (typeof RISK_SIGNALS)[number];
206
242
  type ReviewStatusAction = (typeof STATUS_ACTIONS)[number];
207
- type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
208
243
 
209
244
  export interface ReviewFeatureV2 {
210
245
  name: (typeof FEATURE_NAMES)[number];
@@ -300,6 +335,11 @@ export interface ReviewStartV3 {
300
335
  raw: Readonly<Record<string, unknown>>;
301
336
  }
302
337
 
338
+ export interface ReviewStartV4 extends Omit<ReviewStartV3, "action"> {
339
+ action: "created" | "replayed" | "closed" | "blocked-scope-action";
340
+ nextTransition?: ReviewNextTransitionV3;
341
+ }
342
+
303
343
  export interface ReviewProjectionDescriptorV1 {
304
344
  schema: "gentle-ai.review-integration.projection/v1";
305
345
  kind: ReviewProjectionKind;
@@ -347,6 +387,7 @@ export interface ReviewTransitionArgumentV3 {
347
387
  export interface ReviewNextTransitionExecuteV3 {
348
388
  operation: string;
349
389
  arguments: readonly ReviewTransitionArgumentV3[];
390
+ selectorArguments?: readonly ReviewTransitionArgumentV3[];
350
391
  preconditions: readonly ReviewTransitionArgumentV3[];
351
392
  binding: { targetIdentity: string; lineageId?: string; revision?: string };
352
393
  command?: string;
@@ -369,6 +410,8 @@ export interface ReviewCaptureSubmissionValueV1 {
369
410
  * array rows never carry it and existing consumers stay untouched.
370
411
  */
371
412
  schema?: string;
413
+ minimum?: number;
414
+ maximum?: number;
372
415
  }
373
416
 
374
417
  export interface ReviewCaptureSubmissionV1 {
@@ -411,27 +454,6 @@ export interface ReviewProviderTaskV1 {
411
454
  prompt: string;
412
455
  }
413
456
 
414
- export interface ReviewSubmissionDescriptorValueV5 {
415
- slot: string;
416
- domain: string;
417
- substitutionLocation: number;
418
- schema?: string;
419
- minimum?: number;
420
- maximum?: number;
421
- allowedValues?: readonly string[];
422
- }
423
-
424
- // The two v5 Go-owned submission descriptor forms (finalize carries one
425
- // singular `value`; capture-evidence carries an outcome/input `values` pair).
426
- // Distinct from the host-relay ReviewCaptureSubmissionV1 form, which stays
427
- // exactly as it is.
428
- export interface ReviewSubmissionDescriptorV5 {
429
- operationToken: "finalize" | "capture-evidence";
430
- argumentTokens: readonly string[];
431
- value?: ReviewSubmissionDescriptorValueV5;
432
- values?: readonly ReviewSubmissionDescriptorValueV5[];
433
- }
434
-
435
457
  export interface ReviewCorrectionPlanFindingV1 {
436
458
  id: string;
437
459
  lens: "risk" | "resilience" | "readability" | "reliability";
@@ -455,6 +477,45 @@ export interface ReviewCorrectionPlanRequestV1 {
455
477
  findings: readonly ReviewCorrectionPlanFindingV1[];
456
478
  }
457
479
 
480
+ export interface ReviewTargetedValidationFindingV1 {
481
+ id: string;
482
+ lens: "risk" | "resilience" | "readability" | "reliability";
483
+ location: string;
484
+ severity: "BLOCKER" | "CRITICAL";
485
+ claim: string;
486
+ proofRefs: readonly string[];
487
+ evidenceClass: "deterministic" | "inferential";
488
+ causalDisposition: "introduced" | "behavior-activated" | "worsened";
489
+ }
490
+
491
+ export interface ReviewTargetedValidationClassificationV1 {
492
+ findingId: string;
493
+ // The provider's FindingEvidence carries severity as an omitempty field, so
494
+ // a classification may or may not name one. Rejecting it made every
495
+ // targeted-validation STATUS that carried one undecodable.
496
+ severity?: string;
497
+ class: "deterministic" | "inferential";
498
+ causalDisposition: "introduced" | "behavior-activated" | "worsened";
499
+ proof: string;
500
+ }
501
+
502
+ export interface ReviewTargetedValidationRequestV1 {
503
+ schema: "gentle-ai.review-targeted-validation-request/v1";
504
+ requestHash: string;
505
+ lineageId: string;
506
+ expectedRevision: string;
507
+ targetIdentity: string;
508
+ fixFindingIds: readonly string[];
509
+ policyContent: string;
510
+ fixFindings: readonly ReviewTargetedValidationFindingV1[];
511
+ fixClassifications: readonly ReviewTargetedValidationClassificationV1[];
512
+ projection: ReviewProjection;
513
+ correctionCandidateTree: string;
514
+ correctionTargetIdentity: string;
515
+ correctionPaths: readonly string[];
516
+ correctionPathsDigest: string;
517
+ }
518
+
458
519
  export interface ReviewCollectInputV3 {
459
520
  name: string;
460
521
  schema: string;
@@ -464,12 +525,11 @@ export interface ReviewCollectInputV3 {
464
525
  baseTree?: string;
465
526
  candidateTree?: string;
466
527
  changedPathManifest?: readonly ChangedPathEntry[];
467
- validationRequest?: ReviewTargetedValidationRequestV1;
468
528
  submission?: ReviewCaptureSubmissionV1;
469
- /** status/v5 only: the finalize/capture-evidence descriptor forms. */
470
- submissionDescriptor?: ReviewSubmissionDescriptorV5;
471
529
  /** status/v5 only: the self-contained external.run_provider_role task. */
472
530
  providerTask?: ReviewProviderTaskV1;
531
+ /** status/v5 only: the provider-owned request for a Go-owned targeted validator. */
532
+ validationRequest?: ReviewTargetedValidationRequestV1;
473
533
  }
474
534
 
475
535
  export interface ReviewNextTransitionV3 {
@@ -481,35 +541,12 @@ export interface ReviewNextTransitionV3 {
481
541
  correctionRequest?: ReviewCorrectionPlanRequestV1;
482
542
  }
483
543
 
484
- export interface ReviewTargetedValidationRequestV1 {
485
- schema: "gentle-ai.review-targeted-validation-request/v1";
486
- requestHash: string;
487
- lineageId: string;
488
- expectedRevision: string;
489
- targetIdentity: string;
490
- fixFindingIds: readonly string[];
491
- projection: ReviewProjection;
492
- correctionCandidateTree: string;
493
- correctionTargetIdentity: string;
494
- correctionPaths: readonly string[];
495
- correctionPathsDigest: string;
496
- }
497
-
498
- export interface ReviewFinalVerificationRetryV1 {
499
- incidentSchema: "gentle-ai.review-final-verification-incident/v1";
500
- incidentClass: "procedural_tooling_failure";
501
- validatingRevision: string;
502
- targetIdentity: string;
503
- failedEvidenceHash: string;
504
- failedEvidenceRecordDigest?: string;
505
- finalizeRequestDigest: string;
506
- }
507
-
508
544
  export interface ReviewStatusV3 {
509
545
  contract: typeof REVIEW_INTEGRATION_CONTRACT;
510
546
  applicability: Exclude<ReviewAuthorityApplicability, "not_evaluated">;
511
547
  authority?: ReviewStatusAuthorityV1;
512
- receipt: ReviewStatusReceiptV1;
548
+ /** status/v5 compatibility metadata; it is decoded but never used as routing authority. */
549
+ receipt?: ReviewStatusReceiptV1;
513
550
  action: ReviewStatusAction;
514
551
  actionDisposition?: ReviewStatusActionDisposition;
515
552
  replayability: ReviewReplayability;
@@ -521,8 +558,6 @@ export interface ReviewStatusV3 {
521
558
  repair: AuthorityRepairAssessmentV1;
522
559
  candidates: readonly string[];
523
560
  nextTransition?: ReviewNextTransitionV3;
524
- validationRequest?: ReviewTargetedValidationRequestV1;
525
- finalVerificationRetry?: ReviewFinalVerificationRetryV1;
526
561
  /** status/v5 only: the descriptive, non-routing transition preview. */
527
562
  forecast?: ReviewForecastV1;
528
563
  /**
@@ -532,6 +567,8 @@ export interface ReviewStatusV3 {
532
567
  * published status-v5.schema.json omits it (capture is authoritative).
533
568
  */
534
569
  repositoryContext?: ReviewRepositoryContextV2;
570
+ /** status/v5 only: the provider-owned request mirrored by the targeted-validator collect input. */
571
+ validationRequest?: ReviewTargetedValidationRequestV1;
535
572
  raw: Readonly<Record<string, unknown>>;
536
573
  }
537
574
 
@@ -566,9 +603,17 @@ export interface ReviewConsentV2 {
566
603
  // blocking question with one net-new required member, the provider-fixed
567
604
  // `agent` runtime binding. Everything shared with v2 keeps its exact shape so
568
605
  // consumers of either identity read one structural surface.
606
+ export const REVIEW_CONSENT_AGENT_V3 = {
607
+ CLAUDE_CODE: "claude-code",
608
+ OPENCODE: "opencode",
609
+ CODEX: "codex",
610
+ PI: "pi",
611
+ } as const;
612
+ export type ReviewConsentAgentV3 = (typeof REVIEW_CONSENT_AGENT_V3)[keyof typeof REVIEW_CONSENT_AGENT_V3];
613
+
569
614
  export interface ReviewConsentV3 extends Omit<ReviewConsentV2, "schema"> {
570
615
  schema: "gentle-ai.review-integration.consent/v3";
571
- agent: "claude-code";
616
+ agent: ReviewConsentAgentV3;
572
617
  }
573
618
 
574
619
  // Either accepted consent identity. Consumers that relay the envelope (rather
@@ -585,12 +630,11 @@ const FAILURE_REQUIRED_INPUTS = [
585
630
  "disposition",
586
631
  "reason",
587
632
  "actor",
588
- "incident",
589
633
  "maintainer_authorization",
590
634
  "base_ref",
591
635
  ] as const;
592
636
  export type ReviewFailureRequiredInputV2 = (typeof FAILURE_REQUIRED_INPUTS)[number];
593
- const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.finalize", "review.repair", "review.bind_sdd", "explicit-maintainer-action", "stop"] as const;
637
+ const FAILURE_NEXT_ACTIONS = ["correct_request", "retry", "retry_with_bounded_backoff", "review.status", "review.repair", "explicit-maintainer-action", "stop"] as const;
594
638
  export type ReviewFailureNextActionV2 = (typeof FAILURE_NEXT_ACTIONS)[number];
595
639
  // Known cause_category values: the vendored failure.schema.json enum plus
596
640
  // "incomplete_store_entry", which the v2.1.8 emitter produces beyond that enum.
@@ -625,7 +669,7 @@ export interface ReviewFailureContextV2 {
625
669
  bindingRevision?: ReviewFailureBindingRevisionV1;
626
670
  }
627
671
 
628
- export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-evidence" | "review.capture-refuter" | "review.capture-validation";
672
+ export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-correction-plan" | "review.capture-refuter" | "review.capture-validation";
629
673
 
630
674
  export interface ReviewFailureV2 {
631
675
  schema: "gentle-ai.review-integration.failure/v2";
@@ -649,11 +693,32 @@ export interface ReviewFailureV2 {
649
693
  raw: Readonly<Record<string, unknown>>;
650
694
  }
651
695
 
652
- export interface ReviewOperationV2 {
653
- contract: typeof REVIEW_INTEGRATION_CONTRACT;
654
- operation: "review.finalize" | "review.validate" | "review.bind_sdd" | "review.retry_final_verification";
655
- result: Readonly<Record<string, unknown>>;
656
- raw: Readonly<Record<string, unknown>>;
696
+ export const REVIEW_ADVISORY_FINDING_SEVERITY = {
697
+ BLOCKER: "BLOCKER",
698
+ CRITICAL: "CRITICAL",
699
+ WARNING: "WARNING",
700
+ SUGGESTION: "SUGGESTION",
701
+ } as const;
702
+ export type ReviewAdvisoryFindingSeverity = (typeof REVIEW_ADVISORY_FINDING_SEVERITY)[keyof typeof REVIEW_ADVISORY_FINDING_SEVERITY];
703
+
704
+ export const REVIEW_ADVISORY_FINDING_DISPOSITION = {
705
+ INFORMATIONAL: "informational",
706
+ FOLLOW_UP: "follow_up",
707
+ REFUTED: "refuted",
708
+ } as const;
709
+ export type ReviewAdvisoryFindingDisposition = (typeof REVIEW_ADVISORY_FINDING_DISPOSITION)[keyof typeof REVIEW_ADVISORY_FINDING_DISPOSITION];
710
+
711
+ export interface ReviewAdvisoryFindingV1 {
712
+ id: string;
713
+ lens?: string;
714
+ location?: string;
715
+ severity: ReviewAdvisoryFindingSeverity;
716
+ disposition: ReviewAdvisoryFindingDisposition;
717
+ }
718
+
719
+ export interface ReviewAdvisoryFindingsV1 {
720
+ statement: string;
721
+ findings: readonly ReviewAdvisoryFindingV1[];
657
722
  }
658
723
 
659
724
  export interface AuthorityRepairAssessmentCandidateV1 {
@@ -905,11 +970,12 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
905
970
  assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
906
971
 
907
972
  const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
908
- const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
909
- const optional = array(features.optional, "capabilities.features.optional", (entry, label) => decodeOptionalFeature(entry, label), { minimum: 17, unique: true });
973
+ const requiredMandatoryFeatures = identity.requiredMandatoryFeatures ?? REQUIRED_MANDATORY_FEATURES;
974
+ const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: requiredMandatoryFeatures.length });
975
+ const optional = array(features.optional, "capabilities.features.optional", (entry, label) => decodeOptionalFeature(entry, label), { minimum: identity.optionalFeatureFloor ?? 17, unique: true });
910
976
  const mandatoryNames = mandatory.map((feature) => feature.name);
911
977
  const optionalNames = optional.map((feature) => feature.name);
912
- assertExactSet(mandatoryNames, REQUIRED_MANDATORY_FEATURES, "mandatory capabilities");
978
+ assertExactSet(mandatoryNames, requiredMandatoryFeatures, "mandatory capabilities");
913
979
  if (new Set(optionalNames).size !== optionalNames.length) throw new TypeError("optional capabilities contain duplicate names");
914
980
  if (optionalNames.some((name) => mandatoryNames.includes(name as ReviewFeatureV2["name"]))) throw new TypeError("mandatory and optional capabilities overlap");
915
981
  if (mandatory.some((feature) => !feature.supported)) throw new TypeError("mandatory capability is unsupported");
@@ -1001,12 +1067,12 @@ function decodeChangedPathEntry(value: unknown, label: string): ChangedPathEntry
1001
1067
  // start/v3
1002
1068
  // ---------------------------------------------------------------------------
1003
1069
 
1004
- export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
1070
+ export function decodeReviewStartV3(value: unknown, overlayIdentitySatisfiesRepositoryContext = false): ReviewStartV3 {
1005
1071
  const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
1006
1072
  const body = exactRecord(value, "start", [
1007
1073
  "schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
1008
1074
  "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
1009
- ], [...overlayFields, "changed_path_manifest", "repository_context"]);
1075
+ ], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement"]);
1010
1076
  requireIdentity(body, "gentle-ai.review-integration.start/v3", REVIEW_INTEGRATION_OPERATION.START);
1011
1077
 
1012
1078
  // dependentRequired binds base_tree<->candidate_tree bidirectionally, and
@@ -1035,9 +1101,12 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
1035
1101
  throw new TypeError("start with selected_lenses requires base_tree, candidate_tree, and changed_path_manifest");
1036
1102
  }
1037
1103
 
1038
- const requiresRepositoryContext = (action === "created" || action === "resumed") && state === REVIEW_START_STATE.REVIEWING;
1104
+ const reviewingCreatedOrResumed = (action === "created" || action === "resumed") && state === REVIEW_START_STATE.REVIEWING;
1105
+ // START/v4 may render the frozen target as the overlay identity instead of
1106
+ // repository_context; absence is only excused when that identity is present.
1107
+ const requiresRepositoryContext = reviewingCreatedOrResumed && !(overlayIdentitySatisfiesRepositoryContext && targetIdentity !== undefined);
1039
1108
  if (requiresRepositoryContext && body.repository_context === undefined) throw new TypeError("start.repository_context is required when action is created/resumed and state is reviewing");
1040
- if (!requiresRepositoryContext && body.repository_context !== undefined) throw new TypeError("start.repository_context is only valid when action is created/resumed and state is reviewing");
1109
+ if (!reviewingCreatedOrResumed && body.repository_context !== undefined) throw new TypeError("start.repository_context is only valid when action is created/resumed and state is reviewing");
1041
1110
 
1042
1111
  let repositoryContext: ReviewRepositoryContextV2 | undefined;
1043
1112
  if (body.repository_context !== undefined) {
@@ -1045,7 +1114,7 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
1045
1114
  if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
1046
1115
  repositoryContext = {
1047
1116
  capability: "review.opaque_repository_context",
1048
- handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
1117
+ handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
1049
1118
  revision: sha256(source.revision, "start.repository_context.revision"),
1050
1119
  targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
1051
1120
  ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
@@ -1090,6 +1159,78 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
1090
1159
  };
1091
1160
  }
1092
1161
 
1162
+ function assertReviewStartV4StatusBinding(execute: ReviewNextTransitionExecuteV3, start: ReviewStartV3): void {
1163
+ const expectedTargetIdentity = start.repositoryContext?.targetIdentity ?? start.targetIdentity;
1164
+ const targetIdentityConflicts = expectedTargetIdentity === undefined || execute.binding.targetIdentity !== expectedTargetIdentity
1165
+ || (start.targetIdentity !== undefined && start.targetIdentity !== expectedTargetIdentity)
1166
+ || start.artifactSubjects.some((subject) => subject.targetIdentity !== expectedTargetIdentity);
1167
+ if (targetIdentityConflicts) throw new TypeError("START/v4 status target identity binding conflicts with frozen START");
1168
+ if (execute.binding.lineageId !== undefined && execute.binding.lineageId !== start.lineageId) {
1169
+ throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
1170
+ }
1171
+ const optionalSelectors: Readonly<Record<string, string | undefined>> = {
1172
+ "base-ref": start.baseTree,
1173
+ "base-tree": start.baseTree,
1174
+ projection: start.projection,
1175
+ target: expectedTargetIdentity,
1176
+ };
1177
+ const argumentsByName = new Map<string, string>();
1178
+ for (const argument of execute.arguments) {
1179
+ if (argumentsByName.has(argument.name)) throw new TypeError(`START/v4 status arguments duplicate ${argument.name} binding`);
1180
+ argumentsByName.set(argument.name, argument.value);
1181
+ if (Object.hasOwn(optionalSelectors, argument.name) && argument.value !== optionalSelectors[argument.name]) throw new TypeError(`START/v4 status ${argument.name} binding conflicts with frozen START`);
1182
+ }
1183
+ if (argumentsByName.get("lineage") !== start.lineageId) throw new TypeError("START/v4 status requires exactly one lineage binding for the frozen START");
1184
+ if (execute.arguments.find((argument) => argument.name === "lineage")?.token !== `--lineage=${start.lineageId}`) throw new TypeError("START/v4 status lineage binding conflicts with frozen START");
1185
+ for (const [name, value] of [["contract", REVIEW_INTEGRATION_CONTRACT], ["next-transition", "true"], ["agent", "pi"]]) {
1186
+ if (argumentsByName.get(name) !== value || execute.arguments.find((argument) => argument.name === name)?.token !== `--${name}=${value}`) throw new TypeError(`START/v4 status ${name} binding conflicts with frozen START`);
1187
+ }
1188
+ const selectorArguments = execute.selectorArguments;
1189
+ if (selectorArguments === undefined || selectorArguments.length === 0) throw new TypeError("START/v4 status selector arguments are required");
1190
+ const selectorsByName = new Map<string, ReviewTransitionArgumentV3>();
1191
+ for (const selector of selectorArguments) {
1192
+ const full = execute.arguments.find((argument) => argument.name === selector.name);
1193
+ if (selectorsByName.has(selector.name) || full === undefined || full.value !== selector.value || full.token !== selector.token) throw new TypeError(`START/v4 status selector ${selector.name} binding conflicts with full arguments`);
1194
+ selectorsByName.set(selector.name, selector);
1195
+ }
1196
+ const selector = (name: string, value: string): void => {
1197
+ if (selectorsByName.get(name)?.value !== value || selectorsByName.get(name)?.token !== `--${name}=${value}` || execute.arguments.find((argument) => argument.name === name)?.token !== `--${name}=${value}`) throw new TypeError(`START/v4 status selector ${name} binding conflicts with frozen START`);
1198
+ };
1199
+ const baseRef = argumentsByName.get("base-ref");
1200
+ if (start.targetMode === "base-workspace-overlay") {
1201
+ if (baseRef !== start.baseTree || argumentsByName.has("committed-only")) throw new TypeError("START/v4 status workspace overlay binding conflicts with frozen START");
1202
+ selector("base-ref", start.baseTree!); selector("workspace-overlay", "true");
1203
+ } else if (baseRef !== undefined) {
1204
+ if (baseRef !== start.baseTree || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status committed range binding conflicts with frozen START");
1205
+ selector("base-ref", start.baseTree!); selector("committed-only", "true");
1206
+ } else {
1207
+ if (argumentsByName.has("committed-only") || argumentsByName.has("workspace-overlay")) throw new TypeError("START/v4 status current projection binding conflicts with frozen START");
1208
+ selector("projection", start.projection);
1209
+ }
1210
+ }
1211
+
1212
+ export function decodeReviewStartV4(value: unknown): ReviewStartV4 {
1213
+ const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] as const;
1214
+ const body = exactRecord(value, "start", [
1215
+ "schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
1216
+ "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
1217
+ ], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement", "next_transition"]);
1218
+ requireIdentity(body, "gentle-ai.review-integration.start/v4", REVIEW_INTEGRATION_OPERATION.START);
1219
+ const action = enumeration(body.action, ["created", "replayed", "closed", "blocked-scope-action"] as const, "start.action");
1220
+ const v3Action = action === "replayed" ? "resumed" : action;
1221
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
1222
+ const reviewing = (action === "created" || action === "replayed") && body.state === REVIEW_START_STATE.REVIEWING;
1223
+ if (reviewing && nextTransition?.kind !== "execute") throw new TypeError("reviewing created/replayed START requires next_transition.execute");
1224
+ if (reviewing && nextTransition.execute?.operation !== "review.status") throw new TypeError("reviewing created/replayed START next_transition.execute.operation must be review.status");
1225
+ const closedApprovedZeroLens = action === "closed" && body.state === REVIEW_START_STATE.APPROVED && Array.isArray(body.selected_lenses) && body.selected_lenses.length === 0;
1226
+ if (closedApprovedZeroLens && nextTransition !== undefined) throw new TypeError("closed approved zero-lens START cannot carry next_transition");
1227
+ const v3Body = { ...body };
1228
+ delete v3Body.next_transition;
1229
+ const decoded = decodeReviewStartV3({ ...v3Body, schema: "gentle-ai.review-integration.start/v3", action: v3Action }, true);
1230
+ if (reviewing) assertReviewStartV4StatusBinding(nextTransition!.execute!, decoded);
1231
+ return { ...decoded, action, ...(nextTransition === undefined ? {} : { nextTransition }), raw: body };
1232
+ }
1233
+
1093
1234
  // ---------------------------------------------------------------------------
1094
1235
  // projection/v1 — reused verbatim; the v2 capabilities schema still advertises
1095
1236
  // gentle-ai.review-integration.projection/v1, so renaming this decoder would
@@ -1187,36 +1328,13 @@ export function decodeAuthorityRepairAssessmentV1(value: unknown): AuthorityRepa
1187
1328
  };
1188
1329
  }
1189
1330
 
1190
- // ---------------------------------------------------------------------------
1191
- // targeted-validation-request/v1 — reused for status.validation_request,
1192
- // operation.result.validation_request, and next_transition collect inputs.
1193
- // ---------------------------------------------------------------------------
1194
-
1195
- function decodeTargetedValidationRequestV1(value: unknown, label: string): ReviewTargetedValidationRequestV1 {
1196
- const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest"]);
1197
- if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1198
- return {
1199
- schema: "gentle-ai.review-targeted-validation-request/v1",
1200
- requestHash: sha256(body.request_hash, `${label}.request_hash`),
1201
- lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1202
- expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1203
- targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1204
- fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1205
- projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
1206
- correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
1207
- correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
1208
- correctionPaths: stringArray(body.correction_paths, `${label}.correction_paths`, { minimum: 1, unique: true }),
1209
- correctionPathsDigest: sha256(body.correction_paths_digest, `${label}.correction_paths_digest`),
1210
- };
1211
- }
1212
-
1213
1331
  // ---------------------------------------------------------------------------
1214
1332
  // next-transition/v3 — net-new; unlike v1's decodeNextTransition (void),
1215
1333
  // this decoder returns a typed value so callers can read the manifest-bound
1216
1334
  // collect inputs and the execute binding.
1217
1335
  // ---------------------------------------------------------------------------
1218
1336
 
1219
- const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"] as const;
1337
+ const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.status", "review.recover", "review.repair", "review.acknowledge-approved"] as const;
1220
1338
 
1221
1339
  function decodeTransitionArguments(value: unknown, label: string): readonly ReviewTransitionArgumentV3[] {
1222
1340
  return array(value, label, (entry, entryLabel) => {
@@ -1228,7 +1346,29 @@ function decodeTransitionArguments(value: unknown, label: string): readonly Revi
1228
1346
  });
1229
1347
  }
1230
1348
 
1231
- function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): ReviewCaptureSubmissionV1 {
1349
+ function decodeCaptureSubmission(value: unknown, label: string, v5: boolean, v6: boolean, captureOperation: string): ReviewCaptureSubmissionV1 {
1350
+ // status/v6 adds the provider-owned intended-untracked selection form.
1351
+ if (v6 && captureOperation === "external.select_intended_untracked" && typeof value === "object" && value !== null && (value as Record<string, unknown>).operation_token === "status") {
1352
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1353
+ const expectedTokens = [
1354
+ "--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi",
1355
+ "--projection=workspace", "--intended-untracked-selection={{value}}",
1356
+ ] as const;
1357
+ const operationToken = enumeration(submission.operation_token, ["status"] as const, `${label}.operation_token`);
1358
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: expectedTokens.length, maximum: expectedTokens.length });
1359
+ if (argumentTokens.some((token, index) => token !== expectedTokens[index])) throw new TypeError(`${label}.argument_tokens substitution binding is invalid`);
1360
+ const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
1361
+ return {
1362
+ operationToken,
1363
+ argumentTokens,
1364
+ values: [{
1365
+ slot: enumeration(row.slot, ["intended_untracked_selection"] as const, `${label}.value.slot`),
1366
+ domain: enumeration(row.domain, ["schema_bound_json"] as const, `${label}.value.domain`),
1367
+ schema: enumeration(row.schema, ["gentle-ai.review-intended-untracked-selection/v1"] as const, `${label}.value.schema`),
1368
+ substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 4, 4),
1369
+ }],
1370
+ };
1371
+ }
1232
1372
  // status/v5: the live 2.4.0-main binary emits the materialize
1233
1373
  // capture-result submission as a SINGULAR `value` object carrying a
1234
1374
  // `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
@@ -1239,16 +1379,22 @@ function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): Re
1239
1379
  // through to the legacy decoder, which rejects the unknown `value` key.
1240
1380
  if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1241
1381
  const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1242
- if (submission.operation_token !== "capture-result") throw new TypeError(`${label}.operation_token must be capture-result for the singular value form`);
1382
+ const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] as const, `${label}.operation_token`);
1243
1383
  const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1244
- const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
1384
+ const row = operationToken === "capture-result"
1385
+ ? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
1386
+ : exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
1245
1387
  return {
1246
- operationToken: "capture-result",
1388
+ operationToken,
1247
1389
  argumentTokens,
1248
1390
  values: [{
1249
- slot: enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`),
1391
+ slot: operationToken === "capture-result"
1392
+ ? enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`)
1393
+ : enumeration(row.slot, ["correction_lines"] as const, `${label}.value.slot`),
1250
1394
  domain: nonempty(row.domain, `${label}.value.domain`),
1251
- schema: nonempty(row.schema, `${label}.value.schema`),
1395
+ ...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
1396
+ ...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
1397
+ ...(row.maximum === undefined ? {} : { maximum: integer(row.maximum, `${label}.value.maximum`, 1, 200) }),
1252
1398
  substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1253
1399
  }],
1254
1400
  };
@@ -1279,40 +1425,6 @@ function decodeProviderTask(value: unknown, label: string): ReviewProviderTaskV1
1279
1425
  };
1280
1426
  }
1281
1427
 
1282
- function decodeSubmissionDescriptorV5(value: unknown, label: string, operationToken: "finalize" | "capture-evidence"): ReviewSubmissionDescriptorV5 {
1283
- if (operationToken === "finalize") {
1284
- const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1285
- const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 7 });
1286
- const source = exactRecord(descriptor.value, `${label}.value`, ["slot", "domain", "substitution_location"], ["schema", "minimum", "maximum"]);
1287
- const slot = enumeration(source.slot, ["correction_lines", "validation"] as const, `${label}.value.slot`);
1288
- return {
1289
- operationToken,
1290
- argumentTokens,
1291
- value: {
1292
- slot,
1293
- domain: nonempty(source.domain, `${label}.value.domain`),
1294
- substitutionLocation: integer(source.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1295
- ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${label}.value.schema`) }),
1296
- ...(source.minimum === undefined ? {} : { minimum: integer(source.minimum, `${label}.value.minimum`, 1, 200) }),
1297
- ...(source.maximum === undefined ? {} : { maximum: integer(source.maximum, `${label}.value.maximum`, 1, 200) }),
1298
- },
1299
- };
1300
- }
1301
- const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1302
- const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 6, maximum: 6 });
1303
- const values = array(descriptor.values, `${label}.values`, (entry, entryLabel) => {
1304
- const source = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"], ["schema", "allowed_values"]);
1305
- return {
1306
- slot: nonempty(source.slot, `${entryLabel}.slot`),
1307
- domain: nonempty(source.domain, `${entryLabel}.domain`),
1308
- substitutionLocation: integer(source.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1309
- ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${entryLabel}.schema`) }),
1310
- ...(source.allowed_values === undefined ? {} : { allowedValues: stringArray(source.allowed_values, `${entryLabel}.allowed_values`, { minimum: 1, unique: true }) }),
1311
- };
1312
- }, { minimum: 2, maximum: 2 });
1313
- return { operationToken, argumentTokens, values };
1314
- }
1315
-
1316
1428
  function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCorrectionPlanRequestV1 {
1317
1429
  const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
1318
1430
  if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
@@ -1341,6 +1453,59 @@ function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCor
1341
1453
  };
1342
1454
  }
1343
1455
 
1456
+ export function decodeReviewTargetedValidationRequestV1(value: unknown, label = "targeted_validation_request"): ReviewTargetedValidationRequestV1 {
1457
+ const body = exactRecord(value, label, [
1458
+ "schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "fix_finding_ids", "policy_content", "fix_findings", "fix_classifications",
1459
+ "projection", "correction_candidate_tree", "correction_target_identity", "correction_paths", "correction_paths_digest",
1460
+ ]);
1461
+ if (body.schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1462
+ const fixFindingIds = stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true });
1463
+ const fixFindings = array(body.fix_findings, `${label}.fix_findings`, (entry, entryLabel): ReviewTargetedValidationFindingV1 => {
1464
+ const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence_class", "causal_disposition"]);
1465
+ return {
1466
+ id: nonempty(finding.id, `${entryLabel}.id`),
1467
+ lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
1468
+ location: nonempty(finding.location, `${entryLabel}.location`),
1469
+ severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
1470
+ claim: nonempty(finding.claim, `${entryLabel}.claim`),
1471
+ proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
1472
+ evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
1473
+ causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
1474
+ };
1475
+ }, { minimum: 1 });
1476
+ const fixClassifications = array(body.fix_classifications, `${label}.fix_classifications`, (entry, entryLabel): ReviewTargetedValidationClassificationV1 => {
1477
+ const classification = exactRecord(entry, entryLabel, ["finding_id", "class", "causal_disposition", "proof"], ["severity"]);
1478
+ const severity = classification.severity === undefined ? undefined : nonempty(classification.severity, `${entryLabel}.severity`);
1479
+ return {
1480
+ findingId: nonempty(classification.finding_id, `${entryLabel}.finding_id`),
1481
+ ...(severity === undefined ? {} : { severity }),
1482
+ class: enumeration(classification.class, ["deterministic", "inferential"] as const, `${entryLabel}.class`),
1483
+ causalDisposition: enumeration(classification.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
1484
+ proof: nonempty(classification.proof, `${entryLabel}.proof`),
1485
+ };
1486
+ }, { minimum: 1 });
1487
+ const findingIds = fixFindings.map((finding) => finding.id);
1488
+ const classificationFindingIds = fixClassifications.map((classification) => classification.findingId);
1489
+ assertExactSet(findingIds, fixFindingIds, `${label}.fix_findings`);
1490
+ assertExactSet(classificationFindingIds, fixFindingIds, `${label}.fix_classifications`);
1491
+ return {
1492
+ schema: "gentle-ai.review-targeted-validation-request/v1",
1493
+ requestHash: sha256(body.request_hash, `${label}.request_hash`),
1494
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1495
+ expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1496
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1497
+ fixFindingIds,
1498
+ policyContent: nonempty(body.policy_content, `${label}.policy_content`),
1499
+ fixFindings,
1500
+ fixClassifications,
1501
+ projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
1502
+ correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
1503
+ correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
1504
+ correctionPaths: stringArray(body.correction_paths, `${label}.correction_paths`, { minimum: 1, unique: true }),
1505
+ correctionPathsDigest: sha256(body.correction_paths_digest, `${label}.correction_paths_digest`),
1506
+ };
1507
+ }
1508
+
1344
1509
  export function decodeReviewForecastV1(value: unknown, label = "status.forecast"): ReviewForecastV1 {
1345
1510
  const body = exactRecord(value, label, ["horizon", "steps"]);
1346
1511
  const horizon = enumeration(body.horizon, ["partial", "terminal"] as const, `${label}.horizon`);
@@ -1364,24 +1529,15 @@ export function decodeReviewForecastV1(value: unknown, label = "status.forecast"
1364
1529
  // request; every other reason code must not.
1365
1530
  const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] as const);
1366
1531
  // v5 capture operations that must carry a submission descriptor.
1367
- const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["external.plan_correction", "external.run_targeted_validation", "review.capture-evidence"] as const);
1532
+ const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] as const);
1368
1533
 
1369
- function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewCollectInputV3 {
1370
- const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request", "submission", ...(v5 ? ["provider_task"] : [])]);
1534
+ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: boolean): ReviewCollectInputV3 {
1535
+ const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "submission", ...(v5 ? ["provider_task", "validation_request"] : [])]);
1371
1536
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1372
1537
  const schema = nonempty(input.schema, `${label}.schema`);
1373
1538
  const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
1374
1539
  const argumentsList = decodeTransitionArguments(input.arguments, `${label}.arguments`);
1375
1540
 
1376
- if (captureOperation === "external.run_targeted_validation") {
1377
- if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1378
- if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1379
- } else if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION) {
1380
- if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1381
- } else if (input.validation_request !== undefined) {
1382
- throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation or review.capture-validation`);
1383
- }
1384
-
1385
1541
  // v5-only surfaces (vendored status-v5.schema.json): the provider role task
1386
1542
  // rides exactly the external.run_provider_role vector, and the finalize/
1387
1543
  // capture-evidence submission descriptor forms ride their own vectors.
@@ -1393,11 +1549,39 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
1393
1549
  }
1394
1550
  if ((V5_SUBMISSION_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
1395
1551
  if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
1396
- if (captureOperation === "external.plan_correction" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
1397
- if (captureOperation === "review.capture-evidence" && schema !== "https://gentle-ai.dev/schema/review/verification-evidence/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/verification-evidence/v1`);
1552
+ if (captureOperation === "review.capture-correction-plan" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
1398
1553
  }
1399
1554
  }
1400
1555
 
1556
+ const intendedUntracked = v6
1557
+ && name === "intended_untracked_selection"
1558
+ && schema === "gentle-ai.review-intended-untracked-selection/v1"
1559
+ && captureOperation === "external.select_intended_untracked";
1560
+ if (captureOperation === "external.select_intended_untracked" && (name !== "intended_untracked_selection" || schema !== "gentle-ai.review-intended-untracked-selection/v1")) {
1561
+ throw new TypeError(`${label} external.select_intended_untracked requires the intended_untracked_selection binding`);
1562
+ }
1563
+ if (intendedUntracked) {
1564
+ const expectedNames = ["target_identity", "projection", "base_tree", "candidate_tree", "eligible_paths_json", "expected_untracked_inventory"] as const;
1565
+ if (argumentsList.length !== expectedNames.length || argumentsList.some((argument, index) => argument.name !== expectedNames[index] || argument.token !== undefined)) {
1566
+ throw new TypeError(`${label}.arguments must preserve the exact intended-untracked metadata binding`);
1567
+ }
1568
+ sha256(argumentsList[0]!.value, `${label}.arguments[0].value`);
1569
+ enumeration(argumentsList[1]!.value, ["workspace"] as const, `${label}.arguments[1].value`);
1570
+ gitTree(argumentsList[2]!.value, `${label}.arguments[2].value`);
1571
+ gitTree(argumentsList[3]!.value, `${label}.arguments[3].value`);
1572
+ const eligiblePathsJson = argumentsList[4]!.value;
1573
+ let eligiblePaths: unknown;
1574
+ try { eligiblePaths = JSON.parse(eligiblePathsJson); } catch { throw new TypeError(`${label}.arguments[4].value must be canonical JSON`); }
1575
+ if (!Array.isArray(eligiblePaths) || eligiblePaths.length === 0 || eligiblePaths.some((path) => {
1576
+ if (typeof path !== "string" || path.length === 0 || path.startsWith("/") || /^[A-Za-z]:\//.test(path) || path.includes("\\") || path.includes("\0")) return true;
1577
+ const segments = path.split("/");
1578
+ return segments.some((segment) => segment === "" || segment === "." || segment === "..");
1579
+ }) || new Set(eligiblePaths).size !== eligiblePaths.length || JSON.stringify(eligiblePaths) !== eligiblePathsJson) {
1580
+ throw new TypeError(`${label}.arguments[4].value must contain unique canonical repository-relative paths`);
1581
+ }
1582
+ sha256(argumentsList[5]!.value, `${label}.arguments[5].value`);
1583
+ }
1584
+
1401
1585
  // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1402
1586
  // operations render SELF-CONTAINED authority-advancing vectors. Executing
1403
1587
  // the exact rendered tokens makes Go materialize the role prompt, run its
@@ -1411,6 +1595,30 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
1411
1595
  if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
1412
1596
  throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
1413
1597
  }
1598
+ const targetedValidatorInput = v5
1599
+ && name === "provider_targeted_validator"
1600
+ && captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION
1601
+ && schema === "https://gentle-ai.dev/schema/review/validator/v1";
1602
+ if (input.validation_request !== undefined && !targetedValidatorInput) {
1603
+ throw new TypeError(`${label}.validation_request is only valid for the v5 provider-targeted-validator capture input`);
1604
+ }
1605
+ if (targetedValidatorInput && input.validation_request === undefined) {
1606
+ throw new TypeError(`${label}.validation_request is required for the v5 provider-targeted-validator capture input`);
1607
+ }
1608
+ const validationRequest = input.validation_request === undefined
1609
+ ? undefined
1610
+ : decodeReviewTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`);
1611
+ if (validationRequest !== undefined) {
1612
+ const providerArgument = (argumentName: string): string => {
1613
+ const matches = argumentsList.filter((argument) => argument.name === argumentName);
1614
+ if (matches.length !== 1) throw new TypeError(`${label}.arguments must carry exactly one ${argumentName} binding`);
1615
+ return matches[0]!.value;
1616
+ };
1617
+ if (providerArgument("lineage") !== validationRequest.lineageId) throw new TypeError(`${label}.arguments lineage must bind validation_request.lineage_id`);
1618
+ if (providerArgument("expected-revision") !== validationRequest.expectedRevision) throw new TypeError(`${label}.arguments expected-revision must bind validation_request.expected_revision`);
1619
+ if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
1620
+ if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
1621
+ }
1414
1622
  if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
1415
1623
  throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1416
1624
  }
@@ -1423,23 +1631,17 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
1423
1631
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
1424
1632
  throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
1425
1633
  }
1426
- const submissionOperations: readonly string[] = v5 ? ["review.capture-result", ...V5_SUBMISSION_CAPTURE_OPERATIONS] : ["review.capture-result"];
1634
+ const submissionOperations: readonly string[] = v5
1635
+ ? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
1636
+ : ["review.capture-result"];
1427
1637
  if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1428
1638
  throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1429
1639
  }
1640
+ if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
1430
1641
 
1431
- // The v5 descriptor forms are discriminated by their closed operation
1432
- // tokens; every other submission stays the host-relay completing form.
1433
- let submission: ReviewCaptureSubmissionV1 | undefined;
1434
- let submissionDescriptor: ReviewSubmissionDescriptorV5 | undefined;
1435
- if (input.submission !== undefined) {
1436
- const operationToken = (input.submission as Record<string, unknown> | null)?.operation_token;
1437
- if (v5 && (operationToken === "finalize" || operationToken === "capture-evidence")) {
1438
- submissionDescriptor = decodeSubmissionDescriptorV5(input.submission, `${label}.submission`, operationToken);
1439
- } else {
1440
- submission = decodeCaptureSubmission(input.submission, `${label}.submission`, v5);
1441
- }
1442
- }
1642
+ const submission = input.submission === undefined
1643
+ ? undefined
1644
+ : decodeCaptureSubmission(input.submission, `${label}.submission`, v5, v6, captureOperation);
1443
1645
 
1444
1646
  return {
1445
1647
  name,
@@ -1450,15 +1652,15 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewC
1450
1652
  ...(input.base_tree === undefined ? {} : { baseTree: gitTree(input.base_tree, `${label}.base_tree`) }),
1451
1653
  ...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
1452
1654
  ...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
1453
- ...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
1454
1655
  ...(submission === undefined ? {} : { submission }),
1455
- ...(submissionDescriptor === undefined ? {} : { submissionDescriptor }),
1456
1656
  ...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
1657
+ ...(validationRequest === undefined ? {} : { validationRequest }),
1457
1658
  };
1458
1659
  }
1459
1660
 
1460
- export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean } = {}): ReviewNextTransitionV3 {
1461
- const v5 = options.v5 === true;
1661
+ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
1662
+ const v6 = options.v6 === true;
1663
+ const v5 = options.v5 === true || v6;
1462
1664
  const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1463
1665
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
1464
1666
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
@@ -1488,6 +1690,7 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
1488
1690
  const execute = exactRecord(transition.execute, "next_transition.execute", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
1489
1691
  const operation = enumeration(execute.operation, NEXT_TRANSITION_OPERATIONS, "next_transition.execute.operation");
1490
1692
  const argumentsList = decodeTransitionArguments(execute.arguments, "next_transition.execute.arguments");
1693
+ const selectorArguments = execute.selector_arguments === undefined ? undefined : decodeTransitionArguments(execute.selector_arguments, "next_transition.execute.selector_arguments");
1491
1694
  const preconditions = decodeTransitionArguments(execute.preconditions, "next_transition.execute.preconditions");
1492
1695
  // The schema gives `binding` no declared properties and no required list:
1493
1696
  // it is an OPEN object. Closing it here made Pi stricter than the
@@ -1498,12 +1701,14 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
1498
1701
  const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, "next_transition.execute.binding.lineage_id");
1499
1702
  const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
1500
1703
  const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
1704
+ const decodedExecute: ReviewNextTransitionExecuteV3 = { operation, arguments: argumentsList, ...(selectorArguments === undefined ? {} : { selectorArguments }), preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) };
1705
+ if (operation === "review.acknowledge-approved") assertReviewApprovedAcknowledgementExecuteV1(decodedExecute);
1501
1706
  if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
1502
- return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1707
+ return { kind, reasonCode, execute: decodedExecute, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1503
1708
  }
1504
1709
  if (kind === "collect") {
1505
1710
  const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1506
- const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5), { minimum: 1 });
1711
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
1507
1712
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1508
1713
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1509
1714
  }
@@ -1519,8 +1724,8 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
1519
1724
  // status-v2.schema.json action_eligibility definition v2 also $refs.
1520
1725
  // ---------------------------------------------------------------------------
1521
1726
 
1522
- const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.finalize", "review.validate", "review.recover", "review.repair", "review.retry_final_verification"] as const;
1523
- const FORBIDDEN_ACTIONS = ["review.abandon", "review.finalize", "review.invalidate", "review.quarantine-legacy", "review.reclaim", "review.reconcile-authority", "review.reconcile-authority-batch", "review.recover", "review.repair", "review.retry_final_verification", "review.start", "review.validate"] as const;
1727
+ const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.recover", "review.repair"] as const;
1728
+ const FORBIDDEN_ACTIONS = ["review.abandon", "review.invalidate", "review.quarantine-legacy", "review.reclaim", "review.reconcile-authority", "review.reconcile-authority-batch", "review.recover", "review.repair", "review.start"] as const;
1524
1729
 
1525
1730
  function decodeEligibility(value: unknown, label: string): void {
1526
1731
  const eligibility = exactRecord(value, label, ["allowed_actions", "forbidden_actions"]);
@@ -1529,14 +1734,14 @@ function decodeEligibility(value: unknown, label: string): void {
1529
1734
  const selected = enumeration(action.action, ELIGIBLE_ACTIONS, `${entryLabel}.action`);
1530
1735
  text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1531
1736
  array(action.required_inputs, `${entryLabel}.required_inputs`, (input, inputLabel) => text(input, inputLabel, { minimum: 1, pattern: /^[a-z0-9_]+$/ }), { unique: true });
1532
- if (selected === "review.recover" || selected === "review.retry_final_verification") {
1737
+ if (selected === "review.recover") {
1533
1738
  const binding = exactRecord(action.binding, `${entryLabel}.binding`, ["lineage_id", "revision", "target_identity"]);
1534
1739
  enumeration(action.disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), `${entryLabel}.disposition`);
1535
1740
  lineage(binding.lineage_id, `${entryLabel}.binding.lineage_id`);
1536
1741
  sha256(binding.revision, `${entryLabel}.binding.revision`);
1537
1742
  sha256(binding.target_identity, `${entryLabel}.binding.target_identity`);
1538
1743
  } else if (action.disposition !== undefined || action.binding !== undefined) {
1539
- throw new TypeError(`${entryLabel} recovery fields require review.recover or review.retry_final_verification`);
1744
+ throw new TypeError(`${entryLabel} recovery fields require review.recover`);
1540
1745
  }
1541
1746
  return action;
1542
1747
  }, { minimum: 1, maximum: 1 });
@@ -1548,21 +1753,6 @@ function decodeEligibility(value: unknown, label: string): void {
1548
1753
  });
1549
1754
  }
1550
1755
 
1551
- function decodeFinalVerificationRetry(value: unknown, label: string): ReviewFinalVerificationRetryV1 {
1552
- const body = exactRecord(value, label, ["incident_schema", "incident_class", "validating_revision", "target_identity", "failed_evidence_hash", "finalize_request_digest"], ["failed_evidence_record_digest"]);
1553
- if (body.incident_schema !== "gentle-ai.review-final-verification-incident/v1") throw new TypeError(`${label}.incident_schema is unsupported`);
1554
- if (body.incident_class !== "procedural_tooling_failure") throw new TypeError(`${label}.incident_class is unsupported`);
1555
- return {
1556
- incidentSchema: "gentle-ai.review-final-verification-incident/v1",
1557
- incidentClass: "procedural_tooling_failure",
1558
- validatingRevision: sha256(body.validating_revision, `${label}.validating_revision`),
1559
- targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1560
- failedEvidenceHash: sha256(body.failed_evidence_hash, `${label}.failed_evidence_hash`),
1561
- ...(body.failed_evidence_record_digest === undefined ? {} : { failedEvidenceRecordDigest: sha256(body.failed_evidence_record_digest, `${label}.failed_evidence_record_digest`) }),
1562
- finalizeRequestDigest: sha256(body.finalize_request_digest, `${label}.finalize_request_digest`),
1563
- };
1564
- }
1565
-
1566
1756
  // ---------------------------------------------------------------------------
1567
1757
  // status/v3
1568
1758
  // ---------------------------------------------------------------------------
@@ -1571,18 +1761,25 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1571
1761
  // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1572
1762
  // against a live capture and the vendored status-v5.schema.json) is the v3
1573
1763
  // key set plus the optional forecast and the v5-only next_transition
1574
- // surfaces. The v3 identity keeps rejecting every v5-only field.
1575
- const v5 = typeof value === "object" && value !== null && (value as Record<string, unknown>).schema === "gentle-ai.review-integration.status/v5";
1764
+ // surfaces. status/v6 adds the intended-untracked selection; v3 keeps
1765
+ // rejecting every v5/v6-only field.
1766
+ const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
1767
+ const v6 = schema === "gentle-ai.review-integration.status/v6";
1768
+ const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1576
1769
  const body = exactRecord(value, "status", [
1577
- "schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1578
- ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry", ...(v5 ? ["forecast", "repository_context"] : [])]);
1579
- requireIdentity(body, v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1770
+ "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1771
+ ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : [])]);
1772
+ requireIdentity(body, v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1580
1773
 
1581
1774
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
1582
- const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
1583
- const receiptStatus = enumeration(receiptBody.status, RECEIPT_STATUSES, "status.receipt.status");
1584
- const receipt: ReviewStatusReceiptV1 = { status: receiptStatus, ...(receiptBody.identity === undefined ? {} : { identity: sha256(receiptBody.identity, "status.receipt.identity") }) };
1585
-
1775
+ let receipt: ReviewStatusReceiptV1 | undefined;
1776
+ if (body.receipt !== undefined) {
1777
+ const source = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
1778
+ receipt = {
1779
+ status: enumeration(source.status, RECEIPT_STATUSES, "status.receipt.status"),
1780
+ ...(source.identity === undefined ? {} : { identity: sha256(source.identity, "status.receipt.identity") }),
1781
+ };
1782
+ }
1586
1783
  let authority: ReviewStatusAuthorityV1 | undefined;
1587
1784
  if (body.authority !== undefined) {
1588
1785
  const source = exactRecord(body.authority, "status.authority", ["version", "lineage_id", "state", "generation", "revision"]);
@@ -1610,52 +1807,32 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1610
1807
  }
1611
1808
  if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
1612
1809
  if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && (frozen !== undefined || body.authority_target_identity !== undefined)) throw new TypeError("legacy status cannot expose frozen metadata or authority_target_identity");
1613
- if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && receiptStatus !== "expected_missing" && receiptStatus !== "present") throw new TypeError("legacy status receipt is incompatible");
1810
+ if (authority?.version === REVIEW_AUTHORITY_VERSION.LEGACY_V1 && receipt !== undefined && receipt.status !== "expected_missing" && receipt.status !== "present") {
1811
+ throw new TypeError("legacy status receipt is incompatible");
1812
+ }
1614
1813
 
1615
1814
  const action = enumeration(body.action, STATUS_ACTIONS, "status.action");
1616
1815
  const actionDisposition = body.action_disposition === undefined ? undefined : enumeration(body.action_disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), "status.action_disposition");
1617
- if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
1618
- if (action !== "recover" && action !== "retry_final_verification" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover or retry_final_verification action");
1816
+ if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
1817
+ if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
1619
1818
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1620
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5 });
1819
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
1820
+ const validationRequest = v5 && body.validation_request !== undefined
1821
+ ? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
1822
+ : undefined;
1823
+ if (validationRequest !== undefined) {
1824
+ const collectInputs = nextTransition?.kind === "collect" ? nextTransition.collect?.inputs ?? [] : [];
1825
+ const matchingInputs = collectInputs.filter((input) => input.validationRequest !== undefined);
1826
+ if (matchingInputs.length !== 1 || canonicalJson(matchingInputs[0]!.validationRequest) !== canonicalJson(validationRequest)) {
1827
+ throw new TypeError("status.validation_request must exactly match the provider-targeted-validator collect input");
1828
+ }
1829
+ }
1621
1830
  // status/v5 dependentRequired: a forecast previews the transition head, so
1622
1831
  // it can only accompany an actual next_transition.
1623
1832
  const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
1624
1833
  if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
1625
1834
  const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1626
1835
 
1627
- let reconciliation: ReviewStatusReconciliationV1 | undefined;
1628
- if (action === "reconcile_finalize") {
1629
- if (body.reconciliation === undefined) throw new TypeError("reconcile_finalize status requires reconciliation");
1630
- const source = exactRecord(body.reconciliation, "status.reconciliation", ["required"]);
1631
- if (source.required !== true) throw new TypeError("status.reconciliation.required must be true");
1632
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET) throw new TypeError("reconcile_finalize status requires current_target applicability");
1633
- if (replayability !== REVIEW_REPLAYABILITY.STATUS_REQUIRED) throw new TypeError("reconcile_finalize status requires status_required replayability");
1634
- reconciliation = { required: true };
1635
- } else if (body.reconciliation !== undefined) {
1636
- throw new TypeError("status.reconciliation is only valid for the reconcile_finalize action");
1637
- }
1638
-
1639
- let validationRequest: ReviewTargetedValidationRequestV1 | undefined;
1640
- if (body.validation_request !== undefined) {
1641
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.state !== "correction_required") {
1642
- throw new TypeError("status.validation_request requires current_target applicability and correction_required authority state");
1643
- }
1644
- validationRequest = decodeTargetedValidationRequestV1(body.validation_request, "status.validation_request");
1645
- }
1646
-
1647
- let finalVerificationRetry: ReviewFinalVerificationRetryV1 | undefined;
1648
- if (action === "retry_final_verification") {
1649
- if (body.final_verification_retry === undefined) throw new TypeError("retry_final_verification status requires final_verification_retry");
1650
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.version !== REVIEW_AUTHORITY_VERSION.COMPACT_V2 || authority?.state !== "escalated") {
1651
- throw new TypeError("retry_final_verification status requires current_target compact-v2 escalated authority");
1652
- }
1653
- if (actionDisposition !== REVIEW_STATUS_ACTION_DISPOSITION.FINAL_VERIFICATION_RETRY) throw new TypeError("retry_final_verification status requires final_verification_retry disposition");
1654
- finalVerificationRetry = decodeFinalVerificationRetry(body.final_verification_retry, "status.final_verification_retry");
1655
- } else if (body.final_verification_retry !== undefined) {
1656
- throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
1657
- }
1658
-
1659
1836
  // status/v5 additive top-level repository context reference (captured
1660
1837
  // 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
1661
1838
  // status-v5.schema.json, so the live capture is authoritative). Same
@@ -1666,7 +1843,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1666
1843
  if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
1667
1844
  repositoryContext = {
1668
1845
  capability: "review.opaque_repository_context",
1669
- handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
1846
+ handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
1670
1847
  revision: sha256(source.revision, "status.repository_context.revision"),
1671
1848
  targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
1672
1849
  ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
@@ -1678,22 +1855,20 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1678
1855
  contract: REVIEW_INTEGRATION_CONTRACT,
1679
1856
  applicability,
1680
1857
  ...(authority === undefined ? {} : { authority }),
1681
- receipt,
1858
+ ...(receipt === undefined ? {} : { receipt }),
1682
1859
  action,
1683
1860
  ...(actionDisposition === undefined ? {} : { actionDisposition }),
1684
1861
  replayability,
1685
1862
  ...(frozen === undefined ? {} : { frozen }),
1686
- ...(reconciliation === undefined ? {} : { reconciliation }),
1687
1863
  targetIdentity: sha256(body.target_identity, "status.target_identity"),
1688
1864
  ...(body.authority_target_identity === undefined ? {} : { authorityTargetIdentity: sha256(body.authority_target_identity, "status.authority_target_identity") }),
1689
1865
  projection: decodeReviewProjectionV1(body.projection),
1690
1866
  repair: decodeAuthorityRepairAssessmentV1(body.repair),
1691
1867
  candidates: stringArray(body.candidates, "status.candidates", { unique: true, pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }),
1692
1868
  ...(nextTransition === undefined ? {} : { nextTransition }),
1693
- ...(validationRequest === undefined ? {} : { validationRequest }),
1694
- ...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
1695
1869
  ...(forecast === undefined ? {} : { forecast }),
1696
1870
  ...(repositoryContext === undefined ? {} : { repositoryContext }),
1871
+ ...(validationRequest === undefined ? {} : { validationRequest }),
1697
1872
  raw: body,
1698
1873
  };
1699
1874
  }
@@ -1774,13 +1949,14 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
1774
1949
  // published v3 schema demands an `--agent claude-code` token that the live
1775
1950
  // emitter omits when the caller declared no --agent, so the capture is
1776
1951
  // authoritative and Pi replays whichever provider-owned invocation arrived.
1777
- export function decodeReviewConsentV3(value: unknown): ReviewConsentV3 {
1952
+ export function decodeReviewConsentV3(value: unknown, expectedAgent?: ReviewConsentAgentV3): ReviewConsentV3 {
1778
1953
  const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
1779
1954
  requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
1780
- if (body.agent !== "claude-code") throw new TypeError("consent.agent must be claude-code");
1955
+ const agent = enumeration(body.agent, Object.values(REVIEW_CONSENT_AGENT_V3), "consent.agent") as ReviewConsentAgentV3;
1956
+ if (expectedAgent !== undefined && agent !== expectedAgent) throw new TypeError("consent.agent does not match the expected runtime binding");
1781
1957
  return {
1782
1958
  schema: "gentle-ai.review-integration.consent/v3",
1783
- agent: "claude-code",
1959
+ agent,
1784
1960
  ...decodeConsentSemantics(body),
1785
1961
  raw: body,
1786
1962
  };
@@ -1882,56 +2058,21 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
1882
2058
  // operation/v2
1883
2059
  // ---------------------------------------------------------------------------
1884
2060
 
1885
- export function decodeReviewOperationV2(value: unknown): ReviewOperationV2 {
1886
- const body = exactRecord(value, "operation", ["schema", "contract", "operation", "result"]);
1887
- requireIdentity(body, "gentle-ai.review-integration.operation/v2");
1888
- const operation = enumeration(body.operation, ["review.finalize", "review.validate", "review.bind_sdd", "review.retry_final_verification"] as const, "operation.operation");
1889
- let result: Record<string, unknown>;
1890
- if (operation === REVIEW_INTEGRATION_OPERATION.FINALIZE) {
1891
- result = exactRecord(body.result, "operation.result", ["operation", "lineage_id", "state", "action", "store_revision"], ["eligibility", "next_transition", "validation_request", "escalation"]);
1892
- if (result.operation !== "review/finalize") throw new TypeError("operation.result does not match review.finalize");
1893
- nonempty(result.lineage_id, "operation.result.lineage_id");
1894
- nonempty(result.state, "operation.result.state");
1895
- nonempty(result.action, "operation.result.action");
1896
- sha256(result.store_revision, "operation.result.store_revision");
1897
- if (result.eligibility !== undefined) decodeEligibility(result.eligibility, "operation.result.eligibility");
1898
- if (result.next_transition !== undefined) decodeReviewNextTransitionV3(result.next_transition);
1899
- if (result.validation_request !== undefined) {
1900
- decodeTargetedValidationRequestV1(result.validation_request, "operation.result.validation_request");
1901
- if (result.state !== "correction_required") throw new TypeError("operation.result.validation_request requires state correction_required");
1902
- }
1903
- if (result.escalation !== undefined) nonempty(result.escalation, "operation.result.escalation");
1904
- } else if (operation === REVIEW_INTEGRATION_OPERATION.VALIDATE) {
1905
- result = exactRecord(body.result, "operation.result", ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1906
- if (result.schema !== "gentle-ai.review-gate-result/v1") throw new TypeError("operation.result does not match review.validate");
1907
- enumeration(result.result, ["allow", "scope-changed", "invalidated", "escalated"] as const, "operation.result.result");
1908
- boolean(result.allowed, "operation.result.allowed");
1909
- nonempty(result.action, "operation.result.action");
1910
- nonempty(result.reason, "operation.result.reason");
1911
- record(result.context, "operation.result.context");
1912
- if (result.delivery !== undefined && result.delivery !== "disabled/unmanaged") throw new TypeError("operation.result.delivery is unsupported");
1913
- } else if (operation === REVIEW_INTEGRATION_OPERATION.BIND_SDD) {
1914
- result = exactRecord(body.result, "operation.result", ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1915
- if (result.schema !== "gentle-ai.sdd-review-binding/v1") throw new TypeError("operation.result does not match review.bind_sdd");
1916
- sha256(result.revision, "operation.result.revision");
1917
- nonempty(result.change, "operation.result.change");
1918
- nonempty(result.lineage, "operation.result.lineage");
1919
- sha256(result.authority_revision, "operation.result.authority_revision");
1920
- sha256(result.receipt_hash, "operation.result.receipt_hash");
1921
- record(result.gate_context, "operation.result.gate_context");
1922
- } else {
1923
- result = exactRecord(body.result, "operation.result", ["operation", "predecessor_lineage_id", "predecessor_revision", "lineage_id", "state", "store_revision", "target_identity", "incident_digest", "recovery_disposition"]);
1924
- if (result.operation !== "review.retry_final_verification") throw new TypeError("operation.result does not match review.retry_final_verification");
1925
- lineage(result.predecessor_lineage_id, "operation.result.predecessor_lineage_id");
1926
- sha256(result.predecessor_revision, "operation.result.predecessor_revision");
1927
- lineage(result.lineage_id, "operation.result.lineage_id");
1928
- if (result.state !== "validating") throw new TypeError("operation.result.state must be validating");
1929
- sha256(result.store_revision, "operation.result.store_revision");
1930
- sha256(result.target_identity, "operation.result.target_identity");
1931
- sha256(result.incident_digest, "operation.result.incident_digest");
1932
- if (result.recovery_disposition !== "final_verification_retry") throw new TypeError("operation.result.recovery_disposition must be final_verification_retry");
1933
- }
1934
- return { contract: REVIEW_INTEGRATION_CONTRACT, operation, result, raw: body };
2061
+ export function decodeReviewAdvisoryFindingsV1(value: unknown, label: string): ReviewAdvisoryFindingsV1 {
2062
+ const body = exactRecord(value, label, ["statement", "findings"]);
2063
+ return {
2064
+ statement: nonempty(body.statement, `${label}.statement`),
2065
+ findings: array(body.findings, `${label}.findings`, (entry, itemLabel) => {
2066
+ const finding = exactRecord(entry, itemLabel, ["id", "severity", "disposition"], ["lens", "location"]);
2067
+ return {
2068
+ id: nonempty(finding.id, `${itemLabel}.id`),
2069
+ ...(finding.lens === undefined ? {} : { lens: nonempty(finding.lens, `${itemLabel}.lens`) }),
2070
+ ...(finding.location === undefined ? {} : { location: nonempty(finding.location, `${itemLabel}.location`) }),
2071
+ severity: enumeration(finding.severity, Object.values(REVIEW_ADVISORY_FINDING_SEVERITY), `${itemLabel}.severity`),
2072
+ disposition: enumeration(finding.disposition, Object.values(REVIEW_ADVISORY_FINDING_DISPOSITION), `${itemLabel}.disposition`),
2073
+ };
2074
+ }, { minimum: 1 }),
2075
+ };
1935
2076
  }
1936
2077
 
1937
2078
  // ---------------------------------------------------------------------------
@@ -2066,3 +2207,394 @@ export function decodeReviewResultArtifactV2(value: unknown): ReviewResultArtifa
2066
2207
  raw: body,
2067
2208
  };
2068
2209
  }
2210
+
2211
+ // Terminal capture answers are emitted directly by the native provider. Pi
2212
+ // validates their closed shape and identity, then treats all lifecycle meaning
2213
+ // as opaque provider-owned state.
2214
+ export const REVIEW_LAST_EVENT_CLOSURE_SCHEMA = "gentle-ai.review-last-event-closure/v1";
2215
+ export const REVIEW_LAST_EVENT_CLOSURE_OPERATION = {
2216
+ CAPTURE_RESULT: "review/capture-result",
2217
+ CAPTURE_CORRECTION_PLAN: "review.capture-correction-plan",
2218
+ CAPTURE_REFUTER: "review.capture-refuter",
2219
+ CAPTURE_VALIDATION: "review/capture-validation",
2220
+ } as const;
2221
+ export type ReviewLastEventClosureOperation = (typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION)[keyof typeof REVIEW_LAST_EVENT_CLOSURE_OPERATION];
2222
+
2223
+ const REVIEW_LAST_EVENT_TERMINAL_STATES = ["approved", "correction_required", "escalated"] as const;
2224
+ export type ReviewLastEventClosureState = (typeof REVIEW_LAST_EVENT_TERMINAL_STATES)[number];
2225
+
2226
+ const REVIEW_STATUS_CONTINUATION_OPERATION = {
2227
+ STATUS: "review.status",
2228
+ } as const;
2229
+
2230
+ export interface ReviewStatusContinuationBindingV1 {
2231
+ targetIdentity: string;
2232
+ lineageId?: string;
2233
+ revision?: string;
2234
+ repositoryContext?: string;
2235
+ }
2236
+
2237
+ export interface ReviewStatusContinuationArtifactV1 {
2238
+ schema: "gentle-ai.review-result-artifact/v2";
2239
+ capability: "review.native_result_artifact";
2240
+ sha256: string;
2241
+ lineageId: string;
2242
+ targetIdentity: string;
2243
+ lens: string;
2244
+ selectedOrder: number;
2245
+ subjectHash: string;
2246
+ admissionDecision: "completed";
2247
+ }
2248
+
2249
+ export interface ReviewStatusContinuationV1 {
2250
+ operation: typeof REVIEW_STATUS_CONTINUATION_OPERATION.STATUS;
2251
+ arguments: readonly ReviewTransitionArgumentV3[];
2252
+ selectorArguments?: readonly ReviewTransitionArgumentV3[];
2253
+ preconditions: readonly ReviewTransitionArgumentV3[];
2254
+ binding: ReviewStatusContinuationBindingV1;
2255
+ artifacts?: readonly ReviewStatusContinuationArtifactV1[];
2256
+ command?: string;
2257
+ raw: Record<string, unknown>;
2258
+ }
2259
+
2260
+ const REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION = "review.acknowledge-approved" as const;
2261
+
2262
+ // The exact ordered argument names the provider renders for one approved
2263
+ // acknowledgement. The list is closed and positional on the wire, so decoding
2264
+ // by position is what proves the relay is replaying the provider's own
2265
+ // invocation rather than one the host assembled.
2266
+ const REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS = ["cwd", "lineage", "target", "expected-revision", "token"] as const;
2267
+
2268
+ export interface ReviewApprovedAcknowledgementBindingV1 {
2269
+ lineageId: string;
2270
+ revision: string;
2271
+ targetIdentity: string;
2272
+ repositoryContext?: string;
2273
+ }
2274
+
2275
+ export interface ReviewApprovedAcknowledgementV1 {
2276
+ operation: typeof REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION;
2277
+ command: string;
2278
+ arguments: readonly ReviewTransitionArgumentV3[];
2279
+ preconditions: readonly ReviewTransitionArgumentV3[];
2280
+ binding: ReviewApprovedAcknowledgementBindingV1;
2281
+ raw: Record<string, unknown>;
2282
+ }
2283
+
2284
+ export interface ReviewApprovedAcknowledgementExecuteExpectationV1 { cwd?: string; lineageId?: string; targetIdentity?: string; revision?: string; }
2285
+
2286
+ export type ReviewApprovedAcknowledgementExecuteTokensV1 = readonly [string, string, string, string, string];
2287
+
2288
+ interface ReviewApprovedAcknowledgementExecuteShapeV1 { tokens: ReviewApprovedAcknowledgementExecuteTokensV1; values: readonly [string, string, string, string, string]; lineageId: string; targetIdentity: string; revision: string; }
2289
+
2290
+ function assertReviewApprovedAcknowledgementExecuteShapeV1(execute: ReviewNextTransitionExecuteV3): ReviewApprovedAcknowledgementExecuteShapeV1 {
2291
+ if (execute.operation !== REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION) throw new TypeError(`acknowledgement.execute.operation must be ${REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION}`);
2292
+ if (execute.command === undefined) throw new TypeError("acknowledgement.execute.command is required");
2293
+ text(execute.command, "acknowledgement.execute.command", { minimum: 1, pattern: /^gentle-ai review acknowledge-approved(?: |$)/ });
2294
+ if (execute.arguments.length !== REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length) throw new TypeError(`acknowledgement.execute.arguments must carry exactly ${REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length} provider-issued arguments`);
2295
+ const values = execute.arguments.map((argument, index) => { const name = REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS[index]!; if (argument.name !== name) throw new TypeError(`acknowledgement.execute.arguments[${index}].name must be ${name}`); const value = nonempty(argument.value, `acknowledgement.execute.arguments[${index}].value`); if (nonempty(argument.token, `acknowledgement.execute.arguments[${index}].token`) !== `--${name}=${value}`) throw new TypeError(`acknowledgement.execute.arguments[${index}].token must exactly match ${name}`); return value; });
2296
+ if (execute.preconditions.length !== 1 || execute.preconditions[0]?.name !== "state" || execute.preconditions[0]?.value !== "approved") throw new TypeError("acknowledgement.execute.preconditions must be the single approved state precondition");
2297
+ return { tokens: execute.arguments.map((argument) => argument.token!) as ReviewApprovedAcknowledgementExecuteTokensV1, values: values as readonly [string, string, string, string, string], lineageId: lineage(execute.binding.lineageId, "acknowledgement.execute.binding.lineage_id"), targetIdentity: sha256(execute.binding.targetIdentity, "acknowledgement.execute.binding.target_identity"), revision: sha256(execute.binding.revision, "acknowledgement.execute.binding.revision") };
2298
+ }
2299
+
2300
+ /**
2301
+ * Proves that an approved-acknowledgement execute vector is the exact closed,
2302
+ * provider-issued burn invocation. Callers can additionally bind its values to
2303
+ * their own current STATUS before invoking the returned tokens.
2304
+ */
2305
+ export function assertReviewApprovedAcknowledgementExecuteV1(
2306
+ execute: ReviewNextTransitionExecuteV3,
2307
+ expected: ReviewApprovedAcknowledgementExecuteExpectationV1 = {},
2308
+ ): ReviewApprovedAcknowledgementExecuteTokensV1 {
2309
+ const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(execute);
2310
+ if (shape.values[1] !== shape.lineageId) throw new TypeError("acknowledgement lineage argument does not match its binding");
2311
+ if (shape.values[2] !== shape.targetIdentity) throw new TypeError("acknowledgement target argument does not match its binding");
2312
+ if (shape.values[3] !== shape.revision) throw new TypeError("acknowledgement revision argument does not match its binding");
2313
+ if (expected.cwd !== undefined && shape.values[0] !== expected.cwd) throw new TypeError("acknowledgement.execute.cwd does not match the current target");
2314
+ if (expected.lineageId !== undefined && shape.lineageId !== expected.lineageId) throw new TypeError("acknowledgement.execute.lineage does not match the current target");
2315
+ if (expected.targetIdentity !== undefined && shape.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledgement.execute.target does not match the current target");
2316
+ if (expected.revision !== undefined && shape.revision !== expected.revision) throw new TypeError("acknowledgement.execute.revision does not match the current target"); return shape.tokens;
2317
+ }
2318
+
2319
+ // ---------------------------------------------------------------------------
2320
+ // review-acknowledged/v1 — the answer the burn prints (gentle-ai #3947)
2321
+ // ---------------------------------------------------------------------------
2322
+
2323
+ // Until gentle-ai #3947 a successful `review acknowledge-approved` burned its
2324
+ // authority in silence and a host could only infer the burn from a later
2325
+ // STATUS offering a fresh START. From main commit bc9f74d2 the command prints
2326
+ // exactly this envelope on success. Like result-artifact/v2 it carries no
2327
+ // `contract`: the schema constant plus the slash-form operation are its whole
2328
+ // identity, so requireIdentity (which demands the contract pair) does not
2329
+ // apply. Every published release up to v2.5.0-rc.3 still prints nothing; the
2330
+ // native client owns that distinction, this decoder only ever sees bytes.
2331
+ export const REVIEW_ACKNOWLEDGED_SCHEMA = "gentle-ai.review-acknowledged/v1" as const;
2332
+ const REVIEW_ACKNOWLEDGED_OPERATION = "review/acknowledge-approved" as const;
2333
+
2334
+ export interface ReviewAcknowledgedV1 {
2335
+ schema: typeof REVIEW_ACKNOWLEDGED_SCHEMA;
2336
+ operation: typeof REVIEW_ACKNOWLEDGED_OPERATION;
2337
+ action: "acknowledged";
2338
+ lineageId: string;
2339
+ targetIdentity: string;
2340
+ consumedRevision: string;
2341
+ authority: "burned";
2342
+ raw: Record<string, unknown>;
2343
+ }
2344
+
2345
+ /** The binding the caller already holds from STATUS; the envelope must name exactly that burn. */
2346
+ export interface ReviewAcknowledgedExpectationV1 {
2347
+ lineageId?: string;
2348
+ targetIdentity?: string;
2349
+ revision?: string;
2350
+ }
2351
+
2352
+ export function decodeReviewAcknowledgedV1(value: unknown, expected: ReviewAcknowledgedExpectationV1 = {}): ReviewAcknowledgedV1 {
2353
+ if (record(value, "acknowledged").schema !== REVIEW_ACKNOWLEDGED_SCHEMA) throw new TypeError(`schema must be ${REVIEW_ACKNOWLEDGED_SCHEMA}`);
2354
+ const body = exactRecord(value, "acknowledged", ["schema", "operation", "action", "lineage_id", "target_identity", "consumed_revision", "authority"]);
2355
+ if (body.operation !== REVIEW_ACKNOWLEDGED_OPERATION) throw new TypeError(`acknowledged.operation must be ${REVIEW_ACKNOWLEDGED_OPERATION}`);
2356
+ if (body.action !== "acknowledged") throw new TypeError("acknowledged.action must be acknowledged");
2357
+ if (body.authority !== "burned") throw new TypeError("acknowledged.authority must be burned");
2358
+ const decoded: ReviewAcknowledgedV1 = {
2359
+ schema: REVIEW_ACKNOWLEDGED_SCHEMA,
2360
+ operation: REVIEW_ACKNOWLEDGED_OPERATION,
2361
+ action: "acknowledged",
2362
+ lineageId: lineage(body.lineage_id, "acknowledged.lineage_id"),
2363
+ targetIdentity: sha256(body.target_identity, "acknowledged.target_identity"),
2364
+ consumedRevision: sha256(body.consumed_revision, "acknowledged.consumed_revision"),
2365
+ authority: "burned",
2366
+ raw: body,
2367
+ };
2368
+ if (expected.lineageId !== undefined && decoded.lineageId !== expected.lineageId) throw new TypeError("acknowledged.lineage_id does not name the acknowledged lineage");
2369
+ if (expected.targetIdentity !== undefined && decoded.targetIdentity !== expected.targetIdentity) throw new TypeError("acknowledged.target_identity does not name the acknowledged target");
2370
+ if (expected.revision !== undefined && decoded.consumedRevision !== expected.revision) throw new TypeError("acknowledged.consumed_revision does not name the consumed revision");
2371
+ return decoded;
2372
+ }
2373
+
2374
+ export interface ReviewLastEventClosureV1 {
2375
+ schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
2376
+ operation: ReviewLastEventClosureOperation;
2377
+ lineageId: string;
2378
+ state: ReviewLastEventClosureState;
2379
+ storeRevision: string;
2380
+ action?: string;
2381
+ targetIdentity?: string;
2382
+ requestHash?: string;
2383
+ correctionLines?: number;
2384
+ advisoryFindings?: ReviewAdvisoryFindingsV1;
2385
+ statusContinuation?: ReviewStatusContinuationV1;
2386
+ acknowledgement?: ReviewApprovedAcknowledgementV1;
2387
+ // Set when the provider sent an acknowledgement this decoder could not
2388
+ // read. It is deliberately distinct from an absent one: the host must be
2389
+ // able to tell "approved, nothing to run" from "approved, and the thing
2390
+ // that ends this is present but unreadable here".
2391
+ acknowledgementUndecodable?: true;
2392
+ }
2393
+
2394
+ export interface ReviewLastEventClosureBinding {
2395
+ lineageId: string;
2396
+ targetIdentity?: string;
2397
+ requestHash?: string;
2398
+ }
2399
+
2400
+ function decodeReviewStatusContinuationArtifactV1(value: unknown, label: string): ReviewStatusContinuationArtifactV1 {
2401
+ const artifact = exactRecord(value, label, ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"]);
2402
+ if (artifact.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError(`${label}.schema must be gentle-ai.review-result-artifact/v2`);
2403
+ if (artifact.capability !== "review.native_result_artifact") throw new TypeError(`${label}.capability must be review.native_result_artifact`);
2404
+ if (artifact.admission_decision !== "completed") throw new TypeError(`${label}.admission_decision must be completed`);
2405
+ return {
2406
+ schema: "gentle-ai.review-result-artifact/v2",
2407
+ capability: "review.native_result_artifact",
2408
+ sha256: sha256(artifact.sha256, `${label}.sha256`),
2409
+ lineageId: lineage(artifact.lineage_id, `${label}.lineage_id`),
2410
+ targetIdentity: sha256(artifact.target_identity, `${label}.target_identity`),
2411
+ lens: nonempty(artifact.lens, `${label}.lens`),
2412
+ selectedOrder: integer(artifact.selected_order, `${label}.selected_order`, 0),
2413
+ subjectHash: sha256(artifact.subject_hash, `${label}.subject_hash`),
2414
+ admissionDecision: "completed",
2415
+ };
2416
+ }
2417
+
2418
+ // decodeReviewApprovedAcknowledgementV1 retains the closure-specific wire
2419
+ // boundary with the shared shape validator; its semantic binding stays outside
2420
+ // the defensive closure catch in the exported assertion.
2421
+ function decodeReviewApprovedAcknowledgementV1(value: unknown, label: string): ReviewApprovedAcknowledgementV1 {
2422
+ const body = exactRecord(value, label, ["operation", "command", "arguments", "preconditions", "binding"]);
2423
+ const argumentsList = decodeTransitionArguments(body.arguments, `${label}.arguments`);
2424
+ const preconditions = decodeTransitionArguments(body.preconditions, `${label}.preconditions`);
2425
+ const sourceBinding = exactRecord(body.binding, `${label}.binding`, ["lineage_id", "revision", "target_identity"], ["repository_context"]);
2426
+ const repositoryContext = sourceBinding.repository_context === undefined
2427
+ ? undefined
2428
+ : text(sourceBinding.repository_context, `${label}.binding.repository_context`, { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
2429
+ const acknowledgement = {
2430
+ operation: body.operation,
2431
+ command: body.command,
2432
+ arguments: argumentsList,
2433
+ preconditions,
2434
+ binding: {
2435
+ lineageId: sourceBinding.lineage_id,
2436
+ revision: sourceBinding.revision,
2437
+ targetIdentity: sourceBinding.target_identity,
2438
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
2439
+ },
2440
+ } as unknown as ReviewNextTransitionExecuteV3;
2441
+ const shape = assertReviewApprovedAcknowledgementExecuteShapeV1(acknowledgement);
2442
+ return {
2443
+ operation: REVIEW_APPROVED_ACKNOWLEDGEMENT_OPERATION,
2444
+ command: acknowledgement.command!,
2445
+ arguments: argumentsList,
2446
+ preconditions,
2447
+ binding: {
2448
+ lineageId: shape.lineageId,
2449
+ revision: shape.revision,
2450
+ targetIdentity: shape.targetIdentity,
2451
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
2452
+ },
2453
+ raw: body,
2454
+ };
2455
+ }
2456
+
2457
+ function decodeReviewStatusContinuationV1(value: unknown): ReviewStatusContinuationV1 {
2458
+ const body = exactRecord(value, "last_event_closure.status_continuation", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
2459
+ if (body.operation !== REVIEW_STATUS_CONTINUATION_OPERATION.STATUS) throw new TypeError("last_event_closure.status_continuation.operation must be review.status");
2460
+ const argumentsList = decodeTransitionArguments(body.arguments, "last_event_closure.status_continuation.arguments");
2461
+ if (argumentsList.some((argument) => argument.token === undefined)) throw new TypeError("last_event_closure.status_continuation.arguments require exact tokens");
2462
+ const preconditions = decodeTransitionArguments(body.preconditions, "last_event_closure.status_continuation.preconditions");
2463
+ if (preconditions.length === 0) throw new TypeError("last_event_closure.status_continuation.preconditions requires at least one entry");
2464
+ const sourceBinding = exactRecord(body.binding, "last_event_closure.status_continuation.binding", ["target_identity"], ["lineage_id", "revision", "repository_context"]);
2465
+ const lineageId = sourceBinding.lineage_id === undefined ? undefined : lineage(sourceBinding.lineage_id, "last_event_closure.status_continuation.binding.lineage_id");
2466
+ const revision = sourceBinding.revision === undefined ? undefined : sha256(sourceBinding.revision, "last_event_closure.status_continuation.binding.revision");
2467
+ const repositoryContext = sourceBinding.repository_context === undefined
2468
+ ? undefined
2469
+ : text(sourceBinding.repository_context, "last_event_closure.status_continuation.binding.repository_context", { pattern: /^rctx[12]_[0-9a-f]{64}$/ });
2470
+ const selectorArguments = body.selector_arguments === undefined
2471
+ ? undefined
2472
+ : decodeTransitionArguments(body.selector_arguments, "last_event_closure.status_continuation.selector_arguments");
2473
+ const artifacts = body.artifacts === undefined
2474
+ ? undefined
2475
+ : array(body.artifacts, "last_event_closure.status_continuation.artifacts", decodeReviewStatusContinuationArtifactV1);
2476
+ const command = body.command === undefined
2477
+ ? undefined
2478
+ : text(body.command, "last_event_closure.status_continuation.command", { minimum: 1, pattern: /^gentle-ai review [a-z][a-z-]*/ });
2479
+ return {
2480
+ operation: REVIEW_STATUS_CONTINUATION_OPERATION.STATUS,
2481
+ arguments: argumentsList,
2482
+ ...(selectorArguments === undefined ? {} : { selectorArguments }),
2483
+ preconditions,
2484
+ binding: {
2485
+ targetIdentity: sha256(sourceBinding.target_identity, "last_event_closure.status_continuation.binding.target_identity"),
2486
+ ...(lineageId === undefined ? {} : { lineageId }),
2487
+ ...(revision === undefined ? {} : { revision }),
2488
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
2489
+ },
2490
+ ...(artifacts === undefined ? {} : { artifacts }),
2491
+ ...(command === undefined ? {} : { command }),
2492
+ raw: body,
2493
+ };
2494
+ }
2495
+
2496
+ export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
2497
+ const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "status_continuation", "acknowledgement"]);
2498
+ if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
2499
+ const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
2500
+ const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
2501
+ const shared = {
2502
+ schema: REVIEW_LAST_EVENT_CLOSURE_SCHEMA,
2503
+ operation,
2504
+ lineageId: lineage(body.lineage_id, "last_event_closure.lineage_id"),
2505
+ state,
2506
+ storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
2507
+ };
2508
+ if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
2509
+ if (body.action !== undefined || body.advisory_findings !== undefined || body.status_continuation !== undefined) throw new TypeError("last_event_closure correction-plan cannot carry action, advisory_findings, or status_continuation");
2510
+ if (state !== "correction_required") throw new TypeError("last_event_closure correction-plan requires correction_required state");
2511
+ return {
2512
+ ...shared,
2513
+ targetIdentity: sha256(body.target_identity, "last_event_closure.target_identity"),
2514
+ requestHash: sha256(body.request_hash, "last_event_closure.request_hash"),
2515
+ correctionLines: integer(body.correction_lines, "last_event_closure.correction_lines", 1, 200),
2516
+ };
2517
+ }
2518
+ if (body.target_identity !== undefined || body.request_hash !== undefined || body.correction_lines !== undefined) throw new TypeError("last_event_closure terminal capture cannot carry correction-plan fields");
2519
+ const requiresStatusContinuation = state === "correction_required" && (
2520
+ operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_RESULT || operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_REFUTER
2521
+ );
2522
+ if (requiresStatusContinuation && body.status_continuation === undefined) throw new TypeError("last_event_closure requires status_continuation for correction-required result or refuter capture");
2523
+ if (!requiresStatusContinuation && body.status_continuation !== undefined) throw new TypeError("last_event_closure status_continuation is only valid for correction-required result or refuter capture");
2524
+ const statusContinuation = body.status_continuation === undefined ? undefined : decodeReviewStatusContinuationV1(body.status_continuation);
2525
+ if (statusContinuation !== undefined) {
2526
+ if (statusContinuation.binding.lineageId !== shared.lineageId) {
2527
+ throw new TypeError("last_event_closure status continuation lineage does not match its enclosing closure");
2528
+ }
2529
+ if (statusContinuation.binding.revision !== shared.storeRevision) {
2530
+ throw new TypeError("last_event_closure status continuation revision does not match its enclosing closure");
2531
+ }
2532
+ const lineageArguments = statusContinuation.arguments.filter((argument) => argument.name === "lineage");
2533
+ if (lineageArguments.length !== 1 || lineageArguments[0]?.value !== shared.lineageId || lineageArguments[0]?.token !== `--lineage=${shared.lineageId}`) {
2534
+ throw new TypeError("last_event_closure status continuation lineage argument does not match its enclosing closure");
2535
+ }
2536
+ }
2537
+ // Only an approved closure may carry the continuation that burns its
2538
+ // authority: any other state offering an acknowledgement would be inviting
2539
+ // a burn the provider never authorized.
2540
+ //
2541
+ // It stays optional because the pinned installer binary predates it. A
2542
+ // provider that emits one is decoded strictly; a provider that does not is
2543
+ // still the contract this build ships against. Requiring it belongs with
2544
+ // the pin bump that makes it always present.
2545
+ // Decoded defensively on purpose. This decoder pins the acknowledgement to a
2546
+ // closed positional shape, and the same patch that added it already shows
2547
+ // what strictness costs when the provider moves: rejecting one omitempty
2548
+ // field made every targeted-validation STATUS that carried it undecodable.
2549
+ // Here the blast radius would be worse than a lost field. Throwing would
2550
+ // fail the whole approved closure, so the host would lose the approval
2551
+ // outcome AND the only invocation that burns the authority, leaving the
2552
+ // lineage approved and un-burnable at once. An unreadable continuation
2553
+ // degrades to a flag; everything else about the approval still arrives.
2554
+ let acknowledgement: ReviewApprovedAcknowledgementV1 | undefined;
2555
+ let acknowledgementUndecodable = false;
2556
+ if (body.acknowledgement !== undefined) {
2557
+ try {
2558
+ acknowledgement = decodeReviewApprovedAcknowledgementV1(body.acknowledgement, "last_event_closure.acknowledgement");
2559
+ } catch {
2560
+ acknowledgement = undefined;
2561
+ acknowledgementUndecodable = true;
2562
+ }
2563
+ }
2564
+ if (acknowledgementUndecodable && state !== "approved") {
2565
+ throw new TypeError("last_event_closure acknowledgement requires approved state");
2566
+ }
2567
+ if (acknowledgement !== undefined) {
2568
+ if (state !== "approved") throw new TypeError("last_event_closure acknowledgement requires approved state");
2569
+ assertReviewApprovedAcknowledgementExecuteV1(acknowledgement, { lineageId: shared.lineageId, revision: shared.storeRevision });
2570
+ }
2571
+ const action = nonempty(body.action, "last_event_closure.action");
2572
+ const advisoryFindings = body.advisory_findings === undefined
2573
+ ? undefined
2574
+ : decodeReviewAdvisoryFindingsV1(body.advisory_findings, "last_event_closure.advisory_findings");
2575
+ if (advisoryFindings !== undefined && state !== "approved") throw new TypeError("last_event_closure advisory_findings requires approved state");
2576
+ return {
2577
+ ...shared,
2578
+ action,
2579
+ ...(advisoryFindings === undefined ? {} : { advisoryFindings }),
2580
+ ...(statusContinuation === undefined ? {} : { statusContinuation }),
2581
+ ...(acknowledgement === undefined ? {} : { acknowledgement }),
2582
+ ...(acknowledgementUndecodable ? { acknowledgementUndecodable: true as const } : {}),
2583
+ };
2584
+ }
2585
+
2586
+ export function assertReviewLastEventClosureBinding(
2587
+ closure: ReviewLastEventClosureV1,
2588
+ binding: ReviewLastEventClosureBinding,
2589
+ ): void {
2590
+ if (closure.lineageId !== binding.lineageId) throw new TypeError("last_event_closure lineage does not match its provider binding");
2591
+ if (binding.targetIdentity !== undefined && closure.targetIdentity !== undefined && closure.targetIdentity !== binding.targetIdentity) {
2592
+ throw new TypeError("last_event_closure target does not match its provider binding");
2593
+ }
2594
+ if (binding.targetIdentity !== undefined && closure.statusContinuation !== undefined && closure.statusContinuation.binding.targetIdentity !== binding.targetIdentity) {
2595
+ throw new TypeError("last_event_closure status continuation target does not match its provider binding");
2596
+ }
2597
+ if (binding.requestHash !== undefined && closure.requestHash !== undefined && closure.requestHash !== binding.requestHash) {
2598
+ throw new TypeError("last_event_closure request hash does not match its provider binding");
2599
+ }
2600
+ }