gentle-pi 2.5.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 (189) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -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 +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +799 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +130 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +283 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +199 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +166 -0
  68. package/lib/runtime-metrics-pi-identity.ts +113 -0
  69. package/lib/runtime-metrics-policy.ts +51 -0
  70. package/lib/runtime-metrics.ts +255 -0
  71. package/lib/sdd-preflight.ts +362 -81
  72. package/lib/sdd-research-capabilities.ts +228 -0
  73. package/lib/sdd-status.ts +29 -7
  74. package/lib/session-worktree-registry.ts +118 -0
  75. package/lib/shell-bar.ts +47 -1
  76. package/lib/shell-card.ts +1 -4
  77. package/lib/shell-changes-view.ts +362 -37
  78. package/lib/shell-changes.ts +81 -1
  79. package/lib/shell-prompt.ts +11 -15
  80. package/lib/shell-sidebar-banner.ts +11 -0
  81. package/lib/shell-sidebar-layout.ts +213 -0
  82. package/lib/shell-sidebar.ts +41 -0
  83. package/lib/shell-todo.ts +28 -11
  84. package/lib/telemetry-trigger.ts +2 -0
  85. package/package.json +6 -3
  86. package/runtime/gentle-ai-binary.mjs +3 -1
  87. package/runtime/native-review-cli.mjs +283 -1
  88. package/runtime/review-integration-v2.mjs +187 -5
  89. package/runtime/telemetry-trigger.mjs +2 -0
  90. package/scripts/build-runtime-modules.mjs +9 -1
  91. package/scripts/check-types.mjs +125 -0
  92. package/scripts/gentle-ai-installer.mjs +10 -10
  93. package/scripts/install-gentle-ai.mjs +12 -0
  94. package/scripts/install-tui-mode-setting.mjs +114 -0
  95. package/scripts/test-packed-runner.mjs +16 -2
  96. package/scripts/types-baseline.json +99 -0
  97. package/scripts/verify-package-files.mjs +4 -2
  98. package/skills/_shared/review-ledger-contract.md +17 -1
  99. package/skills/issue-creation/SKILL.md +3 -3
  100. package/skills/judgment-day/SKILL.md +17 -3
  101. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  102. package/tests/agent-profiles.test.ts +722 -0
  103. package/tests/agents-completion-delivery.test.ts +94 -0
  104. package/tests/agents-config.test.ts +62 -0
  105. package/tests/agents-fake-child.ts +15 -1
  106. package/tests/agents-grouping.test.ts +179 -0
  107. package/tests/agents-integration.test.ts +100 -0
  108. package/tests/agents-messaging.test.ts +94 -0
  109. package/tests/agents-protocol.test.ts +45 -0
  110. package/tests/agents-queries.test.ts +190 -0
  111. package/tests/agents-responsive.test.ts +43 -0
  112. package/tests/agents-runner.test.ts +571 -14
  113. package/tests/agents-thread-view.test.ts +45 -0
  114. package/tests/agents-view.test.ts +476 -65
  115. package/tests/agents-widget.test.ts +31 -1
  116. package/tests/artifact-language.test.ts +25 -2
  117. package/tests/ask-user-choice.test.ts +169 -3
  118. package/tests/asset-installation-runtime.test.ts +108 -0
  119. package/tests/autonomous-guard.test.ts +116 -1
  120. package/tests/codegraph-tools.test.ts +2 -1
  121. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  122. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  123. package/tests/feature-request-form.test.ts +67 -0
  124. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  125. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  126. package/tests/gentle-agents.test.ts +1460 -33
  127. package/tests/gentle-ai-binary.test.ts +7 -2
  128. package/tests/gentle-ai-installer.test.ts +47 -47
  129. package/tests/gentle-ai-renderer.test.ts +38 -0
  130. package/tests/gentle-ai.test.ts +944 -4
  131. package/tests/gentle-shell.test.ts +303 -12
  132. package/tests/gentle-todo.test.ts +54 -10
  133. package/tests/install-tui-mode-setting.test.ts +324 -0
  134. package/tests/issue-creation-skill.test.ts +22 -0
  135. package/tests/model-routing-authority.test.ts +12 -0
  136. package/tests/native-review-capability-contract.test.ts +12 -1
  137. package/tests/native-review-cli.test.ts +277 -3
  138. package/tests/native-review-parity.test.ts +14 -7
  139. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  140. package/tests/orchestrator-presence.test.ts +389 -0
  141. package/tests/package-manifest.test.ts +232 -7
  142. package/tests/profiles-orchestrator.test.ts +208 -0
  143. package/tests/quiet-tool-rendering.test.ts +1 -0
  144. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  145. package/tests/review-agent-end-preflight.test.ts +332 -24
  146. package/tests/review-candidate-view.test.ts +304 -6
  147. package/tests/review-consent-ui.test.ts +352 -0
  148. package/tests/review-contract-prompt.test.ts +14 -0
  149. package/tests/review-controller-native-routing.test.ts +563 -3
  150. package/tests/review-controller.test.ts +1 -1
  151. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  152. package/tests/review-host-relay-routing.test.ts +364 -4
  153. package/tests/review-host-relay.test.ts +29 -0
  154. package/tests/review-integration-v2-forward.test.ts +44 -0
  155. package/tests/review-integration-v2.test.ts +164 -0
  156. package/tests/review-last-event-closure.test.ts +105 -1
  157. package/tests/review-ledger-contract.test.ts +61 -6
  158. package/tests/review-reminder-receipt.test.ts +62 -0
  159. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  160. package/tests/review-session-standing-permission.test.ts +30 -0
  161. package/tests/runtime-harness.mjs +447 -39
  162. package/tests/runtime-metrics-children.test.ts +206 -0
  163. package/tests/runtime-metrics-delivery.test.ts +85 -0
  164. package/tests/runtime-metrics-extension.test.ts +187 -0
  165. package/tests/runtime-metrics-native.test.ts +209 -0
  166. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  167. package/tests/runtime-metrics-policy.test.ts +62 -0
  168. package/tests/runtime-metrics.test.ts +184 -0
  169. package/tests/sdd-agent-tools.test.ts +10 -1
  170. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  171. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  172. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  173. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  174. package/tests/sdd-preflight.test.ts +252 -8
  175. package/tests/sdd-research-capabilities.test.ts +256 -0
  176. package/tests/sdd-research-live.test.ts +241 -0
  177. package/tests/sdd-selection-transport.test.ts +504 -0
  178. package/tests/sdd-status.test.ts +51 -0
  179. package/tests/session-worktree-registry.test.ts +135 -0
  180. package/tests/shell-card.test.ts +24 -3
  181. package/tests/shell-changes-view.test.ts +471 -8
  182. package/tests/shell-changes.test.ts +168 -0
  183. package/tests/shell-prompt.test.ts +28 -6
  184. package/tests/shell-sidebar-banner.test.ts +23 -0
  185. package/tests/shell-sidebar-layout.test.ts +387 -0
  186. package/tests/shell-sidebar.test.ts +50 -0
  187. package/tests/shell-todo.test.ts +100 -11
  188. package/tests/startup-banner.test.ts +126 -0
  189. package/tests/telemetry-trigger.test.ts +3 -1
