gentle-pi 2.4.0 → 2.6.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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -79,7 +79,10 @@ const RISK_LEVELS = ["low", "medium", "high"] as const;
79
79
  const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
80
80
  const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
81
81
  const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
82
- const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop"] as const;
82
+ // "collect" and "execute" are the v2 envelope's projection of a live
83
+ // transaction whose next_transition is mandatory: the root action names the
84
+ // transition kind instead of reading as a terminal stop.
85
+ const STATUS_ACTIONS = ["start", "recover", "maintainer_action", "select_lineage", "repair_authority", "stop", "collect", "execute"] as const;
83
86
  const RECEIPT_STATUSES = ["expected_missing", "present", "publication_pending", "not_applicable"] as const;
84
87
  type ReviewReceiptStatus = (typeof RECEIPT_STATUSES)[number];
85
88
  export const REVIEW_STATUS_ACTION_DISPOSITION = {
@@ -385,6 +388,8 @@ export interface ReviewStatusFrozenV1 {
385
388
  tier: RiskLevel;
386
389
  originalChangedLines: number;
387
390
  correctionBudget: number;
391
+ /** Digest of the frozen changed-path manifest; binds manifest-less capture inputs (gentle-ai#3922). */
392
+ changedPathManifestSha256?: string;
388
393
  }
389
394
 
390
395
  export interface ReviewStatusReconciliationV1 {
@@ -545,6 +550,39 @@ export interface ReviewCollectInputV3 {
545
550
  validationRequest?: ReviewTargetedValidationRequestV1;
546
551
  }
547
552
 
553
+ // gentle-pi#627: the exact `gentle-ai sync` invocation that resolves a stale
554
+ // managed-asset set (STATUS next_transition stop, and START's preflight
555
+ // failure envelope both carry this same shape).
556
+ export interface ReviewManagedAssetsContinuationV1 {
557
+ operation: "sync";
558
+ command: string;
559
+ agent?: string;
560
+ staleAssets?: readonly string[];
561
+ }
562
+
563
+ // gentle-pi#638: one selected lens slot the host declared unachievable
564
+ // through the native capture-unachievable verb, mirrored from Go's
565
+ // ReviewUnachievableLensSlot (internal/cli/review_next_transition.go). The
566
+ // withdraw form is deliberately narrower than a full execute transition:
567
+ // it is the literally runnable retraction command for exactly this slot, so
568
+ // a restart that never saw the collect offer can still take the declaration
569
+ // back from the stop alone.
570
+ export interface ReviewUnachievableLensWithdrawV3 {
571
+ operation: "review.capture-unachievable";
572
+ command: string;
573
+ arguments: readonly ReviewTransitionArgumentV3[];
574
+ binding: { targetIdentity: string; lineageId?: string; revision?: string };
575
+ }
576
+
577
+ export interface ReviewUnachievableLensSlotV3 {
578
+ lens: string;
579
+ selectedOrder: number;
580
+ subjectHash: string;
581
+ reason: string;
582
+ detail?: string;
583
+ withdraw: ReviewUnachievableLensWithdrawV3;
584
+ }
585
+
548
586
  export interface ReviewNextTransitionV3 {
549
587
  kind: "execute" | "collect" | "stop";
550
588
  reasonCode: string;
@@ -552,9 +590,28 @@ export interface ReviewNextTransitionV3 {
552
590
  collect?: { inputs: readonly ReviewCollectInputV3[] };
553
591
  /** status/v5 only: the bounded correction plan request. */
554
592
  correctionRequest?: ReviewCorrectionPlanRequestV1;
593
+ /** stop only, reason_code managed_assets_outdated: the sync continuation. */
594
+ continuation?: ReviewManagedAssetsContinuationV1;
595
+ /** stop only, reason_code unachievable_lens_slot: one entry per declared slot. */
596
+ unachievableLensSlots?: readonly ReviewUnachievableLensSlotV3[];
597
+ }
598
+
599
+ const ESCALATION_CAUSES = ["unknown_causality", "insufficient_evidence", "missing_refuter_outcome", "targeted_validator_rejected", "correction_budget_exceeded", "unresolved_severe_findings"] as const;
600
+ const ESCALATION_REFUTER_OUTCOMES = ["corroborated", "refuted", "inconclusive"] as const;
601
+ export interface ReviewEscalationRefuterOutcomeV1 {
602
+ findingId: string;
603
+ outcome: (typeof ESCALATION_REFUTER_OUTCOMES)[number];
604
+ proof: string;
605
+ }
606
+ export interface ReviewEscalationV1 {
607
+ cause: (typeof ESCALATION_CAUSES)[number];
608
+ findingIds: readonly string[];
609
+ refuterOutcomes?: readonly ReviewEscalationRefuterOutcomeV1[];
555
610
  }
556
611
 
557
612
  export interface ReviewStatusV3 {
613
+ /** Optional v7 diagnostic evidence, never routing or reconstructed authority. */
614
+ escalation?: ReviewEscalationV1;
558
615
  contract: typeof REVIEW_INTEGRATION_CONTRACT;
559
616
  applicability: Exclude<ReviewAuthorityApplicability, "not_evaluated">;
560
617
  authority?: ReviewStatusAuthorityV1;
@@ -705,6 +762,8 @@ export interface ReviewFailureV2 {
705
762
  causeCategory?: ReviewFailureCauseCategoryV2;
706
763
  cause?: string;
707
764
  context?: ReviewFailureContextV2;
765
+ /** code=managed_assets_outdated only: the sync continuation (gentle-pi#627). */
766
+ continuation?: ReviewManagedAssetsContinuationV1;
708
767
  raw: Readonly<Record<string, unknown>>;
709
768
  }
710
769
 
@@ -1639,8 +1698,11 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
1639
1698
  }
1640
1699
 
1641
1700
  if (captureOperation === "review.capture-result") {
1642
- if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined || input.changed_path_manifest === undefined) {
1643
- throw new TypeError(`${label} requires artifact_subject, base_tree, candidate_tree, and changed_path_manifest`);
1701
+ // changed_path_manifest is optional here: the artifact subject's
1702
+ // changed_path_manifest_sha256 already binds the frozen manifest, and
1703
+ // gentle-ai stops inlining one copy per lens (gentle-ai#3922).
1704
+ if (input.artifact_subject === undefined || input.base_tree === undefined || input.candidate_tree === undefined) {
1705
+ throw new TypeError(`${label} requires artifact_subject, base_tree, and candidate_tree`);
1644
1706
  }
1645
1707
  if (schema !== "https://gentle-ai.dev/schema/review/reviewer/v1") throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/reviewer/v1`);
1646
1708
  } else if (input.artifact_subject !== undefined || input.base_tree !== undefined || input.candidate_tree !== undefined || input.changed_path_manifest !== undefined) {
@@ -1673,12 +1735,39 @@ function decodeCollectInput(value: unknown, label: string, v5: boolean, v6: bool
1673
1735
  };
1674
1736
  }
1675
1737
 
1738
+ // gentle-pi#627: gentle-ai's managed-assets continuation (STATUS next_transition
1739
+ // stop reason_code=managed_assets_outdated, and START's preflight failure
1740
+ // envelope) names the exact `gentle-ai sync` invocation that resolves a stale
1741
+ // managed-asset set without abandoning the frozen candidate. Decoded verbatim
1742
+ // from internal/cli/review_operation_contract.go's ReviewManagedAssetsContinuation.
1743
+ export function decodeReviewManagedAssetsContinuationV1(value: unknown, label: string): ReviewManagedAssetsContinuationV1 {
1744
+ const body = exactRecord(value, label, ["operation", "command"], ["agent", "stale_assets"]);
1745
+ const operation = enumeration(body.operation, ["sync"] as const, `${label}.operation`);
1746
+ const command = nonempty(body.command, `${label}.command`);
1747
+ const agent = body.agent === undefined ? undefined : nonempty(body.agent, `${label}.agent`);
1748
+ const staleAssets = body.stale_assets === undefined ? undefined : stringArray(body.stale_assets, `${label}.stale_assets`, { minimum: 1 });
1749
+ return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
1750
+ }
1751
+
1676
1752
  export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boolean; v6?: boolean } = {}): ReviewNextTransitionV3 {
1677
1753
  const v6 = options.v6 === true;
1678
1754
  const v5 = options.v5 === true || v6;
1679
- const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : [])]);
1755
+ const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation", "unachievable_lens_slots"]);
1680
1756
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] as const, "next_transition.kind");
1681
1757
  const reasonCode = text(transition.reason_code, "next_transition.reason_code", { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1758
+ const continuation = transition.continuation === undefined ? undefined : decodeReviewManagedAssetsContinuationV1(transition.continuation, "next_transition.continuation");
1759
+ // gentle-pi#627: continuation is optional on every status version an older
1760
+ // gentle-ai may emit a managed_assets_outdated stop with no continuation at
1761
+ // all, and decode must not refuse the whole envelope for that. When present
1762
+ // it is only valid on a stop with this exact reason_code.
1763
+ if (continuation !== undefined && !(kind === "stop" && reasonCode === "managed_assets_outdated")) throw new TypeError("next_transition.continuation is only valid for a stop transition with reason_code managed_assets_outdated");
1764
+
1765
+ // gentle-pi#638: the unachievable stop names one entry per declared slot, each carrying its complete withdraw command (schemas/status-v7.schema.json, the stop variant). Like the continuation, the field is only valid on its own exact stop reason code, and an entry is refused rather than silently dropped when the provider drifts.
1766
+ let unachievableLensSlots: readonly ReviewUnachievableLensSlotV3[] | undefined;
1767
+ if (transition.unachievable_lens_slots !== undefined) {
1768
+ if (!(kind === "stop" && reasonCode === "unachievable_lens_slot")) throw new TypeError("next_transition.unachievable_lens_slots is only valid for a stop transition with reason_code unachievable_lens_slot");
1769
+ unachievableLensSlots = array(transition.unachievable_lens_slots, "next_transition.unachievable_lens_slots", decodeUnachievableLensSlot, { minimum: 1 });
1770
+ }
1682
1771
 
1683
1772
  // status/v5: the bounded correction plan request rides exactly its two
1684
1773
  // reason codes and never any other (vendored status-v5.schema.json).
@@ -1728,7 +1817,64 @@ export function decodeReviewNextTransitionV3(value: unknown, options: { v5?: boo
1728
1817
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1729
1818
  }
1730
1819
  if (transition.execute !== undefined || transition.collect !== undefined) throw new TypeError("next_transition stop cannot carry a transition");
1731
- return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1820
+ return { kind, reasonCode, ...(correctionRequest === undefined ? {} : { correctionRequest }), ...(continuation === undefined ? {} : { continuation }), ...(unachievableLensSlots === undefined ? {} : { unachievableLensSlots }) };
1821
+ }
1822
+
1823
+ // Mirrors NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT (lib/native-review-cli.ts) and Go's 512-byte CAPTURE_UNACHIEVABLE detail bound.
1824
+ export const REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT = 512;
1825
+
1826
+ function decodeUnachievableLensSlot(value: unknown, label: string): ReviewUnachievableLensSlotV3 {
1827
+ const body = exactRecord(value, label, ["lens", "selected_order", "subject_hash", "reason", "withdraw"], ["detail"]);
1828
+ const lens = nonempty(body.lens, `${label}.lens`);
1829
+ const selectedOrder = integer(body.selected_order, `${label}.selected_order`);
1830
+ const subjectHash = sha256(body.subject_hash, `${label}.subject_hash`);
1831
+ const reason = nonempty(body.reason, `${label}.reason`);
1832
+ const detail = body.detail === undefined ? undefined : nonempty(body.detail, `${label}.detail`);
1833
+ // gentle-pi#822: Go refuses a CAPTURE_UNACHIEVABLE detail above 512 UTF-8 bytes, so the stop decoder mirrors the same bound — measured in bytes, not UTF-16 code units — and a STATUS stop can never carry a detail this client would have refused to declare.
1834
+ if (detail !== undefined && Buffer.byteLength(detail, "utf8") > REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT) throw new TypeError(`${label}.detail exceeds ${REVIEW_INTEGRATION_UNACHIEVABLE_LENS_DETAIL_LIMIT} bytes`);
1835
+ const withdraw = exactRecord(body.withdraw, `${label}.withdraw`, ["operation", "command", "arguments", "binding"]);
1836
+ const operation = enumeration(withdraw.operation, ["review.capture-unachievable"] as const, `${label}.withdraw.operation`);
1837
+ const command = nonempty(withdraw.command, `${label}.withdraw.command`);
1838
+ const arguments_ = decodeTransitionArguments(withdraw.arguments, `${label}.withdraw.arguments`);
1839
+ // Withdrawal is a single affirmative native action, never an optional or
1840
+ // negatable provider argument. The general token and command checks below
1841
+ // then prove its exact --withdraw=true rendering is the complete vector.
1842
+ const withdrawArguments = arguments_.filter((argument) => argument.name === "withdraw");
1843
+ if (withdrawArguments.length !== 1) throw new TypeError(`${label}.withdraw.arguments withdraw must appear exactly once`);
1844
+ if (withdrawArguments[0]!.value !== "true") throw new TypeError(`${label}.withdraw.arguments withdraw must be true`);
1845
+ // The binding keeps the execute branch's open-record discipline: the Go shape is target_identity plus optional lineage_id, revision, and repository_context, and closing it here would make Pi stricter than the contract it implements.
1846
+ const binding = exactRecord(withdraw.binding, `${label}.withdraw.binding`, ["target_identity"], ["lineage_id", "revision", "repository_context"], true);
1847
+ const targetIdentity = sha256(binding.target_identity, `${label}.withdraw.binding.target_identity`);
1848
+ const lineageId = binding.lineage_id === undefined ? undefined : lineage(binding.lineage_id, `${label}.withdraw.binding.lineage_id`);
1849
+ const revision = binding.revision === undefined ? undefined : sha256(binding.revision, `${label}.withdraw.binding.revision`);
1850
+ // gentle-pi#822: the withdraw form names the slot identity twice — as named arguments and as the binding object — and the two renderings must agree before the slot decodes. Strict equality also enforces both-or-neither on the optional lineage and revision fields, so a partially rendered binding never slips through and restart cannot withdraw a different slot. Each identity argument must appear EXACTLY once: a first-match lookup let a duplicate {name} entry smuggle a second value past the identity checks.
1851
+ const withdrawIdentityArgument = (name: string): ReviewTransitionArgumentV3 | undefined => {
1852
+ const matches = arguments_.filter((argument) => argument.name === name);
1853
+ if (matches.length > 1) throw new TypeError(`${label}.withdraw.arguments ${name} must appear exactly once`);
1854
+ return matches[0];
1855
+ };
1856
+ const withdrawRequestHash = withdrawIdentityArgument("request-hash");
1857
+ if (withdrawRequestHash?.value !== subjectHash) throw new TypeError(`${label}.withdraw.arguments request-hash does not match the slot subject_hash`);
1858
+ const withdrawTarget = withdrawIdentityArgument("target");
1859
+ if (withdrawTarget?.value !== targetIdentity) throw new TypeError(`${label}.withdraw.arguments target does not match the withdraw binding target_identity`);
1860
+ const withdrawLineage = withdrawIdentityArgument("lineage");
1861
+ if (withdrawLineage?.value !== lineageId) throw new TypeError(`${label}.withdraw.arguments lineage does not match the withdraw binding lineage_id`);
1862
+ const withdrawExpectedRevision = withdrawIdentityArgument("expected-revision");
1863
+ if (withdrawExpectedRevision?.value !== revision) throw new TypeError(`${label}.withdraw.arguments expected-revision does not match the withdraw binding revision`);
1864
+ // gentle-pi#822: command is executable authority, not a display hint. Every
1865
+ // provider-issued argument token must be the canonical --name=value rendering,
1866
+ // and command must render the canonical operation plus those tokens exactly in
1867
+ // order. This rejects omitted tokens, a second command, suffixes, and shell
1868
+ // payloads rather than merely finding identity-token substrings.
1869
+ const tokens = arguments_.map((argument, index) => {
1870
+ if (argument.token === undefined) throw new TypeError(`${label}.withdraw.arguments[${index}].token is required`);
1871
+ const expectedToken = `--${argument.name}=${argument.value}`;
1872
+ if (argument.token !== expectedToken) throw new TypeError(`${label}.withdraw.arguments[${index}].token must exactly render its name and value`);
1873
+ return argument.token;
1874
+ });
1875
+ const expectedCommand = `gentle-ai review capture-unachievable ${tokens.join(" ")}`;
1876
+ if (command !== expectedCommand) throw new TypeError(`${label}.withdraw.command must exactly render the canonical operation and withdraw arguments`);
1877
+ return { lens, selectedOrder, subjectHash, reason, ...(detail === undefined ? {} : { detail }), withdraw: { operation, command, arguments: arguments_, binding: { targetIdentity, ...(lineageId === undefined ? {} : { lineageId }), ...(revision === undefined ? {} : { revision }) } } };
1732
1878
  }
1733
1879
 
1734
1880
  // ---------------------------------------------------------------------------
@@ -1778,7 +1924,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1778
1924
  // key set plus the optional forecast and the v5-only next_transition
1779
1925
  // surfaces. status/v6 adds the intended-untracked selection; status/v7
1780
1926
  // (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
1781
- // adds only the top-level optional `eligible_untracked_inventory` digest,
1927
+ // adds optional `eligible_untracked_inventory` and escalation metadata,
1782
1928
  // so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
1783
1929
  // field.
1784
1930
  const schema = typeof value === "object" && value !== null ? (value as Record<string, unknown>).schema : undefined;
@@ -1787,7 +1933,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1787
1933
  const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1788
1934
  const body = exactRecord(value, "status", [
1789
1935
  "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1790
- ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory"] : [])]);
1936
+ ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory", "escalation"] : [])]);
1791
1937
  requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1792
1938
 
1793
1939
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] as const, "status.applicability");
@@ -1817,11 +1963,12 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1817
1963
 
1818
1964
  let frozen: ReviewStatusFrozenV1 | undefined;
1819
1965
  if (body.frozen !== undefined) {
1820
- const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"]);
1966
+ const source = exactRecord(body.frozen, "status.frozen", ["tier", "original_changed_lines", "correction_budget"], ["changed_path_manifest_sha256"]);
1821
1967
  frozen = {
1822
1968
  tier: enumeration(source.tier, RISK_LEVELS, "status.frozen.tier"),
1823
1969
  originalChangedLines: integer(source.original_changed_lines, "status.frozen.original_changed_lines"),
1824
1970
  correctionBudget: integer(source.correction_budget, "status.frozen.correction_budget", 0, 200),
1971
+ ...(source.changed_path_manifest_sha256 === undefined ? {} : { changedPathManifestSha256: sha256(source.changed_path_manifest_sha256, "status.frozen.changed_path_manifest_sha256") }),
1825
1972
  };
1826
1973
  }
1827
1974
  if (authority?.version === REVIEW_AUTHORITY_VERSION.COMPACT_V2 && frozen === undefined) throw new TypeError("compact-v2 status requires frozen metadata");
@@ -1870,6 +2017,24 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1870
2017
  };
1871
2018
  }
1872
2019
 
2020
+ let escalation: ReviewEscalationV1 | undefined;
2021
+ if (Object.hasOwn(body, "escalation")) {
2022
+ const label = "status.escalation";
2023
+ const source = exactRecord(body.escalation, label, ["cause", "finding_ids"], ["refuter_outcomes"]);
2024
+ escalation = {
2025
+ cause: enumeration(source.cause, ESCALATION_CAUSES, `${label}.cause`),
2026
+ findingIds: stringArray(source.finding_ids, `${label}.finding_ids`),
2027
+ };
2028
+ if (Object.hasOwn(source, "refuter_outcomes")) escalation.refuterOutcomes = array(source.refuter_outcomes, `${label}.refuter_outcomes`, (entry, itemLabel) => {
2029
+ const row = exactRecord(entry, itemLabel, ["finding_id", "outcome", "proof"]);
2030
+ return {
2031
+ findingId: nonempty(row.finding_id, `${itemLabel}.finding_id`),
2032
+ outcome: enumeration(row.outcome, ESCALATION_REFUTER_OUTCOMES, `${itemLabel}.outcome`),
2033
+ proof: nonempty(row.proof, `${itemLabel}.proof`),
2034
+ };
2035
+ });
2036
+ }
2037
+
1873
2038
  // status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
1874
2039
  // closed loop where `sdd-attempt finish` named a digest status never
1875
2040
  // published. Absent on the `staged` projection, which never resolves an
@@ -1897,6 +2062,7 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
1897
2062
  ...(forecast === undefined ? {} : { forecast }),
1898
2063
  ...(repositoryContext === undefined ? {} : { repositoryContext }),
1899
2064
  ...(validationRequest === undefined ? {} : { validationRequest }),
2065
+ ...(escalation === undefined ? {} : { escalation }),
1900
2066
  ...(eligibleUntrackedInventory === undefined ? {} : { eligibleUntrackedInventory }),
1901
2067
  raw: body,
1902
2068
  };
@@ -2048,9 +2214,10 @@ function decodeFailureContext(value: unknown, label: string): ReviewFailureConte
2048
2214
  export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2049
2215
  const body = exactRecord(value, "failure", [
2050
2216
  "schema", "contract", "operation", "phase", "code", "message", "mutation_outcome", "authority_applicability", "retry_safe", "replayability", "required_inputs", "next_action",
2051
- ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context"]);
2217
+ ], ["lineage_id", "request_digest", "progress_identity", "cause_category", "cause", "context", "continuation"]);
2052
2218
  requireIdentity(body, "gentle-ai.review-integration.failure/v2");
2053
2219
  const operation = enumeration(body.operation, FAILURE_OPERATIONS, "failure.operation");
2220
+ const code = text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ });
2054
2221
 
2055
2222
  if (body.progress_identity !== undefined) {
2056
2223
  if (body.lineage_id === undefined || body.request_digest === undefined) throw new TypeError("failure.progress_identity requires lineage_id and request_digest");
@@ -2059,13 +2226,18 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2059
2226
  if (body.request_digest !== undefined && operation === REVIEW_INTEGRATION_OPERATION.REPAIR && body.progress_identity === undefined) {
2060
2227
  throw new TypeError("failure.request_digest with review.repair requires progress_identity");
2061
2228
  }
2229
+ // gentle-pi#627: continuation is optional on every version — an older
2230
+ // gentle-ai may emit managed_assets_outdated with no continuation, and
2231
+ // decode must not refuse the whole envelope for that. When present it is
2232
+ // only valid on this exact code.
2233
+ if (body.continuation !== undefined && code !== "managed_assets_outdated") throw new TypeError("failure.continuation is only valid for code managed_assets_outdated");
2062
2234
 
2063
2235
  return {
2064
2236
  schema: "gentle-ai.review-integration.failure/v2",
2065
2237
  contract: REVIEW_INTEGRATION_CONTRACT,
2066
2238
  operation,
2067
2239
  phase: enumeration(body.phase, ["preflight", "pre_native", "native_running", "native_committed", "reconciliation"] as const, "failure.phase"),
2068
- code: text(body.code, "failure.code", { pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }),
2240
+ code,
2069
2241
  message: text(body.message, "failure.message", { minimum: 1, maximum: 240, pattern: /^[^\r\n]+$/ }),
2070
2242
  mutationOutcome: enumeration(body.mutation_outcome, Object.values(REVIEW_MUTATION_OUTCOME), "failure.mutation_outcome"),
2071
2243
  authorityApplicability: enumeration(body.authority_applicability, Object.values(REVIEW_AUTHORITY_APPLICABILITY), "failure.authority_applicability"),
@@ -2079,6 +2251,7 @@ export function decodeReviewFailureV2(value: unknown): ReviewFailureV2 {
2079
2251
  ...(body.cause_category === undefined ? {} : { causeCategory: text(body.cause_category, "failure.cause_category", { minimum: 1, pattern: /^[a-z0-9]+(?:_[a-z0-9]+)*$/ }) }),
2080
2252
  ...(body.cause === undefined ? {} : { cause: text(body.cause, "failure.cause", { minimum: 1, maximum: 4000, pattern: /^[^\r\n]+$/ }) }),
2081
2253
  ...(body.context === undefined ? {} : { context: decodeFailureContext(body.context, "failure.context") }),
2254
+ ...(body.continuation === undefined ? {} : { continuation: decodeReviewManagedAssetsContinuationV1(body.continuation, "failure.continuation") }),
2082
2255
  raw: body,
2083
2256
  };
2084
2257
  }
@@ -2400,6 +2573,28 @@ export function decodeReviewAcknowledgedV1(value: unknown, expected: ReviewAckno
2400
2573
  return decoded;
2401
2574
  }
2402
2575
 
2576
+ const REVIEW_LAST_EVENT_FINDING_SEVERITIES = ["BLOCKER", "CRITICAL", "WARNING", "SUGGESTION"] as const;
2577
+ const REVIEW_LAST_EVENT_EVIDENCE_CLASSES = ["deterministic", "inferential", "insufficient"] as const;
2578
+ const REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS = ["introduced", "behavior-activated", "worsened", "pre-existing", "base-only", "unknown"] as const;
2579
+
2580
+ export interface ReviewLastEventReviewerFindingV1 {
2581
+ id: string;
2582
+ lens: string;
2583
+ location: string;
2584
+ severity: (typeof REVIEW_LAST_EVENT_FINDING_SEVERITIES)[number];
2585
+ claim: string;
2586
+ proofRefs: readonly string[];
2587
+ evidenceClass?: (typeof REVIEW_LAST_EVENT_EVIDENCE_CLASSES)[number];
2588
+ causalDisposition?: (typeof REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS)[number];
2589
+ }
2590
+
2591
+ export interface ReviewLastEventReviewerResultV1 {
2592
+ lens: string;
2593
+ findings: readonly ReviewLastEventReviewerFindingV1[];
2594
+ evidence: readonly string[];
2595
+ resultHash: string;
2596
+ }
2597
+
2403
2598
  export interface ReviewLastEventClosureV1 {
2404
2599
  schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
2405
2600
  operation: ReviewLastEventClosureOperation;
@@ -2411,6 +2606,7 @@ export interface ReviewLastEventClosureV1 {
2411
2606
  requestHash?: string;
2412
2607
  correctionLines?: number;
2413
2608
  advisoryFindings?: ReviewAdvisoryFindingsV1;
2609
+ reviewerResults?: readonly ReviewLastEventReviewerResultV1[];
2414
2610
  statusContinuation?: ReviewStatusContinuationV1;
2415
2611
  acknowledgement?: ReviewApprovedAcknowledgementV1;
2416
2612
  // Set when the provider sent an acknowledgement this decoder could not
@@ -2522,8 +2718,38 @@ function decodeReviewStatusContinuationV1(value: unknown): ReviewStatusContinuat
2522
2718
  };
2523
2719
  }
2524
2720
 
2721
+ function decodeReviewLastEventReviewerFindingV1(value: unknown, label: string): ReviewLastEventReviewerFindingV1 {
2722
+ const finding = exactRecord(value, label, ["id", "lens", "location", "severity", "claim", "proof_refs"], ["evidence_class", "causal_disposition"]);
2723
+ const evidenceClass = finding.evidence_class === undefined
2724
+ ? undefined
2725
+ : enumeration(finding.evidence_class, REVIEW_LAST_EVENT_EVIDENCE_CLASSES, `${label}.evidence_class`);
2726
+ const causalDisposition = finding.causal_disposition === undefined
2727
+ ? undefined
2728
+ : enumeration(finding.causal_disposition, REVIEW_LAST_EVENT_CAUSAL_DISPOSITIONS, `${label}.causal_disposition`);
2729
+ return {
2730
+ id: nonempty(finding.id, `${label}.id`),
2731
+ lens: nonempty(finding.lens, `${label}.lens`),
2732
+ location: nonempty(finding.location, `${label}.location`),
2733
+ severity: enumeration(finding.severity, REVIEW_LAST_EVENT_FINDING_SEVERITIES, `${label}.severity`),
2734
+ claim: nonempty(finding.claim, `${label}.claim`),
2735
+ proofRefs: stringArray(finding.proof_refs, `${label}.proof_refs`, { minimum: 1 }),
2736
+ ...(evidenceClass === undefined ? {} : { evidenceClass }),
2737
+ ...(causalDisposition === undefined ? {} : { causalDisposition }),
2738
+ };
2739
+ }
2740
+
2741
+ function decodeReviewLastEventReviewerResultV1(value: unknown, label: string): ReviewLastEventReviewerResultV1 {
2742
+ const result = exactRecord(value, label, ["lens", "findings", "evidence", "result_hash"]);
2743
+ return {
2744
+ lens: nonempty(result.lens, `${label}.lens`),
2745
+ findings: array(result.findings, `${label}.findings`, decodeReviewLastEventReviewerFindingV1),
2746
+ evidence: stringArray(result.evidence, `${label}.evidence`),
2747
+ resultHash: sha256(result.result_hash, `${label}.result_hash`),
2748
+ };
2749
+ }
2750
+
2525
2751
  export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
2526
- const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "status_continuation", "acknowledgement"]);
2752
+ const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "reviewer_results", "status_continuation", "acknowledgement"]);
2527
2753
  if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
2528
2754
  const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
2529
2755
  const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
@@ -2535,6 +2761,7 @@ export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventC
2535
2761
  storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
2536
2762
  };
2537
2763
  if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
2764
+ if (body.reviewer_results !== undefined) throw new TypeError("last_event_closure reviewer_results requires approved state");
2538
2765
  if (body.action !== undefined || body.advisory_findings !== undefined || body.status_continuation !== undefined) throw new TypeError("last_event_closure correction-plan cannot carry action, advisory_findings, or status_continuation");
2539
2766
  if (state !== "correction_required") throw new TypeError("last_event_closure correction-plan requires correction_required state");
2540
2767
  return {
@@ -2602,10 +2829,15 @@ export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventC
2602
2829
  ? undefined
2603
2830
  : decodeReviewAdvisoryFindingsV1(body.advisory_findings, "last_event_closure.advisory_findings");
2604
2831
  if (advisoryFindings !== undefined && state !== "approved") throw new TypeError("last_event_closure advisory_findings requires approved state");
2832
+ const reviewerResults = body.reviewer_results === undefined
2833
+ ? undefined
2834
+ : array(body.reviewer_results, "last_event_closure.reviewer_results", decodeReviewLastEventReviewerResultV1);
2835
+ if (reviewerResults !== undefined && state !== "approved") throw new TypeError("last_event_closure reviewer_results requires approved state");
2605
2836
  return {
2606
2837
  ...shared,
2607
2838
  action,
2608
2839
  ...(advisoryFindings === undefined ? {} : { advisoryFindings }),
2840
+ ...(reviewerResults === undefined ? {} : { reviewerResults }),
2609
2841
  ...(statusContinuation === undefined ? {} : { statusContinuation }),
2610
2842
  ...(acknowledgement === undefined ? {} : { acknowledgement }),
2611
2843
  ...(acknowledgementUndecodable ? { acknowledgementUndecodable: true as const } : {}),
@@ -4,6 +4,7 @@ import type { ReviewLastEventClosureBinding, ReviewStatusV3 } from "./review-int
4
4
  export interface ReviewLastEventCaptureSelector {
5
5
  readonly baseRef?: string;
6
6
  readonly committedOnly?: true;
7
+ readonly agent?: "pi";
7
8
  }
8
9
 
9
10
  /**
@@ -25,11 +26,14 @@ export async function reconcileUnknownReviewLastEventCapture(
25
26
  lineageId: binding.lineageId,
26
27
  ...(selector === undefined ? {} : selector),
27
28
  });
28
- if (binding.targetIdentity !== undefined && status.targetIdentity !== binding.targetIdentity) {
29
- throw new TypeError("capture reconciliation returned a different target");
29
+ if (status.applicability !== "current_target") {
30
+ throw new TypeError("capture reconciliation STATUS is not current for the bound target");
30
31
  }
31
- if (status.authority?.lineageId !== undefined && status.authority.lineageId !== binding.lineageId) {
32
- throw new TypeError("capture reconciliation returned a different lineage");
32
+ if (status.authority?.lineageId !== binding.lineageId) {
33
+ throw new TypeError("capture reconciliation returned missing or different lineage authority");
34
+ }
35
+ if (binding.targetIdentity === undefined || status.targetIdentity !== binding.targetIdentity) {
36
+ throw new TypeError("capture reconciliation returned missing or different target");
33
37
  }
34
38
  return status;
35
39
  }
@@ -14,3 +14,14 @@
14
14
 
15
15
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
16
16
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
17
+
18
+ // The host is this extension: every process the session starts (pi's shell
19
+ // tools, Gentle Agents children) runs under it, so the handshake belongs in
20
+ // the session environment too, not only in the extension's own invocations.
21
+ // Without it the first `gentle-ai review status --agent pi` typed in the
22
+ // session shell fails closed (gentle-pi#550). Returns whether anything changed.
23
+ export function declareReviewRelayHandshake(env: NodeJS.ProcessEnv): boolean {
24
+ if (env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] === GENTLE_PI_REVIEW_RELAY_CONTRACT) return false;
25
+ env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = GENTLE_PI_REVIEW_RELAY_CONTRACT;
26
+ return true;
27
+ }
@@ -0,0 +1,74 @@
1
+ import { isAbsolute } from "node:path";
2
+
3
+ // Reminder bookkeeping only: these entries neither define review scope nor grant
4
+ // authority. Both extensions append to the parent's active Pi session branch.
5
+ export const REVIEW_REMINDER_RECEIPT = "gentle-pi.review-reminder-receipt/v1";
6
+ export interface ReceiptSession {
7
+ getSessionId(): string;
8
+ getBranch(): readonly { type: string; customType?: string; data?: unknown }[];
9
+ }
10
+ interface ReceiptHost { appendEntry(type: string, data: unknown): void }
11
+ export interface MutationEvidence {
12
+ source: "direct" | "subagent";
13
+ toolName: "write" | "edit";
14
+ toolCallId: string;
15
+ taskId?: string;
16
+ }
17
+ type Mutation = MutationEvidence & { kind: "mutation"; sessionId: string; root: string; id: string };
18
+ type Consumption = { kind: "nudged" | "acknowledged"; sessionId: string; root: string; through: string; targetIdentity: string };
19
+ type Receipt = Mutation | Consumption;
20
+ const text = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
21
+
22
+ function valid(value: unknown): value is Receipt {
23
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
24
+ const data = value as Record<string, unknown>;
25
+ if (!text(data.sessionId) || !text(data.root) || !isAbsolute(data.root)) return false;
26
+ let fields: string[];
27
+ if (data.kind === "mutation") {
28
+ if (!text(data.id) || !text(data.toolCallId) || (data.toolName !== "write" && data.toolName !== "edit")) return false;
29
+ if (data.source !== "direct" && data.source !== "subagent") return false;
30
+ if (data.source === "subagent" ? !text(data.taskId) : data.taskId !== undefined) return false;
31
+ if (data.id !== JSON.stringify([data.source, data.taskId ?? "", data.toolCallId])) return false;
32
+ fields = ["kind", "sessionId", "root", "id", "source", "toolName", "toolCallId", ...(data.source === "subagent" ? ["taskId"] : [])];
33
+ } else {
34
+ if (data.kind !== "nudged" && data.kind !== "acknowledged") return false;
35
+ if (!text(data.through) || !text(data.targetIdentity)) return false;
36
+ fields = ["kind", "sessionId", "root", "through", "targetIdentity"];
37
+ }
38
+ return Object.keys(data).length === fields.length && fields.every((field) => Object.hasOwn(data, field));
39
+ }
40
+
41
+ function receipts(session: ReceiptSession, root: string): Receipt[] {
42
+ const sessionId = session.getSessionId();
43
+ return session.getBranch().flatMap((entry) => entry.type === "custom" && entry.customType === REVIEW_REMINDER_RECEIPT &&
44
+ valid(entry.data) && entry.data.sessionId === sessionId && entry.data.root === root ? [entry.data] : []);
45
+ }
46
+
47
+ export function pendingReviewMutation(session: ReceiptSession, root: string, captured?: string): string | undefined {
48
+ const mutations: string[] = [];
49
+ let consumed = -1;
50
+ for (const receipt of receipts(session, root)) {
51
+ if (receipt.kind === "mutation") {
52
+ if (!mutations.includes(receipt.id)) mutations.push(receipt.id);
53
+ } else {
54
+ // Only consume the captured prefix, not writes arriving while native
55
+ // STATUS/ACK was awaited. Unknown or off-branch watermarks do nothing.
56
+ consumed = Math.max(consumed, mutations.indexOf(receipt.through));
57
+ }
58
+ }
59
+ if (captured !== undefined) return mutations.indexOf(captured) > consumed ? captured : undefined;
60
+ return mutations.length - 1 > consumed ? mutations.at(-1) : undefined;
61
+ }
62
+
63
+ export function recordReviewMutation(host: ReceiptHost, session: ReceiptSession, root: string, evidence: MutationEvidence): void {
64
+ const id = JSON.stringify([evidence.source, evidence.taskId ?? "", evidence.toolCallId]);
65
+ const receipt: Mutation = { kind: "mutation", sessionId: session.getSessionId(), root, id, ...evidence };
66
+ if (!valid(receipt) || receipts(session, root).some((entry) => entry.kind === "mutation" && entry.id === id)) return;
67
+ host.appendEntry(REVIEW_REMINDER_RECEIPT, receipt);
68
+ }
69
+
70
+ export function consumeReviewMutation(host: ReceiptHost, session: ReceiptSession, root: string, through: string | undefined, kind: Consumption["kind"], targetIdentity: string): void {
71
+ if (through === undefined) return;
72
+ const receipt: Consumption = { kind, sessionId: session.getSessionId(), root, through, targetIdentity };
73
+ if (valid(receipt)) host.appendEntry(REVIEW_REMINDER_RECEIPT, receipt);
74
+ }
@@ -55,8 +55,8 @@ export function reviewGitEnvironment(): NodeJS.ProcessEnv {
55
55
  const environment: NodeJS.ProcessEnv = {};
56
56
  for (const [key, value] of Object.entries(process.env)) if (!key.startsWith("GIT_")) environment[key] = value;
57
57
  environment.GIT_CONFIG_NOSYSTEM = "1";
58
- environment.GIT_CONFIG_GLOBAL = process.platform === "win32" ? "NUL" : "/dev/null";
59
- environment.GIT_CONFIG_SYSTEM = process.platform === "win32" ? "NUL" : "/dev/null";
58
+ environment.GIT_CONFIG_GLOBAL = "/dev/null";
59
+ environment.GIT_CONFIG_SYSTEM = "/dev/null";
60
60
  environment.GIT_OPTIONAL_LOCKS = "0";
61
61
  environment.LC_ALL = "C";
62
62
  environment.LANG = "C";