gentle-pi 2.1.2 → 2.2.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 (171) hide show
  1. package/README.md +70 -12
  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 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  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 +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -94,23 +94,38 @@ export const REVIEW_STATUS_ACTION_DISPOSITION = {
94
94
 
95
95
  const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] ;
96
96
  const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
97
+ // failure/v2 operations: the capability floor plus the four collect-capture
98
+ // verbs that emit typed refusals on the gentle-ai main line (commit a2d57117,
99
+ // fix/capture-evidence-typed-refusal; exact strings from that branch's
100
+ // published failure.schema.json 12-value operation enum). Additive forward
101
+ // surface: every operation outside the published enum still rejects.
102
+ const FAILURE_OPERATIONS = Object.freeze([
103
+ ...REQUIRED_OPERATIONS,
104
+ "review.capture-result",
105
+ "review.capture-evidence",
106
+ "review.capture-refuter",
107
+ "review.capture-validation",
108
+ ] );
97
109
  const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] );
98
110
  const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
99
- const REQUIRED_SCHEMAS = Object.freeze([
111
+ // The schema floor shared by every accepted capabilities identity. Each minor
112
+ // then adds its own capabilities/consent/status identities below: the provider
113
+ // swaps those three advertisements as the protocol minor advances (v2.1 moved
114
+ // consent to v3; v2.2 moved status to v5), ground-truthed against the vendored
115
+ // capabilities[-v2.1|-v2.2].schema.json contracts on gentle-ai main and a live
116
+ // v2.2 capture from a main-line dev build.
117
+ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
100
118
  "gentle-ai.review-admitted-result/v2",
101
119
  "gentle-ai.review-artifact-subject/v2",
102
120
  "gentle-ai.review-authority-repair-assessment/v1",
103
121
  "gentle-ai.review-authority-status/v1",
104
122
  "gentle-ai.review-gate-request/v1",
105
- "gentle-ai.review-integration.capabilities/v2",
106
- "gentle-ai.review-integration.consent/v2",
107
123
  "gentle-ai.review-integration.failure/v2",
108
124
  "gentle-ai.review-final-verification-incident/v1",
109
125
  "gentle-ai.review-integration.operation/v2",
110
126
  "gentle-ai.review-integration.projection/v1",
111
127
  "gentle-ai.review-integration.repair/v2",
112
128
  "gentle-ai.review-integration.start/v3",
113
- "gentle-ai.review-integration.status/v3",
114
129
  "gentle-ai.review-receipt/v1",
115
130
  "gentle-ai.review-receipt/v2",
116
131
  "gentle-ai.review-result-artifact/v2",
@@ -120,6 +135,20 @@ const REQUIRED_SCHEMAS = Object.freeze([
120
135
  "https://gentle-ai.dev/schema/review/reviewer/v1",
121
136
  "https://gentle-ai.dev/schema/review/validator/v1",
122
137
  ] );
138
+ const CAPABILITIES_SCHEMA_IDENTITIES = Object.freeze({
139
+ "gentle-ai.review-integration.capabilities/v2": Object.freeze({
140
+ protocolMinor: 0,
141
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.status/v3"]),
142
+ }),
143
+ "gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
144
+ protocolMinor: 1,
145
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v3"]),
146
+ }),
147
+ "gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
148
+ protocolMinor: 2,
149
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v5"]),
150
+ }),
151
+ });
123
152
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
124
153
  "base_ref_workspace_overlay",
125
154
  "bounded_process_waits",
@@ -246,6 +275,131 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
246
275
 
247
276
 
248
277
 
