gentle-pi 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -1,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,38 +75,49 @@ 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));
93
+ // Typed failures are accepted only for the supported start, status, repair,
94
+ // and last-event capture operations. Retired lifecycle verbs fail closed.
95
+ const FAILURE_OPERATIONS = Object.freeze([
96
+ ...REQUIRED_OPERATIONS,
97
+ "review.capture-result",
98
+ "review.capture-correction-plan",
99
+ "review.capture-refuter",
100
+ "review.capture-validation",
101
+ ] );
97
102
  const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] );
98
103
  const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
99
- const REQUIRED_SCHEMAS = Object.freeze([
104
+ // The schema floor shared by every accepted capabilities identity. Each minor
105
+ // then adds its own capabilities/consent/status identities below: the provider
106
+ // swaps those three advertisements as the protocol minor advances (v2.1 moved
107
+ // consent to v3; v2.2 moved status to v5), ground-truthed against the vendored
108
+ // capabilities[-v2.1|-v2.2].schema.json contracts on gentle-ai main and a live
109
+ // v2.2 capture from a main-line dev build.
110
+ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
100
111
  "gentle-ai.review-admitted-result/v2",
101
112
  "gentle-ai.review-artifact-subject/v2",
102
113
  "gentle-ai.review-authority-repair-assessment/v1",
103
114
  "gentle-ai.review-authority-status/v1",
104
115
  "gentle-ai.review-gate-request/v1",
105
- "gentle-ai.review-integration.capabilities/v2",
106
- "gentle-ai.review-integration.consent/v2",
107
116
  "gentle-ai.review-integration.failure/v2",
108
117
  "gentle-ai.review-final-verification-incident/v1",
109
118
  "gentle-ai.review-integration.operation/v2",
110
119
  "gentle-ai.review-integration.projection/v1",
111
120
  "gentle-ai.review-integration.repair/v2",
112
- "gentle-ai.review-integration.start/v3",
113
- "gentle-ai.review-integration.status/v3",
114
121
  "gentle-ai.review-receipt/v1",
115
122
  "gentle-ai.review-receipt/v2",
116
123
  "gentle-ai.review-result-artifact/v2",
@@ -120,6 +127,64 @@ const REQUIRED_SCHEMAS = Object.freeze([
120
127
  "https://gentle-ai.dev/schema/review/reviewer/v1",
121
128
  "https://gentle-ai.dev/schema/review/validator/v1",
122
129
  ] );
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({
163
+ "gentle-ai.review-integration.capabilities/v2": Object.freeze({
164
+ protocolMinor: 0,
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"]),
166
+ }),
167
+ "gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
168
+ protocolMinor: 1,
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"]),
170
+ }),
171
+ "gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
172
+ protocolMinor: 2,
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,
186
+ }),
187
+ });
123
188
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
124
189
  "base_ref_workspace_overlay",
125
190
  "bounded_process_waits",
@@ -224,6 +289,51 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
224
289
 
225
290
 
226
291
 
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] ;
314
+
315
+
316
+
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
227
337
 
228
338
 
229
339
 
@@ -269,6 +379,84 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
269
379
 
270
380
 
271
381
 
382
+
383
+
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+
392
+
393
+
394
+
395
+
396
+
397
+ // Provider-owned completing form for a host-mediated capture slot
398
+ // (gentle-pi#311 P4). The provider issues the exact operation and argument
399
+ // tokens that submit the captured bytes; the host substitutes only the
400
+ // artifact location into the declared {{value}} slot and never synthesizes
401
+ // or filters the form itself.
402
+
403
+
404
+
405
+
406
+
407
+
408
+
409
+
410
+
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+ // The two Go-owned non-lens provider role capture operations (gentle-pi#311
425
+ // P4-roles; provider side gentle-ai#3264). Their collect inputs are
426
+ // self-contained authority-advancing vectors: binding tokens plus
427
+ // `--agent=pi --execute=true`, with NO submission descriptor. The known set
428
+ // is closed — an unknown role capture operation is never executed.
429
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
430
+ CAPTURE_REFUTER: "review.capture-refuter",
431
+ CAPTURE_VALIDATION: "review.capture-validation",
432
+ } ;
433
+
434
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.values(REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION));
435
+
436
+ // status/v5 structural surfaces. Pi has no consumer for these yet; they are
437
+ // decoded strictly (exact key sets per the vendored status-v5.schema.json and
438
+ // correction-plan-request.schema.json on gentle-ai main) and carried through
439
+ // so a v5 provider payload is never rejected for being newer than v3.
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
272
460
 
273
461
 
274
462
 
@@ -376,6 +564,7 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
376
564
 
377
565
 
378
566
 
567
+
379
568
 
380
569
 
381
570
 
@@ -402,6 +591,34 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
402
591
 
403
592
 
404
593
 
594
+
595
+
596
+
597
+
598
+
599
+
600
+
601
+
602
+
603
+ // consent/v3 (gentle-ai >= 2.3.0, capabilities/v2.1+): the same per-candidate
604
+ // blocking question with one net-new required member, the provider-fixed
605
+ // `agent` runtime binding. Everything shared with v2 keeps its exact shape so
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
+
615
+
616
+
617
+
618
+
619
+
620
+ // Either accepted consent identity. Consumers that relay the envelope (rather
621
+ // than decode it) accept both; each decoder still admits exactly one schema.
405
622
 
406
623
 
