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
@@ -93,23 +93,38 @@ export const REVIEW_STATUS_ACTION_DISPOSITION = {
93
93
  export type ReviewStatusActionDisposition = (typeof REVIEW_STATUS_ACTION_DISPOSITION)[keyof typeof REVIEW_STATUS_ACTION_DISPOSITION];
94
94
  const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
95
95
  const REQUIRED_OPERATIONS = Object.freeze(Object.values(REVIEW_INTEGRATION_OPERATION));
96
+ // failure/v2 operations: the capability floor plus the four collect-capture
97
+ // verbs that emit typed refusals on the gentle-ai main line (commit a2d57117,
98
+ // fix/capture-evidence-typed-refusal; exact strings from that branch's
99
+ // published failure.schema.json 12-value operation enum). Additive forward
100
+ // surface: every operation outside the published enum still rejects.
101
+ const FAILURE_OPERATIONS = Object.freeze([
102
+ ...REQUIRED_OPERATIONS,
103
+ "review.capture-result",
104
+ "review.capture-evidence",
105
+ "review.capture-refuter",
106
+ "review.capture-validation",
107
+ ] as const);
96
108
  const REQUIRED_GATES = Object.freeze(["post-apply", "pre-commit", "pre-push", "pre-pr", "release"] as const);
97
109
  const REQUIRED_PROJECTIONS = Object.freeze(Object.values(REVIEW_PROJECTION));
98
- const REQUIRED_SCHEMAS = Object.freeze([
110
+ // The schema floor shared by every accepted capabilities identity. Each minor
111
+ // then adds its own capabilities/consent/status identities below: the provider
112
+ // swaps those three advertisements as the protocol minor advances (v2.1 moved
113
+ // consent to v3; v2.2 moved status to v5), ground-truthed against the vendored
114
+ // capabilities[-v2.1|-v2.2].schema.json contracts on gentle-ai main and a live
115
+ // v2.2 capture from a main-line dev build.
116
+ const REQUIRED_SCHEMAS_COMMON = Object.freeze([
99
117
  "gentle-ai.review-admitted-result/v2",
100
118
  "gentle-ai.review-artifact-subject/v2",
101
119
  "gentle-ai.review-authority-repair-assessment/v1",
102
120
  "gentle-ai.review-authority-status/v1",
103
121
  "gentle-ai.review-gate-request/v1",
104
- "gentle-ai.review-integration.capabilities/v2",
105
- "gentle-ai.review-integration.consent/v2",
106
122
  "gentle-ai.review-integration.failure/v2",
107
123
  "gentle-ai.review-final-verification-incident/v1",
108
124
  "gentle-ai.review-integration.operation/v2",
109
125
  "gentle-ai.review-integration.projection/v1",
110
126
  "gentle-ai.review-integration.repair/v2",
111
127
  "gentle-ai.review-integration.start/v3",
112
- "gentle-ai.review-integration.status/v3",
113
128
  "gentle-ai.review-receipt/v1",
114
129
  "gentle-ai.review-receipt/v2",
115
130
  "gentle-ai.review-result-artifact/v2",
@@ -119,6 +134,20 @@ const REQUIRED_SCHEMAS = Object.freeze([
119
134
  "https://gentle-ai.dev/schema/review/reviewer/v1",
120
135
  "https://gentle-ai.dev/schema/review/validator/v1",
121
136
  ] as const);
137
+ const CAPABILITIES_SCHEMA_IDENTITIES: Readonly<Record<string, { protocolMinor: number; requiredSchemas: readonly string[] }>> = Object.freeze({
138
+ "gentle-ai.review-integration.capabilities/v2": Object.freeze({
139
+ protocolMinor: 0,
140
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2", "gentle-ai.review-integration.consent/v2", "gentle-ai.review-integration.status/v3"]),
141
+ }),
142
+ "gentle-ai.review-integration.capabilities/v2.1": Object.freeze({
143
+ protocolMinor: 1,
144
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.1", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v3"]),
145
+ }),
146
+ "gentle-ai.review-integration.capabilities/v2.2": Object.freeze({
147
+ protocolMinor: 2,
148
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON, "gentle-ai.review-integration.capabilities/v2.2", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.status/v5"]),
149
+ }),
150
+ });
122
151
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
123
152
  "base_ref_workspace_overlay",
124
153
  "bounded_process_waits",
@@ -235,8 +264,19 @@ export interface ReviewRepositoryContextV2 {
235
264
  handle: string;
236
265
  revision: string;
237
266
  targetIdentity: string;
267
+ // Optional additive members on the same start/v3 identity (gentle-ai main,
268
+ // Go `ReviewRepositoryContextReference` with `omitempty`): the compact
269
+ // effects event this context applied and its recorded outcome. Ground
270
+ // truth is the captured granted start from a 2.4.0-main binary
271
+ // (tests/fixtures/devbinary/start-v3-consent-granted.captured.json); the
272
+ // published start.schema.json is narrower than the emitter here.
273
+ eventId?: string;
274
+ outcome?: ReviewRepositoryContextOutcomeV1;
238
275
  }
239
276
 
277
+ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] as const;
278
+ export type ReviewRepositoryContextOutcomeV1 = (typeof REPOSITORY_CONTEXT_OUTCOMES)[number];
279
+
240
280
  export interface ReviewStartV3 {
241
281
  contract: typeof REVIEW_INTEGRATION_CONTRACT;
242
282
  action: StartAction;
@@ -312,6 +352,109 @@ export interface ReviewNextTransitionExecuteV3 {
312
352
  command?: string;
313
353
  }
314
354
 
355
+ // Provider-owned completing form for a host-mediated capture slot
356
+ // (gentle-pi#311 P4). The provider issues the exact operation and argument
357
+ // tokens that submit the captured bytes; the host substitutes only the
358
+ // artifact location into the declared {{value}} slot and never synthesizes
359
+ // or filters the form itself.
360
+ export interface ReviewCaptureSubmissionValueV1 {
361
+ slot: string;
362
+ domain: string;
363
+ substitutionLocation: number;
364
+ /**
365
+ * status/v5 only: the artifact schema the substituted value must satisfy.
366
+ * NEW optional member carried by the singular wire `value` form the live
367
+ * 2.4.0-main binary emits for the materialize capture-result slot
368
+ * (captured 2026-08-16 from 2.4.0-main.b1afef46); the legacy `values`
369
+ * array rows never carry it and existing consumers stay untouched.
370
+ */
371
+ schema?: string;
372
+ }
373
+
374
+ export interface ReviewCaptureSubmissionV1 {
375
+ operationToken: string;
376
+ argumentTokens: readonly string[];
377
+ values: readonly ReviewCaptureSubmissionValueV1[];
378
+ }
379
+
380
+ // The two Go-owned non-lens provider role capture operations (gentle-pi#311
381
+ // P4-roles; provider side gentle-ai#3264). Their collect inputs are
382
+ // self-contained authority-advancing vectors: binding tokens plus
383
+ // `--agent=pi --execute=true`, with NO submission descriptor. The known set
384
+ // is closed — an unknown role capture operation is never executed.
385
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
386
+ CAPTURE_REFUTER: "review.capture-refuter",
387
+ CAPTURE_VALIDATION: "review.capture-validation",
388
+ } as const;
389
+ export type ReviewProviderRoleCaptureOperation = (typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION)[keyof typeof REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION];
390
+ export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS = Object.freeze(Object.values(REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION));
391
+
392
+ // status/v5 structural surfaces. Pi has no consumer for these yet; they are
393
+ // decoded strictly (exact key sets per the vendored status-v5.schema.json and
394
+ // correction-plan-request.schema.json on gentle-ai main) and carried through
395
+ // so a v5 provider payload is never rejected for being newer than v3.
396
+ export interface ReviewForecastStepV1 {
397
+ step: 1;
398
+ kind: "execute" | "collect" | "stop";
399
+ reasonCode: string;
400
+ description: string;
401
+ }
402
+
403
+ export interface ReviewForecastV1 {
404
+ horizon: "partial" | "terminal";
405
+ steps: readonly ReviewForecastStepV1[];
406
+ }
407
+
408
+ export interface ReviewProviderTaskV1 {
409
+ agent: "review-refuter" | "review-validator";
410
+ role: "refuter" | "targeted-validator";
411
+ prompt: string;
412
+ }
413
+
414
+ export interface ReviewSubmissionDescriptorValueV5 {
415
+ slot: string;
416
+ domain: string;
417
+ substitutionLocation: number;
418
+ schema?: string;
419
+ minimum?: number;
420
+ maximum?: number;
421
+ allowedValues?: readonly string[];
422
+ }
423
+
424
+ // The two v5 Go-owned submission descriptor forms (finalize carries one
425
+ // singular `value`; capture-evidence carries an outcome/input `values` pair).
426
+ // Distinct from the host-relay ReviewCaptureSubmissionV1 form, which stays
427
+ // exactly as it is.
428
+ export interface ReviewSubmissionDescriptorV5 {
429
+ operationToken: "finalize" | "capture-evidence";
430
+ argumentTokens: readonly string[];
431
+ value?: ReviewSubmissionDescriptorValueV5;
432
+ values?: readonly ReviewSubmissionDescriptorValueV5[];
433
+ }
434
+
435
+ export interface ReviewCorrectionPlanFindingV1 {
436
+ id: string;
437
+ lens: "risk" | "resilience" | "readability" | "reliability";
438
+ location: string;
439
+ severity: "BLOCKER" | "CRITICAL";
440
+ claim: string;
441
+ proofRefs: readonly string[];
442
+ evidence: string;
443
+ evidenceClass: "deterministic" | "inferential";
444
+ causalDisposition: "introduced" | "behavior-activated" | "worsened";
445
+ }
446
+
447
+ export interface ReviewCorrectionPlanRequestV1 {
448
+ schema: "gentle-ai.review-correction-plan-request/v1";
449
+ requestHash: string;
450
+ lineageId: string;
451
+ expectedRevision: string;
452
+ targetIdentity: string;
453
+ correctionBudget: number;
454
+ fixFindingIds: readonly string[];
455
+ findings: readonly ReviewCorrectionPlanFindingV1[];
456
+ }
457
+
315
458
  export interface ReviewCollectInputV3 {
316
459
  name: string;
317
460
  schema: string;
@@ -322,6 +465,11 @@ export interface ReviewCollectInputV3 {
322
465
  candidateTree?: string;
323
466
  changedPathManifest?: readonly ChangedPathEntry[];
324
467
  validationRequest?: ReviewTargetedValidationRequestV1;
468
+ submission?: ReviewCaptureSubmissionV1;
469
+ /** status/v5 only: the finalize/capture-evidence descriptor forms. */
470
+ submissionDescriptor?: ReviewSubmissionDescriptorV5;
471
+ /** status/v5 only: the self-contained external.run_provider_role task. */
472
+ providerTask?: ReviewProviderTaskV1;
325
473
  }
326
474
 
327
475
  export interface ReviewNextTransitionV3 {
@@ -329,6 +477,8 @@ export interface ReviewNextTransitionV3 {
329
477
  reasonCode: string;
330
478
  execute?: ReviewNextTransitionExecuteV3;
331
479
  collect?: { inputs: readonly ReviewCollectInputV3[] };
480
+ /** status/v5 only: the bounded correction plan request. */
481
+ correctionRequest?: ReviewCorrectionPlanRequestV1;
332
482
  }
333
483
 
334
484
  export interface ReviewTargetedValidationRequestV1 {
@@ -373,6 +523,15 @@ export interface ReviewStatusV3 {
373
523
  nextTransition?: ReviewNextTransitionV3;
374
524
  validationRequest?: ReviewTargetedValidationRequestV1;
375
525
  finalVerificationRetry?: ReviewFinalVerificationRetryV1;
526
+ /** status/v5 only: the descriptive, non-routing transition preview. */
527
+ forecast?: ReviewForecastV1;
528
+ /**
529
+ * status/v5 only: the opaque repository-context reference the live
530
+ * 2.4.0-main binary publishes once a reviewing lineage has bound one.
531
+ * NEW struct member — captured 2026-08-16 from 2.4.0-main.b1afef46; the
532
+ * published status-v5.schema.json omits it (capture is authoritative).
533
+ */
534
+ repositoryContext?: ReviewRepositoryContextV2;
376
535
  raw: Readonly<Record<string, unknown>>;
377
536
  }
378
537
 
@@ -403,6 +562,19 @@ export interface ReviewConsentV2 {
403
562
  raw: Readonly<Record<string, unknown>>;
404
563
  }
405
564
 
565
+ // consent/v3 (gentle-ai >= 2.3.0, capabilities/v2.1+): the same per-candidate
566
+ // blocking question with one net-new required member, the provider-fixed
567
+ // `agent` runtime binding. Everything shared with v2 keeps its exact shape so
568
+ // consumers of either identity read one structural surface.
569
+ export interface ReviewConsentV3 extends Omit<ReviewConsentV2, "schema"> {
570
+ schema: "gentle-ai.review-integration.consent/v3";
571
+ agent: "claude-code";
572
+ }
573
+
574
+ // Either accepted consent identity. Consumers that relay the envelope (rather
575
+ // than decode it) accept both; each decoder still admits exactly one schema.
576
+ export type ReviewConsentEnvelope = ReviewConsentV2 | ReviewConsentV3;
577
+
406
578
  const FAILURE_REQUIRED_INPUTS = [
407
579
  "lineage_id",
408
580
  "change",
@@ -453,10 +625,12 @@ export interface ReviewFailureContextV2 {
453
625
  bindingRevision?: ReviewFailureBindingRevisionV1;
454
626
  }
455
627
 
628
+ export type ReviewFailureOperation = ReviewIntegrationOperation | "review.capture-result" | "review.capture-evidence" | "review.capture-refuter" | "review.capture-validation";
629
+
456
630
  export interface ReviewFailureV2 {
457
631
  schema: "gentle-ai.review-integration.failure/v2";
458
632
  contract: typeof REVIEW_INTEGRATION_CONTRACT;
459
- operation: ReviewIntegrationOperation;
633
+ operation: ReviewFailureOperation;
460
634
  phase: "preflight" | "pre_native" | "native_running" | "native_committed" | "reconciliation";
461
635
  code: string;
462
636
  message: string;
@@ -689,10 +863,15 @@ function decodeOptionalFeature(value: unknown, label: string): { name: string; s
689
863
  export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDigest: string): ReviewCapabilitiesV2 {
690
864
  const requiredFields = ["schema", "contract", "protocol", "package", "build", "executable", "operations", "gates", "projections", "schemas", "features", "compatibility"] as const;
691
865
  const body = exactRecord(value, "capabilities", requiredFields, ["bootstrap"]);
692
- requireIdentity(body, "gentle-ai.review-integration.capabilities/v2");
866
+ // Additive minor acceptance: each accepted schema identity keeps its own
867
+ // exact protocol minor and required-schema floor; v2 stays exactly as it
868
+ // was, and an unknown identity is rejected before anything else.
869
+ const identity = CAPABILITIES_SCHEMA_IDENTITIES[typeof body.schema === "string" ? body.schema : ""];
870
+ if (identity === undefined) throw new TypeError(`schema must be one of ${Object.keys(CAPABILITIES_SCHEMA_IDENTITIES).join(", ")}`);
871
+ requireIdentity(body, body.schema as string);
693
872
 
694
873
  const protocol = exactRecord(body.protocol, "capabilities.protocol", ["major", "minor"]);
695
- if (protocol.major !== 2 || protocol.minor !== 0) throw new TypeError("incompatible review integration protocol");
874
+ if (protocol.major !== 2 || protocol.minor !== identity.protocolMinor) throw new TypeError("incompatible review integration protocol");
696
875
 
697
876
  const packageIdentity = exactRecord(body.package, "capabilities.package", ["name", "version", "release_channel"]);
698
877
  if (packageIdentity.name !== "gentle-ai") throw new TypeError("capabilities package identity mismatch");
@@ -712,13 +891,18 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
712
891
  if (selfReportedDigest !== normalizedVerifiedDigest) throw new TypeError("review provider executable digest mismatch");
713
892
 
714
893
  const advertisedOperations = stringArray(body.operations, "capabilities.operations", { minimum: REQUIRED_OPERATIONS.length, unique: true });
715
- const gates = enumArray(body.gates, REQUIRED_GATES, "capabilities.gates", { minimum: 5, maximum: 5, unique: true });
716
- const projections = enumArray(body.projections, REQUIRED_PROJECTIONS, "capabilities.projections", { minimum: 2, maximum: 2, unique: true });
717
- const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: REQUIRED_SCHEMAS.length, unique: true });
894
+ // Gates and projections are, like operations and schemas, a superset promise
895
+ // rather than an exact manifest: a compatible provider release may advertise
896
+ // an additional gate or projection name beyond the required floor. Decode as
897
+ // a plain string array (not `enumArray` against the known enum) so an
898
+ // unknown addition is not rejected before assertSupersetOf can even run.
899
+ const advertisedGates = stringArray(body.gates, "capabilities.gates", { minimum: REQUIRED_GATES.length, unique: true });
900
+ const advertisedProjections = stringArray(body.projections, "capabilities.projections", { minimum: REQUIRED_PROJECTIONS.length, unique: true });
901
+ const advertisedSchemas = stringArray(body.schemas, "capabilities.schemas", { minimum: identity.requiredSchemas.length, unique: true });
718
902
  assertSupersetOf(advertisedOperations, REQUIRED_OPERATIONS, "capabilities operations");
719
- assertExactSet(gates, REQUIRED_GATES, "capabilities gates");
720
- assertExactSet(projections, REQUIRED_PROJECTIONS, "capabilities projections");
721
- assertSupersetOf(advertisedSchemas, REQUIRED_SCHEMAS, "capabilities schemas");
903
+ assertSupersetOf(advertisedGates, REQUIRED_GATES, "capabilities gates");
904
+ assertSupersetOf(advertisedProjections, REQUIRED_PROJECTIONS, "capabilities projections");
905
+ assertSupersetOf(advertisedSchemas, identity.requiredSchemas, "capabilities schemas");
722
906
 
723
907
  const features = exactRecord(body.features, "capabilities.features", ["mandatory", "optional"]);
724
908
  const mandatory = array(features.mandatory, "capabilities.features.mandatory", (entry, label) => decodeFeature(entry, label), { minimum: 10 });
@@ -764,9 +948,9 @@ export function decodeReviewCapabilitiesV2(value: unknown, verifiedExecutableDig
764
948
  buildId,
765
949
  executableDigest: selfReportedDigest,
766
950
  operations: new Set(REQUIRED_OPERATIONS),
767
- gates: new Set(gates),
768
- projections: new Set(projections),
769
- schemas: new Set(REQUIRED_SCHEMAS),
951
+ gates: new Set(REQUIRED_GATES),
952
+ projections: new Set(REQUIRED_PROJECTIONS),
953
+ schemas: new Set(identity.requiredSchemas),
770
954
  mandatoryFeatures: new Set(mandatoryNames),
771
955
  optionalFeatures: new Set(optional.filter((feature) => feature.supported && (FEATURE_NAMES as readonly string[]).includes(feature.name)).map((feature) => feature.name)),
772
956
  raw: body,
@@ -857,13 +1041,15 @@ export function decodeReviewStartV3(value: unknown): ReviewStartV3 {
857
1041
 
858
1042
  let repositoryContext: ReviewRepositoryContextV2 | undefined;
859
1043
  if (body.repository_context !== undefined) {
860
- const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"]);
1044
+ const source = exactRecord(body.repository_context, "start.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
861
1045
  if (source.capability !== "review.opaque_repository_context") throw new TypeError("start.repository_context.capability is unsupported");
862
1046
  repositoryContext = {
863
1047
  capability: "review.opaque_repository_context",
864
1048
  handle: text(source.handle, "start.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
865
1049
  revision: sha256(source.revision, "start.repository_context.revision"),
866
1050
  targetIdentity: sha256(source.target_identity, "start.repository_context.target_identity"),
1051
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "start.repository_context.event_id") }),
1052
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "start.repository_context.outcome") }),
867
1053
  };
868
1054
  }
869
1055
 
@@ -1042,8 +1228,146 @@ function decodeTransitionArguments(value: unknown, label: string): readonly Revi
1042
1228
  });
1043
1229
  }
1044
1230
 
1045
- function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3 {
1046
- const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request"]);
1231
+ function decodeCaptureSubmission(value: unknown, label: string, v5: boolean): ReviewCaptureSubmissionV1 {
1232
+ // status/v5: the live 2.4.0-main binary emits the materialize
1233
+ // capture-result submission as a SINGULAR `value` object carrying a
1234
+ // `schema` key (captured 2026-08-16 from 2.4.0-main.b1afef46; the
1235
+ // emitter has been singular since gentle-ai f1a95179). The form is
1236
+ // closed to that exact captured shape and normalizes into the typed
1237
+ // one-entry values array the host relay already consumes. A payload
1238
+ // carrying both wire forms at once matches no captured shape and falls
1239
+ // through to the legacy decoder, which rejects the unknown `value` key.
1240
+ if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1241
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1242
+ if (submission.operation_token !== "capture-result") throw new TypeError(`${label}.operation_token must be capture-result for the singular value form`);
1243
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1244
+ const row = exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"]);
1245
+ return {
1246
+ operationToken: "capture-result",
1247
+ argumentTokens,
1248
+ values: [{
1249
+ slot: enumeration(row.slot, ["reviewer_result"] as const, `${label}.value.slot`),
1250
+ domain: nonempty(row.domain, `${label}.value.domain`),
1251
+ schema: nonempty(row.schema, `${label}.value.schema`),
1252
+ substitutionLocation: integer(row.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1253
+ }],
1254
+ };
1255
+ }
1256
+ const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1257
+ const operationToken = text(submission.operation_token, `${label}.operation_token`, { minimum: 1, pattern: /^[a-z0-9-]+$/ });
1258
+ const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1259
+ const values = array(submission.values, `${label}.values`, (entry, entryLabel) => {
1260
+ const row = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"]);
1261
+ return {
1262
+ slot: nonempty(row.slot, `${entryLabel}.slot`),
1263
+ domain: nonempty(row.domain, `${entryLabel}.domain`),
1264
+ substitutionLocation: integer(row.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1265
+ };
1266
+ }, { minimum: 1 });
1267
+ return { operationToken, argumentTokens, values };
1268
+ }
1269
+
1270
+ // v5-only structural decoders, exact to the vendored status-v5.schema.json
1271
+ // and correction-plan-request.schema.json on gentle-ai main.
1272
+
1273
+ function decodeProviderTask(value: unknown, label: string): ReviewProviderTaskV1 {
1274
+ const task = exactRecord(value, label, ["agent", "role", "prompt"]);
1275
+ return {
1276
+ agent: enumeration(task.agent, ["review-refuter", "review-validator"] as const, `${label}.agent`),
1277
+ role: enumeration(task.role, ["refuter", "targeted-validator"] as const, `${label}.role`),
1278
+ prompt: nonempty(task.prompt, `${label}.prompt`),
1279
+ };
1280
+ }
1281
+
1282
+ function decodeSubmissionDescriptorV5(value: unknown, label: string, operationToken: "finalize" | "capture-evidence"): ReviewSubmissionDescriptorV5 {
1283
+ if (operationToken === "finalize") {
1284
+ const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1285
+ const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 7 });
1286
+ const source = exactRecord(descriptor.value, `${label}.value`, ["slot", "domain", "substitution_location"], ["schema", "minimum", "maximum"]);
1287
+ const slot = enumeration(source.slot, ["correction_lines", "validation"] as const, `${label}.value.slot`);
1288
+ return {
1289
+ operationToken,
1290
+ argumentTokens,
1291
+ value: {
1292
+ slot,
1293
+ domain: nonempty(source.domain, `${label}.value.domain`),
1294
+ substitutionLocation: integer(source.substitution_location, `${label}.value.substitution_location`, 0, argumentTokens.length - 1),
1295
+ ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${label}.value.schema`) }),
1296
+ ...(source.minimum === undefined ? {} : { minimum: integer(source.minimum, `${label}.value.minimum`, 1, 200) }),
1297
+ ...(source.maximum === undefined ? {} : { maximum: integer(source.maximum, `${label}.value.maximum`, 1, 200) }),
1298
+ },
1299
+ };
1300
+ }
1301
+ const descriptor = exactRecord(value, label, ["operation_token", "argument_tokens", "values"]);
1302
+ const argumentTokens = stringArray(descriptor.argument_tokens, `${label}.argument_tokens`, { minimum: 6, maximum: 6 });
1303
+ const values = array(descriptor.values, `${label}.values`, (entry, entryLabel) => {
1304
+ const source = exactRecord(entry, entryLabel, ["slot", "domain", "substitution_location"], ["schema", "allowed_values"]);
1305
+ return {
1306
+ slot: nonempty(source.slot, `${entryLabel}.slot`),
1307
+ domain: nonempty(source.domain, `${entryLabel}.domain`),
1308
+ substitutionLocation: integer(source.substitution_location, `${entryLabel}.substitution_location`, 0, argumentTokens.length - 1),
1309
+ ...(source.schema === undefined ? {} : { schema: nonempty(source.schema, `${entryLabel}.schema`) }),
1310
+ ...(source.allowed_values === undefined ? {} : { allowedValues: stringArray(source.allowed_values, `${entryLabel}.allowed_values`, { minimum: 1, unique: true }) }),
1311
+ };
1312
+ }, { minimum: 2, maximum: 2 });
1313
+ return { operationToken, argumentTokens, values };
1314
+ }
1315
+
1316
+ function decodeCorrectionPlanRequestV1(value: unknown, label: string): ReviewCorrectionPlanRequestV1 {
1317
+ const body = exactRecord(value, label, ["schema", "request_hash", "lineage_id", "expected_revision", "target_identity", "correction_budget", "fix_finding_ids", "findings"]);
1318
+ if (body.schema !== "gentle-ai.review-correction-plan-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan-request/v1`);
1319
+ return {
1320
+ schema: "gentle-ai.review-correction-plan-request/v1",
1321
+ requestHash: sha256(body.request_hash, `${label}.request_hash`),
1322
+ lineageId: lineage(body.lineage_id, `${label}.lineage_id`),
1323
+ expectedRevision: sha256(body.expected_revision, `${label}.expected_revision`),
1324
+ targetIdentity: sha256(body.target_identity, `${label}.target_identity`),
1325
+ correctionBudget: integer(body.correction_budget, `${label}.correction_budget`, 1, 200),
1326
+ fixFindingIds: stringArray(body.fix_finding_ids, `${label}.fix_finding_ids`, { minimum: 1, unique: true }),
1327
+ findings: array(body.findings, `${label}.findings`, (entry, entryLabel) => {
1328
+ const finding = exactRecord(entry, entryLabel, ["id", "lens", "location", "severity", "claim", "proof_refs", "evidence", "evidence_class", "causal_disposition"]);
1329
+ return {
1330
+ id: nonempty(finding.id, `${entryLabel}.id`),
1331
+ lens: enumeration(finding.lens, ["risk", "resilience", "readability", "reliability"] as const, `${entryLabel}.lens`),
1332
+ location: nonempty(finding.location, `${entryLabel}.location`),
1333
+ severity: enumeration(finding.severity, ["BLOCKER", "CRITICAL"] as const, `${entryLabel}.severity`),
1334
+ claim: nonempty(finding.claim, `${entryLabel}.claim`),
1335
+ proofRefs: stringArray(finding.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
1336
+ evidence: nonempty(finding.evidence, `${entryLabel}.evidence`),
1337
+ evidenceClass: enumeration(finding.evidence_class, ["deterministic", "inferential"] as const, `${entryLabel}.evidence_class`),
1338
+ causalDisposition: enumeration(finding.causal_disposition, ["introduced", "behavior-activated", "worsened"] as const, `${entryLabel}.causal_disposition`),
1339
+ };
1340
+ }, { minimum: 1 }),
1341
+ };
1342
+ }
1343
+
1344
+ export function decodeReviewForecastV1(value: unknown, label = "status.forecast"): ReviewForecastV1 {
1345
+ const body = exactRecord(value, label, ["horizon", "steps"]);
1346
+ const horizon = enumeration(body.horizon, ["partial", "terminal"] as const, `${label}.horizon`);
1347
+ const steps = array(body.steps, `${label}.steps`, (entry, entryLabel) => {
1348
+ const step = exactRecord(entry, entryLabel, ["step", "kind", "reason_code", "description"]);
1349
+ if (step.step !== 1) throw new TypeError(`${entryLabel}.step must be 1`);
1350
+ return {
1351
+ step: 1 as const,
1352
+ kind: enumeration(step.kind, ["execute", "collect", "stop"] as const, `${entryLabel}.kind`),
1353
+ reasonCode: text(step.reason_code, `${entryLabel}.reason_code`, { minimum: 1, pattern: /^[a-z0-9_]+$/ }),
1354
+ description: nonempty(step.description, `${entryLabel}.description`),
1355
+ };
1356
+ }, { minimum: 1, maximum: 1 });
1357
+ // The published horizon-to-step invariant: a stop head is terminal, any
1358
+ // other head is partial.
1359
+ if ((horizon === "terminal") !== (steps[0]!.kind === "stop")) throw new TypeError(`${label}.horizon does not match its step kind`);
1360
+ return { horizon, steps };
1361
+ }
1362
+
1363
+ // The two v5 reason codes whose transitions must carry the correction plan
1364
+ // request; every other reason code must not.
1365
+ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required", "corrected_candidate_unavailable"] as const);
1366
+ // v5 capture operations that must carry a submission descriptor.
1367
+ const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["external.plan_correction", "external.run_targeted_validation", "review.capture-evidence"] as const);
1368
+
1369
+ function decodeCollectInput(value: unknown, label: string, v5: boolean): ReviewCollectInputV3 {
1370
+ const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "validation_request", "submission", ...(v5 ? ["provider_task"] : [])]);
1047
1371
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1048
1372
  const schema = nonempty(input.schema, `${label}.schema`);
1049
1373
  const captureOperation = nonempty(input.capture_operation, `${label}.capture_operation`);
@@ -1052,8 +1376,43 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
1052
1376
  if (captureOperation === "external.run_targeted_validation") {
1053
1377
  if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1054
1378
  if (schema !== "gentle-ai.review-targeted-validation-request/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-targeted-validation-request/v1`);
1379
+ } else if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION) {
1380
+ if (input.validation_request === undefined) throw new TypeError(`${label}.validation_request is required`);
1055
1381
  } else if (input.validation_request !== undefined) {
1056
- throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation`);
1382
+ throw new TypeError(`${label}.validation_request is only valid for external.run_targeted_validation or review.capture-validation`);
1383
+ }
1384
+
1385
+ // v5-only surfaces (vendored status-v5.schema.json): the provider role task
1386
+ // rides exactly the external.run_provider_role vector, and the finalize/
1387
+ // capture-evidence submission descriptor forms ride their own vectors.
1388
+ if (v5) {
1389
+ if (captureOperation === "external.run_provider_role") {
1390
+ if (input.provider_task === undefined) throw new TypeError(`${label}.provider_task is required for external.run_provider_role`);
1391
+ } else if (input.provider_task !== undefined) {
1392
+ throw new TypeError(`${label}.provider_task is only valid for external.run_provider_role`);
1393
+ }
1394
+ if ((V5_SUBMISSION_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
1395
+ if (input.submission === undefined) throw new TypeError(`${label}.submission is required for ${captureOperation}`);
1396
+ if (captureOperation === "external.plan_correction" && schema !== "gentle-ai.review-correction-plan/v1") throw new TypeError(`${label}.schema must be gentle-ai.review-correction-plan/v1`);
1397
+ if (captureOperation === "review.capture-evidence" && schema !== "https://gentle-ai.dev/schema/review/verification-evidence/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/verification-evidence/v1`);
1398
+ }
1399
+ }
1400
+
1401
+ // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1402
+ // operations render SELF-CONTAINED authority-advancing vectors. Executing
1403
+ // the exact rendered tokens makes Go materialize the role prompt, run its
1404
+ // own locked-down pi subprocess, and admit the raw verdict — so a
1405
+ // submission descriptor (the host-mediated completing form) on one of
1406
+ // these inputs would hand the caller a way to author the verdict and is
1407
+ // rejected as a provider contract violation.
1408
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
1409
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
1410
+ }
1411
+ if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION && schema !== "https://gentle-ai.dev/schema/review/validator/v1") {
1412
+ throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/validator/v1`);
1413
+ }
1414
+ if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(captureOperation)) {
1415
+ throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1057
1416
  }
1058
1417
 
1059
1418
  if (captureOperation === "review.capture-result") {
@@ -1064,6 +1423,23 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
1064
1423
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
1065
1424
  throw new TypeError(`${label} carries capture-result fields without review.capture-result`);
1066
1425
  }
1426
+ const submissionOperations: readonly string[] = v5 ? ["review.capture-result", ...V5_SUBMISSION_CAPTURE_OPERATIONS] : ["review.capture-result"];
1427
+ if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1428
+ throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1429
+ }
1430
+
1431
+ // The v5 descriptor forms are discriminated by their closed operation
1432
+ // tokens; every other submission stays the host-relay completing form.
1433
+ let submission: ReviewCaptureSubmissionV1 | undefined;
1434
+ let submissionDescriptor: ReviewSubmissionDescriptorV5 | undefined;
1435
+ if (input.submission !== undefined) {
1436
+ const operationToken = (input.submission as Record<string, unknown> | null)?.operation_token;
1437
+ if (v5 && (operationToken === "finalize" || operationToken === "capture-evidence")) {
1438
+ submissionDescriptor = decodeSubmissionDescriptorV5(input.submission, `${label}.submission`, operationToken);
1439
+ } else {
1440
+ submission = decodeCaptureSubmission(input.submission, `${label}.submission`, v5);
1441
+ }
1442
+ }
1067
1443
 
1068
1444
  return {
1069
1445
  name,
@@ -1075,14 +1451,28 @@ function decodeCollectInput(value: unknown, label: string): ReviewCollectInputV3
1075
1451
  ...(input.candidate_tree === undefined ? {} : { candidateTree: gitTree(input.candidate_tree, `${label}.candidate_tree`) }),
1076
1452
  ...(input.changed_path_manifest === undefined ? {} : { changedPathManifest: array(input.changed_path_manifest, `${label}.changed_path_manifest`, decodeChangedPathEntry, { unique: true }) }),
1077
1453
  ...(input.validation_request === undefined ? {} : { validationRequest: decodeTargetedValidationRequestV1(input.validation_request, `${label}.validation_request`) }),
1454
+ ...(submission === undefined ? {} : { submission }),
1455
+ ...(submissionDescriptor === undefined ? {} : { submissionDescriptor }),
1456
+ ...(v5 && input.provider_task !== undefined ? { providerTask: decodeProviderTask(input.provider_task, `${label}.provider_task`) } : {}),
1078
1457
  };
1079
1458
  }
1080
1459
 
1081
- export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransitionV3 {
1082
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect"]);
1460
+ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean } = {}): ReviewNextTransitionV3 {
1461
+ const v5 = options.v5 === true;
1462
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1083
1463
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
1084
1464
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1085
1465
 
1466
+ // status/v5: the bounded correction plan request rides exactly its two
1467
+ // reason codes and never any other (vendored status-v5.schema.json).
1468
+ let correctionRequest: ReviewCorrectionPlanRequestV1 | undefined;
1469
+ if (v5) {
1470
+ const required = (CORRECTION_REQUEST_REASON_CODES as readonly string[]).includes(reasonCode);
1471
+ if (required && transition.correction_request === undefined) throw new TypeError(`next_transition.correction_request is required for ${reasonCode}`);
1472
+ if (!required && transition.correction_request !== undefined) throw new TypeError(`next_transition.correction_request is only valid for ${CORRECTION_REQUEST_REASON_CODES.join(", ")}`);
1473
+ if (transition.correction_request !== undefined) correctionRequest = decodeCorrectionPlanRequestV1(transition.correction_request, "next_transition.correction_request");
1474
+ }
1475
+
1086
1476
  if (kind === "execute") {
1087
1477
  // `command` is an optional, ready-to-paste rendering of `arguments` (the
1088
1478
  // exact same binding as a single shell-ready string) — observed live
@@ -1109,16 +1499,16 @@ export function decodeReviewNextTransitionV3(value: unknown): ReviewNextTransiti
1109
1499
  const revision = binding.revision === undefined ? undefined : sha256(binding.revision, "next_transition.execute.binding.revision");
1110
1500
  const command = execute.command === undefined ? undefined : nonempty(execute.command, "next_transition.execute.command");
1111
1501
  if (transition.collect !== undefined) throw new TypeError("next_transition.collect is incompatible with execute");
1112
- return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) } };
1502
+ return { kind, reasonCode, execute: { operation, arguments: argumentsList, preconditions, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) }, ...(command === undefined ? {} : { command }) }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1113
1503
  }
1114
1504
  if (kind === "collect") {
1115
1505
  const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1116
- const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label), { minimum: 1 });
1506
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5), { minimum: 1 });
1117
1507
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1118
- return { kind, reasonCode, collect: { inputs } };
1508
+ return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1119
1509
  }
1120
1510
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1121
- return { kind, reasonCode };
1511
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1122
1512
  }
1123
1513
 
1124
1514
  // ---------------------------------------------------------------------------
@@ -1178,10 +1568,15 @@ function decodeFinalVerificationRetry(value: unknown, label: string): ReviewFina
1178
1568
  // ---------------------------------------------------------------------------
1179
1569
 
1180
1570
  export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1571
+ // Additive forward acceptance: status/v5 (gentle-ai main; ground-truthed
1572
+ // against a live capture and the vendored status-v5.schema.json) is the v3
1573
+ // key set plus the optional forecast and the v5-only next_transition
1574
+ // surfaces. The v3 identity keeps rejecting every v5-only field.
1575
+ const v5 = typeof value === "object" && value !== null && (value as Record<string, unknown>).schema === "gentle-ai.review-integration.status/v5";
1181
1576
  const body = exactRecord(value, "status", [
1182
1577
  "schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1183
- ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry"]);
1184
- requireIdentity(body, "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1578
+ ], ["authority", "frozen", "reconciliation", "action_disposition", "eligibility", "next_transition", "authority_target_identity", "validation_request", "final_verification_retry", ...(v5 ? ["forecast", "repository_context"] : [])]);
1579
+ requireIdentity(body, v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1185
1580
 
1186
1581
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
1187
1582
  const receiptBody = exactRecord(body.receipt, "status.receipt", ["status"], ["identity"]);
@@ -1222,7 +1617,11 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1222
1617
  if ((action === "recover" || action === "retry_final_verification") && actionDisposition === undefined) throw new TypeError(`${action} status requires action_disposition`);
1223
1618
  if (action !== "recover" && action !== "retry_final_verification" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover or retry_final_verification action");
1224
1619
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1225
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition);
1620
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5 });
1621
+ // status/v5 dependentRequired: a forecast previews the transition head, so
1622
+ // it can only accompany an actual next_transition.
1623
+ const forecast = v5 && body.forecast !== undefined ? decodeReviewForecastV1(body.forecast) : undefined;
1624
+ if (forecast !== undefined && nextTransition === undefined) throw new TypeError("status.forecast requires status.next_transition");
1226
1625
  const replayability = enumeration(body.replayability, Object.values(REVIEW_REPLAYABILITY), "status.replayability");
1227
1626
 
1228
1627
  let reconciliation: ReviewStatusReconciliationV1 | undefined;
@@ -1257,6 +1656,24 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1257
1656
  throw new TypeError("status.final_verification_retry is only valid for the retry_final_verification action");
1258
1657
  }
1259
1658
 
1659
+ // status/v5 additive top-level repository context reference (captured
1660
+ // 2026-08-16 from 2.4.0-main.b1afef46; missing from the published
1661
+ // status-v5.schema.json, so the live capture is authoritative). Same
1662
+ // reference shape as start/v3's repository_context.
1663
+ let repositoryContext: ReviewRepositoryContextV2 | undefined;
1664
+ if (body.repository_context !== undefined) {
1665
+ const source = exactRecord(body.repository_context, "status.repository_context", ["capability", "handle", "revision", "target_identity"], ["event_id", "outcome"]);
1666
+ if (source.capability !== "review.opaque_repository_context") throw new TypeError("status.repository_context.capability is unsupported");
1667
+ repositoryContext = {
1668
+ capability: "review.opaque_repository_context",
1669
+ handle: text(source.handle, "status.repository_context.handle", { pattern: /^rctx1_[0-9a-f]{64}$/ }),
1670
+ revision: sha256(source.revision, "status.repository_context.revision"),
1671
+ targetIdentity: sha256(source.target_identity, "status.repository_context.target_identity"),
1672
+ ...(source.event_id === undefined ? {} : { eventId: sha256(source.event_id, "status.repository_context.event_id") }),
1673
+ ...(source.outcome === undefined ? {} : { outcome: enumeration(source.outcome, REPOSITORY_CONTEXT_OUTCOMES, "status.repository_context.outcome") }),
1674
+ };
1675
+ }
1676
+
1260
1677
  return {
1261
1678
  contract: REVIEW_INTEGRATION_CONTRACT,
1262
1679
  applicability,
@@ -1275,6 +1692,8 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1275
1692
  ...(nextTransition === undefined ? {} : { nextTransition }),
1276
1693
  ...(validationRequest === undefined ? {} : { validationRequest }),
1277
1694
  ...(finalVerificationRetry === undefined ? {} : { finalVerificationRetry }),
1695
+ ...(forecast === undefined ? {} : { forecast }),
1696
+ ...(repositoryContext === undefined ? {} : { repositoryContext }),
1278
1697
  raw: body,
1279
1698
  };
1280
1699
  }
@@ -1295,11 +1714,10 @@ function decodeConsentChoice(value: unknown, label: string, answer: "granted" |
1295
1714
  };
1296
1715
  }
1297
1716
 
1298
- export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
1299
- const body = exactRecord(value, "consent", [
1300
- "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1301
- ]);
1302
- requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1717
+ // The semantic surface shared verbatim by both accepted consent identities.
1718
+ // Every label and guard predates consent/v3, so the v2 decode stays
1719
+ // byte-identical (test-locked) while v3 adds only its own identity gate.
1720
+ function decodeConsentSemantics(body: Record<string, unknown>): Omit<ReviewConsentV2, "schema" | "raw"> {
1303
1721
  if (body.action !== "consent_required") throw new TypeError("consent.action must be consent_required");
1304
1722
  if (body.blocking !== true) throw new TypeError("consent.blocking must be true");
1305
1723
 
@@ -1316,7 +1734,6 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
1316
1734
  if (offPathSource.command !== "gentle-ai review mode disable") throw new TypeError("consent.off_path.command is unsupported");
1317
1735
 
1318
1736
  return {
1319
- schema: "gentle-ai.review-integration.consent/v2",
1320
1737
  contract: REVIEW_INTEGRATION_CONTRACT,
1321
1738
  operation: "review.start",
1322
1739
  action: "consent_required",
@@ -1332,6 +1749,39 @@ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
1332
1749
  riskEvidence: stringArray(body.risk_evidence, "consent.risk_evidence"),
1333
1750
  choices: [granted, declined],
1334
1751
  offPath: { note: nonempty(offPathSource.note, "consent.off_path.note"), command: "gentle-ai review mode disable" },
1752
+ };
1753
+ }
1754
+
1755
+ const CONSENT_KEYS_V2 = Object.freeze([
1756
+ "schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path",
1757
+ ] as const);
1758
+
1759
+ export function decodeReviewConsentV2(value: unknown): ReviewConsentV2 {
1760
+ const body = exactRecord(value, "consent", CONSENT_KEYS_V2);
1761
+ requireIdentity(body, "gentle-ai.review-integration.consent/v2", "review.start");
1762
+ return {
1763
+ schema: "gentle-ai.review-integration.consent/v2",
1764
+ ...decodeConsentSemantics(body),
1765
+ raw: body,
1766
+ };
1767
+ }
1768
+
1769
+ // consent/v3 (gentle-ai >= 2.3.0): the v2 surface plus the required, fixed
1770
+ // `agent` runtime binding. Ground truth is the captured envelope from a
1771
+ // 2.4.0-main binary (tests/fixtures/devbinary/consent-v3.captured.json) plus
1772
+ // gentle-ai main contracts/review-integration/v2/schemas/consent-v3.schema.json.
1773
+ // The choice-invocation shape deliberately stays the shared v2 pattern: the
1774
+ // published v3 schema demands an `--agent claude-code` token that the live
1775
+ // emitter omits when the caller declared no --agent, so the capture is
1776
+ // authoritative and Pi replays whichever provider-owned invocation arrived.
1777
+ export function decodeReviewConsentV3(value: unknown): ReviewConsentV3 {
1778
+ const body = exactRecord(value, "consent", [...CONSENT_KEYS_V2, "agent"]);
1779
+ requireIdentity(body, "gentle-ai.review-integration.consent/v3", "review.start");
1780
+ if (body.agent !== "claude-code") throw new TypeError("consent.agent must be claude-code");
1781
+ return {
1782
+ schema: "gentle-ai.review-integration.consent/v3",
1783
+ agent: "claude-code",
1784
+ ...decodeConsentSemantics(body),
1335
1785
  raw: body,
1336
1786
  };
1337
1787
  }
@@ -1395,7 +1845,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
1395
1845
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
1396
1846
  ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
1397
1847
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
1398
- const operation = enumeration(body.operation, REQUIRED_OPERATIONS, "failure.operation");
1848
+ const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
1399
1849
 
1400
1850
  if (body.progress_identity !== undefined) {
1401
1851
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -1565,3 +2015,54 @@ export function decodeReviewRepairV2(value: unknown): ReviewRepairV2 {
1565
2015
  raw: body,
1566
2016
  };
1567
2017
  }
2018
+
2019
+ // ---------------------------------------------------------------------------
2020
+ // result-artifact/v2 — the `review capture-result` admission answer
2021
+ // ---------------------------------------------------------------------------
2022
+
2023
+ // The provider's admitted-reviewer-result envelope, printed by `review
2024
+ // capture-result` when a reviewer result is admitted and re-discovered by
2025
+ // STATUS artifact discovery. Unlike the negotiated envelopes above it carries
2026
+ // no `contract`/`operation` identity pair — the schema constant plus the
2027
+ // capability constant are its complete identity (vendored ground truth:
2028
+ // contracts/review-integration/v1/schemas/result-artifact-v2.schema.json,
2029
+ // confirmed against a live 2.4.0-main capture; the v2.2.3 pinned emitter
2030
+ // shares the exact same struct). Exactly one locator is present: a
2031
+ // provider-owned store `path`, or the opaque `rart1_` `reference` minted for
2032
+ // repository-context captures.
2033
+ export interface ReviewResultArtifactV2 {
2034
+ schema: "gentle-ai.review-result-artifact/v2";
2035
+ capability: "review.native_result_artifact";
2036
+ sha256: string;
2037
+ lineageId: string;
2038
+ targetIdentity: string;
2039
+ lens: (typeof REVIEW_LENSES)[number];
2040
+ selectedOrder: number;
2041
+ subjectHash: string;
2042
+ admissionDecision: "completed";
2043
+ path?: string;
2044
+ reference?: string;
2045
+ raw: Record<string, unknown>;
2046
+ }
2047
+
2048
+ export function decodeReviewResultArtifactV2(value: unknown): ReviewResultArtifactV2 {
2049
+ const body = exactRecord(value, "result_artifact", ["schema", "capability", "sha256", "lineage_id", "target_identity", "lens", "selected_order", "subject_hash", "admission_decision"], ["path", "reference"]);
2050
+ if (body.schema !== "gentle-ai.review-result-artifact/v2") throw new TypeError("result_artifact.schema must be gentle-ai.review-result-artifact/v2");
2051
+ if (body.capability !== "review.native_result_artifact") throw new TypeError("result_artifact.capability must be review.native_result_artifact");
2052
+ if (body.admission_decision !== "completed") throw new TypeError("result_artifact.admission_decision must be completed");
2053
+ if ((body.path === undefined) === (body.reference === undefined)) throw new TypeError("result_artifact must carry exactly one of path or reference");
2054
+ return {
2055
+ schema: "gentle-ai.review-result-artifact/v2",
2056
+ capability: "review.native_result_artifact",
2057
+ sha256: sha256(body.sha256, "result_artifact.sha256"),
2058
+ lineageId: lineage(body.lineage_id, "result_artifact.lineage_id"),
2059
+ targetIdentity: sha256(body.target_identity, "result_artifact.target_identity"),
2060
+ lens: enumeration(body.lens, REVIEW_LENSES, "result_artifact.lens"),
2061
+ selectedOrder: integer(body.selected_order, "result_artifact.selected_order", 0, 3),
2062
+ subjectHash: sha256(body.subject_hash, "result_artifact.subject_hash"),
2063
+ admissionDecision: "completed",
2064
+ ...(body.path === undefined ? {} : { path: nonempty(body.path, "result_artifact.path") }),
2065
+ ...(body.reference === undefined ? {} : { reference: text(body.reference, "result_artifact.reference", { pattern: /^rart1_[0-9a-f]{64}$/ }) }),
2066
+ raw: body,
2067
+ };
2068
+ }