278
+ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] ;
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+
314
+
315
+
316
+
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+ // Provider-owned completing form for a host-mediated capture slot
357
+ // (gentle-pi#311 P4). The provider issues the exact operation and argument
358
+ // tokens that submit the captured bytes; the host substitutes only the
359
+ // artifact location into the declared {{value}} slot and never synthesizes
360
+ // or filters the form itself.
361
+
362
+
363
+
364
+
365
+
366
+
367
+
368
+
369
+
370
+
371
+
372
+
373
+
374
+
375
+
376
+
377
+
378
+
379
+
380
+
381
+ // The two Go-owned non-lens provider role capture operations (gentle-pi#311
382
+ // P4-roles; provider side gentle-ai#3264). Their collect inputs are
383
+ // self-contained authority-advancing vectors: binding tokens plus
384
+ // `--agent=pi --execute=true`, with NO submission descriptor. The known set
385
+ // is closed — an unknown role capture operation is never executed.
386
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
387
+ CAPTURE_REFUTER: "review.capture-refuter",
388
+ CAPTURE_VALIDATION: "review.capture-validation",
389
+ } ;
390
+
391
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.values(REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION));
392
+
393
+ // status/v5 structural surfaces. Pi has no consumer for these yet; they are
394
+ // decoded strictly (exact key sets per the vendored status-v5.schema.json and
395
+ // correction-plan-request.schema.json on gentle-ai main) and carried through
396
+ // so a v5 provider payload is never rejected for being newer than v3.
397
+
398
+
399
+
400
+
401
+
402
+
249
403
 
250
404
 
251
405
 
@@ -268,6 +422,10 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
268
422
 
269
423
 
270
424
 
425
+ // The two v5 Go-owned submission descriptor forms (finalize carries one
426
+ // singular `value`; capture-evidence carries an outcome/input `values` pair).
427
+ // Distinct from the host-relay ReviewCaptureSubmissionV1 form, which stays
428
+ // exactly as it is.
271
429
 
272
430
 
273
431
 
@@ -371,6 +529,7 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
371
529
 
372
530
 
373
531
 
532
+
374
533
 
375
534
 
376
535
 
@@ -402,6 +561,19 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
402
561
 
403
562
 
404
563
 
564
+
565
+
566
+ // consent/v3 (gentle-ai >= 2.3.0, capabilities/v2.1+): the same per-candidate
567
+ // blocking question with one net-new required member, the provider-fixed
568
+ // `agent` runtime binding. Everything shared with v2 keeps its exact shape so
569
+ // consumers of either identity read one structural surface.
570
+
571
+
572
+
573
+
574
+
575
+ // Either accepted consent identity. Consumers that relay the envelope (rather
576
+ // than decode it) accept both; each decoder still admits exactly one schema.
405
577
 
406
578
 
407
579
  const FAILURE_REQUIRED_INPUTS = [
@@ -548,6 +720,8 @@ const FAILURE_CAUSE_CATEGORIES = ["inventory_io_or_layout", "lock_ambiguous", "r
548
720
 
549
721
 
550
722
 
723
+
724
+
551
725
 
552
726
 
553
727
 
@@ -690,10 +864,15 @@ function decodeOptionalFeature(value , label )
690
864
  export function decodeReviewCapabilitiesV2(value , verifiedExecutableDigest ) {
691
865
  const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] ;
692
866
  const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
693
- requireIdentity(body, "gentle-ai.review-integration.capabilities/v2");
867
+ // Additive minor acceptance: each accepted schema identity keeps its own
868
+ // exact protocol minor and required-schema floor; v2 stays exactly as it
869
+ // was, and an unknown identity is rejected before anything else.
870
+ const identity = CAPABILITIES_SCHEMA_IDENTITIES[typeof body.schema === "string" ? body.schema : ""];
871
+ if (identity === undefined) throw new TypeError(`schema must be one of ${Object.keys(CAPABILITIES_SCHEMA_IDENTITIES).join(", ")}`);
872
+ requireIdentity(body, body.schema );
694
873
 
695
874
  const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
696
- if (protocol.major !== 2 || protocol.minor !== 0) throw new TypeError("incompatible review integration protocol");
875
+ if (protocol.major !== 2 || protocol.minor !== identity.protocolMinor) throw new TypeError("incompatible review integration protocol");
697
876
 
698
877
  const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
699
878
  if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
@@ -713,13 +892,18 @@ export function decodeReviewCapabilitiesV2(value , verifiedExecutableDig
713
892
  if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
714
893
 
715
894
  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 });
895
+ // Gates and projections are, like operations and schemas, a superset promise
896
+ // rather than an exact manifest: a compatible provider release may advertise
897
+ // an additional gate or projection name beyond the required floor. Decode as
898
+ // a plain string array (not `enumArray` against the known enum) so an
899
+ // unknown addition is not rejected before assertSupersetOf can even run.
900
+ const advertisedGates = stringArray(body.gates, "capabilities.gates", { minimum: REQUIRED_GATES.length, unique: true });
901
+ const advertisedProjections = stringArray(body.projections, "capabilities.projections", { minimum: REQUIRED_PROJECTIONS.length, unique: true });
902
+ const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: identity.requiredSchemas.length, unique: true });
719
903
  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");