@@ -6,7 +6,7 @@ import { CARD_TONE, cardInnerWidth, renderCard, type CardTheme, type CardTone }
6
6
  // Gentle Agents widget: the card above the editor. Reads task records only
7
7
  // (status, prompt, counters, timestamps), so drawing it costs nothing per
8
8
  // event. One row per task: glyph, agent, task summary, then
9
- // model · tokens · cost · time right-aligned.
9
+ // model · effort · tokens · cost · time right-aligned.
10
10
 
11
11
  export const AGENTS_GLYPH = "❀";
12
12
 
@@ -29,7 +29,7 @@ interface Columns {
29
29
  name: number;
30
30
  task: number;
31
31
  meta: number;
32
- withModel: boolean;
32
+ fullMetrics: boolean;
33
33
  }
34
34
 
35
35
  const LOOK: Record<TaskStatus, StatusLook> = {
@@ -73,6 +73,7 @@ export function formatElapsed(ms: number): string {
73
73
  }
74
74
 
75
75
  function clip(text: string, width: number): string {
76
+ if (width <= 0) return "";
76
77
  if (visibleWidth(text) <= width) return text;
77
78
  let out = "";
78
79
  for (const char of text) {
@@ -139,9 +140,20 @@ function modelLabel(task: TaskRecord): string {
139
140
  return id === "default" ? "" : id;
140
141
  }
141
142
 
142
- function meta(task: TaskRecord, now: number, withModel: boolean): string {
143
+ function executionLabel(task: TaskRecord, width = Infinity): string {
144
+ const model = modelLabel(task);
145
+ const effort = task.thinking ?? "";
146
+ if (!model) return clip(effort, width);
147
+ if (!effort) return clip(model, width);
148
+ const suffix = ` · ${effort}`;
149
+ if (width <= visibleWidth(suffix)) return clip(`${model} · ${effort}`, width);
150
+ return clip(model, width - visibleWidth(suffix)) + suffix;
151
+ }
152
+
153
+ function meta(task: TaskRecord, now: number, fullMetrics: boolean): string {
143
154
  if (task.status === TASK_STATUS.QUEUED) return "queued";
144
- const parts = [withModel ? modelLabel(task) : "", task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? `$${task.cost.toFixed(2)}` : "", elapsed(task, now)];
155
+ if (!fullMetrics) return executionLabel(task);
156
+ const parts = [executionLabel(task), task.tokens > 0 ? formatTokens(task.tokens) : "", task.cost > 0 ? `$${task.cost.toFixed(2)}` : "", elapsed(task, now)];
145
157
  return parts.filter((part) => part.length > 0).join(" · ");
146
158
  }
147
159
 
@@ -151,23 +163,30 @@ function taskText(task: TaskRecord): string {
151
163
  return task.label;
152
164
  }
153
165
 
154
- // Narrow cards give up the task column first, then the model label.
166
+ // Narrow cards give up the task and usage columns before execution metadata.
155
167
  function columns(tasks: readonly TaskRecord[], inner: number, now: number): Columns {
156
- const name = Math.min(NAME_MAX, Math.max(...tasks.map((task) => visibleWidth(task.agent))));
168
+ const name = Math.max(0, Math.min(NAME_MAX, inner - 3, Math.max(...tasks.map((task) => visibleWidth(task.agent)))));
157
169
  const fixed = 1 + GLYPH_GAP.length + name + COLUMN_GAP.length;
158
- const metaWidth = (withModel: boolean) => Math.max(...tasks.map((task) => visibleWidth(meta(task, now, withModel))));
170
+ const metaWidth = (fullMetrics: boolean) => Math.max(...tasks.map((task) => visibleWidth(meta(task, now, fullMetrics))));
159
171
  const full = metaWidth(true);
160
172
  const task = inner - fixed - full - COLUMN_GAP.length;
161
- if (task >= TASK_MIN) return { inner, name, meta: full, task, withModel: true };
162
- return { inner, name, meta: metaWidth(false), task: 0, withModel: false };
173
+ if (task >= TASK_MIN) return { inner, name, meta: full, task, fullMetrics: true };
174
+ return { inner, name, meta: Math.max(0, Math.min(inner - fixed, metaWidth(false))), task: 0, fullMetrics: false };
163
175
  }
164
176
 
165
- function row(task: TaskRecord, theme: CardTheme, cols: Columns, now: number): string[] {
177
+ function row(task: TaskRecord, theme: CardTheme, cols: Columns, now: number, allowMetadataRow: boolean): string[] {
166
178
  const look = LOOK[task.status];
167
179
  const name = clip(task.agent, cols.name);
168
180
  const head = `${theme.fg(look.role, look.glyph)}${GLYPH_GAP}${theme.fg(NAME_ROLE, name)}${" ".repeat(cols.name - visibleWidth(name))}`;
169
- const tail = theme.fg(META_ROLE, meta(task, now, cols.withModel).padStart(cols.meta));
170
- if (cols.task === 0) return [`${head}${" ".repeat(Math.max(COLUMN_GAP.length, cols.inner - visibleWidth(head) - visibleWidth(tail)))}${tail}`];
181
+ const metadata = cols.fullMetrics ? meta(task, now, true) : task.status === TASK_STATUS.QUEUED ? clip("queued", cols.meta) : executionLabel(task, cols.meta);
182
+ const tail = theme.fg(META_ROLE, " ".repeat(Math.max(0, cols.meta - visibleWidth(metadata))) + metadata);
183
+ if (cols.inner < 3) return [theme.fg(look.role, clip(look.glyph, cols.inner))];
184
+ // The scrollable sidebar can preserve identity and execution metadata on
185
+ // separate rows. The height-capped above-editor widget keeps its row budget.
186
+ if (allowMetadataRow && cols.task === 0 && task.status !== TASK_STATUS.QUEUED && visibleWidth(executionLabel(task)) > cols.meta) {
187
+ return [head, theme.fg(META_ROLE, executionLabel(task, cols.inner))];
188
+ }
189
+ if (cols.task === 0) return [`${head}${" ".repeat(Math.max(0, cols.inner - visibleWidth(head) - visibleWidth(tail)))}${tail}`];
171
190
  const text = clip(taskText(task), cols.task);
172
191
  return [`${head}${COLUMN_GAP}${theme.fg(TASK_ROLE, text)}${" ".repeat(cols.task - visibleWidth(text))}${COLUMN_GAP}${tail}`];
173
192
  }
@@ -210,8 +229,8 @@ export function renderAgentsCard(tasks: readonly TaskRecord[], theme: CardTheme,
210
229
  if (shown.length === 0) return [];
211
230
  const cols = columns(shown, cardInnerWidth(width), now);
212
231
  const { listed, hidden } = options.collapsed ? { listed: [shown[0]], hidden: 0 } : visibleRows(shown, options.maxRows);
213
- const hint = options.collapsed && options.collapseKey ? `${options.collapseKey} expand` : batchElapsed(shown, now);
214
- const body = listed.flatMap((task) => row(task, theme, cols, now));
232
+ const hint = options.collapsed && options.collapseKey ? `${options.collapseKey} expand` : shown.length > 1 ? batchElapsed(shown, now) : undefined;
233
+ const body = listed.flatMap((task) => row(task, theme, cols, now, options.maxRows === undefined));
215
234
  if (hidden > 0) body.push(overflowRow(hidden, theme, options.viewKey));
216
235
  return renderCard(
217
236
  { title: "Agents", subtitle: counts(shown), body, tone: tone(shown), glyph: AGENTS_GLYPH },
@@ -16,6 +16,8 @@ import {
16
16
  import { fileURLToPath } from "node:url";
17
17
 
18
18
  export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
19
+ export const GENTLE_AI_INSTALL_RECOVERY_COMMAND = "node scripts/install-gentle-ai.mjs";
20
+ export const GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS = `If GENTLE_PI_SKIP_GENTLE_AI_INSTALL is set, remove or unset it before changing to the installed gentle-pi package directory and running \`${GENTLE_AI_INSTALL_RECOVERY_COMMAND}\`.`;
19
21
  // Derived from the one authoritative pinned version in
20
22
  // scripts/gentle-ai-installer.mjs rather than repeating the literal here, so
21
23
  // the two can never independently drift apart the way they once did.
@@ -25,7 +27,7 @@ export class PackageLocalGentleAiBinaryMissingError extends Error {
25
27
  readonly code = GENTLE_AI_BINARY_MISSING_CODE;
26
28
  constructor(path: string) {
27
29
  super(
28
- `${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}. Reinstall gentle-pi, or use GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1 only for development/offline installs.`,
30
+ `${GENTLE_AI_BINARY_MISSING_CODE}: Gentle AI v${GENTLE_AI_VERSION} is not installed at ${path}. ${GENTLE_AI_INSTALL_RECOVERY_INSTRUCTIONS} A missing binary can result from skipped lifecycle scripts, but does not prove install lifecycle scripts were disabled.`,
29
31
  );
30
32
  this.name = "PackageLocalGentleAiBinaryMissingError";
31
33
  }
@@ -7,7 +7,9 @@ import { sanitizeTerminalText } from "./terminal-theme.ts";
7
7
  // gentle_review tool draws the same card as the other Gentle notices. The
8
8
  // call component owns the top rule; the result component closes the frame.
9
9
 
10
- export type GentleAiRenderTheme = CardTheme;
10
+ export interface GentleAiRenderTheme extends CardTheme {
11
+ bg?(color: string, text: string): string;
12
+ }
11
13
 
12
14
  export interface GentleAiRenderState {
13
15
  lifecycleComponent?: boolean;
@@ -67,12 +69,12 @@ export function getGentleAiRenderState(state: unknown): GentleAiRenderState | un
67
69
  // result is in, the result card closes it instead.
68
70
  export class GentleAiCallCard {
69
71
  private card: Card = { title: CARD_TITLE, body: [], tone: CARD_TONE.WARNING };
70
- private theme: CardTheme = passthroughTheme;
72
+ private theme: GentleAiRenderTheme = passthroughTheme;
71
73
  private detail: string | undefined;
72
74
  private hint: string | undefined;
73
75
  private open = true;
74
76
 
75
- update(status: LifecycleStatus, operationPath: string, theme: CardTheme, detail?: string, hint?: string): void {
77
+ update(status: LifecycleStatus, operationPath: string, theme: GentleAiRenderTheme, detail?: string, hint?: string): void {
76
78
  this.card = { title: CARD_TITLE, subtitle: `${status} · ${operationPath}`, body: [], tone: STATUS_TONE[status], glyph: CARD_GLYPH };
77
79
  this.theme = theme;
78
80
  this.detail = detail;
@@ -98,10 +100,10 @@ export class GentleAiResultCard {
98
100
  private readonly text: string;
99
101
  private readonly expanded: boolean;
100
102
  private readonly tone: CardTone;
101
- private readonly theme: CardTheme;
103
+ private readonly theme: GentleAiRenderTheme;
102
104
  private readonly partial: boolean;
103
105
 
104
- constructor(text: string, expanded: boolean, tone: CardTone, theme: CardTheme, partial = false) {
106
+ constructor(text: string, expanded: boolean, tone: CardTone, theme: GentleAiRenderTheme, partial = false) {
105
107
  this.text = text;
106
108
  this.expanded = expanded;
107
109
  this.tone = tone;
@@ -139,7 +141,7 @@ export interface GentleAiResultRenderOptions {
139
141
  export function renderGentleAiResult(
140
142
  result: AgentToolResult<unknown>,
141
143
  options: GentleAiResultRenderOptions,
142
- theme: CardTheme = passthroughTheme,
144
+ theme: GentleAiRenderTheme = passthroughTheme,
143
145
  context?: GentleAiRenderContext,
144
146
  ): GentleAiResultCard {
145
147
  const textItems = result.content.flatMap((content) => (content.type === "text" ? [sanitizeTerminalText(content.text)] : []));
@@ -65,7 +65,11 @@ export const NATIVE_REVIEW_OPERATION = {
65
65
  CAPTURE_RESULT: "review/capture-result",
66
66
  CAPTURE_CORRECTION_PLAN: "review/capture-correction-plan",
67
67
  CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
68
+ CAPTURE_UNACHIEVABLE: "review/capture-unachievable",
68
69
  ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
70
+ SDD_STATUS: "sdd-status",
71
+ SDD_ATTEMPT: "sdd-attempt",
72
+ SDD_CONTINUE: "sdd-continue",
69
73
  } as const;
70
74
  export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
71
75
 
@@ -106,6 +110,8 @@ export interface NativeReviewCli {
106
110
  captureResult?(request: NativeReviewCaptureResultRequest): Promise<NativeReviewCaptureResultOutcome>;
107
111
  captureCorrectionPlan?(request: NativeReviewCorrectionPlanCaptureRequest): Promise<ReviewLastEventClosureV1>;
108
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>;
109
115
  // Dark until a negotiated version reports the `mode` capability true
110
116
  // (Design Decision #7, organic-rdd-parity). Plain versioned CLI operation,
111
117
  // outside the negotiated review-integration protocol — same shape as
@@ -116,6 +122,44 @@ export interface NativeReviewCli {
116
122
  // an older binary without the verb, or any other process/decode failure,
117
123
  // rejects the returned promise -- callers fail closed to `high` risk.
118
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;
119
163
  }
120
164
 
121
165
  export const NATIVE_REVIEW_MODE_OPERATION = {
@@ -152,6 +196,35 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
152
196
  } as const;
153
197
  export type NativeReviewModeScope = (typeof NATIVE_REVIEW_MODE_SCOPE)[keyof typeof NATIVE_REVIEW_MODE_SCOPE];
154
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
+
155
228
  export interface NativeReviewModeRequest {
156
229
  cwd: string;
157
230
  operation: NativeReviewModeOperation;
@@ -431,6 +504,49 @@ export interface NativeReviewProviderRoleCaptureArtifact {
431
504
  readonly captured: true;
432
505
  }
433
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
+
434
550
  /** Refuter and validator captures close only when they are the native last event. */
435
551
  export type NativeReviewProviderRoleCaptureOutcome = NativeReviewProviderRoleCaptureArtifact | ReviewLastEventClosureV1;
436
552
 
@@ -843,6 +959,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
843
959
  // negotiated START path Pi reads. The closed fields Pi consumes did not
844
960
  // change between 2.6.0 and 2.7.0, so the columns match the 2.6.0 row.
845
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 }),
846
969
  });
847
970
 
848
971
  export interface NativeReviewProcessDiagnostics {
@@ -874,7 +997,7 @@ export class NativeReviewCliError extends Error {
874
997
  this.launchAttempted = launchAttempted;
875
998
  this.mutating = mutating;
876
999
  this.mutationOutcome = launchAttempted && mutating ? "unknown" : "none";
877
- this.nextAction = this.mutationOutcome === "unknown" ? "review.status" : undefined;
1000
+ this.nextAction = this.mutationOutcome === "unknown" && operation !== NATIVE_REVIEW_OPERATION.SDD_ATTEMPT ? "review.status" : undefined;
878
1001
  this.diagnostics = diagnostics ?? { operation, error_code: code, timed_out: false, output_limit_exceeded: false };
879
1002
  this.auditRecord = auditRecord;
880
1003
  }
@@ -1299,6 +1422,46 @@ interface NativeJsonExecution {
1299
1422
  process: ExecFileResult;
1300
1423
  }
1301
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
+
1302
1465
  class NativeReviewPlainCli {
1303
1466
  private readonly adapter: ExecFileAdapter;
1304
1467
  private readonly executable: string | (() => string);
@@ -1829,6 +1992,23 @@ function decodeNativeProviderRoleCaptureArtifact(value: unknown): NativeReviewPr
1829
1992
  });
1830
1993
  }
1831
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
+
1832
2012
  export class NativeReviewCliV216 implements NativeReviewCli {
1833
2013
  private readonly plain: NativeReviewPlainCli;
1834
2014
  private readonly adapter: ExecFileAdapter;
@@ -1920,6 +2100,89 @@ export class NativeReviewCliV216 implements NativeReviewCli {
1920
2100
  return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
1921
2101
  }
1922
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
+
1923
2186
  async start(request: NativeStartRequest): Promise<NativeStartResult> {
1924
2187
  if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
1925
2188
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
@@ -2194,6 +2457,25 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2194
2457
  });
2195
2458
  }
2196
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
+
2197
2479
  // gentle-pi#311 P5: executes one provider-rendered `review.finalize`
2198
2480
  // execute transition exactly as rendered. The tokens come verbatim from
2199
2481
  // the negotiated next_transition (e.g. `--lineage=<id>