407
624
  const FAILURE_REQUIRED_INPUTS = [
@@ -414,12 +631,11 @@ const FAILURE_REQUIRED_INPUTS = [
414
631
  "disposition",
415
632
  "reason",
416
633
  "actor",
417
- "incident",
418
634
  "maintainer_authorization",
419
635
  "base_ref",
420
636
  ] ;
421
637
 
422
- 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"] ;
423
639
 
424
640
  // Known cause_category values: the vendored failure.schema.json enum plus
425
641
  // "incomplete_store_entry", which the v2.1.8 emitter produces beyond that enum.
@@ -478,6 +694,29 @@ const FAILURE_CAUSE_CATEGORIES = ["inventory_io_or_layout", "lock_ambiguous", "r
478
694
 
479
695
 
480
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
+
718
+
719
+
481
720
 
482
721
 
483
722
 
@@ -690,10 +929,15 @@ function decodeOptionalFeature(value , label )
690
929
  export function decodeReviewCapabilitiesV2(value , verifiedExecutableDigest ) {
691
930
  const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] ;
692
931
  const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
693
- requireIdentity(body, "gentle-ai.review-integration.capabilities/v2");
932
+ // Additive minor acceptance: each accepted schema identity keeps its own
933
+ // exact protocol minor and required-schema floor; v2 stays exactly as it
934
+ // was, and an unknown identity is rejected before anything else.
935
+ const identity = CAPABILITIES_SCHEMA_IDENTITIES[typeof body.schema === "string" ? body.schema : ""];
936
+ if (identity === undefined) throw new TypeError(`schema must be one of ${Object.keys(CAPABILITIES_SCHEMA_IDENTITIES).join(", ")}`);
937
+ requireIdentity(body, body.schema );
694
938
 
695
939
  const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
696
- if (protocol.major !== 2 || protocol.minor !== 0) throw new TypeError("incompatible review integration protocol");
940
+ if (protocol.major !== 2 || protocol.minor !== identity.protocolMinor) throw new TypeError("incompatible review integration protocol");
697
941
 
698
942
  const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
699
943
  if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
@@ -713,20 +957,26 @@ export function decodeReviewCapabilitiesV2(value , verifiedExecutableDig
713
957
  if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
714
958
 
715
959
  const advertisedOperations = stringArray(body.operations, "capabilities.operations", { minimum: REQUIRED_OPERATIONS.length, unique: true });
716
- const gates = enumArray(body.gates, REQUIRED_GATES, "capabilities.gates", { minimum: 5, maximum: 5, unique: true });
717
- const projections = enumArray(body.projections, REQUIRED_PROJECTIONS, "capabilities.projections", { minimum: 2, maximum: 2, unique: true });
718
- const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: REQUIRED_SCHEMAS.length, unique: true });
960
+ // Gates and projections are, like operations and schemas, a superset promise
961
+ // rather than an exact manifest: a compatible provider release may advertise
962
+ // an additional gate or projection name beyond the required floor. Decode as
963
+ // a plain string array (not `enumArray` against the known enum) so an
964
+ // unknown addition is not rejected before assertSupersetOf can even run.
965
+ const advertisedGates = stringArray(body.gates, "capabilities.gates", { minimum: REQUIRED_GATES.length, unique: true });
966
+ const advertisedProjections = stringArray(body.projections, "capabilities.projections", { minimum: REQUIRED_PROJECTIONS.length, unique: true });
967
+ const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: identity.requiredSchemas.length, unique: true });
719
968
  assertSupersetOf(advertisedOperations, REQUIRED_OPERATIONS, "capabilities operations");
720
- assertExactSet(gates, REQUIRED_GATES, "capabilities gates");
721
- assertExactSet(projections, REQUIRED_PROJECTIONS, "capabilities projections");
722
- assertSupersetOf(advertisedSchemas, REQUIRED_SCHEMAS, "capabilities schemas");
969
+ assertSupersetOf(advertisedGates, REQUIRED_GATES, "capabilities gates");
970
+ assertSupersetOf(advertisedProjections, REQUIRED_PROJECTIONS, "capabilities projections");
971
+ assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
723
972
 
724
973
  const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
725
- const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
726
- 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 });
727
977
  const mandatoryNames = mandatory.map((feature) => feature.name);
728
978
  const optionalNames = optional.map((feature) => feature.name);
729
- assertExactSet(mandatoryNames, REQUIRED_MANDATORY_FEATURES, "mandatory capabilities");
979
+ assertExactSet(mandatoryNames, requiredMandatoryFeatures, "mandatory capabilities");
730
980
  if (new Set(optionalNames).size !== optionalNames.length) throw new TypeError("optional capabilities contain duplicate names");
731
981
  if (optionalNames.some((name) => mandatoryNames.includes(name ))) throw new TypeError("mandatory and optional capabilities overlap");
732
982
  if (mandatory.some((feature) => !feature.supported)) throw new TypeError("mandatory capability is unsupported");
@@ -765,9 +1015,9 @@ export function decodeReviewCapabilitiesV2(value , verifiedExecutableDig
765
1015
  buildId,
766
1016
  executableDigest: selfReportedDigest,
767
1017
  operations: new Set(REQUIRED_OPERATIONS),
768
- gates: new Set(gates),
769
- projections: new Set(projections),
770
- schemas: new Set(REQUIRED_SCHEMAS),
1018
+ gates: new Set(REQUIRED_GATES),
1019
+ projections: new Set(REQUIRED_PROJECTIONS),
1020
+ schemas: new Set(identity.requiredSchemas),
771
1021
  mandatoryFeatures: new Set(mandatoryNames),
772
1022
  optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES ).includes(feature.name)).map((feature) => feature.name)),
773
1023
  raw: body,
@@ -818,12 +1068,12 @@ function decodeChangedPathEntry(value , label )
818
1068
  // start/v3
819
1069
  // ---------------------------------------------------------------------------
820
1070
 
821
- export function decodeReviewStartV3(value ) {
1071
+ export function decodeReviewStartV3(value , overlayIdentitySatisfiesRepositoryContext = false) {
822
1072
  const overlayFields = ["target_mode", "target_identity", "base_tree", "candidate_tree"] ;
823
1073
  const body = exactRecord(value, "start", [
824
1074
  "schema", "contract", "operation", "action", "lenses_required", "lineage_id", "state", "risk_level",
825
1075
  "selected_lenses", "projection", "changed_files", "changed_lines", "correction_budget", "risk_reasons", "artifact_subjects",
826
- ], [...overlayFields, "changed_path_manifest", "repository_context"]);
1076
+ ], [...overlayFields, "changed_path_manifest", "repository_context", "acknowledgement"]);
827
1077
  requireIdentity(body, "gentle-ai.review-integration.start/v3", REVIEW_INTEGRATION_OPERATION.START);
828
1078
 
829
1079
  // dependentRequired binds base_tree<->candidate_tree bidirectionally, and