904
+ assertSupersetOf(advertisedGates, REQUIRED_GATES, "capabilities gates");
905
+ assertSupersetOf(advertisedProjections, REQUIRED_PROJECTIONS, "capabilities projections");
906
+ assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
723
907
 
724
908
  const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
725
909
  const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
@@ -765,9 +949,9 @@ export function decodeReviewCapabilitiesV2(value , verifiedExecutableDig
765
949
  buildId,
766
950
  executableDigest: selfReportedDigest,
767
951
  operations: new Set(REQUIRED_OPERATIONS),
768
- gates: new Set(gates),
769
- projections: new Set(projections),
770
- schemas: new Set(REQUIRED_SCHEMAS),
952
+ gates: new Set(REQUIRED_GATES),
953
+ projections: new Set(REQUIRED_PROJECTIONS),
954
+ schemas: new Set(identity.requiredSchemas),
771
955
  mandatoryFeatures: new Set(mandatoryNames),
772
956
  optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES ).includes(feature.name)).map((feature) => feature.name)),
773
957
  raw: body,
@@ -858,13 +1042,15 @@ export function decodeReviewStartV3(value ) {
858
1042
 
859
1043
  let repositoryContext ;
860
1044
  if (body.repository_context !== undefined) {
861
- const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
1045
+ const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
862
1046
  if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
863
1047
  repositoryContext = {
864
1048
  capability: "review.opaque_repository_context",
865
1049
  handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
866
1050
  revision: sha256(source.revision, "start.repository_context.revision"),
867
1051
  targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
1052
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
1053
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "start.repository_context.outcome") }),
868
1054
  };
869
1055
  }
870
1056
 
@@ -1043,8 +1229,146 @@ function decodeTransitionArguments(value , label )
1043
1229
  });
1044
1230
  }
