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
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, posix, win32 } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
7
7
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
8
+ import { decodeReviewAssessmentV1, type ReviewAssessmentV1 } from "./review-risk-assessment.ts";
8
9
  import {
9
10
  REVIEW_INTEGRATION_CONTRACT,
10
11
  decodeReviewAcknowledgedV1,
@@ -59,11 +60,16 @@ export const NATIVE_REVIEW_OPERATION = {
59
60
  RECONCILE_AUTHORITY: "review/reconcile-authority",
60
61
  REPAIR_LEGACY_ALIAS: "review/repair-legacy-alias",
61
62
  MODE: "review/mode",
63
+ ASSESS: "review/assess",
62
64
  REPAIR: "review/repair",
63
65
  CAPTURE_RESULT: "review/capture-result",
64
66
  CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
65
67
  CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
68
+ CAPTURE_UNACHIEVABLE: "review/capture-unachievable",
66
69
  ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
70
+ SDD_STATUS: "sdd-status",
71
+ SDD_ATTEMPT: "sdd-attempt",
72
+ SDD_CONTINUE: "sdd-continue",
67
73
  } as const;
68
74
  export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
69
75
 
@@ -104,11 +110,56 @@ export interface NativeReviewCli {
104
110
  captureResult?(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome>;
105
111
  captureCorrectionPlan?(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1>;
106
112
  captureProviderRole?(request: NativeReviewProviderRoleCaptureRequest): Promise<NativeReviewProviderRoleCaptureOutcome>;
113
+ // gentle-pi#638: records the host relay's declaration that one bound selected lens slot cannot be completed under current conditions. Younger than every binary pinned in NATIVE_CLI_CONTRACTS, so the capability is gated invocation-adjacent (see isNativeReviewUnachievableVerbRefused) instead of by a contract row.
114
+ captureUnachievableLens?(request: NativeReviewUnachievableLensCaptureRequest): Promise<NativeReviewUnachievableLensCaptureArtifact>;
107
115
  // Dark until a negotiated version reports the `mode` capability true
108
116
  // (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
109
117
  // outside the negotiated review-integration protocol — same shape as
110
118
  // reviewStatus/reclaim above.
111
119
  reviewMode?(request: NativeReviewModeRequest): Promise<NativeReviewModeResult>;
120
+ // Read-only risk assessment (gentle-ai#4295, landing in parallel with
121
+ // gentle-pi#662). Same plain-versioned shape as reviewMode/reviewStatus:
122
+ // an older binary without the verb, or any other process/decode failure,
123
+ // rejects the returned promise -- callers fail closed to `high` risk.
124
+ assess?(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1>;
125
+ // Exact native SDD readiness authority for an explicitly selected phase.
126
+ // This is intentionally separate from review/RDD gates and carries no
127
+ // delivery or review-transaction authority.
128
+ sddStatus?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
129
+ sddContinue?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
130
+ sddAttemptAcquire?(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult>;
131
+ sddAttemptSettle?(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult>;
132
+ }
133
+
134
+ export interface NativeSddAttemptRequest extends Pick<NativeUntrackedSelectionRequest, "untrackedScope" | "expectedUntrackedInventory" | "intendedUntracked"> {
135
+ workspaceRoot: string;
136
+ changeName: string;
137
+ requestId: string;
138
+ remediatesEvidenceRevision?: string;
139
+ }
140
+ export interface NativeSddAcquireRequest extends NativeSddAttemptRequest {
141
+ workUnit: string;
142
+ evidenceGoal: string;
143
+ maxAttempts?: number;
144
+ maxChangedLines?: number;
145
+ expectedRevision?: string;
146
+ token?: string;
147
+ }
148
+ export const SDD_ATTEMPT_OUTCOME = { PASSED: "passed", FAILED: "failed", INTERRUPTED: "interrupted" } as const;
149
+ export interface NativeSddSettleRequest extends NativeSddAttemptRequest {
150
+ token: string;
151
+ outcome: (typeof SDD_ATTEMPT_OUTCOME)[keyof typeof SDD_ATTEMPT_OUTCOME];
152
+ evidenceRevision?: string;
153
+ remediationEvidence?: string;
154
+ diagnosis: string;
155
+ harnessDisposition: "reused" | "invalidated";
156
+ cleanupEvidence: string;
157
+ processEvidence: string;
158
+ }
159
+ export interface NativeSddAttemptResult {
160
+ state: "proceed" | "blocked" | "complete";
161
+ token?: string;
162
+ reason?: string;
112
163
  }
113
164
 
114
165
  export const NATIVE_REVIEW_MODE_OPERATION = {
@@ -145,12 +196,52 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
145
196
  } as const;
146
197
  export type NativeReviewModeScope = (typeof NATIVE_REVIEW_MODE_SCOPE)[keyof typeof NATIVE_REVIEW_MODE_SCOPE];
147
198
 
199
+ export interface NativeSddStatusRequest {
200
+ changeName?: string;
201
+ workspaceRoot: string;
202
+ signal?: AbortSignal;
203
+ }
204
+
205
+ export type NativeSddPhase = "apply" | "verify" | "archive";
206
+ export type NativeSddDependencyState = "blocked" | "ready" | "all_done";
207
+
208
+ /**
209
+ * The native CLI owns this complete v2 record. The decoder validates the fields
210
+ * Pi relies on and returns the original object without adding, omitting, or
211
+ * reconciling local SDD state.
212
+ */
213
+ export interface NativeSddStatusV2 extends Readonly<Record<string, unknown>> {
214
+ schemaName: "gentle-ai.sdd-status";
215
+ schemaVersion: 2;
216
+ changeName: string | null;
217
+ artifactStore: "openspec" | "engram" | "hybrid" | "none";
218
+ planningHome: Readonly<Record<string, unknown>> & { mode: "repo-local"; path: string };
219
+ changeRoot: string | null;
220
+ actionContext: Readonly<Record<string, unknown>> & { mode: "repo-local"; workspaceRoot: string; allowedEditRoots: readonly string[] };
221
+ dependencies: Readonly<Record<(typeof NATIVE_SDD_DEPENDENCIES)[number], NativeSddDependencyState>>;
222
+ phaseInstructions?: Readonly<Record<(typeof NATIVE_SDD_INSTRUCTION_PHASES)[number], readonly string[]>>;
223
+ blockedReasons: readonly string[];
224
+ nextRecommended: string;
225
+ remediationState?: { required: boolean; complete: boolean; failedEvidenceRevision: string };
226
+ }
227
+
148
228
  export interface NativeReviewModeRequest {
149
229
  cwd: string;
150
230
  operation: NativeReviewModeOperation;
151
231
  signal?: AbortSignal;
152
232
  }
153
233
 
234
+ // Read-only risk assessment request (gentle-pi#662). `baseRef` requires
235
+ // explicit `committedOnly` acknowledgement, exactly like Native START's
236
+ // baseRef/committedOnly pairing, because both select a committed range
237
+ // instead of the ambient working tree.
238
+ export interface NativeReviewAssessRequest {
239
+ cwd: string;
240
+ baseRef?: string;
241
+ committedOnly?: boolean;
242
+ signal?: AbortSignal;
243
+ }
244
+
154
245
  export interface NativeReviewModeStatus {
155
246
  global: NativeReviewModeValue;
156
247
  cloneLocal: NativeReviewModeValue;
@@ -261,7 +352,6 @@ export interface NativeReviewAbandonRequest {
261
352
  snapshotIdentity: string;
262
353
  capturedLensResults: readonly string[];
263
354
  findingsPresent: boolean;
264
- evidenceRecordsPresent: boolean;
265
355
  actor: string;
266
356
  reason: string;
267
357
  maintainerAuthorization: string;
@@ -414,6 +504,49 @@ export interface NativeReviewProviderRoleCaptureArtifact {
414
504
  readonly captured: true;
415
505
  }
416
506
 
507
+ // gentle-pi#638: the typed declaration that one bound selected lens slot cannot be completed under current conditions. Mirrored from Go's reviewUnachievableLensCaptureArtifact (internal/cli/review_capture_unachievable.go): same schema identity, same closed field set, and the same 512-byte detail bound Go enforces, so a declaration this client refuses locally can never reach a binary that would accept it, and vice versa.
508
+ export const NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA = "gentle-ai.review-capture-unachievable/v1";
509
+ export const NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT = 512;
510
+
511
+ export interface NativeReviewUnachievableLensCaptureRequest {
512
+ /** Exact frozen review lineage identifier of the offered slot. */
513
+ readonly lineageId: string;
514
+ /** Exact frozen target identity of the offered slot. */
515
+ readonly targetIdentity: string;
516
+ /** Exact reviewing-authority revision the collect transition offered. */
517
+ readonly expectedRevision: string;
518
+ /** The offered slot's subject-hash; Go resolves the lens from it. */
519
+ readonly requestHash: string;
520
+ /** Short machine-readable cause, e.g. relay_transport_bound_exceeded. */
521
+ readonly reason: string;
522
+ /** Optional bounded evidence; refused above the Go-side byte limit. */
523
+ readonly detail?: string;
524
+ /** Opaque provider-issued repository context the slot carried, if any. */
525
+ readonly repositoryContext?: string;
526
+ /** Process working directory only; never rendered into the invocation. */
527
+ readonly cwd: string;
528
+ readonly signal?: AbortSignal;
529
+ }
530
+
531
+ export interface NativeReviewUnachievableLensCaptureArtifact {
532
+ readonly schema: typeof NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA;
533
+ readonly lineageId: string;
534
+ readonly targetIdentity: string;
535
+ readonly lens: string;
536
+ readonly selectedOrder: number;
537
+ readonly reason: string;
538
+ readonly recorded: true;
539
+ }
540
+
541
+ // gentle-pi#638 fail-open capability gate: `review capture-unachievable` is younger than every released binary pinned in NATIVE_CLI_CONTRACTS, so the verb is gated invocation-adjacent instead of by a capability row. An older binary renders Go's exact `unknown review command "capture-unachievable"` refusal (internal/cli/review_facade.go) on stderr with no stdout, so the invocation rejects before any decode and the captured diagnostics are the only place that text survives. Every other failure -- a typed binding-mismatch refusal, a timeout, a decode failure -- is a real outcome the caller must surface, never a capability signal. Duck-typed on purpose: the classifier must survive a duplicated module instance exactly like the error it inspects.
542
+ const NATIVE_REVIEW_UNKNOWN_UNACHIEVABLE_VERB_REFUSAL = /unknown review command "capture-unachievable"/;
543
+
544
+ export function isNativeReviewUnachievableVerbRefused(error: unknown): boolean {
545
+ if (typeof error !== "object" || error === null) return false;
546
+ const stderr = (error as { diagnostics?: { stderr?: unknown } }).diagnostics?.stderr;
547
+ return typeof stderr === "string" && NATIVE_REVIEW_UNKNOWN_UNACHIEVABLE_VERB_REFUSAL.test(stderr);
548
+ }
549
+
417
550
  /** Refuter and validator captures close only when they are the native last event. */
418
551
  export type NativeReviewProviderRoleCaptureOutcome = NativeReviewProviderRoleCaptureArtifact | ReviewLastEventClosureV1;
419
552
 
@@ -498,8 +631,13 @@ export const NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION = {
498
631
  } as const;
499
632
  export type NativeReviewAuthorityEntryVersion = (typeof NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)[keyof typeof NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION];
500
633
 
501
- export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = NATIVE_REVIEW_AUTHORITY_STATUS;
502
- export type NativeReviewAuthorityEntryStatus = NativeReviewAuthorityStatus;
634
+ export const NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS = {
635
+ ...NATIVE_REVIEW_AUTHORITY_STATUS,
636
+ INCOMPLETE_STORE_ENTRY: "incomplete-store-entry",
637
+ HISTORICAL_PRE_RECEIPT: "historical-pre-receipt",
638
+ INVALIDATED: "invalidated",
639
+ } as const;
640
+ export type NativeReviewAuthorityEntryStatus = (typeof NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS)[keyof typeof NATIVE_REVIEW_AUTHORITY_ENTRY_STATUS];
503
641
 
504
642
  export const NATIVE_REVIEW_LOCK_STATUS = {
505
643
  OWNED: "owned",
@@ -541,6 +679,10 @@ export interface NativeReviewRecovery {
541
679
  recoveredAt: string;
542
680
  maintainerAuthorization?: string;
543
681
  }
682
+ export interface NativeReviewDiscardedWorkSummary {
683
+ capturedLensResults: readonly string[];
684
+ findingsPresent: boolean;
685
+ }
544
686
  export interface NativeReviewAuthorityEntry {
545
687
  version: NativeReviewAuthorityEntryVersion;
546
688
  lineageId?: string;
@@ -551,6 +693,7 @@ export interface NativeReviewAuthorityEntry {
551
693
  snapshotIdentity?: string;
552
694
  chainIdentity?: string;
553
695
  recovery?: NativeReviewRecovery;
696
+ discardedWork?: NativeReviewDiscardedWorkSummary;
554
697
  problems: readonly string[];
555
698
  }
556
699
  export interface NativeReviewAuthorityLock {
@@ -804,6 +947,25 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
804
947
  // consumes did not change between 2.5.0 and 2.6.0, so the columns match
805
948
  // the 2.5.0 row.
806
949
  "2.6.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
950
+ // Ground-truthed against the published v2.7.0 binary installed from its
951
+ // signed archive: `review capabilities` on the v2 lane still advertises
952
+ // capabilities/v2.5 (protocol minor 5) and answers status/v7, consent/v3,
953
+ // and the `start/v4` continuation, all of which the decoders already read.
954
+ // `review mode status --json` still answers `gentle-ai.rdd-mode-status/v1`
955
+ // and `review validate --gate pre-commit` still answers
956
+ // `gentle-ai.review-gate-result/v1` carrying `delivery`, both verified
957
+ // against the binary. riskEvidence and hint stay dark for the same reason
958
+ // they have on every row since 2.2.0: still not proven to reach the
959
+ // negotiated START path Pi reads. The closed fields Pi consumes did not
960
+ // change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
961
+ "2.7.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
962
+ // Ground-truthed against the published v2.8.0 linux/amd64 binary from its
963
+ // signed release archive. The v2 lane still advertises capabilities/v2.5
964
+ // (protocol minor 5) with status/v7, consent/v3, and start/v4 schemas.
965
+ // The closed fields Pi consumes did not change between 2.7.0 and 2.8.0,
966
+ // so this row repeats 2.7.0. riskEvidence and hint remain dark because
967
+ // neither is proven to reach the negotiated START path Pi consumes.
968
+ "2.8.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
807
969
  });
808
970
 
809
971
  export interface NativeReviewProcessDiagnostics {
@@ -835,7 +997,7 @@ export class NativeReviewCliError extends Error {
835
997
  this.launchAttempted = launchAttempted;
836
998
  this.mutating = mutating;
837
999
  this.mutationOutcome = launchAttempted && mutating ? "unknown" : "none";
838
- this.nextAction = this.mutationOutcome === "unknown" ? "review.status" : undefined;
1000
+ this.nextAction = this.mutationOutcome === "unknown" && operation !== NATIVE_REVIEW_OPERATION.SDD_ATTEMPT ? "review.status" : undefined;
839
1001
  this.diagnostics = diagnostics ?? { operation, error_code: code, timed_out: false, output_limit_exceeded: false };
840
1002
  this.auditRecord = auditRecord;
841
1003
  }
@@ -1046,8 +1208,15 @@ function decodeNativeReviewRecovery(value: unknown): NativeReviewRecovery {
1046
1208
  ...(recovery.maintainer_authorization === undefined ? {} : { maintainerAuthorization: requiredString(recovery.maintainer_authorization) }),
1047
1209
  };
1048
1210
  }
1211
+ function decodeNativeReviewDiscardedWorkSummary(value: unknown): NativeReviewDiscardedWorkSummary {
1212
+ const discardedWork = exactObject(value, ["captured_lens_results", "findings_present"]);
1213
+ return {
1214
+ capturedLensResults: stringArray(discardedWork.captured_lens_results),
1215
+ findingsPresent: booleanValue(discardedWork.findings_present),
1216
+ };
1217
+ }
1049
1218
  function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEntry {
1050
- const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery"]);
1219
+ const entry = exactObject(value, ["version", "path", "status", "problems"], ["lineage_id", "state", "revision", "snapshot_identity", "chain_identity", "recovery", "discarded_work"]);
1051
1220
  return {
1052
1221
  version: enumString(entry.version, Object.values(NATIVE_REVIEW_AUTHORITY_ENTRY_VERSION)) as NativeReviewAuthorityEntryVersion,
1053
1222
  ...(entry.lineage_id === undefined ? {} : { lineageId: requiredString(entry.lineage_id) }),
@@ -1058,6 +1227,7 @@ function decodeNativeReviewStatusEntry(value: unknown): NativeReviewAuthorityEnt
1058
1227
  ...(entry.snapshot_identity === undefined ? {} : { snapshotIdentity: sha256Identity(entry.snapshot_identity) }),
1059
1228
  ...(entry.chain_identity === undefined ? {} : { chainIdentity: requiredString(entry.chain_identity) }),
1060
1229
  ...(entry.recovery === undefined ? {} : { recovery: decodeNativeReviewRecovery(entry.recovery) }),
1230
+ ...(entry.discarded_work === undefined ? {} : { discardedWork: decodeNativeReviewDiscardedWorkSummary(entry.discarded_work) }),
1061
1231
  problems: stringArray(entry.problems),
1062
1232
  };
1063
1233
  }
@@ -1252,6 +1422,46 @@ interface NativeJsonExecution {
1252
1422
  process: ExecFileResult;
1253
1423
  }
1254
1424
 
1425
+ const NATIVE_SDD_DEPENDENCIES = ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"] as const;
1426
+ const NATIVE_SDD_INSTRUCTION_PHASES = ["apply", "verify", "remediate", "archive"] as const;
1427
+ const NATIVE_SDD_NEXT_RECOMMENDATIONS = ["apply", "verify", "remediate", "archive", "archived", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
1428
+ const NATIVE_SDD_DEPENDENCY_STATES = ["blocked", "ready", "all_done"] as const;
1429
+
1430
+ /** Strictly validates the native v2 contract while preserving its whole record. */
1431
+ export function decodeNativeSddStatusV2(value: unknown, request: Pick<NativeSddStatusRequest, "changeName" | "workspaceRoot">): NativeSddStatusV2 {
1432
+ const status = object(value);
1433
+ if (status.schemaName !== "gentle-ai.sdd-status" || status.schemaVersion !== 2) throw new Error("wrong native SDD status schema");
1434
+ if ((request.changeName !== undefined && status.changeName !== request.changeName) || (status.changeName !== null && !isCanonicalProcessString(status.changeName))) throw new Error("native SDD status change identity mismatch");
1435
+ const artifactStore = enumString(status.artifactStore, ["openspec", "engram", "hybrid", "none"]);
1436
+ const planningHome = object(status.planningHome);
1437
+ if (planningHome.mode !== "repo-local" || !isCanonicalProcessString(planningHome.path)) throw new Error("invalid native SDD planning home");
1438
+ const expectedOpenSpecHome = join(request.workspaceRoot, "openspec");
1439
+ if (planningHome.path !== expectedOpenSpecHome && !((artifactStore === "engram" || artifactStore === "hybrid") && planningHome.path === "engram:sdd")) throw new Error("native SDD planning home escaped its workspace");
1440
+ if (status.changeRoot !== null && !isCanonicalProcessString(status.changeRoot)) throw new Error("invalid native SDD change root");
1441
+ const actionContext = object(status.actionContext);
1442
+ if (actionContext.mode !== "repo-local" || actionContext.workspaceRoot !== request.workspaceRoot || !isCanonicalProcessString(actionContext.workspaceRoot)) throw new Error("native SDD status workspace root mismatch");
1443
+ const allowedEditRoots = stringArray(actionContext.allowedEditRoots);
1444
+ if (!allowedEditRoots.includes(request.workspaceRoot) || allowedEditRoots.some((root) => !isAbsolute(root) || root !== join(root))) throw new Error("invalid native SDD allowed edit roots");
1445
+ const dependencies = object(status.dependencies);
1446
+ for (const phase of NATIVE_SDD_DEPENDENCIES) {
1447
+ if (enumString(dependencies[phase], NATIVE_SDD_DEPENDENCY_STATES) !== dependencies[phase]) throw new Error("invalid native SDD dependency");
1448
+ }
1449
+ if (Object.keys(dependencies).length !== NATIVE_SDD_DEPENDENCIES.length) throw new Error("native SDD dependencies have an unsupported shape");
1450
+ if (status.instructions !== undefined) throw new Error("native SDD status uses phaseInstructions, not instructions");
1451
+ if (status.phaseInstructions !== undefined) {
1452
+ const instructions = object(status.phaseInstructions);
1453
+ for (const phase of NATIVE_SDD_INSTRUCTION_PHASES) stringArray(instructions[phase]);
1454
+ if (Object.keys(instructions).length !== NATIVE_SDD_INSTRUCTION_PHASES.length) throw new Error("native SDD instructions have an unsupported shape");
1455
+ }
1456
+ if (status.nextRecommended === "remediate" || status.remediationState !== undefined) {
1457
+ const remediation = object(status.remediationState);
1458
+ if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || typeof remediation.failedEvidenceRevision !== "string" || (remediation.failedEvidenceRevision !== "" && !/^sha256:[0-9a-f]{64}$/.test(remediation.failedEvidenceRevision)) || (status.nextRecommended === "remediate" && (!remediation.required || remediation.complete || !remediation.failedEvidenceRevision))) throw new Error("Invalid native remediation state");
1459
+ }
1460
+ stringArray(status.blockedReasons);
1461
+ enumString(status.nextRecommended, NATIVE_SDD_NEXT_RECOMMENDATIONS);
1462
+ return status as NativeSddStatusV2;
1463
+ }
1464
+
1255
1465
  class NativeReviewPlainCli {
1256
1466
  private readonly adapter: ExecFileAdapter;
1257
1467
  private readonly executable: string | (() => string);
@@ -1372,9 +1582,7 @@ class NativeReviewPlainCli {
1372
1582
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
1373
1583
  }
1374
1584
  if (!Array.isArray(request.capturedLensResults) || request.capturedLensResults.some((entry) => !isCanonicalProcessString(entry))) throw new TypeError("Native ABANDON capturedLensResults must be an array of non-empty, trimmed, NUL-free strings");
1375
- for (const [name, value] of [["findingsPresent", request.findingsPresent], ["evidenceRecordsPresent", request.evidenceRecordsPresent]] as const) {
1376
- if (typeof value !== "boolean") throw new TypeError(`Native ABANDON ${name} must be a boolean`);
1377
- }
1585
+ if (typeof request.findingsPresent !== "boolean") throw new TypeError("Native ABANDON findingsPresent must be a boolean");
1378
1586
  if (request.maintainerAuthorization !== nativeReviewAbandonAuthorization(request)) throw new TypeError("Native ABANDON maintainerAuthorization must match the exact lineage, revision, snapshot, reason, discarded-work, and actor binding");
1379
1587
  const execution = await this.execute(NATIVE_REVIEW_OPERATION.ABANDON, request.cwd, [
1380
1588
  "review", "abandon", "--cwd", request.cwd,
@@ -1463,7 +1671,11 @@ class NativeReviewPlainCli {
1463
1671
  }
1464
1672
  }
1465
1673
 
1466
- export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "capturedLensResults" | "findingsPresent" | "evidenceRecordsPresent" | "actor" | "reason">): string {
1674
+ export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAbandonRequest, "lineage" | "expectedRevision" | "snapshotIdentity" | "capturedLensResults" | "findingsPresent" | "actor" | "reason">): string {
1675
+ // gentle-ai 0ed9225f removed evidence records from the discarded-work summary,
1676
+ // so the native v2 gate verifies an exact eight-line binding (schema, lineage,
1677
+ // revision, snapshot_identity, reason, captured_lens_results, findings_present,
1678
+ // actor) — there is no evidence_records_present line to derive or relay.
1467
1679
  return [
1468
1680
  "gentle-ai.review-abandon-authorization/v2",
1469
1681
  `lineage=${request.lineage}`,
@@ -1472,7 +1684,6 @@ export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAband
1472
1684
  `reason=${request.reason}`,
1473
1685
  `captured_lens_results=${request.capturedLensResults.join(",")}`,
1474
1686
  `findings_present=${request.findingsPresent}`,
1475
- `evidence_records_present=${request.evidenceRecordsPresent}`,
1476
1687
  `actor=${request.actor.trim()}`,
1477
1688
  ].join("\n");
1478
1689
  }
@@ -1595,14 +1806,20 @@ function splitNativeConsentInvocation(invocation: string): readonly string[] {
1595
1806
  let quote: "'" | '"' | undefined;
1596
1807
  let escaping = false;
1597
1808
  let started = false;
1598
- for (const character of invocation.trim()) {
1809
+ const source = invocation.trim();
1810
+ for (let index = 0; index < source.length; index += 1) {
1811
+ const character = source[index]!;
1599
1812
  if (escaping) {
1600
1813
  current += character;
1601
1814
  escaping = false;
1602
1815
  started = true;
1603
1816
  continue;
1604
1817
  }
1605
- if (character === "\\" && quote !== "'") {
1818
+ // Provider invocations are not shell commands. Keep every path backslash
1819
+ // verbatim, including quoted UNC and drive-root paths. Outside quotes,
1820
+ // only a backslash before whitespace joins a space-containing token.
1821
+ const next = source[index + 1];
1822
+ if (character === "\\" && quote === undefined && next !== undefined && /\s/.test(next)) {
1606
1823
  escaping = true;
1607
1824
  started = true;
1608
1825
  continue;
@@ -1693,7 +1910,9 @@ function consentInvocationArguments(request: NativeReviewConsentAnswerRequest):
1693
1910
  if (words[0] !== "gentle-ai" || words[1] !== "review" || words[2] !== "start") throw new NativeReviewConsentBindingError("consent-invocation-not-start", "Native consent invocation is not a provider review START");
1694
1911
  const arguments_ = words.slice(1);
1695
1912
  if (exactConsentOption(arguments_, "--contract") !== REVIEW_INTEGRATION_CONTRACT) throw new NativeReviewConsentBindingError("consent-invocation-contract-changed", "Native consent invocation contract changed");
1696
- if (exactConsentOption(arguments_, "--cwd") !== request.cwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1913
+ const providerCwd = exactConsentOption(arguments_, "--cwd");
1914
+ const requestedCwd = normalizeNativeReviewCwd(request.cwd);
1915
+ if (normalizeNativeReviewCwd(providerCwd) !== requestedCwd) throw new NativeReviewConsentBindingError("consent-invocation-cwd-changed", "Native consent invocation repository binding changed");
1697
1916
  if (exactConsentOption(arguments_, "--target") !== request.consent.targetIdentity) throw new NativeReviewConsentBindingError("consent-invocation-target-changed", "Native consent invocation target binding changed");
1698
1917
  if (exactConsentOption(arguments_, "--projection") !== request.consent.projection) throw new NativeReviewConsentBindingError("consent-invocation-projection-changed", "Native consent invocation projection binding changed");
1699
1918
  const lineageId = optionalConsentLineageOption(arguments_);
@@ -1773,6 +1992,23 @@ function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewPr
1773
1992
  });
1774
1993
  }
1775
1994
 
1995
+ // gentle-pi#638: the recorded declaration Go prints for one unachievable slot. Closed key set, exact schema identity, and the lens Go itself resolved from the request hash -- the host never asserts which lens failed, only reads back the one the provider bound.
1996
+ function decodeNativeUnachievableLensCaptureArtifact(value: unknown): NativeReviewUnachievableLensCaptureArtifact {
1997
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("native unachievable lens capture artifact must be an object");
1998
+ const body = value as Record<string, unknown>;
1999
+ const allowed = new Set(["schema", "lineage_id", "target_identity", "lens", "selected_order", "reason", "recorded"]);
2000
+ for (const key of Object.keys(body)) if (!allowed.has(key)) throw new TypeError(`native unachievable lens capture artifact carries unexpected key ${key}`);
2001
+ const text = (key: string): string => {
2002
+ const found = body[key];
2003
+ if (typeof found !== "string" || found.trim() !== found || found.length === 0) throw new TypeError(`native unachievable lens capture artifact ${key} must be a non-empty trimmed string`);
2004
+ return found;
2005
+ };
2006
+ if (text("schema") !== NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA) throw new TypeError(`native unachievable lens capture artifact schema must be ${NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA}`);
2007
+ if (typeof body.selected_order !== "number" || !Number.isSafeInteger(body.selected_order) || body.selected_order < 0) throw new TypeError("native unachievable lens capture artifact selected_order must be a non-negative integer");
2008
+ if (body.recorded !== true) throw new TypeError("native unachievable lens capture artifact must report recorded: true");
2009
+ return Object.freeze({ schema: NATIVE_REVIEW_UNACHIEVABLE_LENS_CAPTURE_SCHEMA, lineageId: text("lineage_id"), targetIdentity: text("target_identity"), lens: text("lens"), selectedOrder: body.selected_order, reason: text("reason"), recorded: true });
2010
+ }
2011
+
1776
2012
  export class NativeReviewCliV216 implements NativeReviewCli {
1777
2013
  private readonly plain: NativeReviewPlainCli;
1778
2014
  private readonly adapter: ExecFileAdapter;
@@ -1864,6 +2100,89 @@ export class NativeReviewCliV216 implements NativeReviewCli {
1864
2100
  return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
1865
2101
  }
1866
2102
 
2103
+ async sddAttemptAcquire(request: NativeSddAcquireRequest): Promise<NativeSddAttemptResult> {
2104
+ return this.sddAttempt("acquire", request);
2105
+ }
2106
+
2107
+ async sddAttemptSettle(request: NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
2108
+ return this.sddAttempt("settle", request);
2109
+ }
2110
+
2111
+ private async sddAttempt(verb: "acquire" | "settle", request: NativeSddAcquireRequest | NativeSddSettleRequest): Promise<NativeSddAttemptResult> {
2112
+ const args = ["sdd-attempt", verb, "--cwd", request.workspaceRoot, "--change", request.changeName, "--request-id", request.requestId];
2113
+ if (!isAbsolute(request.workspaceRoot) || !isCanonicalProcessString(request.workspaceRoot) || !isCanonicalProcessString(request.changeName) || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(request.requestId)) throw new TypeError("Invalid SDD attempt identity");
2114
+ const text = (flag: string, value: string, max: number) => {
2115
+ if (!isCanonicalProcessString(value) || /[\r\n]/.test(value) || Buffer.byteLength(value) > max) throw new TypeError(`Invalid ${flag}`);
2116
+ args.push(flag, value);
2117
+ };
2118
+ const revision = (flag: string, value: string | undefined) => {
2119
+ if (value === undefined) return;
2120
+ if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`Invalid ${flag}`);
2121
+ args.push(flag, value);
2122
+ };
2123
+ if (verb === "acquire") {
2124
+ const acquire = request as NativeSddAcquireRequest;
2125
+ text("--work-unit", acquire.workUnit, 160);
2126
+ text("--evidence-goal", acquire.evidenceGoal, 240);
2127
+ for (const [flag, value, max] of [["--max-attempts", acquire.maxAttempts, 100], ["--max-changed-lines", acquire.maxChangedLines, 1_000_000]] as const) {
2128
+ if (value === undefined) continue;
2129
+ if (!Number.isInteger(value) || value < 1 || value > max) throw new TypeError(`Invalid ${flag}`);
2130
+ args.push(flag, String(value));
2131
+ }
2132
+ if (acquire.expectedRevision === "") args.push("--expected-revision", "");
2133
+ else revision("--expected-revision", acquire.expectedRevision);
2134
+ if (acquire.token !== undefined) text("--token", acquire.token, 500);
2135
+ } else {
2136
+ const settle = request as NativeSddSettleRequest;
2137
+ text("--token", settle.token, 500);
2138
+ if (!Object.values(SDD_ATTEMPT_OUTCOME).includes(settle.outcome)) throw new TypeError("Invalid outcome");
2139
+ args.push("--outcome", settle.outcome);
2140
+ if (settle.outcome === "interrupted" ? settle.evidenceRevision !== undefined || settle.remediationEvidence !== undefined : !settle.evidenceRevision && !(settle.outcome === "passed" && settle.remediationEvidence)) throw new TypeError("Invalid terminal evidence");
2141
+ revision("--evidence-revision", settle.evidenceRevision);
2142
+ text("--diagnosis", settle.diagnosis, 500);
2143
+ if (!["reused", "invalidated"].includes(settle.harnessDisposition)) throw new TypeError("Invalid harness disposition");
2144
+ args.push("--harness-disposition", settle.harnessDisposition);
2145
+ text("--cleanup-evidence", settle.cleanupEvidence, 500);
2146
+ text("--process-evidence", settle.processEvidence, 500);
2147
+ if (settle.remediationEvidence !== undefined) args.push("--remediation-evidence", settle.remediationEvidence);
2148
+ }
2149
+ revision("--remediates-evidence-revision", request.remediatesEvidenceRevision);
2150
+ args.push(...nativeUntrackedSelectionArguments(nativeUntrackedSelection(request)));
2151
+ const operation = NATIVE_REVIEW_OPERATION.SDD_ATTEMPT;
2152
+ const { body } = await this.negotiated(operation, request.workspaceRoot, args, true);
2153
+ return decode(operation, true, () => {
2154
+ const result = body as unknown as NativeSddAttemptResult;
2155
+ if (!result || !["proceed", "blocked", "complete"].includes(result.state)) throw new TypeError("Invalid compact state");
2156
+ if (verb === "acquire" && result.state === "proceed" && !isCanonicalProcessString(result.token)) throw new TypeError("Missing compact token");
2157
+ if (result.reason !== undefined && typeof result.reason !== "string") throw new TypeError("Invalid compact reason");
2158
+ return { state: result.state, ...(result.token === undefined ? {} : { token: result.token }), ...(result.reason === undefined ? {} : { reason: result.reason }) };
2159
+ });
2160
+ }
2161
+
2162
+ async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
2163
+ return this.sddProjection(request, false);
2164
+ }
2165
+
2166
+ async sddContinue(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
2167
+ if (!isCanonicalProcessString(request.changeName)) throw new TypeError("Native SDD continuation requires an exact selected change");
2168
+ return this.sddProjection(request, true);
2169
+ }
2170
+
2171
+ private async sddProjection(request: NativeSddStatusRequest, mutating: boolean): Promise<NativeSddStatusV2> {
2172
+ if ((request.changeName !== undefined && !isCanonicalProcessString(request.changeName)) || !isCanonicalProcessString(request.workspaceRoot) || !isAbsolute(request.workspaceRoot)) {
2173
+ throw new TypeError("Native SDD status requires a canonical change and absolute workspace root");
2174
+ }
2175
+ const operation = mutating ? NATIVE_REVIEW_OPERATION.SDD_CONTINUE : NATIVE_REVIEW_OPERATION.SDD_STATUS;
2176
+ const execution = await this.negotiated(
2177
+ operation,
2178
+ request.workspaceRoot,
2179
+ [operation, ...(request.changeName === undefined ? [] : [request.changeName]), "--cwd", request.workspaceRoot, "--json", "--instructions"],
2180
+ mutating,
2181
+ request.signal,
2182
+ );
2183
+ return decode(operation, mutating, () => decodeNativeSddStatusV2(execution.body, request));
2184
+ }
2185
+
1867
2186
  async start(request: NativeStartRequest): Promise<NativeStartResult> {
1868
2187
  if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1869
2188
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
@@ -2138,6 +2457,25 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2138
2457
  });
2139
2458
  }
2140
2459
 
2460
+ // gentle-pi#638: `gentle-ai review capture-unachievable` records one bound declaration that the exact slot the collect transition offered cannot be completed under current conditions. Unlike every other capture verb the host runs, this one reports a failure instead of a result, so it never takes the provider-issued tokens verbatim: the declaration's binding values are named fields re-derived from the slot, and Go verifies them against the frozen lineage, revision, target, and subject hash before recording anything (internal/cli/review_capture_unachievable.go). A binding that no longer matches is a typed refusal the caller surfaces, never a host-side reconstruction.
2461
+ async captureUnachievableLens(request: NativeReviewUnachievableLensCaptureRequest): Promise<NativeReviewUnachievableLensCaptureArtifact> {
2462
+ for (const [field, value] of (["lineageId", "reason"] as const).map((field) => [field, request[field]] as const)) {
2463
+ if (!isCanonicalProcessString(value)) throw new TypeError(`Native CAPTURE_UNACHIEVABLE ${field} must be a non-empty, trimmed, NUL-free string`);
2464
+ }
2465
+ // The three identity fields are digests on every slot the provider offers, so a non-sha value means the declaration binding was parsed wrong; refuse it locally instead of invoking a doomed process.
2466
+ for (const [field, value] of (["targetIdentity", "expectedRevision", "requestHash"] as const).map((field) => [field, request[field]] as const)) {
2467
+ if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`Native CAPTURE_UNACHIEVABLE ${field} must be a canonical SHA-256 identity`);
2468
+ }
2469
+ const detail = request.detail === undefined ? "" : request.detail.trim();
2470
+ // gentle-pi#822: the native limit is 512 UTF-8 bytes, not 512 UTF-16 code units — the Go side measures the encoded payload, so a multibyte detail needs Buffer.byteLength; a .length check would let 300 two-byte characters through.
2471
+ if (Buffer.byteLength(detail, "utf8") > NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT) throw new TypeError(`Native CAPTURE_UNACHIEVABLE detail exceeds ${NATIVE_REVIEW_UNACHIEVABLE_LENS_DETAIL_LIMIT} bytes`);
2472
+ if (request.repositoryContext !== undefined && !isCanonicalProcessString(request.repositoryContext)) throw new TypeError("Native CAPTURE_UNACHIEVABLE repositoryContext must be a non-empty, trimmed, NUL-free string");
2473
+ const executable = this.executablePath(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, true);
2474
+ // gentle-pi#822: --repository-context is authoritative and mutually exclusive with a path (the captureProviderRole discipline), so --cwd rides the invocation only when no repository context names it; the process working directory stays request.cwd either way.
2475
+ const execution = await this.invoke(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, request.cwd, ["review", "capture-unachievable", "--lineage", request.lineageId, "--target", request.targetIdentity, "--expected-revision", request.expectedRevision, "--request-hash", request.requestHash, "--reason", request.reason, ...(detail === "" ? [] : ["--detail", detail]), ...(request.repositoryContext === undefined ? ["--cwd", request.cwd] : ["--repository-context", request.repositoryContext])], true, request.signal, executable);
2476
+ return decode(NATIVE_REVIEW_OPERATION.CAPTURE_UNACHIEVABLE, true, () => decodeNativeUnachievableLensCaptureArtifact(execution.body));
2477
+ }
2478
+
2141
2479
  // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
2142
2480
  // execute transition exactly as rendered. The tokens come verbatim from
2143
2481
  // the negotiated next_transition (e.g. `--lineage=<id>
@@ -2193,6 +2531,26 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2193
2531
  return decode(NATIVE_REVIEW_OPERATION.MODE, mutating, () => decodeNativeReviewMode(execution.body, request.operation));
2194
2532
  }
2195
2533
 
2534
+ // Read-only risk assessment (gentle-ai#4295, gentle-pi#662). Never mutates;
2535
+ // an older binary without the verb, or any other process/decode failure,
2536
+ // rejects -- callers (the `gentle_review` tool's `assess` operation) fail
2537
+ // closed to `high`.
2538
+ async assess(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1> {
2539
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
2540
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
2541
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
2542
+ const cwd = await canonicalNativeReviewCwd(request.cwd);
2543
+ const execution = await this.invoke(
2544
+ NATIVE_REVIEW_OPERATION.ASSESS,
2545
+ cwd,
2546
+ ["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
2547
+ false,
2548
+ request.signal,
2549
+ this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
2550
+ );
2551
+ return decode(NATIVE_REVIEW_OPERATION.ASSESS, false, () => decodeReviewAssessmentV1(execution.body));
2552
+ }
2553
+
2196
2554
  // Recovery commands are version-gated plain CLI operations outside the
2197
2555
  // negotiated integration-v1 contract, exactly like reviewStatus.
2198
2556
  reclaim(request: NativeReviewReclaimRequest): Promise<NativeReviewRecoveryResult> {