@@ -852,19 +1102,24 @@ export function decodeReviewStartV3(value ) {
852
1102
  throw new TypeError("start with selected_lenses requires base_tree, candidate_tree, and changed_path_manifest");
853
1103
  }
854
1104
 
855
- 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);
856
1109
  if (requiresRepositoryContext && body.repository_context === undefined) throw new TypeError("start.repository_context is required when action is created/resumed and state is reviewing");
857
- 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");
858
1111
 
859
1112
  let repositoryContext ;
860
1113
  if (body.repository_context !== undefined) {
861
- const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
1114
+ const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
862
1115
  if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
863
1116
  repositoryContext = {
864
1117
  capability: "review.opaque_repository_context",
865
- 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}$/ }),
866
1119
  revision: sha256(source.revision, "start.repository_context.revision"),
867
1120
  targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
1121
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
1122
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "start.repository_context.outcome") }),
868
1123
  };
869
1124
  }
870
1125
 
@@ -905,6 +1160,78 @@ export function decodeReviewStartV3(value ) {
905
1160
  };
906
1161
  }
907
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
+
908
1235
  // ---------------------------------------------------------------------------
909
1236
  // projection/v1 — reused verbatim; the v2 capabilities schema still advertises
910
1237
  // gentle-ai.review-integration.projection/v1, so renaming this decoder would
@@ -1003,20 +1330,175 @@ export function decodeAuthorityRepairAssessmentV1(value )
1003
1330
  }
1004
1331
 
1005
1332
  // ---------------------------------------------------------------------------
1006
- // targeted-validation-request/v1reused for status.validation_request,
1007
- // operation.result.validation_request, and next_transition collect inputs.
1333
+ // next-transition/v3net-new; unlike v1's decodeNextTransition (void),
1334
+ // this decoder returns a typed value so callers can read the manifest-bound
1335
+ // collect inputs and the execute binding.
1008
1336
  // ---------------------------------------------------------------------------
1009
1337
 