1045
1231
 
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"]);
1232
+ function decodeCaptureSubmission(value , label , v5 ) {
1233
+ // status/v5: the live 2.4.0-main binary emits the materialize
1234
+ // capture-result submission as a SINGULAR `value` object carrying a
1235
+ // `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
1236
+ // emitter has been singular since gentle-ai f1a95179). The form is
1237
+ // closed to that exact captured shape and normalizes into the typed
1238
+ // one-entry values array the host relay already consumes. A payload
1239
+ // carrying both wire forms at once matches no captured shape and falls
1240
+ // through to the legacy decoder, which rejects the unknown `value` key.
1241
+ if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1242
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1243
+ if (submission.operation_token !== "capture-result") throw new TypeError(`${label}.operation_token must be capture-result for the singular value form`);
1244
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1245
+ const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
1246
+ return {
1247
+ operationToken: "capture-result",
1248
+ argumentTokens,
1249
+ values: [{
1250
+ slot: enumeration(row.slot, ["reviewer_result"] , `${label}.value.slot`),
1251
+ domain: nonempty(row.domain, `${label}.value.domain`),
1252
+ schema: nonempty(row.schema, `${label}.value.schema`),
1253
+ substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1254
+ }],
1255
+ };
1256
+ }
1257
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1258
+ const operationToken = text(submission.operation_token, `${label}.operation_token`, { minimum: 1, pattern: /^[a-z0-9-]+$/ });
1259
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1260
+ const values = array(submission.values, `${label}.values`, (entry, entryLabel) => {
1261
+ const row = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"]);
1262
+ return {
1263
+ slot: nonempty(row.slot, `${entryLabel}.slot`),
1264
+ domain: nonempty(row.domain, `${entryLabel}.domain`),
1265
+ substitutionLocation: integer(row.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1266
+ };
1267
+ }, { minimum: 1 });
1268
+ return { operationToken, argumentTokens, values };
1269
+ }
1270
+
1271
+ // v5-only structural decoders, exact to the vendored status-v5.schema.json
1272
+ // and correction-plan-request.schema.json on gentle-ai main.
1273
+
1274
+ function decodeProviderTask(value , label ) {
1275
+ const task = exactRecord(value, label, ["agent", "role", "prompt"]);
1276
+ return {
1277
+ agent: enumeration(task.agent, ["review-refuter", "review-validator"] , `${label}.agent`),
1278
+ role: enumeration(task.role, ["refuter", "targeted-validator"] , `${label}.role`),
1279
+ prompt: nonempty(task.prompt, `${label}.prompt`),
1280
+ };
1281
+ }
1282
+
1283
+ function decodeSubmissionDescriptorV5(value , label , operationToken ) {
1284
+ if (operationToken === "finalize") {
1285
+ const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1286
+ const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 7 });
1287
+ const source = exactRecord(descriptor.value, `${label}.value`, ["slot", "domain", "substitution_location"], ["schema", "minimum", "maximum"]);
1288
+ const slot = enumeration(source.slot, ["correction_lines", "validation"] , `${label}.value.slot`);
1289
+ return {
1290
+ operationToken,
1291
+ argumentTokens,
1292
+ value: {
1293
+ slot,
1294
+ domain: nonempty(source.domain, `${label}.value.domain`),
1295
+ substitutionLocation: integer(source.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1296
+ ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${label}.value.schema`) }),
1297
+ ...(source.minimum === undefined ? {} : { minimum: integer(source.minimum, `${label}.value.minimum`, 1, 200) }),
1298
+ ...(source.maximum === undefined ? {} : { maximum: integer(source.maximum, `${label}.value.maximum`, 1, 200) }),
1299
+ },
1300
+ };
1301
+ }
1302
+ const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1303
+ const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 6, maximum: 6 });
1304
+ const values = array(descriptor.values, `${label}.values`, (entry, entryLabel) => {
1305
+ const source = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"], ["schema", "allowed_values"]);
1306
+ return {
1307
+ slot: nonempty(source.slot, `${entryLabel}.slot`),
1308
+ domain: nonempty(source.domain, `${entryLabel}.domain`),
1309
+ substitutionLocation: integer(source.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1310
+ ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${entryLabel}.schema`) }),
1311
+ ...(source.allowed_values === undefined ? {} : { allowedValues: stringArray(source.allowed_values, `${entryLabel}.allowed_values`, { minimum: 1, unique: true }) }),
1312
+ };
1313
+ }, { minimum: 2, maximum: 2 });
1314
+ return { operationToken, argumentTokens, values };
1315
+ }
1316
+
1317
+ function decodeCorrectionPlanRequestV1(value , label ) {
1318
+ const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
1319
+ if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
1320
+ return {
1321
+ schema: "gentle-ai.review-correction-plan-request/v1",
1322
+ requestHash: sha256(body.request_hash, `${label}.request_hash`),
1323
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1324
+ expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1325
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1326
+ correctionBudget: integer(body.correction_budget, `${label}.correction_budget`, 1, 200),
1327
+ fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1328
+ findings: array(body.findings, `${label}.findings`, (entry, entryLabel) => {
1329
+ const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence", "evidence_class", "causal_disposition"]);
1330
+ return {
1331
+ id: nonempty(finding.id, `${entryLabel}.id`),
1332
+ lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] , `${entryLabel}.lens`),
1333
+ location: nonempty(finding.location, `${entryLabel}.location`),
1334
+ severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] , `${entryLabel}.severity`),
1335
+ claim: nonempty(finding.claim, `${entryLabel}.claim`),
1336
+ proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
1337
+ evidence: nonempty(finding.evidence, `${entryLabel}.evidence`),
1338
+ evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] , `${entryLabel}.evidence_class`),
1339
+ causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] , `${entryLabel}.causal_disposition`),
1340
+ };
1341
+ }, { minimum: 1 }),
1342
+ };
1343
+ }
1344
+
1345
+ export function decodeReviewForecastV1(value , label = "status.forecast") {
1346
+ const body = exactRecord(value, label, ["horizon", "steps"]);
1347
+ const horizon = enumeration(body.horizon, ["partial", "terminal"] , `${label}.horizon`);
1348
+ const steps = array(body.steps, `${label}.steps`, (entry, entryLabel) => {
1349
+ const step = exactRecord(entry, entryLabel, ["step", "kind", "reason_code", "description"]);
1350
+ if (step.step !== 1) throw new TypeError(`${entryLabel}.step must be 1`);
1351
+ return {
1352
+ step: 1 ,
1353
+ kind: enumeration(step.kind, ["execute", "collect", "stop"] , `${entryLabel}.kind`),
1354
+ reasonCode: text(step.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ }),
1355
+ description: nonempty(step.description, `${entryLabel}.description`),
1356
+ };
1357
+ }, { minimum: 1, maximum: 1 });
1358
+ // The published horizon-to-step invariant: a stop head is terminal, any
1359
+ // other head is partial.
1360
+ if ((horizon === "terminal") !== (steps[0] .kind === "stop")) throw new TypeError(`${label}.horizon does not match its step kind`);
1361
+ return { horizon, steps };
1362
+ }
1363
+
1364
+ // The two v5 reason codes whose transitions must carry the correction plan
1365
+ // request; every other reason code must not.
1366
+ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] );
1367
+ // v5 capture operations that must carry a submission descriptor.
1368
+ const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["external.plan_correction", "external.run_targeted_validation", "review.capture-evidence"] );
1369
+
1370
+ function decodeCollectInput(value , label , v5 ) {
1371
+ const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request", "submission", ...(v5 ? ["provider_task"] : [])]);
1048
1372
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1049
1373
  const schema = nonempty(input.schema, `${label}.schema`);
1050
1374
  const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
@@ -1053,8 +1377,43 @@ function decodeCollectInput(value , label )
1053
1377
  if (captureOperation === "external.run_targeted_validation") {
1054
1378
  if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1055
1379
  if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1380
+ } else if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION) {
1381
+ if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1056
1382
  } else if (input.validation_request !== undefined) {
1057
- throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation`);
1383
+ throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation or review.capture-validation`);
1384
+ }
1385
+
1386
+ // v5-only surfaces (vendored status-v5.schema.json): the provider role task
1387
+ // rides exactly the external.run_provider_role vector, and the finalize/
1388
+ // capture-evidence submission descriptor forms ride their own vectors.
1389
+ if (v5) {
1390
+ if (captureOperation === "external.run_provider_role") {
1391
+ if (input.provider_task === undefined) throw new TypeError(`${label}.provider_task is required for external.run_provider_role`);
1392
+ } else if (input.provider_task !== undefined) {
1393
+ throw new TypeError(`${label}.provider_task is only valid for external.run_provider_role`);
1394
+ }
1395
+ if ((V5_SUBMISSION_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1396
+ if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
1397
+ if (captureOperation === "external.plan_correction" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
1398
+ if (captureOperation === "review.capture-evidence" && schema !== "https://gentle-ai.dev/schema/review/verification-evidence/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/verification-evidence/v1`);
1399
+ }
1400
+ }
1401
+
1402
+ // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1403
+ // operations render SELF-CONTAINED authority-advancing vectors. Executing
1404
+ // the exact rendered tokens makes Go materialize the role prompt, run its
1405
+ // own locked-down pi subprocess, and admit the raw verdict — so a
1406
+ // submission descriptor (the host-mediated completing form) on one of
1407
+ // these inputs would hand the caller a way to author the verdict and is
1408
+ // rejected as a provider contract violation.
1409
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
1410
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
1411
+ }
1412
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
1413
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
1414
+ }
1415
+ if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1416
+ throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1058
1417
  }
