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