gentle-pi 4.2.0 → 4.4.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 (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,8 +1,13 @@
1
1
  import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
2
2
  import { dataArgumentRanges, recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
3
+ import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "../lib/yolo-temp-scratch.ts";
3
4
  import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
5
+ import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
6
+ import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
7
+ import { appendContextBackstopNotice, createContextBackstopSignal, createThrottledContextMeasure } from "../lib/context-backstop.ts";
4
8
  import { blockChildDestructiveCommand } from "./child-safety.ts";
5
- import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
9
+ import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
10
+ import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
6
11
  import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProfiles, recordReviewMutation, type ReceiptSession } from "../lib/review-reminder-receipt.ts";
7
12
  import { createReviewSidebarPublisher } from "../lib/review-sidebar-state.ts";
8
13
  import { StatusTimingDiagnostics } from "../lib/status-timing-diagnostics.ts";
@@ -20,6 +25,7 @@ import {
20
25
  readdirSync,
21
26
  readFileSync,
22
27
  realpathSync,
28
+ statSync,
23
29
  writeFileSync,
24
30
  } from "node:fs";
25
31
  import {
@@ -30,6 +36,7 @@ import {
30
36
  writeFile,
31
37
  } from "node:fs/promises";
32
38
  import { homedir } from "node:os";
39
+ import { discoverAgents, loadAgentsConfig, unmatchedProfileKeys } from "../lib/agents-config.ts";
33
40
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
34
41
  import { fileURLToPath } from "node:url";
35
42
  import type {
@@ -38,8 +45,10 @@ import type {
38
45
  Theme,
39
46
  ThemeColor,
40
47
  ToolCallEventResult,
48
+ ToolResultEvent,
49
+ ToolResultEventResult,
41
50
  } from "@earendil-works/pi-coding-agent";
42
- import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
51
+ import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
43
52
  import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
44
53
  import {
45
54
  BACKGROUND_SUBAGENTS_FILE,
@@ -77,6 +86,7 @@ import {
77
86
  PROFILE_ORCHESTRATOR_KEY,
78
87
  profileExportPath,
79
88
  profileRoutingRows,
89
+ unmatchedRoutingNotes,
80
90
  profilesFilePath,
81
91
  readProfileOrchestrator,
82
92
  readProfilesFileResult,
@@ -222,7 +232,7 @@ import {
222
232
  isSmallWriterProfile,
223
233
  resolveWriterProfile,
224
234
  decodeAgentRiskEscalation,
225
- escalatedRisk,
235
+ verificationRisk,
226
236
  HIGH_RISK_ITEMS,
227
237
  RDD_LINE,
228
238
  WRITER_PROFILE,
@@ -264,6 +274,8 @@ import {
264
274
  sameReviewSessionIdentity,
265
275
  type ReviewSessionIdentity,
266
276
  } from "../lib/review-session-standing-permission.ts";
277
+ import { herdrSetupHint } from "../lib/herdr-activity.ts";
278
+ import { isInteractiveMode } from "../lib/rpc-host.ts";
267
279
 
268
280
  const GRAPH_V1_ORDINARY_READ_ONLY = "Graph-v1 ordinary review authority is read-only; use native compact-v2 review operations";
269
281
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -788,6 +800,7 @@ const rddStatusMemo = new Map<string, { readonly status: NativeReviewModeStatus
788
800
  /** @internal test seam: clears the per-cwd RDD status memo. */
789
801
  function clearRddStatusMemoForTesting(): void {
790
802
  rddStatusMemo.clear();
803
+ rddStatusUnavailableWarned = false;
791
804
  }
792
805
 
793
806
  // gentle-pi#668 (corrected): last-known outcome for ONE candidate, keyed by
@@ -852,29 +865,62 @@ async function readRddModeStatusOnce(
852
865
  cwd: string,
853
866
  signal?: AbortSignal,
854
867
  ): Promise<NativeReviewModeStatus | undefined> {
855
- if (!nativeReviewCli?.reviewMode) return undefined;
868
+ const read = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
869
+ return "status" in read ? read.status : undefined;
870
+ }
871
+
872
+ // gentle-shell#2250: the same read, keeping why it failed so the one-shot
873
+ // unavailable notice can tell a timeout from a native refusal (for example a
874
+ // repository on a network share that Git or gentle-ai rejects by ownership).
875
+ async function readRddModeStatusWithCause(
876
+ nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
877
+ cwd: string,
878
+ signal?: AbortSignal,
879
+ ): Promise<{ readonly status: NativeReviewModeStatus } | { readonly cause: string }> {
880
+ if (!nativeReviewCli?.reviewMode) return { cause: "the native gentle-ai review CLI is not available" };
856
881
  try {
857
882
  const call = nativeReviewCli.reviewMode({ cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, signal });
858
883
  const result = signal === undefined ? await call : await Promise.race([call, rddAbortRejection(signal)]);
859
- return result.status;
860
- } catch {
861
- return undefined;
884
+ return { status: result.status };
885
+ } catch (error) {
886
+ return { cause: describeRddStatusFailure(error, signal) };
862
887
  }
863
888
  }
864
889
 
890
+ const RDD_STATUS_CAUSE_LIMIT = 300;
891
+
892
+ function describeRddStatusFailure(error: unknown, signal: AbortSignal | undefined): string {
893
+ const command = "native `gentle-ai review mode status`";
894
+ if (signal?.aborted || (error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.TIMEOUT)) {
895
+ return `${command} did not answer within ${RDD_STATUS_TIMEOUT_MS} ms`;
896
+ }
897
+ if (error instanceof NativeReviewCliError) {
898
+ const exit = error.diagnostics.exit_code === undefined ? "" : `, exit ${error.diagnostics.exit_code}`;
899
+ const detail = (error.diagnostics.stderr ?? "").replace(/\s+/g, " ").trim() || error.message;
900
+ return `${command} failed (${error.code}${exit}): ${boundRddStatusCause(detail)}`;
901
+ }
902
+ return boundRddStatusCause(error instanceof Error ? error.message : String(error));
903
+ }
904
+
905
+ function boundRddStatusCause(text: string): string {
906
+ const trimmed = text.replace(/[.\s]+$/, "");
907
+ return trimmed.length <= RDD_STATUS_CAUSE_LIMIT ? trimmed : `${trimmed.slice(0, RDD_STATUS_CAUSE_LIMIT - 1)}…`;
908
+ }
909
+
865
910
  // gentle-pi#662: read-only combined native risk assessment plus the computed
866
911
  // verification plan (`lib/review-risk-assessment.ts`), for the `gentle_review`
867
912
  // tool's `assess` operation. Never throws: an unavailable/failed native
868
913
  // assess call (older binary without the verb, timeout, malformed response)
869
- // resolves to the `unassessable` tier, which `verificationPlan` treats the
870
- // same as `high` -- the fail-closed rule from gentle-pi#662.
914
+ // resolves to the `unassessable` native tier, which informs only: like a
915
+ // native high, it adds no independent verifier without an agent escalation.
871
916
  interface ReviewAssessmentPlanDetails {
872
917
  schema: "gentle-pi.review-assessment-plan/v1";
873
918
  risk: VerificationTier;
874
- // gentle-shell#1494: the native tier before any agent escalation. `risk`
875
- // differs from it only when the agent raised the candidate to high.
919
+ // gentle-shell#1494: the native tier before any agent escalation. `risk` is
920
+ // the plan tier: high only through an escalation, else passive or medium.
876
921
  nativeRisk: VerificationTier;
877
- // `applied` is false when the native tier was already high or unassessable.
922
+ // `applied` is true whenever an escalation is present: without it the plan
923
+ // would be passive or medium.
878
924
  agentEscalation?: { item: number; label: string; reason: string; applied: boolean };
879
925
  reasons: readonly ReviewAssessmentReason[];
880
926
  changedPaths: number;
@@ -1031,9 +1077,10 @@ async function resolveReviewAssessmentPlan(
1031
1077
  }
1032
1078
 
1033
1079
  const nativeRisk: VerificationTier = assessment?.risk ?? VERIFICATION_TIER.UNASSESSABLE;
1034
- // gentle-shell#1494: an agent escalation raises passive/medium to high and
1035
- // never lowers; the native tier stays visible as `nativeRisk`.
1036
- const risk = escalatedRisk(nativeRisk, input.escalate);
1080
+ // gentle-shell#1494: the plan is high only through an agent escalation
1081
+ // naming a Task Size high-risk item; a native high or unassessable tier
1082
+ // informs as medium. The native tier stays visible as `nativeRisk`.
1083
+ const risk = verificationRisk(nativeRisk, input.escalate);
1037
1084
  // gentle-pi#668: the memo is read only for THIS candidate's own target
1038
1085
  // identity, never repository-only. It is still needed when the caller
1039
1086
  // claims `closed`, because a recorded decline beats closure (gentle-pi#1175).
@@ -1048,7 +1095,7 @@ async function resolveReviewAssessmentPlan(
1048
1095
  nativeRisk,
1049
1096
  ...(input.escalate === undefined
1050
1097
  ? {}
1051
- : { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== nativeRisk } }),
1098
+ : { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== verificationRisk(nativeRisk, undefined) } }),
1052
1099
  // No path is known for an unassessable candidate: omit it rather than
1053
1100
  // emitting an empty string the native reason shape forbids.
1054
1101
  reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, detail: unassessableDetail }]),
@@ -1119,14 +1166,15 @@ async function resolveRddModeStatus(
1119
1166
  // read status instead of flipping the prompt to "unknown" and back, which
1120
1167
  // would invalidate the provider's prefix cache for the whole conversation.
1121
1168
  // Only a status the native reader actually reports replaces a known one.
1122
- const read = await readRddModeStatusOnce(nativeReviewCli, cwd, signal);
1169
+ const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
1170
+ const read = "status" in outcome ? outcome.status : undefined;
1123
1171
  const status = read ?? cached?.status;
1124
1172
  rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
1125
- if (read === undefined && !rddStatusUnavailableWarned) {
1173
+ if ("cause" in outcome && !rddStatusUnavailableWarned) {
1126
1174
  rddStatusUnavailableWarned = true;
1127
1175
  if (ctx?.hasUI) {
1128
1176
  ctx.ui.notify(
1129
- "Gentle AI: receipt-driven-development status is unavailable (native review CLI absent, timed out, or failed). The parent prompt keeps the last known status, or renders \"unknown\" when none was read; this notice will not repeat this session.",
1177
+ `Gentle AI: receipt-driven-development status is unavailable: ${outcome.cause}. The parent prompt keeps the last known status, or renders "unknown" when none was read; this notice will not repeat this session.`,
1130
1178
  "warning",
1131
1179
  );
1132
1180
  }
@@ -1139,6 +1187,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
1139
1187
  return line !== undefined && line.startsWith("Receipt-driven development: off ");
1140
1188
  }
1141
1189
 
1190
+ /**
1191
+ * gentle-shell#2195: the rendered line sits in the cached system-prompt
1192
+ * prefix, so a session keeps the first line it rendered and re-renders only
1193
+ * when the off state changes, because that also adds or drops the review
1194
+ * execution contract (gentle-shell#1494). An unknown-to-on flip or a
1195
+ * deciding-source change keeps the prefix; review tools still read the live
1196
+ * mode whenever they run, so the pinned text never grants or blocks anything.
1197
+ */
1198
+ function pinRddStatusLine(pinned: string | undefined, next: string): string {
1199
+ return pinned !== undefined && isRddStatusLineOff(pinned) === isRddStatusLineOff(next) ? pinned : next;
1200
+ }
1201
+
1202
+ /**
1203
+ * gentle-pi S11 T1: while the pinned line reads off, no review can start and
1204
+ * the orchestrator only calls ASSESS, so the primary request swaps the review
1205
+ * family's long guidance for this lean loadout: one short gentle_review
1206
+ * snippet, guideline, and description, with the capture, capture-group, and
1207
+ * scope declarations hidden (they stay registered and active). It follows the
1208
+ * pinned line, so it changes only when that line's off state flips, together
1209
+ * with the review execution contract; on and unknown keep the registered text.
1210
+ */
1211
+ const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
1212
+ const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
1213
+ 'Receipt-driven development is off, so use gentle_review only for {"operation":"assess"} (read-only): after an authorized implementation, delegated or inline, call it over the diff and follow the returned plan; pass input {"escalate":{"item":1-6,"reason":"<one line>"}} only when the change touches a Task Size high-risk item, or {"baseRef":"<ref>","committedOnly":true} to assess a committed range.',
1214
+ "When ASSESS returns reviewDue or a native continuation, relay it unchanged but do not execute it while RDD is off; assessment metadata does not authorize STATUS, another review lifecycle operation, or enabling RDD. Continue the returned verification plan; tests, builds, and functional checks still run when applicable, and no assessment authorizes delivery.",
1215
+ ];
1216
+ const RDD_OFF_REVIEW_DESCRIPTION = "Assess the current changes, or a committed range, and return the verification plan. Read-only; review outcomes never authorize delivery: commit, push, pull-request, and release commands follow ordinary repository policy.";
1217
+ const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
1218
+
1219
+ interface ReviewToolPromptOptions {
1220
+ toolSnippets?: Record<string, string>;
1221
+ toolGuidelines?: Record<string, string[]>;
1222
+ }
1223
+
1224
+ /** Replaces gentle_review's snippet and guidelines in this run's prompt options only. */
1225
+ function applyRddOffReviewPrompt(options: ReviewToolPromptOptions | null | undefined): void {
1226
+ if (options?.toolSnippets?.gentle_review !== undefined) options.toolSnippets.gentle_review = RDD_OFF_REVIEW_SNIPPET;
1227
+ if (options?.toolGuidelines?.gentle_review !== undefined) options.toolGuidelines.gentle_review = [...RDD_OFF_REVIEW_GUIDELINES];
1228
+ }
1229
+
1142
1230
  /** Resolves and renders the RDD status line for a production call site in one call. */
1143
1231
  async function resolveRddStatusLine(
1144
1232
  nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
@@ -1205,13 +1293,13 @@ function renderOrchestratorPrompt(
1205
1293
  // pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
1206
1294
  const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
1207
1295
  const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
1208
- const PI_ORCHESTRATION_RUNTIME = "pi";
1296
+ const PI_MIRROR_RUNTIME = "pi";
1209
1297
 
1210
1298
  let reviewContractPromptFragmentCache: string | null | undefined;
1211
1299
  let reviewContractPromptMissingWarned = false;
1212
1300
 
1213
- // Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
1214
- function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
1301
+ // Reads one mirrored bundle file after verifying its bytes against the lock's digest (gentle-ai R1/R3).
1302
+ function readVerifiedMirrorFile(mirrorRoot: string, bundlePath: string): { contractSemver: string; text: string } | null {
1215
1303
  try {
1216
1304
  const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
1217
1305
  const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
@@ -1219,20 +1307,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
1219
1307
  entries?: Record<string, unknown>;
1220
1308
  };
1221
1309
  if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
1222
- const expectedSha256 = lock.entries?.[`orchestration/${PI_ORCHESTRATION_RUNTIME}.md`];
1310
+ const expectedSha256 = lock.entries?.[bundlePath];
1223
1311
  if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
1224
- const contractPath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", "orchestration", `${PI_ORCHESTRATION_RUNTIME}.md`);
1225
- const rawBytes = readFileSync(contractPath);
1312
+ const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
1313
+ const rawBytes = readFileSync(filePath);
1226
1314
  const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
1227
1315
  if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
1228
- const text = rawBytes.toString("utf8").trim();
1229
- if (text.length === 0) return null;
1230
- return `## Gentle AI review execution contract (mirrored provider bundle ${lock.contract_semver})\n\n${text}`;
1316
+ return { contractSemver: lock.contract_semver, text: rawBytes.toString("utf8") };
1231
1317
  } catch {
1232
1318
  return null;
1233
1319
  }
1234
1320
  }
1235
1321
 
1322
+ // Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
1323
+ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
1324
+ const file = readVerifiedMirrorFile(mirrorRoot, `orchestration/${PI_MIRROR_RUNTIME}.md`);
1325
+ if (file === null) return null;
1326
+ const text = file.text.trim();
1327
+ if (text.length === 0) return null;
1328
+ return `## Gentle AI review execution contract (mirrored provider bundle ${file.contractSemver})\n\n${text}`;
1329
+ }
1330
+
1331
+ // gentle-ai R3: the harness ODD steps 1-7, the phase-reporting line, and the
1332
+ // resume line come from the mirrored bundle's routing/pi.md (contract >= 1.4.0),
1333
+ // the ODD routing block Gentle AI renders for Pi, instead of a hand-copied
1334
+ // literal. The bundle's blank lines between them are not carried into the
1335
+ // prompt. Like assets/orchestrator.md, this is package-shipped prompt source:
1336
+ // a missing, tampered, or reshaped file fails closed instead of rendering a
1337
+ // harness without its workflow.
1338
+ const ODD_PROTOCOL_HEADING = "### ODD protocol (MANDATORY, in this order, on every request)";
1339
+ const ODD_PROTOCOL_STEP_COUNT = 7;
1340
+
1341
+ let oddProtocolStepsCache: string | undefined;
1342
+
1343
+ function extractOddProtocolLines(routing: string): string {
1344
+ const lines = routing.split("\n");
1345
+ const start = lines.indexOf(ODD_PROTOCOL_HEADING);
1346
+ const section: string[] = [];
1347
+ for (const line of start === -1 ? [] : lines.slice(start + 1)) {
1348
+ if (line.startsWith("#")) break;
1349
+ if (line !== "") section.push(line);
1350
+ }
1351
+ const shapeHolds =
1352
+ section.length === ODD_PROTOCOL_STEP_COUNT + 2 &&
1353
+ section.slice(0, ODD_PROTOCOL_STEP_COUNT).every((line, index) => line.startsWith(`${index + 1}. **`)) &&
1354
+ section[ODD_PROTOCOL_STEP_COUNT]!.startsWith("Phase reporting: ") &&
1355
+ section[ODD_PROTOCOL_STEP_COUNT + 1]!.startsWith("Resume an interrupted feature ");
1356
+ if (!shapeHolds) {
1357
+ throw new Error(`The mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md ODD protocol section has an unexpected shape: expected ${JSON.stringify(ODD_PROTOCOL_HEADING)} followed by steps 1-${ODD_PROTOCOL_STEP_COUNT}, phase reporting, and resume.`);
1358
+ }
1359
+ return section.join("\n");
1360
+ }
1361
+
1362
+ function readMirroredOddProtocolSteps(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string {
1363
+ const file = readVerifiedMirrorFile(mirrorRoot, `routing/${PI_MIRROR_RUNTIME}.md`);
1364
+ if (file === null) {
1365
+ throw new Error(`Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md is missing, unreadable, or fails digest verification. Reinstall gentle-pi.`);
1366
+ }
1367
+ return extractOddProtocolLines(file.text);
1368
+ }
1369
+
1370
+ function oddProtocolSteps(): string {
1371
+ oddProtocolStepsCache ??= readMirroredOddProtocolSteps();
1372
+ return oddProtocolStepsCache;
1373
+ }
1374
+
1236
1375
  function loadReviewContractPromptFragment(
1237
1376
  ctx: Pick<ExtensionContext, "hasUI" | "ui">,
1238
1377
  mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
@@ -1324,27 +1463,27 @@ For background work, launch the bounded action and end the turn when no independ
1324
1463
 
1325
1464
  Default workflow: Organic Driven Development (MANDATORY)
1326
1465
  Organic Driven Development (ODD) is the predefined workflow of this orchestrator. Every request enters it, without the user asking for a workflow, a plan, or task tracking. Never describe this workflow only when asked about it: run it. Run these steps in this order on every request:
1327
- 1. **Authorize.** Investigation, explanation, review, comparison, and proposal-only requests stay read-only: no writer, apply, or implementation artifacts. Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait. A user saying they may stop you or resume later asks for notes and separate commits, not a stop.
1328
- 2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything. Do not delegate exploration of files you will read anyway to work inline; explore only for a map you need to decide or route.
1329
- 3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
1330
- 4. **Classify.** Size the task by the orchestrator's Task Size section: small when understood, risk is contained, and the work could be resumed from the original request and \`git diff\` alone; large only when that resume test fails. Never classify by counting files, commands, tests, fixes, or a requested todo list. Small work stays inline and creates no durable task artifacts.
1331
- 5. **Track before the first write.** For large authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds. The document is the specification subagents read, in this order: a two- or three-line header; \`## Specs\` with numbered \`S#\` that quote the user's exact strings, error messages, and examples verbatim, never summarized and never adding unrequested requirements; \`## Tasks\` with one line per task (ID, linked \`S#\`, route, commit); \`## Log\` last, where \`L1\` is the user's original request verbatim and later user corrections, evidence, and decisions are appended. A requirement change appends its verbatim Log entry, rewrites only the affected \`S#\`, and reopens only its task.
1332
- 6. **Implement task by task.** Hand off by reference, never by paraphrase: name the document, task, and specs (for example \`Spec: odd/tasks/<feature>.md, T2, S3-S4\`), tell workers to read until \`## Log\`, and ask which \`S#\` were covered. Without a feature document, include the user's request verbatim. Verify reads the whole document, runs the spec's examples the parent authorized, against isolated state when they mutate data, and returns a verdict per \`S#\`. When the user reports a failure, reproduce it before deciding it already works. Each test asserts every observable effect of the rule it covers (exit code, exact stdout and stderr, and that rejected input leaves stored data and counters unchanged), covers the cases the rule itself names (its examples, boundaries, and errors), and checks through the public interface, never internal storage. When you add or change a command, option, or message, update the help text and docs that describe it. Route each task through the orchestrator's Mechanisms, honoring its mandatory delegation triggers, with applicable test-first development and checks. These triggers are mandatory, not advisory: executing past a fired trigger inline is a routing defect even if the work succeeds. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every tracked task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized large ODD implementation; push, pull request creation, and merge remain the user's decisions.
1333
- 7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. Before writing \`Risk: none\`, check whether your diff changes code that existing behavior the request did not mention also uses (shared options, parsers, helpers); if it does, that is item 3. Never end with a tracked task pending unless you quote the user's explicit stop. An applicable quick check runs once; an unavailable verifier or subagent is reported as unavailable, never retried or escalated into extra ceremony. Partial, blocked, unavailable, or exhausted proof becomes one **Needs your decision** result naming the open blockers or missing proof, never more verification; that result is a valid stop, hedged wording is not. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
1334
- Phase reporting: the Gentle Shell prompt label is inferred automatically from the primary session's tool activity (reads show \`exploring\`, edits \`implementing\`, test runs \`checking\`, user questions \`deciding\`). When the \`gentle_odd_phase\` tool is available, use it to refine that label with phases tools cannot show (\`authorizing\`, \`researching\`, \`deciding\`, \`closing\`): call \`gentle_odd_phase\` only when the primary session's ODD phase actually changes, never per tool call or on a fixed cadence, and never from a subagent. It drives the Gentle Shell prompt label only.
1335
- Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
1466
+ ${oddProtocolSteps()}
1336
1467
 
1337
1468
  Harness principles:
1338
1469
  - el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
1339
- - Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track large work before the first write, implement task by task with proportionate checks, close each tracked task with a work-unit commit, and close.
1340
1470
  - Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
1341
1471
  - Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
1342
- - Parallel writers only with disjoint Allowed edit surfaces (runtime-enforced) or isolated worktrees.
1343
- - For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
1472
+ - For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Only when your diff changes shared code (options, parsers, helpers) that an existing command the request did not name also uses, run one differential check before finishing, as a single script: for each such command, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a \`mktemp -d\` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test. Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
1344
1473
  - Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
1345
1474
  - Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
1346
1475
 
1347
- ${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}`;
1476
+ ${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}${headlessDeliveryNote(hostMode)}`;
1477
+ }
1478
+
1479
+ // Bench gate-p53-r1-BS40 x6 run 3: in a headless RPC session the ask-on-risk
1480
+ // delivery question ran before any code was written, ask_user_question was
1481
+ // unavailable, and the orchestrator printed the menu and stopped. Hosts where
1482
+ // no human can answer (the same rule ask_user_question applies) resolve that
1483
+ // question to the recommended chain. Interactive sessions render nothing here.
1484
+ function headlessDeliveryNote(hostMode: string | undefined): string {
1485
+ if (hostMode === undefined || isInteractiveMode(hostMode)) return "";
1486
+ return "\n\nNo human can answer questions in this session. For the ask-on-risk delivery question, do not ask: use `feature-branch-chain`, record that choice in the feature document's `## Log`, and continue. Consent, maintenance, and opaque-token prompts keep their own rules.";
1348
1487
  }
1349
1488
 
1350
1489
  // Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
@@ -1859,8 +1998,15 @@ const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
1859
1998
  // most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
1860
1999
  // `--git-dir`) can run or redirect code, so they keep the confirmation.
1861
2000
  const YOLO_GIT_PREFIX_RE = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s'"`$]*\s+|env\s+(?:-i\s+)?|command\s+)*git(?:\s+-C\s+[^\s'"`$;&|]+)?\s+$/;
1862
- const YOLO_PUSH_SEGMENT_RE = /^push(?:\s+(?:-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$/;
1863
- const YOLO_REBASE_SEGMENT_RE = /^rebase(?:\s+(?:--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$/;
2001
+ // Output redirections that write nothing: to stdout/stderr or /dev/null.
2002
+ const YOLO_REDIRECT_SRC = String.raw`[12]?>&[12]|(?:[12]?|&)>>?\s*/dev/null(?![^\s;&|])`;
2003
+ const YOLO_PUSH_SEGMENT_RE = new RegExp(String.raw`^push(?:\s+(?:${YOLO_REDIRECT_SRC}|-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$`);
2004
+ const YOLO_REBASE_SEGMENT_RE = new RegExp(String.raw`^rebase(?:\s+(?:${YOLO_REDIRECT_SRC}|--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$`);
2005
+ // A pipe out of a waived segment may only feed read-only filters with plain or
2006
+ // literally quoted arguments and end at a sequential separator. Git output
2007
+ // carries remote-controlled text, so `| sh`, `| tee`, `| xargs` and a trailing
2008
+ // background `&` keep the confirmation.
2009
+ const YOLO_FILTER_PIPELINE_RE = new RegExp(String.raw`^(?:\|(?!\|)\s*(?:tail|head|cat|grep|wc)(?:\s+(?:${YOLO_REDIRECT_SRC}|[A-Za-z0-9_.,:=+/%@-]+|'[^'\n]*'|"[^"$\x60\\\n]*"))*\s*)*(?=$|&&|\|\||;|\n|\r)`);
1864
2010
 
1865
2011
  function yoloGuardedSegment(command: string, triggerIndex: number): string {
1866
2012
  const rest = command.slice(triggerIndex);
@@ -1875,20 +2021,29 @@ function yoloSegmentPrefix(command: string, triggerIndex: number): string {
1875
2021
  return last === undefined ? head : head.slice(last.index + last[0].length);
1876
2022
  }
1877
2023
 
1878
- /** Every guarded match and every recognized Git side effect must be a reversible push or non-interactive rebase. */
1879
- function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation): boolean {
1880
- if (evaluation.action !== "confirm" || evaluation.dataLoss || evaluation.matches.length === 0) return false;
2024
+ /**
2025
+ * Every guarded match and every recognized Git side effect must be a reversible
2026
+ * push or non-interactive rebase; the only data loss allowed is recursive
2027
+ * removal of the session's own temp scratch (lib/yolo-temp-scratch.ts).
2028
+ */
2029
+ function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation, scratchFs?: TempScratchFs): boolean {
2030
+ if (evaluation.action !== "confirm") return false;
2031
+ const effects = recognizeDestructiveCommands(command);
2032
+ const scratch = effects.map((effect) => effect.removalTargets !== undefined && isTempScratchRemoval(effect, scratchFs ??= defaultTempScratchFs()));
2033
+ if (evaluation.matches.length === 0 && !scratch.includes(true)) return false;
1881
2034
  for (const match of evaluation.matches) {
1882
2035
  if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
1883
2036
  if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
1884
2037
  const segment = yoloGuardedSegment(command, match.triggerIndex);
1885
2038
  if (!(match.key === "gitPush" ? YOLO_PUSH_SEGMENT_RE : YOLO_REBASE_SEGMENT_RE).test(segment)) return false;
2039
+ if (!YOLO_FILTER_PIPELINE_RE.test(command.slice(match.triggerIndex + segment.length))) return false;
1886
2040
  }
1887
2041
  // The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
1888
2042
  // restore, checkout --) to the primary policy; under YOLO only a rebase that
1889
2043
  // a guarded match already vetted may remain.
1890
2044
  const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
1891
- for (const effect of recognizeDestructiveCommands(command)) {
2045
+ for (const [index, effect] of effects.entries()) {
2046
+ if (scratch[index]) continue;
1892
2047
  if (effect.hardDeny || effect.kind !== "git") return false;
1893
2048
  const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
1894
2049
  if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
@@ -1917,6 +2072,21 @@ function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey
1917
2072
  return restriction;
1918
2073
  }
1919
2074
 
2075
+ /** Super Yolo ignores every configurable guardrail: only code-level denies survive. */
2076
+ const SUPER_YOLO_GUARDRAILS: RuntimeGuardrailsConfig = {
2077
+ autonomousMode: true,
2078
+ guardedCommands: { gitPush: "allow", gitRebase: "allow", gitBranchDeleteForce: "allow", npmPublish: "allow", piRemove: "allow" },
2079
+ };
2080
+
2081
+ /**
2082
+ * Super Yolo command decision: run everything without confirmation except the
2083
+ * code-level catastrophic denies (root/home recursive removal, --no-preserve-root,
2084
+ * hard-denied patterns). Configured guardrails are deliberately not consulted.
2085
+ */
2086
+ function evaluateSuperYoloCommand(command: string): GuardAction {
2087
+ return evaluateGuardedCommand(command, SUPER_YOLO_GUARDRAILS).action === "block" ? "block" : "allow";
2088
+ }
2089
+
1920
2090
  async function confirmCommand(
1921
2091
  command: string,
1922
2092
  ctx: ExtensionContext,
@@ -1924,7 +2094,19 @@ async function confirmCommand(
1924
2094
  herdrLifecycle: HerdrConfirmationLifecycle,
1925
2095
  yoloActive = false,
1926
2096
  autoGate?: () => Promise<ToolCallEventResult | undefined>,
2097
+ superActive = false,
1927
2098
  ): Promise<ToolCallEventResult | undefined> {
2099
+ if (superActive) {
2100
+ // Sensitive-path and delivered-test guards already ran before this call;
2101
+ // here only the code-level catastrophic denies remain.
2102
+ if (evaluateSuperYoloCommand(command) === "block") {
2103
+ return {
2104
+ block: true,
2105
+ reason: "Gentle AI safety policy blocked a catastrophic destructive shell command. Ask the user for an explicit safer plan.",
2106
+ };
2107
+ }
2108
+ return autoGate?.();
2109
+ }
1928
2110
  const guardrailsConfig = loadRuntimeGuardrailsConfig(ctx.cwd);
1929
2111
  const evaluation = evaluateGuardedCommand(command, guardrailsConfig);
1930
2112
  // Configuration provenance is distinct from legacy autonomy resolution, which
@@ -2940,7 +3122,7 @@ interface OverlayComponent {
2940
3122
 
2941
3123
  type ModelPanelResult =
2942
3124
  | { type: "save"; config: AgentModelConfig }
2943
- // `save`, then snapshot saved agent routing and the live orchestrator into
3125
+ // `save`, then snapshot saved agent routing and the chosen orchestrator into
2944
3126
  // the current profile without changing the global orchestrator defaults.
2945
3127
  | { type: "save-profile"; config: AgentModelConfig }
2946
3128
  | { type: "custom"; agent: string | "all"; config: AgentModelConfig }
@@ -2949,6 +3131,7 @@ type ModelPanelResult =
2949
3131
  | { type: "cancel" };
2950
3132
 
2951
3133
  const SET_ALL_AGENTS = "Set all agents";
3134
+ const LEAVE_ORCHESTRATOR_UNTOUCHED = "leave untouched";
2952
3135
 
2953
3136
  const PANEL_TONE = {
2954
3137
  BORDER: "border",
@@ -2979,6 +3162,7 @@ class SddModelPanel implements OverlayComponent {
2979
3162
  private query = "";
2980
3163
  private readonly draft: AgentModelConfig;
2981
3164
  private readonly rows: string[];
3165
+ private readonly agents: string[];
2982
3166
  private readonly modelOptions: string[];
2983
3167
  private readonly done: (result: ModelPanelResult) => void;
2984
3168
  private readonly theme: Theme | undefined;
@@ -2997,7 +3181,8 @@ class SddModelPanel implements OverlayComponent {
2997
3181
  terminalRows?: () => number,
2998
3182
  ) {
2999
3183
  this.draft = cloneModelConfig(initialConfig);
3000
- this.rows = [SET_ALL_AGENTS, ...agents];
3184
+ this.rows = [SET_ALL_AGENTS, PROFILE_ORCHESTRATOR_KEY, ...agents];
3185
+ this.agents = [...agents];
3001
3186
  this.modelOptions = modelOptions;
3002
3187
  this.done = done;
3003
3188
  this.theme = theme;
@@ -3120,6 +3305,7 @@ class SddModelPanel implements OverlayComponent {
3120
3305
  }
3121
3306
  if (matchesKey(data, "e")) {
3122
3307
  this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
3308
+ if (this.selectedRow === PROFILE_ORCHESTRATOR_KEY && !this.draft[this.selectedRow]?.model) return;
3123
3309
  this.mode = "effort";
3124
3310
  this.effortCursor = 0;
3125
3311
  return;
@@ -3201,7 +3387,7 @@ class SddModelPanel implements OverlayComponent {
3201
3387
  return;
3202
3388
  }
3203
3389
  this.applyModelSelection(
3204
- selected === INHERIT_MODEL ? undefined : selected,
3390
+ selected === INHERIT_MODEL || selected === LEAVE_ORCHESTRATOR_UNTOUCHED ? undefined : selected,
3205
3391
  );
3206
3392
  this.mode = "agents";
3207
3393
  return;
@@ -3215,7 +3401,7 @@ class SddModelPanel implements OverlayComponent {
3215
3401
  private applyModelSelection(model: string | undefined): void {
3216
3402
  const row = this.rows[this.cursor];
3217
3403
  if (row === SET_ALL_AGENTS) {
3218
- for (const name of this.rows.slice(1)) this.setModel(name, model);
3404
+ for (const name of this.agents) this.setModel(name, model);
3219
3405
  return;
3220
3406
  }
3221
3407
  if (!row) return;
@@ -3225,7 +3411,7 @@ class SddModelPanel implements OverlayComponent {
3225
3411
  private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
3226
3412
  const row = this.selectedRow;
3227
3413
  if (row === SET_ALL_AGENTS) {
3228
- for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
3414
+ for (const name of this.agents) this.setThinking(name, thinking);
3229
3415
  return;
3230
3416
  }
3231
3417
  this.setThinking(row, thinking);
@@ -3234,13 +3420,17 @@ class SddModelPanel implements OverlayComponent {
3234
3420
  private applyInherit(): void {
3235
3421
  const row = this.rows[this.cursor];
3236
3422
  if (row === SET_ALL_AGENTS) {
3237
- for (const name of this.rows.slice(1)) this.clearEntry(name);
3423
+ for (const name of this.agents) this.clearEntry(name);
3238
3424
  return;
3239
3425
  }
3240
3426
  if (row) this.clearEntry(row);
3241
3427
  }
3242
3428
 
3243
3429
  private setModel(name: string, model: string | undefined): void {
3430
+ if (name === PROFILE_ORCHESTRATOR_KEY && model === undefined) {
3431
+ delete this.draft[name];
3432
+ return;
3433
+ }
3244
3434
  const current = this.draft[name] ?? {};
3245
3435
  if (model === undefined) delete current.model;
3246
3436
  else current.model = model;
@@ -3249,6 +3439,7 @@ class SddModelPanel implements OverlayComponent {
3249
3439
  }
3250
3440
 
3251
3441
  private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
3442
+ if (name === PROFILE_ORCHESTRATOR_KEY && !this.draft[name]?.model) return;
3252
3443
  const current = this.draft[name] ?? {};
3253
3444
  if (thinking === undefined) delete current.thinking;
3254
3445
  else current.thinking = thinking;
@@ -3257,13 +3448,17 @@ class SddModelPanel implements OverlayComponent {
3257
3448
  }
3258
3449
 
3259
3450
  private clearEntry(name: string): void {
3260
- this.draft[name] = {};
3451
+ if (name === PROFILE_ORCHESTRATOR_KEY) delete this.draft[name];
3452
+ else this.draft[name] = {};
3261
3453
  }
3262
3454
 
3263
3455
  private filteredModelOptions(): string[] {
3456
+ const options = this.selectedRow === PROFILE_ORCHESTRATOR_KEY
3457
+ ? this.modelOptions.map((option) => option === INHERIT_MODEL ? LEAVE_ORCHESTRATOR_UNTOUCHED : option)
3458
+ : this.modelOptions;
3264
3459
  const query = this.query.trim().toLowerCase();
3265
- if (!query) return this.modelOptions;
3266
- return this.modelOptions.filter((option) =>
3460
+ if (!query) return options;
3461
+ return options.filter((option) =>
3267
3462
  option.toLowerCase().includes(query),
3268
3463
  );
3269
3464
  }
@@ -3320,13 +3515,13 @@ class SddModelPanel implements OverlayComponent {
3320
3515
  // longer fits the minimum width once the profile key joins the save keys.
3321
3516
  lines.push(
3322
3517
  line(
3323
- `j/k scroll • enter model/save • e effort • i ${isProviderReviewRole(this.rows[this.cursor] ?? "") ? "Pi persisted defaults" : "inherit"} • c custom`,
3518
+ `j/k scroll • enter model/save • e effort • i ${this.rows[this.cursor] === PROFILE_ORCHESTRATOR_KEY ? LEAVE_ORCHESTRATOR_UNTOUCHED : isProviderReviewRole(this.rows[this.cursor] ?? "") ? "Pi persisted defaults" : "inherit"} • c custom`,
3324
3519
  "muted",
3325
3520
  ),
3326
3521
  );
3327
3522
  lines.push(
3328
3523
  line(
3329
- `x export • r restore • ctrl+s save • u capture session in "${this.profileLabel}" • esc back`,
3524
+ `x export • r restore • ctrl+s save agents • u save choice in "${this.profileLabel}" • esc back`,
3330
3525
  "muted",
3331
3526
  ),
3332
3527
  );
@@ -3426,11 +3621,9 @@ class SddModelPanel implements OverlayComponent {
3426
3621
  }
3427
3622
 
3428
3623
  private renderSetAllLabel(row: string): string {
3429
- const models = this.rows
3430
- .slice(1)
3624
+ const models = this.agents
3431
3625
  .map((name) => this.draft[name]?.model ?? routingDefaultLabel(name, "model"));
3432
- const efforts = this.rows
3433
- .slice(1)
3626
+ const efforts = this.agents
3434
3627
  .map((name) => this.draft[name]?.thinking ?? routingDefaultLabel(name, "effort"));
3435
3628
  const firstModel = models[0] ?? "inherit";
3436
3629
  const firstEffort = efforts[0] ?? "inherit";
@@ -3447,6 +3640,9 @@ class SddModelPanel implements OverlayComponent {
3447
3640
  }
3448
3641
 
3449
3642
  private renderAgentLabel(row: string): string {
3643
+ if (row === PROFILE_ORCHESTRATOR_KEY && !this.draft[row]?.model) {
3644
+ return `${this.renderText(row.padEnd(20), "text")} ${this.renderText(LEAVE_ORCHESTRATOR_UNTOUCHED, "status")}`;
3645
+ }
3450
3646
  const model = this.draft[row]?.model ?? routingDefaultLabel(row, "model");
3451
3647
  const effort = this.draft[row]?.thinking ?? routingDefaultLabel(row, "effort");
3452
3648
  return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(model, "status")}${this.renderText(
@@ -3521,8 +3717,18 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3521
3717
  );
3522
3718
  return;
3523
3719
  }
3524
- let config = savedConfig.status === "valid" ? savedConfig.config : {};
3525
- const profileLabel = describeCurrentProfileTarget(resolveCurrentProfileTarget(ctx.cwd));
3720
+ let config = savedConfig.status === "valid" ? cloneModelConfig(savedConfig.config) : {};
3721
+ const store = readProfilesFileResult(profilesFilePath(gentleAiConfigHome()));
3722
+ const target = resolveCurrentProfileTarget(ctx.cwd, store);
3723
+ // Existing profiles own the initial choice, including absence. Reopens keep the draft.
3724
+ delete config[PROFILE_ORCHESTRATOR_KEY];
3725
+ if (store.status === "valid" && target && hasOwnProfile(store.file.profiles, target.name)) {
3726
+ const entry = store.file.profiles[target.name][PROFILE_ORCHESTRATOR_KEY];
3727
+ if (entry) config[PROFILE_ORCHESTRATOR_KEY] = { ...entry };
3728
+ } else {
3729
+ config = profileSnapshotFrom(config, liveOrchestratorSnapshot(ctx, pi) ?? readOrchestratorSettings(orchestratorSettingsPath()));
3730
+ }
3731
+ const profileLabel = describeCurrentProfileTarget(target);
3526
3732
  let result = await showSddModelPanel(ctx, config, profileLabel);
3527
3733
  while (result.type === "custom" || result.type === "export" || result.type === "restore") {
3528
3734
  config = cloneModelConfig(result.config);
@@ -3552,7 +3758,10 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3552
3758
  result = await showSddModelPanel(ctx, config, profileLabel);
3553
3759
  continue;
3554
3760
  }
3555
- config = restored;
3761
+ const orchestratorChoice = config[PROFILE_ORCHESTRATOR_KEY];
3762
+ config = cloneModelConfig(restored);
3763
+ delete config[PROFILE_ORCHESTRATOR_KEY];
3764
+ if (orchestratorChoice) config[PROFILE_ORCHESTRATOR_KEY] = orchestratorChoice;
3556
3765
  try {
3557
3766
  const applyResult = await applyModelConfigAsync(ctx.cwd, restored);
3558
3767
  ctx.ui.notify([
@@ -3614,18 +3823,21 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3614
3823
  result = await showSddModelPanel(ctx, config, profileLabel);
3615
3824
  }
3616
3825
  if (result.type !== "save" && result.type !== "save-profile") return;
3617
- writeModelConfig(ctx.cwd, result.config);
3618
- const applyResult = await applyModelConfigAsync(ctx.cwd, result.config);
3826
+ // The panel's orchestrator is snapshot-only, never a global agent route.
3827
+ const routing = cloneModelConfig(result.config);
3828
+ delete routing[PROFILE_ORCHESTRATOR_KEY];
3829
+ writeModelConfig(ctx.cwd, routing);
3830
+ const applyResult = await applyModelConfigAsync(ctx.cwd, routing);
3619
3831
  ctx.ui.notify(
3620
3832
  [
3621
3833
  "el Gentleman global model config saved.",
3622
3834
  `Global config: ${modelConfigPath(ctx.cwd)}`,
3623
3835
  `Agents updated: ${applyResult.updated}`,
3624
- ...describeModelConfig(ctx.cwd, result.config),
3836
+ ...describeModelConfig(ctx.cwd, routing),
3625
3837
  ].join("\n"),
3626
3838
  "info",
3627
3839
  );
3628
- if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, pi);
3840
+ if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, result.config[PROFILE_ORCHESTRATOR_KEY]);
3629
3841
  }
3630
3842
 
3631
3843
  /** The profile `u` in `/gentle:models` writes to, and why it is that one. */
@@ -3657,11 +3869,10 @@ function describeCurrentProfileTarget(target: CurrentProfileTarget | undefined):
3657
3869
 
3658
3870
  /**
3659
3871
  * The second half of `u` captures saved global agent routing (never the pin)
3660
- * and the live session orchestrator. Without a live model it falls back to
3661
- * persisted settings. The global save has already been reported, so every
3662
- * outcome here speaks only about the profile.
3872
+ * and the explicit panel choice, including key absence. The global save has
3873
+ * already been reported, so every outcome here speaks only about the profile.
3663
3874
  */
3664
- function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: ExtensionAPI): void {
3875
+ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, orchestrator: AgentRoutingEntry | undefined): void {
3665
3876
  const path = profilesFilePath(gentleAiConfigHome());
3666
3877
  const read = readProfilesFileResult(path);
3667
3878
  if (read.status === "invalid") {
@@ -3671,16 +3882,9 @@ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: Extensi
3671
3882
  );
3672
3883
  return;
3673
3884
  }
3674
- const snapshot = profileSnapshotFrom(
3675
- readGlobalEffectiveModelConfig(ctx.cwd),
3676
- readOrchestratorSettings(orchestratorSettingsPath()),
3677
- );
3678
- if (ctx.model) {
3679
- snapshot[PROFILE_ORCHESTRATOR_KEY] = {
3680
- model: `${ctx.model.provider}/${ctx.model.id}`,
3681
- thinking: pi.getThinkingLevel(),
3682
- };
3683
- }
3885
+ const snapshot = cloneModelConfig(readGlobalEffectiveModelConfig(ctx.cwd));
3886
+ delete snapshot[PROFILE_ORCHESTRATOR_KEY];
3887
+ if (orchestrator?.model) snapshot[PROFILE_ORCHESTRATOR_KEY] = { ...orchestrator };
3684
3888
  if (read.status === "missing") {
3685
3889
  // The same seed `/gentle:profiles` performs on its first open, so pressing
3686
3890
  // `u` before ever opening that panel lands on the same "current" profile.
@@ -3859,10 +4063,10 @@ function profilePinScopeNote(cwd: string): string | undefined {
3859
4063
  * write that already succeeded must not become an error because a probe about sharing
3860
4064
  * it failed.
3861
4065
  */
3862
- function repoDeclarationSharing(root: string, path: string): string {
4066
+ export function repoDeclarationSharing(root: string, path: string): string {
3863
4067
  const relativePath = relative(root, path).split(sep).join("/");
3864
4068
  const run = (...arguments_: string[]): string =>
3865
- execFileSync("git", arguments_, { cwd: root, encoding: "utf8" }).trim();
4069
+ execFileSync("git", arguments_, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
3866
4070
  try {
3867
4071
  // Exit 0 means gitignore matches the path; the exact negation line is what makes
3868
4072
  // the declaration committable again.
@@ -3914,6 +4118,8 @@ class ProfilesPanel implements OverlayComponent {
3914
4118
  // Actions reopen the panel, refreshing this snapshot without disk reads during rendering.
3915
4119
  private readonly pinStatus: ProfilePinStatus | undefined;
3916
4120
  private readonly sessionBoundName: string | undefined;
4121
+ private readonly discoveredAgentNames: string[];
4122
+ private readonly unmatchedKeys: string[];
3917
4123
  private readonly liveOrchestrator: (() => AgentRoutingEntry | undefined) | undefined;
3918
4124
 
3919
4125
  constructor(
@@ -3929,6 +4135,8 @@ class ProfilesPanel implements OverlayComponent {
3929
4135
  requestRender: () => void,
3930
4136
  pinStatus: () => ProfilePinStatus | undefined,
3931
4137
  sessionBound: () => string | undefined,
4138
+ discoveredAgentNames: string[],
4139
+ unmatchedKeys: string[],
3932
4140
  liveOrchestrator?: () => AgentRoutingEntry | undefined,
3933
4141
  feedback?: string,
3934
4142
  ) {
@@ -3943,6 +4151,8 @@ class ProfilesPanel implements OverlayComponent {
3943
4151
  this.orchestratorSettings = orchestratorSettings;
3944
4152
  this.pinStatus = pinStatus();
3945
4153
  this.sessionBoundName = sessionBound();
4154
+ this.discoveredAgentNames = discoveredAgentNames;
4155
+ this.unmatchedKeys = unmatchedKeys;
3946
4156
  this.liveOrchestrator = liveOrchestrator;
3947
4157
  const items = buildProfileListItems(file, evaluateProfilePin(this.pinStatus, file.profiles).winner?.profile, this.sessionBoundName);
3948
4158
  this.listItems = items;
@@ -4178,10 +4388,30 @@ class ProfilesPanel implements OverlayComponent {
4178
4388
  : [this.renderLine(`session ${sanitizeTerminalText(this.sessionBoundName)} (session) — launches resolve it ahead of pins and the global default; nothing was written`, width, "muted")]),
4179
4389
  "",
4180
4390
  this.renderLine("Profile routing", width, "accent"),
4181
- ...this.indentLines(this.routingLines(profileRows, widths), width),
4391
+ ...this.indentLines(this.routingLines(profileRows, widths, width), width),
4182
4392
  "",
4183
4393
  this.renderLine("Current routing (effective)", width, "accent"),
4184
- ...this.indentLines(this.routingLines(currentRows, widths), width),
4394
+ ...this.indentLines(this.routingLines(currentRows, widths, width), width),
4395
+ ...this.unmatchedKeyLines(currentRows, width),
4396
+ ];
4397
+ }
4398
+
4399
+ private unmatchedKeyLines(currentRows: ProfileRoutingRow[], width: number): string[] {
4400
+ // Issue #2400: a model_profiles key that matches no discovered agent and
4401
+ // suggests none never becomes a routing row, so it is listed here, one key
4402
+ // per line, with the same inline-or-↳ note rule as the routing rows. A key
4403
+ // already shown as a current row carries its note there instead.
4404
+ const shown = new Set(currentRows.map((row) => row.agent));
4405
+ const keys = this.unmatchedKeys.filter((key) => !shown.has(key));
4406
+ if (keys.length === 0) return [];
4407
+ const note = "no matching agent";
4408
+ return [
4409
+ "",
4410
+ this.renderLine("Unmatched keys (ignored)", width, "accent"),
4411
+ ...this.indentLines(keys.flatMap((key) => {
4412
+ const inline = `${key} ← ${note}`;
4413
+ return visibleWidth(inline) + 2 <= width ? [inline] : [key, ` ↳ ${note}`];
4414
+ }), width),
4185
4415
  ];
4186
4416
  }
4187
4417
 
@@ -4203,11 +4433,24 @@ class ProfilesPanel implements OverlayComponent {
4203
4433
  private routingLines(
4204
4434
  rows: ProfileRoutingRow[],
4205
4435
  widths: { agent: number; model: number },
4436
+ width: number,
4206
4437
  ): string[] {
4207
4438
  if (rows.length === 0) {
4208
4439
  return ["No routing entries — every agent inherits its default model."];
4209
4440
  }
4210
- return rows.map((row) => formatRoutingRow(row, widths));
4441
+ // Issue #2196: a key that names no discovered agent was rendered as a
4442
+ // valid-looking row while its setting was silently ignored at launch, so
4443
+ // each unmatched row now carries its note inline. When the row would push
4444
+ // the note past the pane edge (indentLines adds two columns before
4445
+ // truncating), the note moves to its own line so it is never cut off.
4446
+ const notes = unmatchedRoutingNotes(rows, this.discoveredAgentNames);
4447
+ return rows.flatMap((row) => {
4448
+ const line = formatRoutingRow(row, widths);
4449
+ const note = notes.get(row.agent);
4450
+ if (note === undefined) return [line];
4451
+ const inline = `${line} ← ${note}`;
4452
+ return visibleWidth(inline) + 2 <= width ? [inline] : [line, ` ↳ ${note}`];
4453
+ });
4211
4454
  }
4212
4455
 
4213
4456
  private indentLines(lines: string[], width: number): string[] {
@@ -4253,6 +4496,28 @@ async function showProfilesPanel(
4253
4496
  // Both orchestrator and pin state are snapshots for this panel visit.
4254
4497
  // Actions (including p/P) reopen the panel and read fresh state.
4255
4498
  const orchestratorSettings = readOrchestratorSettings(orchestratorSettingsPath());
4499
+ // Issue #2196: the panel flags routing keys that name no discovered agent, so
4500
+ // it needs one discovery snapshot per visit; a failed or partial discovery is
4501
+ // not an error here, it just renders without notes.
4502
+ const discoveryRoots = {
4503
+ cwd: ctx.sessionManager.getCwd?.() ?? ctx.cwd,
4504
+ home: homedir(),
4505
+ agentHome: resolveGentlePiAgentHome(),
4506
+ };
4507
+ const discoveredAgents = discoverAgents(discoveryRoots).agents;
4508
+ // gentle-pi#2271: review-refuter and review-validator are provider review
4509
+ // roles the relay routes by key; they never have an agent file, so they
4510
+ // count as matched instead of being flagged "no matching agent".
4511
+ const discoveredAgentNames = [...new Set([...PROVIDER_REVIEW_ROLES, ...discoveredAgents.map((agent) => agent.name)])];
4512
+ // Issue #2400: launches resolve from the subagents.json model_profiles, which
4513
+ // the effective table only reads per discovered agent, so a key with no match
4514
+ // and no suggestion never appeared. When a session binding or a pin wins, the
4515
+ // table already shows that snapshot's own keys with their notes.
4516
+ const unmatchedKeys = sessionBoundName !== undefined || pinnedEffectiveModelConfig(ctx.cwd) !== undefined
4517
+ ? []
4518
+ : unmatchedProfileKeys(loadAgentsConfig(discoveryRoots).modelProfiles, discoveredAgents)
4519
+ .filter((entry) => entry.suggestion === undefined && !isProviderReviewRole(entry.key))
4520
+ .map((entry) => entry.key);
4256
4521
  return ctx.ui.custom<ProfilesPanelResult>(
4257
4522
  (tui, theme, keybindings, done) => {
4258
4523
  const panel = new ProfilesPanel(
@@ -4268,6 +4533,8 @@ async function showProfilesPanel(
4268
4533
  () => tui.requestRender(),
4269
4534
  () => readProfilePinStatus(ctx.cwd),
4270
4535
  () => sessionBoundName,
4536
+ discoveredAgentNames,
4537
+ unmatchedKeys,
4271
4538
  () => liveOrchestratorSnapshot(ctx, live)?.entry,
4272
4539
  status,
4273
4540
  );
@@ -5140,7 +5407,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
5140
5407
  },
5141
5408
  input: {
5142
5409
  type: "string",
5143
- description: "A JSON object or serialized object string for START/ASSESS only; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
5410
+ description: "A JSON object or serialized object string for START/ASSESS/answer-consent; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). The writer profile is reported but never adds a verifier: the plan adds the independent verifier only when escalate names a Task Size high-risk item. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
5144
5411
  },
5145
5412
  outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
5146
5413
  inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
@@ -5411,7 +5678,7 @@ function parseReviewControllerParameters(value: unknown): ReviewControllerParame
5411
5678
  };
5412
5679
  for (const key of ["changeName", "idempotencyKey", "transition", "input", "outputPath", "inputPath", "operationId", "lineageIds", "acknowledgeUntrustedBundleSource", "workspaceRoot"] as const) {
5413
5680
  const optional = value[key];
5414
- if (key === "input" && key in value && (value.operation === REVIEW_CONTROLLER_OPERATION.START || value.operation === REVIEW_CONTROLLER_OPERATION.ASSESS)) {
5681
+ if (key === "input" && key in value && (value.operation === REVIEW_CONTROLLER_OPERATION.START || value.operation === REVIEW_CONTROLLER_OPERATION.ASSESS || value.operation === REVIEW_CONTROLLER_OPERATION.ANSWER_CONSENT)) {
5415
5682
  parameters.input = serializeReviewJsonArgument(optional);
5416
5683
  continue;
5417
5684
  }
@@ -5639,21 +5906,18 @@ const REVIEW_MODE_DISABLED_OUTCOME = "review-mode-disabled";
5639
5906
  // to make the global opt-in first when needed, then clear this clone override.
5640
5907
  // Pi never mutates the operator's global gentle-ai state automatically.
5641
5908
  //
5642
- // The default branch changed with the pinned v2.4.0 runtime, which made
5643
- // receipt-driven development opt-in. It used to be unreachable as a reason for
5644
- // reviews being off — an all-sources-unset install resolved to ON with source
5645
- // `default` — so naming a continuation for it would have been a guess, and
5646
- // gentle-ai returned an empty scope to say exactly that. v2.4.0 resolves the
5647
- // same install to OFF with source `default`, which makes it the most common
5648
- // refusal there is: every install that never opted in. gentle-ai answers
5649
- // `global` for it now, not because default is a global opinion but because
5650
- // global is the only scope that can turn reviews on at all, and Pi answers the
5651
- // same. Leaving this undefined would hand the single most common state a dead
5652
- // end.
5909
+ // Receipt-driven development is on by default (gentle-ai v4.0.0, #4803): an
5910
+ // all-sources-unset install resolves to ON with source `default`. The pinned
5911
+ // v2.4.0 runtime had briefly made it opt-in, which is why this branch exists.
5912
+ // Today `default` only reaches here as gentle-ai's fail-closed projection: the
5913
+ // mode could not be resolved, so it reports OFF with source `default`. The
5914
+ // continuation says so instead of claiming the install never opted in, and
5915
+ // still names `global`, the only scope that can turn reviews on explicitly.
5916
+ // Leaving this undefined would hand that state a dead end.
5653
5917
  function reviewModeContinuation(source: NativeReviewModeSource): string | undefined {
5654
5918
  if (source === NATIVE_REVIEW_MODE_SOURCE.CLONE_LOCAL) return "Run `gentle-ai review mode enable --scope=global` if global RDD is still off, then run /gentle:review-mode enable to clear this clone-local override.";
5655
5919
  if (source === NATIVE_REVIEW_MODE_SOURCE.GLOBAL) return "Run `gentle-ai review mode enable --scope=global` to turn reviews back on; /gentle:review-mode enable only clears the clone-local setting, which cannot override a global off.";
5656
- return "Run `gentle-ai review mode enable --scope=global` to opt in; RDD is off by default until explicitly enabled. /gentle:review-mode enable only clears a clone-local override and cannot enable global RDD.";
5920
+ return "RDD is on by default, but this install could not resolve the review mode and failed closed to off. Run `gentle-ai review mode status` to see why, or `gentle-ai review mode enable --scope=global` to turn reviews on explicitly. /gentle:review-mode enable only clears a clone-local override and cannot enable global RDD.";
5657
5921
  }
5658
5922
 
5659
5923
  // Names the situation before the mechanism, then the mechanism, mirroring
@@ -6611,6 +6875,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
6611
6875
  // named-agent start increments the depth, a matching end decrements it,
6612
6876
  // and a fresh primary-loop start resets it to 0.
6613
6877
  const processAgentEndSubagentDepth = new Map<PendingReviewConsentSessionKey, number>();
6878
+ // gentle-shell#2195: the RDD status line each primary session last rendered into its system prompt.
6879
+ const pinnedRddStatusLines = new Map<PendingReviewConsentSessionKey, string>();
6614
6880
 
6615
6881
  // gentle-shell#1064 slice 1: the parent-session profile binding store lives in
6616
6882
  // lib/session-profile-binding.ts (in-process, keyed by parent session id). The
@@ -7304,11 +7570,24 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
7304
7570
  // its provider through the live model registry the caller supplies, or it is
7305
7571
  // refused before materialize ever runs (validateReviewerSelectionConfiguration
7306
7572
  // in lib/review-host-relay.ts).
7573
+ //
7574
+ // gentle-pi#2271: review-refuter and review-validator are provider review
7575
+ // roles, not installable agents, so nothing seeds their entries and profiles
7576
+ // never carry them. For those two keys only, a snapshot with no model falls
7577
+ // back to Pi's persisted default model and effort, the default the routing
7578
+ // panel already labels them with. Lens keys keep the fail-closed rule.
7307
7579
  function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
7308
7580
  const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
7309
7581
  const entry = config[routingKey];
7310
- const model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
7311
- const thinking = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).thinking === "string" ? (entry as AgentRoutingEntry).thinking : undefined;
7582
+ let model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
7583
+ let thinking = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).thinking === "string" ? (entry as AgentRoutingEntry).thinking : undefined;
7584
+ if (model === undefined && isProviderReviewRole(routingKey)) {
7585
+ const persisted = readOrchestratorSettings(orchestratorSettingsPath());
7586
+ if (persisted.status === "valid" && persisted.entry?.model !== undefined) {
7587
+ model = persisted.entry.model;
7588
+ thinking ??= persisted.entry.thinking;
7589
+ }
7590
+ }
7312
7591
  return {
7313
7592
  ...(model === undefined ? {} : { selection: model }),
7314
7593
  ...(thinking === undefined ? {} : { thinking }),
@@ -7990,6 +8269,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
7990
8269
 
7991
8270
  function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
7992
8271
 
8272
+ function captureGroupStatusFailure(error: unknown): Record<string, unknown> {
8273
+ return {
8274
+ ...captureGroupRejected(error instanceof Error ? error.message : String(error)),
8275
+ outcome: "native-status-failed",
8276
+ // Only project the decoded native envelope, never arbitrary error fields.
8277
+ // Group mutation reporting belongs to its submitted-reviewer progress.
8278
+ ...(error instanceof NativeReviewIntegrationError ? {
8279
+ native_failure: error.failureEnvelope.raw,
8280
+ replayability: error.failureEnvelope.replayability,
8281
+ next_action: error.failureEnvelope.nextAction,
8282
+ } : {}),
8283
+ };
8284
+ }
8285
+
7993
8286
  function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
7994
8287
  const current = status.nextTransition?.kind === "collect"
7995
8288
  ? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
@@ -8296,7 +8589,7 @@ async function executeReviewCaptureGroupOperation(
8296
8589
  if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
8297
8590
  status = negotiated.status!;
8298
8591
  } catch (error) {
8299
- return { ...captureGroupRejected(error instanceof Error ? error.message : String(error)), outcome: "native-status-failed" };
8592
+ return captureGroupStatusFailure(error);
8300
8593
  }
8301
8594
  const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
8302
8595
  if (!isSelectedReviewCaptureGroup(group)) return group;
@@ -8342,7 +8635,7 @@ async function executeReviewCaptureGroupOperation(
8342
8635
  if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8343
8636
  current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
8344
8637
  } catch (error) {
8345
- return { ...captureGroupRejected(error instanceof Error ? error.message : String(error)), outcome: "native-status-failed", ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8638
+ return { ...captureGroupStatusFailure(error), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8346
8639
  }
8347
8640
  if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8348
8641
  try {
@@ -9461,6 +9754,7 @@ export const __testing = {
9461
9754
  guardedCommandTitle,
9462
9755
  loadRuntimeGuardrailsConfig,
9463
9756
  isReversibleYoloCommand,
9757
+ evaluateSuperYoloCommand,
9464
9758
  yoloConfiguredRestriction,
9465
9759
  buildGentlePrompt,
9466
9760
  nativeStatusUnsupported,
@@ -9477,6 +9771,7 @@ export const __testing = {
9477
9771
  renderOrchestratorPrompt,
9478
9772
  loadReviewContractPromptFragment,
9479
9773
  readMirroredReviewContractFragment,
9774
+ readMirroredOddProtocolSteps,
9480
9775
  loadBackgroundSubagentsPolicy,
9481
9776
  resolveBackgroundSubagentsPolicy,
9482
9777
  renderBackgroundSubagentsReport,
@@ -9486,6 +9781,7 @@ export const __testing = {
9486
9781
  readActiveToolNames,
9487
9782
  renderBackgroundSubagentsStatusLine,
9488
9783
  renderRddStatusLine,
9784
+ pinRddStatusLine,
9489
9785
  isValidRddModeStatus,
9490
9786
  resolveRddModeStatus,
9491
9787
  resolveRddStatusLine,
@@ -9529,6 +9825,9 @@ export interface GentleAiRuntimeDependencies {
9529
9825
  telemetryExecFileAdapter?: ExecFileAdapter;
9530
9826
  /** Test-only inference seam; production always uses the active session model. */
9531
9827
  autoPermissionClassifier?: AutoRuntimeClassifier;
9828
+ // The backstop samples the measured context at most once per window (its measure rebuilds the whole
9829
+ // session projection). Tests inject a clock or a zero window; production resolves the env and default.
9830
+ contextBackstopMeasure?: { now?: () => number; minIntervalMs?: number };
9532
9831
  }
9533
9832
 
9534
9833
  export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencies = {}): (pi: ExtensionAPI) => void {
@@ -9607,11 +9906,17 @@ function createGentleAiExtensionForTesting(
9607
9906
  reviewSidebar.reset(ctx);
9608
9907
  statusTiming.reset(ctx);
9609
9908
  });
9909
+ // Lean RDD-off review loadout (applyRddOffReviewPrompt): every
9910
+ // before_agent_start sets it from the pinned line before Pi prepares that
9911
+ // run's tool loadout, so gentle_review's prepareLoadout reads this run's value.
9912
+ let rddOffReviewLoadout = false;
9610
9913
  let reminderSessionActive = true;
9611
9914
  let reminderEpoch = 0;
9612
9915
  let reminderManager: ExtensionContext["sessionManager"] | undefined;
9613
9916
  let unbindPreparation: (() => void) | undefined;
9614
9917
  pi.on("session_shutdown", (event, context) => {
9918
+ pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
9919
+ rddOffReviewLoadout = false;
9615
9920
  auto.reset();
9616
9921
  yolo.reset(context);
9617
9922
  reviewSidebar.reset();
@@ -9859,10 +10164,20 @@ function createGentleAiExtensionForTesting(
9859
10164
  "For blocked-legacy or blocked-mixed, do not call START repeatedly. Explain invalidation, request explicit user authorization, then call RESET or RECOVER only after authorization. RESET and RECOVER_LOCK route to audited native `gentle-ai review reclaim`; only RESET carries the legacy repositoryId, commonDirHash, inventoryHash, and confirmation challenge. RECOVER routes to native `gentle-ai review recover` with exactly six inputs: predecessorLineage, expectedPredecessorRevision, successorLineage, disposition, actor, and reason. Never send RECOVER the reset challenge and never send it a maintainerAuthorization: Pi reads fresh native target status, pins the predecessor lineage, revision, provider-selected disposition, and target identity, derives the exact six-line native authorization binding, displays it for fresh UI approval, and re-reads status before mutating. Negotiated target status supplies the sole accepted recovery disposition, and a caller-supplied substitute is rejected. Treat a native-input-required envelope as a request for exact values, never as permission to invent them. After a committed native recovery record, INSPECT before any fresh ordinary START.",
9860
10165
  "A consent-required START may be resolved inside the eligible interactive Pi host. Its third UI action is host-owned: it runs this envelope's exact provider grant once and allows later fresh validated envelopes only for the same live SessionManager, nonempty session ID, and canonical Git common-directory identity, including sibling worktrees; an unrelated repository requires a new explicit human grant. Revoke removes the current repository grant, while nonreload replacement, quit, and process exit remove all session grants; reload preserves them. It grants no provider mode, verdict, acknowledgement, maintenance, delivery, or cross-repository authority. A package-owned child may ask its parent only with the canonical digest of its exact pending target; the parent binds that digest to the task repository and fails closed otherwise. If the tool returns an unresolved envelope, present the original two provider choices without changing machine tokens, commands, target IDs, or invocations; never add the host action to the decoded provider envelope. After one explicit relayed human answer, call answer-consent exactly once with only consentBinding and answer (`granted` or `declined`). Never create host permission from tool arguments, model prose, child/headless responses, or an uncertain native result. A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START output, the controller calls target-scoped native status once and returns only its declared action. An ambiguous gentle_review_capture outcome independently reconciles once and never replays the capture.",
9861
10166
  "Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
9862
- 'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it raises passive or medium to high, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. The plan follows the native risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment reports risk "unassessable", verified exactly like "high". This never mutates review authority state.',
10167
+ 'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it is the only way to get an independent verifier, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). The writer profile is reported but never adds a verifier. The plan follows its risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment (nativeRisk "unassessable"), or a native high, informs only: the plan adds the independent verifier only when escalate names a Task Size high-risk item. This never mutates review authority state.',
9863
10168
  ],
9864
10169
  parameters: REVIEW_CONTROLLER_PARAMETERS,
9865
10170
  executionMode: "sequential",
10171
+ // Hidden declarations stay active and callable; Pi leaves their
10172
+ // declarations and guidelines out of the request.
10173
+ prepareLoadout(loadout) {
10174
+ if (!rddOffReviewLoadout) return undefined;
10175
+ const declared = new Set(loadout.declared.map((tool) => tool.name));
10176
+ return {
10177
+ descriptions: { gentle_review: RDD_OFF_REVIEW_DESCRIPTION },
10178
+ hiddenDeclarations: RDD_OFF_HIDDEN_REVIEW_TOOLS.filter((name) => declared.has(name)),
10179
+ };
10180
+ },
9866
10181
  renderCall(args, theme, context) {
9867
10182
  return renderGentleAiLifecycleCall(
9868
10183
  reviewToolOperationPath(args),
@@ -10139,7 +10454,13 @@ function createGentleAiExtensionForTesting(
10139
10454
  if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
10140
10455
  const rddStatusLine = !isPrimarySession
10141
10456
  ? undefined
10142
- : await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx);
10457
+ : pinRddStatusLine(
10458
+ pinnedRddStatusLines.get(subagentDepthKey),
10459
+ await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx),
10460
+ );
10461
+ if (rddStatusLine !== undefined) pinnedRddStatusLines.set(subagentDepthKey, rddStatusLine);
10462
+ rddOffReviewLoadout = isPrimarySession && isRddStatusLineOff(rddStatusLine);
10463
+ if (rddOffReviewLoadout) applyRddOffReviewPrompt(event.systemPromptOptions as ReviewToolPromptOptions | undefined);
10143
10464
  const gentlePrompt = rddStatusLine === undefined
10144
10465
  ? ""
10145
10466
  : `\n\n${buildGentlePrompt(
@@ -10165,7 +10486,7 @@ function createGentleAiExtensionForTesting(
10165
10486
  // and forwards only systemPromptOptions, so the harness is delivered
10166
10487
  // through the mutable appendSystemPrompt section instead of a replacement.
10167
10488
  appendSystemPromptOnce(event.systemPromptOptions, `${gentlePrompt}${reviewContractPrompt}`);
10168
- updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx));
10489
+ updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx), isPrimarySession && await yolo.superActive(ctx));
10169
10490
  updateAutoPrompt(event.systemPromptOptions, isPrimarySession && await yolo.mode(ctx) === "auto");
10170
10491
  return undefined;
10171
10492
  });
@@ -10222,7 +10543,7 @@ function createGentleAiExtensionForTesting(
10222
10543
  consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
10223
10544
  });
10224
10545
 
10225
- pi.on("tool_result", async (event, ctx) => {
10546
+ const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
10226
10547
  if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
10227
10548
  if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
10228
10549
  try {
@@ -10234,21 +10555,69 @@ function createGentleAiExtensionForTesting(
10234
10555
  if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
10235
10556
  if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
10236
10557
  } catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
10558
+ };
10559
+ const contextBackstopSignal = createContextBackstopSignal(permissionEnvironment);
10560
+ // One projection rebuild per session per window, not one per tool result: the measured context is
10561
+ // sampled (see createThrottledContextMeasure), so long sessions stop paying an O(session) scan on
10562
+ // every tool result while the backstop keeps its once-per-crossing notice semantics.
10563
+ const contextBackstopMeasure = createThrottledContextMeasure({
10564
+ now: dependencies.contextBackstopMeasure?.now,
10565
+ minIntervalMs: dependencies.contextBackstopMeasure?.minIntervalMs,
10566
+ env: permissionEnvironment,
10567
+ });
10568
+ pi.on("tool_result", async (event, ctx) => {
10569
+ // Harness roadmap R1: the primary session's measured context is sampled against the backstop; the
10570
+ // first sampled result at or past it carries one notice, after any other patch below. The sample
10571
+ // runs at most once per window per session (default 5 s, GENTLE_PI_CONTEXT_BACKSTOP_MEASURE_MS),
10572
+ // so a crossing is announced on the next sampled result — never later than one window past it.
10573
+ const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
10574
+ const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
10575
+ let notice: string | undefined;
10576
+ try { notice = primary ? contextBackstopSignal(sessionKey, contextBackstopMeasure(sessionKey, () => ctx.getContextUsage?.())) : undefined; } catch { notice = undefined; }
10577
+ const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
10578
+ // Harness roadmap T4: a parent commit reports the branch's authored
10579
+ // changed lines so delivery slicing reads a number, not an estimate.
10580
+ if (SHELL_COMMAND_TOOLS.has(event.toolName)) {
10581
+ if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
10582
+ try { return withNotice(await commitSliceToolResult(event, ctx.cwd)); } catch { return withNotice(undefined); }
10583
+ }
10584
+ await recordWriteResult(event, ctx);
10585
+ return withNotice(undefined);
10237
10586
  });
10238
10587
 
10239
10588
  pi.on("tool_call", async (event, ctx) => {
10589
+ // Native review tools stalled the session when a codemode script called them
10590
+ // (a nested call, which carries parentToolCallId); direct calls return at once.
10591
+ if (event.toolName.startsWith("gentle_review") && typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string") {
10592
+ return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
10593
+ }
10240
10594
  const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
10241
- const yoloActive = primaryToolCall && await yolo.active(ctx);
10595
+ // One early mode read settles the session binding (and its one-time epoch
10596
+ // bump) before the guarded region below captures its own epoch.
10597
+ const settledMode = primaryToolCall ? await yolo.mode(ctx, { toolCallId: event.toolCallId }) : "normal";
10598
+ const yoloActive = primaryToolCall && settledMode === "yolo";
10599
+ const superYoloActive = primaryToolCall && settledMode === "super-yolo";
10242
10600
  const sensitivePathDenied = evaluateSensitivePathTool(
10243
10601
  event.toolName,
10244
10602
  event.input,
10245
10603
  );
10246
10604
  if (sensitivePathDenied) return sensitivePathDenied;
10605
+ const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
10606
+ if (deliveredTestDenied) return deliveredTestDenied;
10247
10607
  if (event.toolName === "subagent_run") {
10248
10608
  const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
10249
10609
  if (judgmentDayFixDenied) return judgmentDayFixDenied;
10250
10610
  const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
10251
10611
  if (writerScopeDenied) return writerScopeDenied;
10612
+ if (primaryToolCall) {
10613
+ const launch = event.input as Record<string, unknown>;
10614
+ const selected = [launch.workspace_root, launch.repository_root].find((value): value is string => typeof value === "string" && value.trim() !== "");
10615
+ const root = selected ? resolve(ctx.cwd, selected) : ctx.cwd;
10616
+ const undocumentedWriter = rejectUndocumentedWriterLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
10617
+ if (undocumentedWriter) return undocumentedWriter;
10618
+ const unriskedVerifier = rejectUnriskedVerifierLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
10619
+ if (unriskedVerifier) return unriskedVerifier;
10620
+ }
10252
10621
  try {
10253
10622
  injectReviewCandidateView(event.input, candidateViews);
10254
10623
  return undefined;
@@ -10273,13 +10642,17 @@ function createGentleAiExtensionForTesting(
10273
10642
  if (childDenied) return childDenied;
10274
10643
  }
10275
10644
  const modeEpoch = yolo.epoch();
10645
+ // The admission's authoritative mode read is always a fresh capture, never
10646
+ // the admission slot: a reselection while this lookup is in flight must
10647
+ // invalidate it (mode() answers Normal, the epoch check below blocks), and
10648
+ // tests/auto-mode-invariants.test.ts holds exactly this lookup to lock it.
10276
10649
  const permissionMode = primaryToolCall ? await yolo.mode(ctx) : "normal";
10277
10650
  // A superseded lookup returns Normal for display, not permission to skip Auto.
10278
10651
  if (primaryToolCall && modeEpoch !== yolo.epoch()) {
10279
10652
  return { block: true, reason: "Permission mode changed during tool admission. This action was not run; retry under the current mode." };
10280
10653
  }
10281
10654
  const autoGate = permissionMode === "auto" ? gate : undefined;
10282
- return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate);
10655
+ return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate, superYoloActive);
10283
10656
  });
10284
10657
 
10285
10658
  for (const owner of ["delegation", "review"] as const) {
@@ -10298,7 +10671,7 @@ function createGentleAiExtensionForTesting(
10298
10671
  }
10299
10672
 
10300
10673
  pi.registerCommand("gentle:models", {
10301
- description: "Configure global per-agent models for el Gentleman.",
10674
+ description: "Configure global agent models and choose a profile orchestrator.",
10302
10675
  handler: async (_args, ctx) => {
10303
10676
  await handleModelsCommand(ctx, pi);
10304
10677
  },
@@ -10402,6 +10775,10 @@ function createGentleAiExtensionForTesting(
10402
10775
  if (modelConfig.status === "invalid") {
10403
10776
  lines.push(`remedy: fix or remove ${modelConfig.path}`);
10404
10777
  }
10778
+ lines.push(...herdrSetupHint(process.env, {
10779
+ interactive: ctx.mode === "tui" && ctx.hasUI,
10780
+ isSocket: (path) => { try { return statSync(path).isSocket(); } catch { return false; } },
10781
+ }));
10405
10782
  ctx.ui.notify(
10406
10783
  lines.join("\n"),
10407
10784
  lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",