1059
1418
 
1060
1419
  if (captureOperation === "review.capture-result") {
@@ -1065,6 +1424,23 @@ function decodeCollectInput(value , label )
1065
1424
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
1066
1425
  throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
1067
1426
  }
1427
+ const submissionOperations = v5 ? ["review.capture-result", ...V5_SUBMISSION_CAPTURE_OPERATIONS] : ["review.capture-result"];
1428
+ if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1429
+ throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1430
+ }
1431
+
1432
+ // The v5 descriptor forms are discriminated by their closed operation
1433
+ // tokens; every other submission stays the host-relay completing form.
1434
+ let submission ;
1435
+ let submissionDescriptor ;
1436
+ if (input.submission !== undefined) {
1437
+ const operationToken = (input.submission )?.operation_token;
1438
+ if (v5 && (operationToken === "finalize" || operationToken === "capture-evidence")) {
1439
+ submissionDescriptor = decodeSubmissionDescriptorV5(input.submission, `${label}.submission`, operationToken);
1440
+ } else {
1441
+ submission = decodeCaptureSubmission(input.submission, `${label}.submission`, v5);
1442
+ }
1443
+ }
1068
1444
 
1069
1445
  return {
1070
1446
  name,
@@ -1076,14 +1452,28 @@ function decodeCollectInput(value , label )
1076
1452
  ...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
1077
1453
  ...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
1078
1454
  ...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
1455
+ ...(submission === undefined ? {} : { submission }),
1456
+ ...(submissionDescriptor === undefined ? {} : { submissionDescriptor }),
1457
+ ...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
1079
1458
  };