1010
- function decodeTargetedValidationRequestV1(value , label ) {
1011
- 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"]);
1338
+ const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.status", "review.recover", "review.repair", "review.acknowledge-approved"] ;
1339
+
1340
+ function decodeTransitionArguments(value , label ) {
1341
+ return array(value, label, (entry, entryLabel) => {
1342
+ const argument = exactRecord(entry, entryLabel, ["name", "value"], ["token"]);
1343
+ const name = text(argument.name, `${entryLabel}.name`, { minimum: 1, pattern: /^[a-z0-9_-]+$/ });
1344
+ const argumentValue = text(argument.value, `${entryLabel}.value`, { minimum: 1 });
1345
+ const token = argument.token === undefined ? undefined : text(argument.token, `${entryLabel}.token`, { minimum: 1 });
1346
+ return { name, value: argumentValue, ...(token === undefined ? {} : { token }) };
1347
+ });
1348
+ }
1349
+
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
+ }
1373
+ // status/v5: the live 2.4.0-main binary emits the materialize
1374
+ // capture-result submission as a SINGULAR `value` object carrying a
1375
+ // `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
1376
+ // emitter has been singular since gentle-ai f1a95179). The form is
1377
+ // closed to that exact captured shape and normalizes into the typed
1378
+ // one-entry values array the host relay already consumes. A payload
1379
+ // carrying both wire forms at once matches no captured shape and falls
1380
+ // through to the legacy decoder, which rejects the unknown `value` key.
1381
+ if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1382
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1383
+ const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] , `${label}.operation_token`);
1384
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
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"]);
1388
+ return {
1389
+ operationToken,
1390
+ argumentTokens,
1391
+ values: [{
1392
+ slot: operationToken === "capture-result"
1393
+ ? enumeration(row.slot, ["reviewer_result"] , `${label}.value.slot`)
1394
+ : enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`),
1395
+ domain: nonempty(row.domain, `${label}.value.domain`),
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) }),
1399
+ substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1400
+ }],
1401
+ };
1402
+ }
1403
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1404
+ const operationToken = text(submission.operation_token, `${label}.operation_token`, { minimum: 1, pattern: /^[a-z0-9-]+$/ });
1405
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1406
+ const values = array(submission.values, `${label}.values`, (entry, entryLabel) => {
1407
+ const row = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"]);
1408
+ return {
1409
+ slot: nonempty(row.slot, `${entryLabel}.slot`),
1410
+ domain: nonempty(row.domain, `${entryLabel}.domain`),
1411
+ substitutionLocation: integer(row.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1412
+ };
1413
+ }, { minimum: 1 });
1414
+ return { operationToken, argumentTokens, values };
1415
+ }
1416
+
1417
+ // v5-only structural decoders, exact to the vendored status-v5.schema.json
1418
+ // and correction-plan-request.schema.json on gentle-ai main.
1419
+
1420
+ function decodeProviderTask(value , label ) {
1421
+ const task = exactRecord(value, label, ["agent", "role", "prompt"]);
1422
+ return {
1423
+ agent: enumeration(task.agent, ["review-refuter", "review-validator"] , `${label}.agent`),
1424
+ role: enumeration(task.role, ["refuter", "targeted-validator"] , `${label}.role`),
1425
+ prompt: nonempty(task.prompt, `${label}.prompt`),
1426
+ };
1427
+ }
1428
+
1429
+ function decodeCorrectionPlanRequestV1(value , label ) {
1430
+ const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
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`);
1432
+ return {
1433
+ schema: "gentle-ai.review-correction-plan-request/v1",
1434
+ requestHash: sha256(body.request_hash, `${label}.request_hash`),
1435
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1436
+ expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1437
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1438
+ correctionBudget: integer(body.correction_budget, `${label}.correction_budget`, 1, 200),
1439
+ fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1440
+ findings: array(body.findings, `${label}.findings`, (entry, entryLabel) => {
1441
+ const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence", "evidence_class", "causal_disposition"]);
1442
+ return {
1443
+ id: nonempty(finding.id, `${entryLabel}.id`),
1444
+ lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] , `${entryLabel}.lens`),
1445
+ location: nonempty(finding.location, `${entryLabel}.location`),
1446
+ severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] , `${entryLabel}.severity`),
1447
+ claim: nonempty(finding.claim, `${entryLabel}.claim`),
1448
+ proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
1449
+ evidence: nonempty(finding.evidence, `${entryLabel}.evidence`),
1450
+ evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] , `${entryLabel}.evidence_class`),
1451
+ causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] , `${entryLabel}.causal_disposition`),
1452
+ };
1453
+ }, { minimum: 1 }),
1454
+ };
1455
+ }
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
+ ]);
1012
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`);
1013
1492
  return {
1014
1493
  schema: "gentle-ai.review-targeted-validation-request/v1",
1015
1494
  requestHash: sha256(body.request_hash, `${label}.request_hash`),
1016
1495
  lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1017
1496
  expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1018
1497
  targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1019
- fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1498
+ fixFindingIds,
1499
+ policyContent: nonempty(body.policy_content, `${label}.policy_content`),
1500
+ fixFindings,
1501
+ fixClassifications,
1020
1502
  projection: enumeration(body.projection, REQUIRED_PROJECTIONS, `${label}.projection`),
1021
1503
  correctionCandidateTree: gitTree(body.correction_candidate_tree, `${label}.correction_candidate_tree`),
1022
1504
  correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
@@ -1025,36 +1507,121 @@ function decodeTargetedValidationRequestV1(value , label )
1025
1507
  };
1026
1508
  }
1027
1509
 
1028
- // ---------------------------------------------------------------------------
1029
- // next-transition/v3 net-new; unlike v1's decodeNextTransition (void),
1030
- // this decoder returns a typed value so callers can read the manifest-bound
1031
- // collect inputs and the execute binding.
1032
- // ---------------------------------------------------------------------------
1033
-
1034
- const NEXT_TRANSITION_OPERATIONS = ["review.start", "review.finalize", "review.recover", "review.repair", "review.validate"] ;
1035
-
1036
- function decodeTransitionArguments(value , label ) {
1037
- return array(value, label, (entry, entryLabel) => {
1038
- const argument = exactRecord(entry, entryLabel, ["name", "value"], ["token"]);
1039
- const name = text(argument.name, `${entryLabel}.name`, { minimum: 1, pattern: /^[a-z0-9_-]+$/ });
1040
- const argumentValue = text(argument.value, `${entryLabel}.value`, { minimum: 1 });
1041
- const token = argument.token === undefined ? undefined : text(argument.token, `${entryLabel}.token`, { minimum: 1 });
1042
- return { name, value: argumentValue, ...(token === undefined ? {} : { token }) };
1043
- });
1510
+ export function decodeReviewForecastV1(value , label = "status.forecast") {
1511
+ const body = exactRecord(value, label, ["horizon", "steps"]);
1512
+ const horizon = enumeration(body.horizon, ["partial", "terminal"] , `${label}.horizon`);
1513
+ const steps = array(body.steps, `${label}.steps`, (entry, entryLabel) => {
1514
+ const step = exactRecord(entry, entryLabel, ["step", "kind", "reason_code", "description"]);
1515
+ if (step.step !== 1) throw new TypeError(`${entryLabel}.step must be 1`);
1516
+ return {
1517
+ step: 1 ,
1518
+ kind: enumeration(step.kind, ["execute", "collect", "stop"] , `${entryLabel}.kind`),
1519
+ reasonCode: text(step.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ }),
1520
+ description: nonempty(step.description, `${entryLabel}.description`),
1521
+ };
1522
+ }, { minimum: 1, maximum: 1 });
1523
+ // The published horizon-to-step invariant: a stop head is terminal, any
1524
+ // other head is partial.
1525
+ if ((horizon === "terminal") !== (steps[0] .kind === "stop")) throw new TypeError(`${label}.horizon does not match its step kind`);
1526
+ return { horizon, steps };
1044
1527
  }
1045
1528
 
1046
- function decodeCollectInput(value , label ) {
1047
- const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request"]);
1529
+ // The two v5 reason codes whose transitions must carry the correction plan
1530
+ // request; every other reason code must not.
1531
+ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] );
1532
+ // v5 capture operations that must carry a submission descriptor.
1533
+ const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] );
1534
+
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"] : [])]);
1048
1537
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1049
1538
  const schema = nonempty(input.schema, `${label}.schema`);
1050
1539
  const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
1051
1540
  const argumentsList = decodeTransitionArguments(input.arguments, `${label}.arguments`);
1052
1541
 
1053
- if (captureOperation === "external.run_targeted_validation") {
1054
- if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1055
- if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1056
- } else if (input.validation_request !== undefined) {
1057
- throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation`);
1542
+ // v5-only surfaces (vendored status-v5.schema.json): the provider role task
1543
+ // rides exactly the external.run_provider_role vector, and the finalize/
1544
+ // capture-evidence submission descriptor forms ride their own vectors.
1545
+ if (v5) {
1546
+ if (captureOperation === "external.run_provider_role") {
1547
+ if (input.provider_task === undefined) throw new TypeError(`${label}.provider_task is required for external.run_provider_role`);
1548
+ } else if (input.provider_task !== undefined) {
1549
+ throw new TypeError(`${label}.provider_task is only valid for external.run_provider_role`);
1550
+ }
1551
+ if ((V5_SUBMISSION_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1552
+ if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
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`);
1582
+ }
1583
+ sha256(argumentsList[5] .value, `${label}.arguments[5].value`);
1584
+ }
1585
+
1586
+ // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1587
+ // operations render SELF-CONTAINED authority-advancing vectors. Executing
1588
+ // the exact rendered tokens makes Go materialize the role prompt, run its
1589
+ // own locked-down pi subprocess, and admit the raw verdict — so a
1590
+ // submission descriptor (the host-mediated completing form) on one of
1591
+ // these inputs would hand the caller a way to author the verdict and is
1592
+ // rejected as a provider contract violation.
1593
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
1594
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
1595
+ }
1596
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
1597
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
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
+ }
1623
+ if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1624
+ throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1058
1625
  }
1059
1626
 
1060
1627
  if (captureOperation === "review.capture-result") {
@@ -1065,6 +1632,17 @@ function decodeCollectInput(value , label )
1065
1632
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
1066
1633
  throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
1067
1634
  }
1635
+ const submissionOperations = v5
1636
+ ? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
1637
+ : ["review.capture-result"];
1638
+ if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1639
+ throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1640
+ }
1641
+ if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
1642
+
1643
+ const submission = input.submission === undefined
1644
+ ? undefined
1645
+ : decodeCaptureSubmission(input.submission, `${label}.submission`, v5, v6, captureOperation);
1068
1646
 
1069
1647
  return {
1070
1648
  name,
@@ -1075,15 +1653,29 @@ function decodeCollectInput(value , label )
1075
1653
  ...(input.base_tree === undefined ? {} : { baseTree: gitTree(input.base_tree, `${label}.base_tree`) }),
1076
1654
  ...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
1077
1655
  ...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
1078
- ...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
1656
+ ...(submission === undefined ? {} : { submission }),
1657
+ ...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
1658
+ ...(validationRequest === undefined ? {} : { validationRequest }),
1079
1659
  };
1080
1660
  }
1081
1661
 
1082
- export function decodeReviewNextTransitionV3(value ) {
1083
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect"]);
1662
+ export function decodeReviewNextTransitionV3(value , options = {}) {
1663
+ const v6 = options.v6 === true;
1664
+ const v5 = options.v5 === true || v6;
1665
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1084
1666
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
1085
1667
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1086
1668
 
1669
+ // status/v5: the bounded correction plan request rides exactly its two
1670
+ // reason codes and never any other (vendored status-v5.schema.json).
1671
+ let correctionRequest ;
1672
+ if (v5) {
1673
+ const required = (CORRECTION_REQUEST_REASON_CODES ).includes(reasonCode);
1674
+ if (required && transition.correction_request === undefined) throw new TypeError(`next_transition.correction_request is required for ${reasonCode}`);
1675
+ if (!required && transition.correction_request !== undefined) throw new TypeError(`next_transition.correction_request is only valid for ${CORRECTION_REQUEST_REASON_CODES.join(", ")}`);
1676
+ if (transition.correction_request !== undefined) correctionRequest = decodeCorrectionPlanRequestV1(transition.correction_request, "next_transition.correction_request");
1677
+ }
1678
+
1087
1679
  if (kind === "execute") {
1088
1680
  // `command` is an optional, ready-to-paste rendering of `arguments` (the
1089
1681
  // exact same binding as a single shell-ready string) — observed live
@@ -1099,6 +1691,7 @@ export function decodeReviewNextTransitionV3(value )
1099
1691
  const execute = exactRecord(transition.execute, "next_transition.execute", ["operation", "arguments", "preconditions", "binding"], ["command", "selector_arguments", "artifacts"]);
1100
1692
  const operation = enumeration(execute.operation, NEXT_TRANSITION_OPERATIONS, "next_transition.execute.operation");
1101
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");
1102
1695
  const preconditions = decodeTransitionArguments(execute.preconditions, "next_transition.execute.preconditions");
1103
1696
  // The schema gives `binding` no declared properties and no required list:
1104
1697
  // it is an OPEN object. Closing it here made Pi stricter than the
@@ -1109,17 +1702,19 @@ export function decodeReviewNextTransitionV3(value )
1109
1702
  const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, "next_transition.execute.binding.lineage_id");
1110
1703
  const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
1111
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);
1112
1707
  if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
1113
- return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) } };
1708
+ return { kind, reasonCode, execute: decodedExecute, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1114
1709
  }
1115
1710
  if (kind === "collect") {
1116
1711
  const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1117
- const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label), { minimum: 1 });
1712
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
1118
1713
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1119
- return { kind, reasonCode, collect: { inputs } };
1714
+ return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1120
1715
  }
1121
1716
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1122
- return { kind, reasonCode };
1717
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1123
1718
  }
1124
1719
 
1125
1720
  // ---------------------------------------------------------------------------
@@ -1130,8 +1725,8 @@ export function decodeReviewNextTransitionV3(value )
1130
1725
  // status-v2.schema.json action_eligibility definition v2 also $refs.
1131
1726
  // ---------------------------------------------------------------------------
1132
1727
 
1133
- const ELIGIBLE_ACTIONS = ["stop", "review.start", "review.finalize", "review.validate", "review.recover", "review.repair", "review.retry_final_verification"] ;
1134
- 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"] ;
1135
1730
 
1136
1731
  function decodeEligibility(value , label ) {
1137
1732
  const eligibility = exactRecord(value, label, ["allowed_actions", "forbidden_actions"]);
@@ -1140,14 +1735,14 @@ function decodeEligibility(value , label ) {
1140
1735
  const selected = enumeration(action.action, ELIGIBLE_ACTIONS, `${entryLabel}.action`);
1141
1736
  text(action.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1142
1737
  array(action.required_inputs, `${entryLabel}.required_inputs`, (input, inputLabel) => text(input, inputLabel, { minimum: 1, pattern: /^[a-z0-9_]+$/ }), { unique: true });
1143
- if (selected === "review.recover" || selected === "review.retry_final_verification") {
1738
+ if (selected === "review.recover") {
1144
1739
  const binding = exactRecord(action.binding, `${entryLabel}.binding`, ["lineage_id", "revision", "target_identity"]);
1145
1740
  enumeration(action.disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), `${entryLabel}.disposition`);
1146
1741
  lineage(binding.lineage_id, `${entryLabel}.binding.lineage_id`);
1147
1742
  sha256(binding.revision, `${entryLabel}.binding.revision`);
1148
1743
  sha256(binding.target_identity, `${entryLabel}.binding.target_identity`);
1149
1744
  } else if (action.disposition !== undefined || action.binding !== undefined) {
1150
- throw new TypeError(`${entryLabel} recovery fields require review.recover or review.retry_final_verification`);
1745
+ throw new TypeError(`${entryLabel} recovery fields require review.recover`);
1151
1746
  }
1152
1747
  return action;
1153
1748
  }, { minimum: 1, maximum: 1 });
@@ -1159,36 +1754,33 @@ function decodeEligibility(value , label ) {
1159
1754
  });
1160
1755
  }
1161
1756
 
1162
- function decodeFinalVerificationRetry(value , label ) {
1163
- const body = exactRecord(value, label, ["incident_schema", "incident_class", "validating_revision", "target_identity", "failed_evidence_hash", "finalize_request_digest"], ["failed_evidence_record_digest"]);
1164
- if (body.incident_schema !== "gentle-ai.review-final-verification-incident/v1") throw new TypeError(`${label}.incident_schema is unsupported`);
1165
- if (body.incident_class !== "procedural_tooling_failure") throw new TypeError(`${label}.incident_class is unsupported`);
1166
- return {
1167
- incidentSchema: "gentle-ai.review-final-verification-incident/v1",
1168
- incidentClass: "procedural_tooling_failure",
1169
- validatingRevision: sha256(body.validating_revision, `${label}.validating_revision`),
1170
- targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1171
- failedEvidenceHash: sha256(body.failed_evidence_hash, `${label}.failed_evidence_hash`),
1172
- ...(body.failed_evidence_record_digest === undefined ? {} : { failedEvidenceRecordDigest: sha256(body.failed_evidence_record_digest, `${label}.failed_evidence_record_digest`) }),
1173
- finalizeRequestDigest: sha256(body.finalize_request_digest, `${label}.finalize_request_digest`),
1174
- };
1175
- }
1176
-
1177
1757
  // ---------------------------------------------------------------------------
1178
1758
  // status/v3
1179
1759
  // ---------------------------------------------------------------------------
1180
1760
 
1181
1761
  export function decodeReviewStatusV3(value ) {
1762
+ // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1763
+ // against a live capture and the vendored status-v5.schema.json) is the v3
1764
+ // key set plus the optional forecast and the v5-only next_transition
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";
1182
1770
  const body = exactRecord(value, "status", [
1183
- "schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1184
- ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry"]);
1185
- requireIdentity(body, "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);
1186
1774
 
1187
1775
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1188
- const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
1189
- const receiptStatus = enumeration(receiptBody.status, RECEIPT_STATUSES, "status.receipt.status");
1190
- const receipt = { status: receiptStatus, ...(receiptBody.identity === undefined ? {} : { identity: sha256(receiptBody.identity, "status.receipt.identity") }) };
1191
-
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
+ }
1192
1784
  let authority ;
1193
1785
  if (body.authority !== undefined) {
1194
1786
  const source = exactRecord(body.authority, "status.authority", ["version", "lineage_id", "state", "generation", "revision"]);
@@ -1216,66 +1808,68 @@ export function decodeReviewStatusV3(value ) {
1216
1808
  }
1217
1809
  if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
1218
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");
1219
- 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
+ }
1220
1814
 
1221
1815
  const action = enumeration(body.action, STATUS_ACTIONS, "status.action");
1222
1816
  const actionDisposition = body.action_disposition === undefined ? undefined : enumeration(body.action_disposition, Object.values(REVIEW_STATUS_ACTION_DISPOSITION), "status.action_disposition");
1223
- if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
1224
- 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");
1225
1819
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1226
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
1227
- const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1228
-
1229
- let reconciliation ;
1230
- if (action === "reconcile_finalize") {
1231
- if (body.reconciliation === undefined) throw new TypeError("reconcile_finalize status requires reconciliation");
1232
- const source = exactRecord(body.reconciliation, "status.reconciliation", ["required"]);
1233
- if (source.required !== true) throw new TypeError("status.reconciliation.required must be true");
1234
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET) throw new TypeError("reconcile_finalize status requires current_target applicability");
1235
- if (replayability !== REVIEW_REPLAYABILITY.STATUS_REQUIRED) throw new TypeError("reconcile_finalize status requires status_required replayability");
1236
- reconciliation = { required: true };
1237
- } else if (body.reconciliation !== undefined) {
1238
- throw new TypeError("status.reconciliation is only valid for the reconcile_finalize action");
1239
- }
1240
-
1241
- let validationRequest ;
1242
- if (body.validation_request !== undefined) {
1243
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.state !== "correction_required") {
1244
- throw new TypeError("status.validation_request requires current_target applicability and correction_required authority state");
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");
1245
1829
  }
1246
- validationRequest = decodeTargetedValidationRequestV1(body.validation_request, "status.validation_request");
1247
1830
  }
1831
+ // status/v5 dependentRequired: a forecast previews the transition head, so
1832
+ // it can only accompany an actual next_transition.
1833
+ const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
1834
+ if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
1835
+ const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1248
1836
 
1249
- let finalVerificationRetry ;
1250
- if (action === "retry_final_verification") {
1251
- if (body.final_verification_retry === undefined) throw new TypeError("retry_final_verification status requires final_verification_retry");
1252
- if (applicability !== REVIEW_AUTHORITY_APPLICABILITY.CURRENT_TARGET || authority?.version !== REVIEW_AUTHORITY_VERSION.COMPACT_V2 || authority?.state !== "escalated") {
1253
- throw new TypeError("retry_final_verification status requires current_target compact-v2 escalated authority");
1254
- }
1255
- if (actionDisposition !== REVIEW_STATUS_ACTION_DISPOSITION.FINAL_VERIFICATION_RETRY) throw new TypeError("retry_final_verification status requires final_verification_retry disposition");
1256
- finalVerificationRetry = decodeFinalVerificationRetry(body.final_verification_retry, "status.final_verification_retry");
1257
- } else if (body.final_verification_retry !== undefined) {
1258
- throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
1837
+ // status/v5 additive top-level repository context reference (captured
1838
+ // 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
1839
+ // status-v5.schema.json, so the live capture is authoritative). Same
1840
+ // reference shape as start/v3's repository_context.
1841
+ let repositoryContext ;
1842
+ if (body.repository_context !== undefined) {
1843
+ const source = exactRecord(body.repository_context, "status.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
1844
+ if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
1845
+ repositoryContext = {
1846
+ capability: "review.opaque_repository_context",
1847
+ handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx[12]_[0-9a-f]{64}$/ }),
1848
+ revision: sha256(source.revision, "status.repository_context.revision"),
1849
+ targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
1850
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
1851
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "status.repository_context.outcome") }),
1852
+ };
1259
1853
  }
1260
1854
 
1261
1855
  return {
1262
1856
  contract: REVIEW_INTEGRATION_CONTRACT,
1263
1857
  applicability,
1264
1858
  ...(authority === undefined ? {} : { authority }),
1265
- receipt,
1859
+ ...(receipt === undefined ? {} : { receipt }),
1266
1860
  action,
1267
1861
  ...(actionDisposition === undefined ? {} : { actionDisposition }),
1268
1862
  replayability,
1269
1863
  ...(frozen === undefined ? {} : { frozen }),
1270
- ...(reconciliation === undefined ? {} : { reconciliation }),
1271
1864
  targetIdentity: sha256(body.target_identity, "status.target_identity"),
1272
1865
  ...(body.authority_target_identity === undefined ? {} : { authorityTargetIdentity: sha256(body.authority_target_identity, "status.authority_target_identity") }),
1273
1866
  projection: decodeReviewProjectionV1(body.projection),
1274
1867
  repair: decodeAuthorityRepairAssessmentV1(body.repair),
1275
1868
  candidates: stringArray(body.candidates, "status.candidates", { unique: true, pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ }),
1276
1869
  ...(nextTransition === undefined ? {} : { nextTransition }),
1870
+ ...(forecast === undefined ? {} : { forecast }),
1871
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
1277
1872
  ...(validationRequest === undefined ? {} : { validationRequest }),
1278
- ...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
1279
1873
  raw: body,
1280
1874
  };
1281
1875
  }
@@ -1296,11 +1890,10 @@ function decodeConsentChoice(value , label , answer
1296
1890
  };
1297
1891
  }
1298
1892
 
1299
- export function decodeReviewConsentV2(value ) {
1300
- const body = exactRecord(value, "consent", [
1301
- "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1302
- ]);
1303
- requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1893
+ // The semantic surface shared verbatim by both accepted consent identities.
1894
+ // Every label and guard predates consent/v3, so the v2 decode stays
1895
+ // byte-identical (test-locked) while v3 adds only its own identity gate.
1896
+ function decodeConsentSemantics(body ) {
1304
1897
  if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
1305
1898
  if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
1306
1899
 
@@ -1317,7 +1910,6 @@ export function decodeReviewConsentV2(value ) {
1317
1910
  if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
1318
1911
 
1319
1912
  return {
1320
- schema: "gentle-ai.review-integration.consent/v2",
1321
1913
  contract: REVIEW_INTEGRATION_CONTRACT,
1322
1914
  operation: "review.start",
1323
1915
  action: "consent_required",
@@ -1333,6 +1925,40 @@ export function decodeReviewConsentV2(value ) {
1333
1925
  riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
1334
1926
  choices: [granted, declined],
1335
1927
  offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
1928
+ };
1929
+ }
1930
+
1931
+ const CONSENT_KEYS_V2 = Object.freeze([
1932
+ "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1933
+ ] );
1934
+
1935
+ export function decodeReviewConsentV2(value ) {
1936
+ const body = exactRecord(value, "consent", CONSENT_KEYS_V2);
1937
+ requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1938
+ return {
1939
+ schema: "gentle-ai.review-integration.consent/v2",
1940
+ ...decodeConsentSemantics(body),
1941
+ raw: body,
1942
+ };
1943
+ }
1944
+
1945
+ // consent/v3 (gentle-ai >= 2.3.0): the v2 surface plus the required, fixed
1946
+ // `agent` runtime binding. Ground truth is the captured envelope from a
1947
+ // 2.4.0-main binary (tests/fixtures/devbinary/consent-v3.captured.json) plus
1948
+ // gentle-ai main contracts/review-integration/v2/schemas/consent-v3.schema.json.
1949
+ // The choice-invocation shape deliberately stays the shared v2 pattern: the
1950
+ // published v3 schema demands an `--agent claude-code` token that the live
1951
+ // emitter omits when the caller declared no --agent, so the capture is
1952
+ // authoritative and Pi replays whichever provider-owned invocation arrived.
1953
+ export function decodeReviewConsentV3(value , expectedAgent ) {
1954
+ const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
1955
+ requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
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");
1958
+ return {
1959
+ schema: "gentle-ai.review-integration.consent/v3",
1960
+ agent,
1961
+ ...decodeConsentSemantics(body),
1336
1962
  raw: body,
1337
1963
  };
1338
1964
  }
@@ -1396,7 +2022,7 @@ export function decodeReviewFailureV2(value ) {
1396
2022
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
1397
2023
  ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
1398
2024
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
1399
- const operation = enumeration(body.operation, REQUIRED_OPERATIONS, "failure.operation");
2025
+ const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
1400
2026
 
1401
2027
  if (body.progress_identity !== undefined) {
1402
2028
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -1433,56 +2059,21 @@ export function decodeReviewFailureV2(value ) {
1433
2059
  // operation/v2
1434
2060
  // ---------------------------------------------------------------------------
1435
2061
 
1436
- export function decodeReviewOperationV2(value ) {
1437
- const body = exactRecord(value, "operation", ["schema", "contract", "operation", "result"]);
1438
- requireIdentity(body, "gentle-ai.review-integration.operation/v2");
1439
- const operation = enumeration(body.operation, ["review.finalize", "review.validate", "review.bind_sdd", "review.retry_final_verification"] , "operation.operation");
1440
- let result ;
1441
- if (operation === REVIEW_INTEGRATION_OPERATION.FINALIZE) {
1442
- result = exactRecord(body.result, "operation.result", ["operation", "lineage_id", "state", "action", "store_revision"], ["eligibility", "next_transition", "validation_request", "escalation"]);
1443
- if (result.operation !== "review/finalize") throw new TypeError("operation.result does not match review.finalize");
1444
- nonempty(result.lineage_id, "operation.result.lineage_id");
1445
- nonempty(result.state, "operation.result.state");
1446
- nonempty(result.action, "operation.result.action");
1447
- sha256(result.store_revision, "operation.result.store_revision");
1448
- if (result.eligibility !== undefined) decodeEligibility(result.eligibility, "operation.result.eligibility");
1449
- if (result.next_transition !== undefined) decodeReviewNextTransitionV3(result.next_transition);
1450
- if (result.validation_request !== undefined) {
1451
- decodeTargetedValidationRequestV1(result.validation_request, "operation.result.validation_request");
1452
- if (result.state !== "correction_required") throw new TypeError("operation.result.validation_request requires state correction_required");
1453
- }
1454
- if (result.escalation !== undefined) nonempty(result.escalation, "operation.result.escalation");
1455
- } else if (operation === REVIEW_INTEGRATION_OPERATION.VALIDATE) {
1456
- result = exactRecord(body.result, "operation.result", ["schema", "result", "allowed", "action", "reason", "context"], ["delivery"]);
1457
- if (result.schema !== "gentle-ai.review-gate-result/v1") throw new TypeError("operation.result does not match review.validate");
1458
- enumeration(result.result, ["allow", "scope-changed", "invalidated", "escalated"] , "operation.result.result");
1459
- boolean(result.allowed, "operation.result.allowed");
1460
- nonempty(result.action, "operation.result.action");
1461
- nonempty(result.reason, "operation.result.reason");
1462
- record(result.context, "operation.result.context");
1463
- if (result.delivery !== undefined && result.delivery !== "disabled/unmanaged") throw new TypeError("operation.result.delivery is unsupported");
1464
- } else if (operation === REVIEW_INTEGRATION_OPERATION.BIND_SDD) {
1465
- result = exactRecord(body.result, "operation.result", ["schema", "revision", "change", "lineage", "authority_revision", "receipt_hash", "gate_context"]);
1466
- if (result.schema !== "gentle-ai.sdd-review-binding/v1") throw new TypeError("operation.result does not match review.bind_sdd");
1467
- sha256(result.revision, "operation.result.revision");
1468
- nonempty(result.change, "operation.result.change");
1469
- nonempty(result.lineage, "operation.result.lineage");
1470
- sha256(result.authority_revision, "operation.result.authority_revision");
1471
- sha256(result.receipt_hash, "operation.result.receipt_hash");
1472
- record(result.gate_context, "operation.result.gate_context");
1473
- } else {
1474
- result = exactRecord(body.result, "operation.result", ["operation", "predecessor_lineage_id", "predecessor_revision", "lineage_id", "state", "store_revision", "target_identity", "incident_digest", "recovery_disposition"]);
1475
- if (result.operation !== "review.retry_final_verification") throw new TypeError("operation.result does not match review.retry_final_verification");
1476
- lineage(result.predecessor_lineage_id, "operation.result.predecessor_lineage_id");
1477
- sha256(result.predecessor_revision, "operation.result.predecessor_revision");
1478
- lineage(result.lineage_id, "operation.result.lineage_id");
1479
- if (result.state !== "validating") throw new TypeError("operation.result.state must be validating");
1480
- sha256(result.store_revision, "operation.result.store_revision");
1481
- sha256(result.target_identity, "operation.result.target_identity");
1482
- sha256(result.incident_digest, "operation.result.incident_digest");
1483
- if (result.recovery_disposition !== "final_verification_retry") throw new TypeError("operation.result.recovery_disposition must be final_verification_retry");
1484
- }
1485
- 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
+ };
1486
2077
  }
1487
2078
 
1488
2079
  // ---------------------------------------------------------------------------
@@ -1566,3 +2157,445 @@ export function decodeReviewRepairV2(value ) {
1566
2157
  raw: body,
1567
2158
  };
1568
2159
  }
2160
+
2161
+ // ---------------------------------------------------------------------------
2162
+ // result-artifact/v2 — the `review capture-result` admission answer
2163
+ // ---------------------------------------------------------------------------
2164
+
2165
+ // The provider's admitted-reviewer-result envelope, printed by `review
2166
+ // capture-result` when a reviewer result is admitted and re-discovered by
2167
+ // STATUS artifact discovery. Unlike the negotiated envelopes above it carries
2168
+ // no `contract`/`operation` identity pair — the schema constant plus the
2169
+ // capability constant are its complete identity (vendored ground truth:
2170
+ // contracts/review-integration/v1/schemas/result-artifact-v2.schema.json,
2171
+ // confirmed against a live 2.4.0-main capture; the v2.2.3 pinned emitter
2172
+ // shares the exact same struct). Exactly one locator is present: a
2173
+ // provider-owned store `path`, or the opaque `rart1_` `reference` minted for
2174
+ // repository-context captures.
2175
+
2176
+
2177
+
2178
+
2179
+
2180
+
2181
+
2182
+
2183
+
2184
+
2185
+
2186
+
2187
+
2188
+
2189
+
2190
+ export function decodeReviewResultArtifactV2(value ) {
2191
+ const body = exactRecord(value, "result_artifact", ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"], ["path", "reference"]);
2192
+ if (body.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError("result_artifact.schema must be gentle-ai.review-result-artifact/v2");
2193
+ if (body.capability !== "review.native_result_artifact") throw new TypeError("result_artifact.capability must be review.native_result_artifact");
2194
+ if (body.admission_decision !== "completed") throw new TypeError("result_artifact.admission_decision must be completed");
2195
+ if ((body.path === undefined) === (body.reference === undefined)) throw new TypeError("result_artifact must carry exactly one of path or reference");
2196
+ return {
2197
+ schema: "gentle-ai.review-result-artifact/v2",
2198
+ capability: "review.native_result_artifact",
2199
+ sha256: sha256(body.sha256, "result_artifact.sha256"),
2200
+ lineageId: lineage(body.lineage_id, "result_artifact.lineage_id"),
2201
+ targetIdentity: sha256(body.target_identity, "result_artifact.target_identity"),
2202
+ lens: enumeration(body.lens, REVIEW_LENSES, "result_artifact.lens"),
2203
+ selectedOrder: integer(body.selected_order, "result_artifact.selected_order", 0, 3),
2204
+ subjectHash: sha256(body.subject_hash, "result_artifact.subject_hash"),
2205
+ admissionDecision: "completed",
2206
+ ...(body.path === undefined ? {} : { path: nonempty(body.path, "result_artifact.path") }),
2207
+ ...(body.reference === undefined ? {} : { reference: text(body.reference, "result_artifact.reference", { pattern: /^rart1_[0-9a-f]{64}$/ }) }),
2208
+ raw: body,
2209
+ };
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
+ }