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
@@ -6,9 +6,9 @@ import { join } from "node:path";
6
6
  import test from "node:test";
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
  import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
9
- import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
10
10
  import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
11
- import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+ import { decodeReviewFailureV2, type AuthorityRepairAssessmentV1, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
12
12
 
13
13
  interface RegisteredTool {
14
14
  execute: (
@@ -240,6 +240,49 @@ test("INSPECT and STATUS operate on the explicit workspace root while the sessio
240
240
  assert.deepEqual(observedCwds, [realpathSync(worktree), realpathSync(worktree), sessionCwd]);
241
241
  });
242
242
 
243
+ // gentle-pi#599: inspect without an explicit workspaceRoot against a parent
244
+ // repository that wraps an untracked nested Git repository fails at the
245
+ // native preflight with a decoded, actionable `invalid_request` envelope. The
246
+ // facade used to discard that envelope's cause, code, retry_safe, and
247
+ // next_action in favor of a generic `native-status-unavailable` outcome
248
+ // indistinguishable from authority corruption. It must preserve the native
249
+ // envelope instead.
250
+ test("inspect without workspaceRoot preserves the native invalid_request cause for a nested foreign Git repository", async (t) => {
251
+ const sessionCwd = repository(t);
252
+ const { controller } = runtime(fakeNative({
253
+ targetStatus: async () => {
254
+ throw new NativeReviewIntegrationError(decodeReviewFailureV2({
255
+ schema: "gentle-ai.review-integration.failure/v2",
256
+ contract: "gentle-ai.review-integration/v2",
257
+ operation: "review.status",
258
+ phase: "preflight",
259
+ code: "invalid_request",
260
+ message: "The negotiated review request is invalid.",
261
+ mutation_outcome: "not_started",
262
+ authority_applicability: "not_evaluated",
263
+ retry_safe: true,
264
+ replayability: "not_replayable",
265
+ required_inputs: [],
266
+ next_action: "correct_request",
267
+ cause: "untracked directory \"engram\" holds another Git repository that is not a linked worktree of this one, so it cannot enter the review candidate: add it to .gitignore, move it outside this repository, or register it as a linked worktree",
268
+ }));
269
+ },
270
+ }));
271
+ const result = await controller.execute("inspect-nested-foreign-repo", { operation: "inspect" }, undefined, undefined, context(sessionCwd));
272
+ const details = result.details as {
273
+ status?: string;
274
+ outcome?: string;
275
+ next_action?: string;
276
+ native_failure?: { code?: string; cause?: string; retry_safe?: boolean };
277
+ };
278
+ assert.equal(details.status, "blocked");
279
+ assert.equal(details.outcome, "native-status-unavailable");
280
+ assert.equal(details.native_failure?.code, "invalid_request");
281
+ assert.equal(details.native_failure?.retry_safe, true);
282
+ assert.match(details.native_failure?.cause ?? "", /engram/);
283
+ assert.equal(details.next_action, "correct_request");
284
+ });
285
+
243
286
  test("START freezes the candidate from the explicit workspace root and returns the actor binding envelope", async (t) => {
244
287
  const sessionCwd = repository(t);
245
288
  writeFileSync(join(sessionCwd, "unrelated.ts"), "export const unrelated = true;\n");
@@ -208,7 +208,7 @@ test("controller SDD status treats removed OpenSpec recovery authority and delet
208
208
  const status = await __testing.resolveControllerSddStatus(fixture.repository, changeName, false, "openspec");
209
209
 
210
210
  assert.equal(status.dependencies.archive, "blocked");
211
- assert.equal(status.nextRecommended, "Active change not found: recover-legacy-review-authority.");
211
+ assert.equal(status.nextRecommended, "blocked");
212
212
  assert.match(status.blockedReasons.join("\n"), /active change not found/i);
213
213
  });
214
214
 
@@ -363,6 +363,7 @@ test("general STATUS returns the typed native-status-unsupported boundary withou
363
363
  mutation_performed: false,
364
364
  inventory_complete: false,
365
365
  next_action: "require-upstream-read-only-native-status-inventory",
366
+ remediation_command: "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent pi --next-transition",
366
367
  evidence: {
367
368
  native_contract: "gentle-ai/2.1.4",
368
369
  general_status: "unsupported",
@@ -371,6 +372,30 @@ test("general STATUS returns the typed native-status-unsupported boundary withou
371
372
  });
372
373
  });
373
374
 
375
+ test("gentle-pi#185: general STATUS on a non-negotiated native CLI names the exact status command to run", async (t) => {
376
+ // The legacy `correctionForecast` restoration guard this issue originally
377
+ // reported (extensions/gentle-ai.ts, then around line 5329) was scoped to
378
+ // `targetStatus !== undefined` and skipped restoring a candidate view when
379
+ // the native CLI lacked negotiated STATUS support, reproducing the #176
380
+ // empty-registry failure. That entire manual FINALIZE lifecycle (and the
381
+ // `correctionForecast` guard with it) has since been replaced by the
382
+ // STATUS/capture-driven flow: every operation now checks negotiated STATUS
383
+ // support up front, so no candidate-view restoration is ever attempted on
384
+ // a non-negotiated CLI. This test proves the current equivalent boundary
385
+ // (`nativeStatusUnsupported`) fails closed with an actionable envelope
386
+ // instead of a bare machine token, so a non-negotiated CLI can never
387
+ // surface as a silent empty-registry failure.
388
+ const fixture = createRepository(t);
389
+ const { controller } = registerRuntime();
390
+ const result = await controllerCall(controller, extensionContext(fixture.repository), { operation: "status" });
391
+ assert.equal(result.status, "blocked");
392
+ assert.equal(result.outcome, "native-status-unsupported");
393
+ assert.equal(
394
+ result.remediation_command,
395
+ "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent pi --next-transition",
396
+ );
397
+ });
398
+
374
399
  test("failed START gives exact mode and serialization guidance and creates no lineage", async (t) => {
375
400
  const fixture = createRepository(t);
376
401
  const { controller } = registerRuntime();
@@ -72,6 +72,21 @@ function relayCollectInput(lineageId, lens, order) {
72
72
  { name: "agent", value: "pi", token: "--agent=pi" },
73
73
  { name: "materialize", value: "true", token: "--materialize=true" },
74
74
  ],
75
+ // gentle-pi#638: the artifact subject is what makes the slot declarable —
76
+ // its subject_hash is the --request-hash the capture-unachievable verb
77
+ // binds to. A slot without one cannot be declared, only retried.
78
+ artifactSubject: {
79
+ schema: "gentle-ai.review-artifact-subject/v2",
80
+ subjectHash: `sha256:${String(order).repeat(64)}`,
81
+ lineageId,
82
+ authorityRevision: SHA,
83
+ targetIdentity: SHA,
84
+ baseTree: TREE,
85
+ candidateTree: TREE,
86
+ changedPathManifestSha256: SHA,
87
+ lens,
88
+ selectedOrder: order,
89
+ },
75
90
  submission: providerSubmission(lineageId, lens, order),
76
91
  };
77
92
  }
@@ -146,6 +161,7 @@ const { ReviewHostRelayError, REVIEW_HOST_RELAY_FAILURE } = await import(${JSON.
146
161
  const statusQueue = JSON.parse(await readFile(statusFile, "utf8"));
147
162
  const relayRequests = [];
148
163
  const statusCalls = [];
164
+ const unachievableCalls = [];
149
165
  let captureCalls = 0;
150
166
 
151
167
  __testing.setReviewHostRelayRunnerForTesting(async (request) => {
@@ -156,6 +172,9 @@ __testing.setReviewHostRelayRunnerForTesting(async (request) => {
156
172
  if (mode === "capture-fail") {
157
173
  throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
158
174
  }
175
+ if (mode === "capture-timeout") {
176
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2256004, timeoutMs: 2256000 });
177
+ }
159
178
  if (mode === "capture-ambiguous") {
160
179
  throw Object.assign(new Error("capture response lost"), { mutationOutcome: "unknown", nextAction: "review.status" });
161
180
  }
@@ -172,6 +191,27 @@ const nativeReviewCli = {
172
191
  if (next === undefined) throw new Error("status queue exhausted");
173
192
  return next;
174
193
  },
194
+ captureUnachievableLens: async (request) => {
195
+ unachievableCalls.push({
196
+ cwd: request.cwd,
197
+ lineageId: request.lineageId,
198
+ targetIdentity: request.targetIdentity,
199
+ expectedRevision: request.expectedRevision,
200
+ requestHash: request.requestHash,
201
+ reason: request.reason,
202
+ ...(request.detail === undefined ? {} : { detail: request.detail }),
203
+ ...(request.repositoryContext === undefined ? {} : { repositoryContext: request.repositoryContext }),
204
+ });
205
+ return {
206
+ schema: "gentle-ai.review-capture-unachievable/v1",
207
+ lineageId: request.lineageId,
208
+ targetIdentity: request.targetIdentity,
209
+ lens: ${JSON.stringify(LENS)},
210
+ selectedOrder: ${JSON.stringify(ORDER)},
211
+ reason: request.reason,
212
+ recorded: true,
213
+ };
214
+ },
175
215
  };
176
216
 
177
217
  let result;
@@ -199,7 +239,7 @@ try {
199
239
  error = caught instanceof Error ? { name: caught.name, message: caught.message } : String(caught);
200
240
  }
201
241
 
202
- await writeFile(outFile, JSON.stringify({ result, inspectResult, captureBinding, error, relayRequests, statusCalls, captureCalls }, null, 2));
242
+ await writeFile(outFile, JSON.stringify({ result, inspectResult, captureBinding, error, relayRequests, statusCalls, captureCalls, unachievableCalls }, null, 2));
203
243
  `;
204
244
  }
205
245
 
@@ -225,6 +265,107 @@ function inspectCollectBinding(inspectResult) {
225
265
  const PENDING = collectStatus(LINEAGE, [relayCollectInput(LINEAGE, LENS, ORDER)]);
226
266
  const CONVERGED = collectStatus(LINEAGE, []);
227
267
 
268
+ // gentle-pi#638: the typed stop the provider renders once the slot is declared
269
+ // unachievable — no collect inputs, one withdraw binding per declared slot.
270
+ // Built in the same plain-object style as collectStatus: typed fields for the
271
+ // controller, raw fields for the envelope passthrough.
272
+ function unachievableStopStatus(lineageId, lens, order) {
273
+ const subjectHash = `sha256:${String(order).repeat(64)}`;
274
+ const repositoryContext = `rctx1_${"e".repeat(64)}`;
275
+ const withdrawArguments = [
276
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
277
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
278
+ { name: "target", value: SHA, token: `--target=${SHA}` },
279
+ { name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
280
+ { name: "request-hash", value: subjectHash, token: `--request-hash=${subjectHash}` },
281
+ { name: "withdraw", value: "true", token: "--withdraw=true" },
282
+ ];
283
+ const withdrawCommand = `gentle-ai review capture-unachievable --lineage=${lineageId} --expected-revision=${SHA} --target=${SHA} --repository-context=${repositoryContext} --request-hash=${subjectHash} --withdraw=true`;
284
+ return {
285
+ ...collectStatus(lineageId, []),
286
+ nextTransition: {
287
+ kind: "stop",
288
+ reasonCode: "unachievable_lens_slot",
289
+ unachievableLensSlots: [
290
+ {
291
+ lens,
292
+ selectedOrder: order,
293
+ subjectHash,
294
+ reason: "relay_transport_bound_exceeded",
295
+ withdraw: {
296
+ operation: "review.capture-unachievable",
297
+ command: withdrawCommand,
298
+ arguments: withdrawArguments,
299
+ binding: { targetIdentity: SHA, lineageId, revision: SHA },
300
+ },
301
+ },
302
+ ],
303
+ },
304
+ raw: {
305
+ schema: "gentle-ai.review-integration.status/v5",
306
+ action: "stop",
307
+ lineage_id: lineageId,
308
+ target_identity: SHA,
309
+ next_transition: {
310
+ kind: "stop",
311
+ reason_code: "unachievable_lens_slot",
312
+ unachievable_lens_slots: [
313
+ {
314
+ lens,
315
+ selected_order: order,
316
+ subject_hash: subjectHash,
317
+ reason: "relay_transport_bound_exceeded",
318
+ withdraw: {
319
+ operation: "review.capture-unachievable",
320
+ command: withdrawCommand,
321
+ arguments: withdrawArguments,
322
+ binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA, repository_context: repositoryContext },
323
+ },
324
+ },
325
+ ],
326
+ },
327
+ },
328
+ };
329
+ }
330
+ const UNACHIEVABLE_STOP = unachievableStopStatus(LINEAGE, LENS, ORDER);
331
+
332
+ // gentle-pi#638: the declared slot must not survive the restart as a reoffer.
333
+ // Process A declares after the deterministic bound kill; fresh Process B sees
334
+ // the provider's typed stop with the withdraw hint instead of the public
335
+ // collectBinding it would have seen before the declaration.
336
+ test("an unachievable declaration survives the controller restart as a typed stop, not a reoffer", async (t) => {
337
+ const cwd = repository(t);
338
+
339
+ // Process A: the deterministic relay bound kills the reviewer; the capture
340
+ // path declares the slot unachievable and renders the provider's stop. The
341
+ // worker always runs INSPECT first, so the queue carries the pending collect
342
+ // twice (inspect + capture selection) before the post-declaration stop.
343
+ const declared = runWorker(t, cwd, [PENDING, PENDING, UNACHIEVABLE_STOP], "capture-timeout");
344
+ assert.equal(declared.error, undefined, `declared run threw: ${JSON.stringify(declared.error)}`);
345
+ assert.equal(declared.captureCalls, 1, "Process A invokes one capture");
346
+ assert.equal(declared.relayRequests.length, 1, "Process A launches one relay transport");
347
+ assert.equal(declared.unachievableCalls.length, 1, "the declaration is recorded exactly once");
348
+ assert.deepEqual(declared.unachievableCalls, [{ cwd, lineageId: LINEAGE, targetIdentity: SHA, expectedRevision: SHA, requestHash: `sha256:${String(ORDER).repeat(64)}`, reason: "relay_transport_bound_exceeded", detail: "killed after 2256004ms against a 2256000ms relay bound", repositoryContext: `rctx1_${"e".repeat(64)}` }]);
349
+ assert.equal(declared.statusCalls.length, 3, "inspect and selection STATUS plus exactly one bound re-query");
350
+ assert.equal(declared.result.tool, "gentle_review_capture");
351
+ assert.equal(declared.result.status, "blocked");
352
+ assert.equal(declared.result.outcome, "unachievable-lens-slot-declared");
353
+ assert.equal(declared.result.mutation_performed, true);
354
+ assert.match(String(declared.result.next_action), /withdraw/);
355
+
356
+ // Process B (fresh): the provider's STATUS no longer reoffers the binding;
357
+ // INSPECT renders the typed stop whose hint is the withdraw command.
358
+ const inspect = runWorker(t, cwd, [UNACHIEVABLE_STOP], "inspect");
359
+ assert.equal(inspect.error, undefined, `inspect run threw: ${JSON.stringify(inspect.error)}`);
360
+ assert.equal(inspect.captureCalls, 0, "no capture runs without a reoffered binding");
361
+ assert.equal(inspect.relayRequests.length, 0, "INSPECT never launches the relay");
362
+ assert.equal(inspect.unachievableCalls.length, 0, "INSPECT never declares");
363
+ assert.equal(inspectCollectBinding(inspect.result), undefined, "the declared slot is not reoffered after restart");
364
+ assert.equal(inspect.result.status, "blocked");
365
+ assert.match(String(inspect.result.hint), /capture-unachievable/);
366
+ assert.match(String(inspect.result.hint), /--withdraw=true/);
367
+ });
368
+
228
369
  test("a fresh controller process reoffers the exact public binding before one capture closes", async (t) => {
229
370
  const cwd = repository(t);
230
371