1080
1459
  }
1081
1460
 
1082
- export function decodeReviewNextTransitionV3(value ) {
1083
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect"]);
1461
+ export function decodeReviewNextTransitionV3(value , options = {}) {
1462
+ const v5 = options.v5 === true;
1463
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1084
1464
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
1085
1465
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1086
1466
 
1467
+ // status/v5: the bounded correction plan request rides exactly its two
1468
+ // reason codes and never any other (vendored status-v5.schema.json).
1469
+ let correctionRequest ;
1470
+ if (v5) {
1471
+ const required = (CORRECTION_REQUEST_REASON_CODES ).includes(reasonCode);
1472
+ if (required && transition.correction_request === undefined) throw new TypeError(`next_transition.correction_request is required for ${reasonCode}`);
1473
+ if (!required && transition.correction_request !== undefined) throw new TypeError(`next_transition.correction_request is only valid for ${CORRECTION_REQUEST_REASON_CODES.join(", ")}`);
1474
+ if (transition.correction_request !== undefined) correctionRequest = decodeCorrectionPlanRequestV1(transition.correction_request, "next_transition.correction_request");
1475
+ }
1476
+
1087
1477
  if (kind === "execute") {
1088
1478
  // `command` is an optional, ready-to-paste rendering of `arguments` (the
1089
1479
  // exact same binding as a single shell-ready string) — observed live
@@ -1110,16 +1500,16 @@ export function decodeReviewNextTransitionV3(value )
1110
1500
  const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
1111
1501
  const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
1112
1502
  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 }) } };
1503
+ return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1114
1504
  }
1115
1505
  if (kind === "collect") {
1116
1506
  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 });
1507
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5), { minimum: 1 });
1118
1508
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1119
- return { kind, reasonCode, collect: { inputs } };
1509
+ return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1120
1510
  }
1121
1511
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1122
- return { kind, reasonCode };
1512
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1123
1513
  }
1124
1514
 
1125
1515
  // ---------------------------------------------------------------------------
@@ -1179,10 +1569,15 @@ function decodeFinalVerificationRetry(value , label )
1179
1569
  // ---------------------------------------------------------------------------
1180
1570
 
1181
1571
  export function decodeReviewStatusV3(value ) {
1572
+ // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1573
+ // against a live capture and the vendored status-v5.schema.json) is the v3
1574
+ // key set plus the optional forecast and the v5-only next_transition
1575
+ // surfaces. The v3 identity keeps rejecting every v5-only field.
1576
+ const v5 = typeof value === "object" && value !== null && (value ).schema === "gentle-ai.review-integration.status/v5";
1182
1577
  const body = exactRecord(value, "status", [
1183
1578
  "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);
1579
+ ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry", ...(v5 ? ["forecast", "repository_context"] : [])]);
1580
+ requireIdentity(body, v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1186
1581
 
1187
1582
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1188
1583
  const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
@@ -1223,7 +1618,11 @@ export function decodeReviewStatusV3(value ) {
1223
1618
  if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
1224
1619
  if (action !== "recover" && action !== "retry_final_verification" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover or retry_final_verification action");
1225
1620
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1226
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
1621
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5 });
1622
+ // status/v5 dependentRequired: a forecast previews the transition head, so
1623
+ // it can only accompany an actual next_transition.
1624
+ const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
1625
+ if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
1227
1626
  const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1228
1627
 
1229
1628
  let reconciliation ;
@@ -1258,6 +1657,24 @@ export function decodeReviewStatusV3(value ) {
1258
1657
  throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
1259
1658
  }
1260
1659
 
1660
+ // status/v5 additive top-level repository context reference (captured
1661
+ // 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
1662
+ // status-v5.schema.json, so the live capture is authoritative). Same
1663
+ // reference shape as start/v3's repository_context.
1664
+ let repositoryContext ;
1665
+ if (body.repository_context !== undefined) {
1666
+ const source = exactRecord(body.repository_context, "status.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
1667
+ if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
1668
+ repositoryContext = {
1669
+ capability: "review.opaque_repository_context",
1670
+ handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
1671
+ revision: sha256(source.revision, "status.repository_context.revision"),
1672
+ targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
1673
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
1674
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "status.repository_context.outcome") }),
1675
+ };
1676
+ }
1677
+
1261
1678
  return {
1262
1679
  contract: REVIEW_INTEGRATION_CONTRACT,
1263
1680
  applicability,
@@ -1276,6 +1693,8 @@ export function decodeReviewStatusV3(value ) {
1276
1693
  ...(nextTransition === undefined ? {} : { nextTransition }),
1277
1694
  ...(validationRequest === undefined ? {} : { validationRequest }),
1278
1695
  ...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
1696
+ ...(forecast === undefined ? {} : { forecast }),
1697
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
1279
1698
  raw: body,
1280
1699
  };
1281
1700
  }
@@ -1296,11 +1715,10 @@ function decodeConsentChoice(value , label , answer
1296
1715
  };
1297
1716
  }
1298
1717
 
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");
1718
+ // The semantic surface shared verbatim by both accepted consent identities.
1719
+ // Every label and guard predates consent/v3, so the v2 decode stays
1720
+ // byte-identical (test-locked) while v3 adds only its own identity gate.
1721
+ function decodeConsentSemantics(body ) {
1304
1722
  if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
1305
1723
  if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
1306
1724
 
@@ -1317,7 +1735,6 @@ export function decodeReviewConsentV2(value ) {
1317
1735
  if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
1318
1736
 
1319
1737
  return {
1320
- schema: "gentle-ai.review-integration.consent/v2",
1321
1738
  contract: REVIEW_INTEGRATION_CONTRACT,
1322
1739
  operation: "review.start",
1323
1740
  action: "consent_required",
@@ -1333,6 +1750,39 @@ export function decodeReviewConsentV2(value ) {
1333
1750
  riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
1334
1751
  choices: [granted, declined],
1335
1752
  offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
1753
+ };
1754
+ }
1755
+
1756
+ const CONSENT_KEYS_V2 = Object.freeze([
1757
+ "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1758
+ ] );
1759
+
1760
+ export function decodeReviewConsentV2(value ) {
1761
+ const body = exactRecord(value, "consent", CONSENT_KEYS_V2);
1762
+ requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1763
+ return {
1764
+ schema: "gentle-ai.review-integration.consent/v2",
1765
+ ...decodeConsentSemantics(body),
1766
+ raw: body,
1767
+ };
1768
+ }
1769
+
1770
+ // consent/v3 (gentle-ai >= 2.3.0): the v2 surface plus the required, fixed
1771
+ // `agent` runtime binding. Ground truth is the captured envelope from a
1772
+ // 2.4.0-main binary (tests/fixtures/devbinary/consent-v3.captured.json) plus
1773
+ // gentle-ai main contracts/review-integration/v2/schemas/consent-v3.schema.json.
1774
+ // The choice-invocation shape deliberately stays the shared v2 pattern: the
1775
+ // published v3 schema demands an `--agent claude-code` token that the live
1776
+ // emitter omits when the caller declared no --agent, so the capture is
1777
+ // authoritative and Pi replays whichever provider-owned invocation arrived.
1778
+ export function decodeReviewConsentV3(value ) {
1779
+ const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
1780
+ requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
1781
+ if (body.agent !== "claude-code") throw new TypeError("consent.agent must be claude-code");
1782
+ return {
1783
+ schema: "gentle-ai.review-integration.consent/v3",
1784
+ agent: "claude-code",
1785
+ ...decodeConsentSemantics(body),
1336
1786
  raw: body,
1337
1787
  };
1338
1788
  }
@@ -1396,7 +1846,7 @@ export function decodeReviewFailureV2(value ) {
1396
1846
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
1397
1847
  ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
1398
1848
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
1399
- const operation = enumeration(body.operation, REQUIRED_OPERATIONS, "failure.operation");
1849
+ const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
1400
1850
 
1401
1851
  if (body.progress_identity !== undefined) {
1402
1852
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -1566,3 +2016,54 @@ export function decodeReviewRepairV2(value ) {
1566
2016
  raw: body,
1567
2017
  };
1568
2018
  }
2019
+
2020
+ // ---------------------------------------------------------------------------
2021
+ // result-artifact/v2 — the `review capture-result` admission answer
2022
+ // ---------------------------------------------------------------------------
2023
+
2024
+ // The provider's admitted-reviewer-result envelope, printed by `review
2025
+ // capture-result` when a reviewer result is admitted and re-discovered by
2026
+ // STATUS artifact discovery. Unlike the negotiated envelopes above it carries
2027
+ // no `contract`/`operation` identity pair — the schema constant plus the
2028
+ // capability constant are its complete identity (vendored ground truth:
2029
+ // contracts/review-integration/v1/schemas/result-artifact-v2.schema.json,
2030
+ // confirmed against a live 2.4.0-main capture; the v2.2.3 pinned emitter
2031
+ // shares the exact same struct). Exactly one locator is present: a
2032
+ // provider-owned store `path`, or the opaque `rart1_` `reference` minted for
2033
+ // repository-context captures.
2034
+
2035
+
2036
+
2037
+
2038
+
2039
+
2040
+
2041
+
2042
+
2043
+
2044
+
2045
+
2046
+
2047
+
2048
+
2049
+ export function decodeReviewResultArtifactV2(value ) {
2050
+ const body = exactRecord(value, "result_artifact", ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"], ["path", "reference"]);
2051
+ if (body.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError("result_artifact.schema must be gentle-ai.review-result-artifact/v2");
2052
+ if (body.capability !== "review.native_result_artifact") throw new TypeError("result_artifact.capability must be review.native_result_artifact");
2053
+ if (body.admission_decision !== "completed") throw new TypeError("result_artifact.admission_decision must be completed");
2054
+ if ((body.path === undefined) === (body.reference === undefined)) throw new TypeError("result_artifact must carry exactly one of path or reference");
2055
+ return {
2056
+ schema: "gentle-ai.review-result-artifact/v2",
2057
+ capability: "review.native_result_artifact",
2058
+ sha256: sha256(body.sha256, "result_artifact.sha256"),
2059
+ lineageId: lineage(body.lineage_id, "result_artifact.lineage_id"),
2060
+ targetIdentity: sha256(body.target_identity, "result_artifact.target_identity"),
2061
+ lens: enumeration(body.lens, REVIEW_LENSES, "result_artifact.lens"),
2062
+ selectedOrder: integer(body.selected_order, "result_artifact.selected_order", 0, 3),
2063
+ subjectHash: sha256(body.subject_hash, "result_artifact.subject_hash"),
2064
+ admissionDecision: "completed",
2065
+ ...(body.path === undefined ? {} : { path: nonempty(body.path, "result_artifact.path") }),
2066
+ ...(body.reference === undefined ? {} : { reference: text(body.reference, "result_artifact.reference", { pattern: /^rart1_[0-9a-f]{64}$/ }) }),
2067
+ raw: body,
2068
+ };
2069
+ }