gentle-pi 4.3.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 (208) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gentle-ai-worker.md +1 -1
  3. package/assets/design-canvas/canvas.css +648 -0
  4. package/assets/design-canvas/canvas.js +1497 -0
  5. package/assets/design-canvas/index.html +88 -0
  6. package/assets/design-canvas/inspector.js +290 -0
  7. package/assets/orchestrator-delegation-reference.md +2 -2
  8. package/assets/orchestrator-verification.md +1 -1
  9. package/assets/orchestrator.md +2 -2
  10. package/bin/engram-guard +1 -1
  11. package/bin/gentle-shell.mjs +101 -151
  12. package/docs/bundled-install.md +47 -42
  13. package/docs/delegated-verification.md +6 -0
  14. package/docs/gentle-agents-activity.md +5 -1
  15. package/docs/gentle-music.md +61 -0
  16. package/docs/gentle-shell.md +77 -7
  17. package/docs/install-wizard.md +30 -6
  18. package/docs/permission-modes.md +7 -2
  19. package/docs/readme-reference.md +19 -15
  20. package/docs/session-profile-format.md +26 -0
  21. package/docs/voice.md +259 -0
  22. package/docs/yolo-mode.md +17 -2
  23. package/extensions/ask-user-choice.ts +4 -1
  24. package/extensions/gentle-agents.ts +173 -38
  25. package/extensions/gentle-ai.ts +214 -69
  26. package/extensions/gentle-design.ts +158 -0
  27. package/extensions/gentle-music.ts +289 -0
  28. package/extensions/gentle-shell.ts +332 -73
  29. package/extensions/gentle-voice.ts +11 -0
  30. package/extensions/session-scratch.ts +41 -22
  31. package/extensions/skill-registry.ts +63 -8
  32. package/lib/agent-profiles.ts +26 -0
  33. package/lib/agents-config.ts +35 -0
  34. package/lib/agents-protocol.ts +57 -1
  35. package/lib/agents-runner.ts +52 -3
  36. package/lib/agents-session-transport.ts +101 -6
  37. package/lib/agents-widget.ts +14 -1
  38. package/lib/auto-permission-memory.ts +48 -18
  39. package/lib/auto-permission-runtime.ts +21 -6
  40. package/lib/clipboard-image.ts +147 -0
  41. package/lib/command-palette-catalog.ts +2 -0
  42. package/lib/commit-slice-signal.ts +1 -1
  43. package/lib/context-backstop.ts +36 -0
  44. package/lib/design-canvas-server.ts +947 -0
  45. package/lib/design-comments.ts +437 -0
  46. package/lib/design-handoffs.ts +313 -0
  47. package/lib/design-open-browser.ts +58 -0
  48. package/lib/design-sources.ts +120 -0
  49. package/lib/external-editor.ts +31 -0
  50. package/lib/gentle-design.ts +513 -0
  51. package/lib/gentle-shell-bundled.ts +123 -49
  52. package/lib/gentle-shell-launcher.ts +9 -4
  53. package/lib/gentle-shell-startup-update.ts +61 -9
  54. package/lib/gentle-shell-uninstall.ts +33 -1
  55. package/lib/image-attachments.ts +150 -0
  56. package/lib/inprocess-reviewer.ts +33 -2
  57. package/lib/nan-provider.ts +3 -0
  58. package/lib/native-review-cli.ts +11 -4
  59. package/lib/notification-audio-native.ts +149 -13
  60. package/lib/notification-audio.ts +4 -0
  61. package/lib/notification-pulse-worker.ts +82 -18
  62. package/lib/notification-service.ts +5 -1
  63. package/lib/orchestrator-presence.ts +78 -8
  64. package/lib/orchestrator-scope.ts +10 -0
  65. package/lib/orchestrator-state.ts +11 -3
  66. package/lib/permission-mode-policy.ts +2 -2
  67. package/lib/review-host-relay.ts +11 -2
  68. package/lib/review-reminder-receipt.ts +24 -1
  69. package/lib/session-profile-append-controller.ts +713 -0
  70. package/lib/session-profile-disk-reader.ts +182 -0
  71. package/lib/session-profile-persistence.ts +15 -7
  72. package/lib/session-scratch.ts +52 -3
  73. package/lib/session-worktree-registry.ts +7 -0
  74. package/lib/shell-bar.ts +4 -3
  75. package/lib/shell-changes.ts +81 -5
  76. package/lib/shell-prompt.ts +4 -4
  77. package/lib/shell-sidebar-layout.ts +26 -6
  78. package/lib/shell-todo.ts +1 -1
  79. package/lib/verifier-admission.ts +5 -4
  80. package/lib/vim-editor-adapter.ts +11 -8
  81. package/lib/voice-artifacts.ts +188 -0
  82. package/lib/voice-command.ts +576 -0
  83. package/lib/voice-install.ts +455 -0
  84. package/lib/voice-platform.ts +99 -0
  85. package/lib/voice-push-to-talk.ts +262 -0
  86. package/lib/voice-session.ts +460 -0
  87. package/lib/voice-settings.ts +50 -0
  88. package/lib/voice-ui.ts +158 -0
  89. package/lib/yolo-session-policy.ts +58 -22
  90. package/package.json +1 -1
  91. package/runtime/gentle-shell-bundled.mjs +122 -48
  92. package/runtime/gentle-shell-launcher.mjs +9 -4
  93. package/runtime/gentle-shell-startup-update.mjs +61 -9
  94. package/runtime/gentle-shell-uninstall.mjs +33 -1
  95. package/runtime/native-review-cli.mjs +11 -4
  96. package/scripts/bundled-engram-migration.mjs +0 -6
  97. package/scripts/gentle-ai-installer.mjs +10 -10
  98. package/scripts/installer-downloads.mjs +7 -7
  99. package/scripts/installer-preflight.mjs +5 -3
  100. package/scripts/installer-windows.mjs +37 -7
  101. package/scripts/main-channel.mjs +91 -17
  102. package/scripts/pi-update.mjs +34 -6
  103. package/scripts/verify-package-files.mjs +26 -2
  104. package/scripts/voice-worker.cjs +476 -0
  105. package/skills/design/SKILL.md +110 -0
  106. package/tests/agent-profiles.test.ts +24 -0
  107. package/tests/agents-config.test.ts +61 -0
  108. package/tests/agents-fake-child.ts +8 -1
  109. package/tests/agents-protocol.test.ts +32 -0
  110. package/tests/agents-runner.test.ts +226 -5
  111. package/tests/agents-session-transport.test.ts +53 -3
  112. package/tests/ask-user-choice.test.ts +30 -0
  113. package/tests/auto-mode.test.ts +17 -0
  114. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  115. package/tests/auto-permission-runtime.test.ts +85 -1
  116. package/tests/autonomous-guard.test.ts +20 -0
  117. package/tests/bundled-install.test.ts +3 -3
  118. package/tests/clipboard-image.test.ts +114 -0
  119. package/tests/command-palette.test.ts +1 -1
  120. package/tests/context-backstop.test.ts +70 -4
  121. package/tests/delegate-for-reason-contract.test.ts +2 -1
  122. package/tests/design-canvas-assets.test.ts +710 -0
  123. package/tests/design-canvas-server.test.ts +1096 -0
  124. package/tests/design-comments.test.ts +201 -0
  125. package/tests/design-handoffs.test.ts +282 -0
  126. package/tests/design-open-browser.test.ts +70 -0
  127. package/tests/design-sources.test.ts +132 -0
  128. package/tests/engram-safety.test.ts +132 -57
  129. package/tests/external-editor.test.ts +84 -0
  130. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
  131. package/tests/gentle-agents.test.ts +365 -20
  132. package/tests/gentle-ai-binary.test.ts +1 -1
  133. package/tests/gentle-ai-installer.test.ts +51 -51
  134. package/tests/gentle-ai.test.ts +292 -13
  135. package/tests/gentle-design.test.ts +1313 -0
  136. package/tests/gentle-music.test.ts +335 -0
  137. package/tests/gentle-shell-bundled.test.ts +301 -248
  138. package/tests/gentle-shell-launcher.test.ts +28 -2
  139. package/tests/gentle-shell-startup-update.test.ts +77 -1
  140. package/tests/gentle-shell-uninstall.test.ts +59 -0
  141. package/tests/gentle-shell.test.ts +183 -8
  142. package/tests/gentle-todo.test.ts +11 -0
  143. package/tests/image-attachments.test.ts +119 -0
  144. package/tests/image-paste-chips.test.ts +236 -0
  145. package/tests/inprocess-reviewer.test.ts +55 -2
  146. package/tests/installer-bundles.test.ts +12 -6
  147. package/tests/installer-posix-bootstrap.test.ts +8 -8
  148. package/tests/installer-preflight.test.ts +16 -0
  149. package/tests/installer-server.test.ts +2 -2
  150. package/tests/installer-windows-bootstrap.test.ts +61 -3
  151. package/tests/installer-windows-diagnostics.test.ts +111 -0
  152. package/tests/main-channel-upgrade.test.ts +232 -2
  153. package/tests/main-channel.test.ts +140 -21
  154. package/tests/nan-provider.test.ts +36 -0
  155. package/tests/native-review-capability-contract.test.ts +11 -4
  156. package/tests/native-review-parity.test.ts +1 -1
  157. package/tests/notification-audio-native.test.ts +128 -9
  158. package/tests/notification-customize.test.ts +17 -5
  159. package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
  160. package/tests/orchestrator-budget.test.ts +5 -2
  161. package/tests/orchestrator-presence.test.ts +100 -2
  162. package/tests/orchestrator-rdd-ownership.test.ts +2 -2
  163. package/tests/orchestrator-scope.test.ts +14 -0
  164. package/tests/orchestrator-state.test.ts +29 -0
  165. package/tests/package-files.test.ts +34 -0
  166. package/tests/package-manifest.test.ts +6 -6
  167. package/tests/permission-mode-policy.test.ts +10 -1
  168. package/tests/pi-update.test.ts +99 -4
  169. package/tests/rdd-aware-verification-contract.test.ts +1 -1
  170. package/tests/review-host-relay-routing.test.ts +88 -1
  171. package/tests/review-host-relay.test.ts +9 -2
  172. package/tests/review-json-arguments.test.ts +20 -3
  173. package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
  174. package/tests/review-relay-transport-agent.test.ts +26 -0
  175. package/tests/review-reminder-receipt.test.ts +31 -1
  176. package/tests/session-changes.test.ts +2 -1
  177. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  178. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  179. package/tests/session-profile-append-controller.test.ts +731 -0
  180. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  181. package/tests/session-profile-disk-reader.test.ts +311 -0
  182. package/tests/session-scratch.test.ts +68 -2
  183. package/tests/session-worktree-registry.test.ts +13 -1
  184. package/tests/shell-bar.test.ts +14 -2
  185. package/tests/shell-changes.test.ts +148 -1
  186. package/tests/shell-prompt.test.ts +3 -3
  187. package/tests/shell-render-cache.test.ts +134 -1
  188. package/tests/shell-sidebar-layout.test.ts +50 -0
  189. package/tests/short-check-routing-contract.test.ts +1 -1
  190. package/tests/skill-collision-prefixes.test.ts +3 -1
  191. package/tests/skill-registry-watcher.test.ts +17 -5
  192. package/tests/skill-registry.test.ts +47 -3
  193. package/tests/task-size-routing-contract.test.ts +1 -1
  194. package/tests/test-strength-contract.test.ts +1 -1
  195. package/tests/verifier-admission.test.ts +18 -1
  196. package/tests/voice-command.test.ts +773 -0
  197. package/tests/voice-install.test.ts +323 -0
  198. package/tests/voice-platform.test.ts +176 -0
  199. package/tests/voice-push-to-talk.test.ts +444 -0
  200. package/tests/voice-session.test.ts +434 -0
  201. package/tests/voice-settings.test.ts +49 -0
  202. package/tests/voice-ui.test.ts +97 -0
  203. package/tests/voice-worker.test.ts +445 -0
  204. package/tests/weak-model-economy-prompt.test.ts +32 -0
  205. package/tests/windows-hidden-processes.test.ts +25 -2
  206. package/tests/yolo-customize.test.ts +5 -1
  207. package/tests/yolo-mode.test.ts +1 -1
  208. package/tests/yolo-session-policy-capture.test.ts +104 -0
@@ -4,7 +4,7 @@ import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "
4
4
  import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
5
5
  import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
6
6
  import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
7
- import { appendContextBackstopNotice, createContextBackstopSignal } from "../lib/context-backstop.ts";
7
+ import { appendContextBackstopNotice, createContextBackstopSignal, createThrottledContextMeasure } from "../lib/context-backstop.ts";
8
8
  import { blockChildDestructiveCommand } from "./child-safety.ts";
9
9
  import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
10
10
  import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
@@ -36,6 +36,7 @@ import {
36
36
  writeFile,
37
37
  } from "node:fs/promises";
38
38
  import { homedir } from "node:os";
39
+ import { discoverAgents, loadAgentsConfig, unmatchedProfileKeys } from "../lib/agents-config.ts";
39
40
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
40
41
  import { fileURLToPath } from "node:url";
41
42
  import type {
@@ -47,7 +48,7 @@ import type {
47
48
  ToolResultEvent,
48
49
  ToolResultEventResult,
49
50
  } from "@earendil-works/pi-coding-agent";
50
- 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";
51
52
  import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
52
53
  import {
53
54
  BACKGROUND_SUBAGENTS_FILE,
@@ -85,6 +86,7 @@ import {
85
86
  PROFILE_ORCHESTRATOR_KEY,
86
87
  profileExportPath,
87
88
  profileRoutingRows,
89
+ unmatchedRoutingNotes,
88
90
  profilesFilePath,
89
91
  readProfileOrchestrator,
90
92
  readProfilesFileResult,
@@ -1209,7 +1211,7 @@ function pinRddStatusLine(pinned: string | undefined, next: string): string {
1209
1211
  const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
1210
1212
  const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
1211
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.',
1212
- "When ASSESS returns reviewDue or a native continuation, relay it unchanged and follow it; tests, builds, and functional checks still run when applicable, and no assessment authorizes delivery.",
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.",
1213
1215
  ];
1214
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.";
1215
1217
  const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
@@ -1467,7 +1469,7 @@ Harness principles:
1467
1469
  - el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
1468
1470
  - Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
1469
1471
  - Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
1470
- - 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). Before finishing, run a differential check: for each existing command that shares the code you changed, 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.
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.
1471
1473
  - Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
1472
1474
  - Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
1473
1475
 
@@ -2070,6 +2072,21 @@ function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey
2070
2072
  return restriction;
2071
2073
  }
2072
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
+
2073
2090
  async function confirmCommand(
2074
2091
  command: string,
2075
2092
  ctx: ExtensionContext,
@@ -2077,7 +2094,19 @@ async function confirmCommand(
2077
2094
  herdrLifecycle: HerdrConfirmationLifecycle,
2078
2095
  yoloActive = false,
2079
2096
  autoGate?: () => Promise<ToolCallEventResult | undefined>,
2097
+ superActive = false,
2080
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
+ }
2081
2110
  const guardrailsConfig = loadRuntimeGuardrailsConfig(ctx.cwd);
2082
2111
  const evaluation = evaluateGuardedCommand(command, guardrailsConfig);
2083
2112
  // Configuration provenance is distinct from legacy autonomy resolution, which
@@ -3093,7 +3122,7 @@ interface OverlayComponent {
3093
3122
 
3094
3123
  type ModelPanelResult =
3095
3124
  | { type: "save"; config: AgentModelConfig }
3096
- // `save`, then snapshot saved agent routing and the live orchestrator into
3125
+ // `save`, then snapshot saved agent routing and the chosen orchestrator into
3097
3126
  // the current profile without changing the global orchestrator defaults.
3098
3127
  | { type: "save-profile"; config: AgentModelConfig }
3099
3128
  | { type: "custom"; agent: string | "all"; config: AgentModelConfig }
@@ -3102,6 +3131,7 @@ type ModelPanelResult =
3102
3131
  | { type: "cancel" };
3103
3132
 
3104
3133
  const SET_ALL_AGENTS = "Set all agents";
3134
+ const LEAVE_ORCHESTRATOR_UNTOUCHED = "leave untouched";
3105
3135
 
3106
3136
  const PANEL_TONE = {
3107
3137
  BORDER: "border",
@@ -3132,6 +3162,7 @@ class SddModelPanel implements OverlayComponent {
3132
3162
  private query = "";
3133
3163
  private readonly draft: AgentModelConfig;
3134
3164
  private readonly rows: string[];
3165
+ private readonly agents: string[];
3135
3166
  private readonly modelOptions: string[];
3136
3167
  private readonly done: (result: ModelPanelResult) => void;
3137
3168
  private readonly theme: Theme | undefined;
@@ -3150,7 +3181,8 @@ class SddModelPanel implements OverlayComponent {
3150
3181
  terminalRows?: () => number,
3151
3182
  ) {
3152
3183
  this.draft = cloneModelConfig(initialConfig);
3153
- this.rows = [SET_ALL_AGENTS, ...agents];
3184
+ this.rows = [SET_ALL_AGENTS, PROFILE_ORCHESTRATOR_KEY, ...agents];
3185
+ this.agents = [...agents];
3154
3186
  this.modelOptions = modelOptions;
3155
3187
  this.done = done;
3156
3188
  this.theme = theme;
@@ -3273,6 +3305,7 @@ class SddModelPanel implements OverlayComponent {
3273
3305
  }
3274
3306
  if (matchesKey(data, "e")) {
3275
3307
  this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
3308
+ if (this.selectedRow === PROFILE_ORCHESTRATOR_KEY && !this.draft[this.selectedRow]?.model) return;
3276
3309
  this.mode = "effort";
3277
3310
  this.effortCursor = 0;
3278
3311
  return;
@@ -3354,7 +3387,7 @@ class SddModelPanel implements OverlayComponent {
3354
3387
  return;
3355
3388
  }
3356
3389
  this.applyModelSelection(
3357
- selected === INHERIT_MODEL ? undefined : selected,
3390
+ selected === INHERIT_MODEL || selected === LEAVE_ORCHESTRATOR_UNTOUCHED ? undefined : selected,
3358
3391
  );
3359
3392
  this.mode = "agents";
3360
3393
  return;
@@ -3368,7 +3401,7 @@ class SddModelPanel implements OverlayComponent {
3368
3401
  private applyModelSelection(model: string | undefined): void {
3369
3402
  const row = this.rows[this.cursor];
3370
3403
  if (row === SET_ALL_AGENTS) {
3371
- for (const name of this.rows.slice(1)) this.setModel(name, model);
3404
+ for (const name of this.agents) this.setModel(name, model);
3372
3405
  return;
3373
3406
  }
3374
3407
  if (!row) return;
@@ -3378,7 +3411,7 @@ class SddModelPanel implements OverlayComponent {
3378
3411
  private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
3379
3412
  const row = this.selectedRow;
3380
3413
  if (row === SET_ALL_AGENTS) {
3381
- for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
3414
+ for (const name of this.agents) this.setThinking(name, thinking);
3382
3415
  return;
3383
3416
  }
3384
3417
  this.setThinking(row, thinking);
@@ -3387,13 +3420,17 @@ class SddModelPanel implements OverlayComponent {
3387
3420
  private applyInherit(): void {
3388
3421
  const row = this.rows[this.cursor];
3389
3422
  if (row === SET_ALL_AGENTS) {
3390
- for (const name of this.rows.slice(1)) this.clearEntry(name);
3423
+ for (const name of this.agents) this.clearEntry(name);
3391
3424
  return;
3392
3425
  }
3393
3426
  if (row) this.clearEntry(row);
3394
3427
  }
3395
3428
 
3396
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
+ }
3397
3434
  const current = this.draft[name] ?? {};
3398
3435
  if (model === undefined) delete current.model;
3399
3436
  else current.model = model;
@@ -3402,6 +3439,7 @@ class SddModelPanel implements OverlayComponent {
3402
3439
  }
3403
3440
 
3404
3441
  private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
3442
+ if (name === PROFILE_ORCHESTRATOR_KEY && !this.draft[name]?.model) return;
3405
3443
  const current = this.draft[name] ?? {};
3406
3444
  if (thinking === undefined) delete current.thinking;
3407
3445
  else current.thinking = thinking;
@@ -3410,13 +3448,17 @@ class SddModelPanel implements OverlayComponent {
3410
3448
  }
3411
3449
 
3412
3450
  private clearEntry(name: string): void {
3413
- this.draft[name] = {};
3451
+ if (name === PROFILE_ORCHESTRATOR_KEY) delete this.draft[name];
3452
+ else this.draft[name] = {};
3414
3453
  }
3415
3454
 
3416
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;
3417
3459
  const query = this.query.trim().toLowerCase();
3418
- if (!query) return this.modelOptions;
3419
- return this.modelOptions.filter((option) =>
3460
+ if (!query) return options;
3461
+ return options.filter((option) =>
3420
3462
  option.toLowerCase().includes(query),
3421
3463
  );
3422
3464
  }
@@ -3473,13 +3515,13 @@ class SddModelPanel implements OverlayComponent {
3473
3515
  // longer fits the minimum width once the profile key joins the save keys.
3474
3516
  lines.push(
3475
3517
  line(
3476
- `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`,
3477
3519
  "muted",
3478
3520
  ),
3479
3521
  );
3480
3522
  lines.push(
3481
3523
  line(
3482
- `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`,
3483
3525
  "muted",
3484
3526
  ),
3485
3527
  );
@@ -3579,11 +3621,9 @@ class SddModelPanel implements OverlayComponent {
3579
3621
  }
3580
3622
 
3581
3623
  private renderSetAllLabel(row: string): string {
3582
- const models = this.rows
3583
- .slice(1)
3624
+ const models = this.agents
3584
3625
  .map((name) => this.draft[name]?.model ?? routingDefaultLabel(name, "model"));
3585
- const efforts = this.rows
3586
- .slice(1)
3626
+ const efforts = this.agents
3587
3627
  .map((name) => this.draft[name]?.thinking ?? routingDefaultLabel(name, "effort"));
3588
3628
  const firstModel = models[0] ?? "inherit";
3589
3629
  const firstEffort = efforts[0] ?? "inherit";
@@ -3600,6 +3640,9 @@ class SddModelPanel implements OverlayComponent {
3600
3640
  }
3601
3641
 
3602
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
+ }
3603
3646
  const model = this.draft[row]?.model ?? routingDefaultLabel(row, "model");
3604
3647
  const effort = this.draft[row]?.thinking ?? routingDefaultLabel(row, "effort");
3605
3648
  return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(model, "status")}${this.renderText(
@@ -3674,8 +3717,18 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3674
3717
  );
3675
3718
  return;
3676
3719
  }
3677
- let config = savedConfig.status === "valid" ? savedConfig.config : {};
3678
- 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);
3679
3732
  let result = await showSddModelPanel(ctx, config, profileLabel);
3680
3733
  while (result.type === "custom" || result.type === "export" || result.type === "restore") {
3681
3734
  config = cloneModelConfig(result.config);
@@ -3705,7 +3758,10 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3705
3758
  result = await showSddModelPanel(ctx, config, profileLabel);
3706
3759
  continue;
3707
3760
  }
3708
- 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;
3709
3765
  try {
3710
3766
  const applyResult = await applyModelConfigAsync(ctx.cwd, restored);
3711
3767
  ctx.ui.notify([
@@ -3767,18 +3823,21 @@ async function handleModelsCommand(ctx: ExtensionContext, pi: ExtensionAPI): Pro
3767
3823
  result = await showSddModelPanel(ctx, config, profileLabel);
3768
3824
  }
3769
3825
  if (result.type !== "save" && result.type !== "save-profile") return;
3770
- writeModelConfig(ctx.cwd, result.config);
3771
- 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);
3772
3831
  ctx.ui.notify(
3773
3832
  [
3774
3833
  "el Gentleman global model config saved.",
3775
3834
  `Global config: ${modelConfigPath(ctx.cwd)}`,
3776
3835
  `Agents updated: ${applyResult.updated}`,
3777
- ...describeModelConfig(ctx.cwd, result.config),
3836
+ ...describeModelConfig(ctx.cwd, routing),
3778
3837
  ].join("\n"),
3779
3838
  "info",
3780
3839
  );
3781
- if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, pi);
3840
+ if (result.type === "save-profile") updateCurrentProfileFromSavedRouting(ctx, result.config[PROFILE_ORCHESTRATOR_KEY]);
3782
3841
  }
3783
3842
 
3784
3843
  /** The profile `u` in `/gentle:models` writes to, and why it is that one. */
@@ -3810,11 +3869,10 @@ function describeCurrentProfileTarget(target: CurrentProfileTarget | undefined):
3810
3869
 
3811
3870
  /**
3812
3871
  * The second half of `u` captures saved global agent routing (never the pin)
3813
- * and the live session orchestrator. Without a live model it falls back to
3814
- * persisted settings. The global save has already been reported, so every
3815
- * 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.
3816
3874
  */
3817
- function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: ExtensionAPI): void {
3875
+ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, orchestrator: AgentRoutingEntry | undefined): void {
3818
3876
  const path = profilesFilePath(gentleAiConfigHome());
3819
3877
  const read = readProfilesFileResult(path);
3820
3878
  if (read.status === "invalid") {
@@ -3824,16 +3882,9 @@ function updateCurrentProfileFromSavedRouting(ctx: ExtensionContext, pi: Extensi
3824
3882
  );
3825
3883
  return;
3826
3884
  }
3827
- const snapshot = profileSnapshotFrom(
3828
- readGlobalEffectiveModelConfig(ctx.cwd),
3829
- readOrchestratorSettings(orchestratorSettingsPath()),
3830
- );
3831
- if (ctx.model) {
3832
- snapshot[PROFILE_ORCHESTRATOR_KEY] = {
3833
- model: `${ctx.model.provider}/${ctx.model.id}`,
3834
- thinking: pi.getThinkingLevel(),
3835
- };
3836
- }
3885
+ const snapshot = cloneModelConfig(readGlobalEffectiveModelConfig(ctx.cwd));
3886
+ delete snapshot[PROFILE_ORCHESTRATOR_KEY];
3887
+ if (orchestrator?.model) snapshot[PROFILE_ORCHESTRATOR_KEY] = { ...orchestrator };
3837
3888
  if (read.status === "missing") {
3838
3889
  // The same seed `/gentle:profiles` performs on its first open, so pressing
3839
3890
  // `u` before ever opening that panel lands on the same "current" profile.
@@ -4012,10 +4063,10 @@ function profilePinScopeNote(cwd: string): string | undefined {
4012
4063
  * write that already succeeded must not become an error because a probe about sharing
4013
4064
  * it failed.
4014
4065
  */
4015
- function repoDeclarationSharing(root: string, path: string): string {
4066
+ export function repoDeclarationSharing(root: string, path: string): string {
4016
4067
  const relativePath = relative(root, path).split(sep).join("/");
4017
4068
  const run = (...arguments_: string[]): string =>
4018
- execFileSync("git", arguments_, { cwd: root, encoding: "utf8" }).trim();
4069
+ execFileSync("git", arguments_, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
4019
4070
  try {
4020
4071
  // Exit 0 means gitignore matches the path; the exact negation line is what makes
4021
4072
  // the declaration committable again.
@@ -4067,6 +4118,8 @@ class ProfilesPanel implements OverlayComponent {
4067
4118
  // Actions reopen the panel, refreshing this snapshot without disk reads during rendering.
4068
4119
  private readonly pinStatus: ProfilePinStatus | undefined;
4069
4120
  private readonly sessionBoundName: string | undefined;
4121
+ private readonly discoveredAgentNames: string[];
4122
+ private readonly unmatchedKeys: string[];
4070
4123
  private readonly liveOrchestrator: (() => AgentRoutingEntry | undefined) | undefined;
4071
4124
 
4072
4125
  constructor(
@@ -4082,6 +4135,8 @@ class ProfilesPanel implements OverlayComponent {
4082
4135
  requestRender: () => void,
4083
4136
  pinStatus: () => ProfilePinStatus | undefined,
4084
4137
  sessionBound: () => string | undefined,
4138
+ discoveredAgentNames: string[],
4139
+ unmatchedKeys: string[],
4085
4140
  liveOrchestrator?: () => AgentRoutingEntry | undefined,
4086
4141
  feedback?: string,
4087
4142
  ) {
@@ -4096,6 +4151,8 @@ class ProfilesPanel implements OverlayComponent {
4096
4151
  this.orchestratorSettings = orchestratorSettings;
4097
4152
  this.pinStatus = pinStatus();
4098
4153
  this.sessionBoundName = sessionBound();
4154
+ this.discoveredAgentNames = discoveredAgentNames;
4155
+ this.unmatchedKeys = unmatchedKeys;
4099
4156
  this.liveOrchestrator = liveOrchestrator;
4100
4157
  const items = buildProfileListItems(file, evaluateProfilePin(this.pinStatus, file.profiles).winner?.profile, this.sessionBoundName);
4101
4158
  this.listItems = items;
@@ -4331,10 +4388,30 @@ class ProfilesPanel implements OverlayComponent {
4331
4388
  : [this.renderLine(`session ${sanitizeTerminalText(this.sessionBoundName)} (session) — launches resolve it ahead of pins and the global default; nothing was written`, width, "muted")]),
4332
4389
  "",
4333
4390
  this.renderLine("Profile routing", width, "accent"),
4334
- ...this.indentLines(this.routingLines(profileRows, widths), width),
4391
+ ...this.indentLines(this.routingLines(profileRows, widths, width), width),
4335
4392
  "",
4336
4393
  this.renderLine("Current routing (effective)", width, "accent"),
4337
- ...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),
4338
4415
  ];
4339
4416
  }
4340
4417
 
@@ -4356,11 +4433,24 @@ class ProfilesPanel implements OverlayComponent {
4356
4433
  private routingLines(
4357
4434
  rows: ProfileRoutingRow[],
4358
4435
  widths: { agent: number; model: number },
4436
+ width: number,
4359
4437
  ): string[] {
4360
4438
  if (rows.length === 0) {
4361
4439
  return ["No routing entries — every agent inherits its default model."];
4362
4440
  }
4363
- 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
+ });
4364
4454
  }
4365
4455
 
4366
4456
  private indentLines(lines: string[], width: number): string[] {
@@ -4406,6 +4496,28 @@ async function showProfilesPanel(
4406
4496
  // Both orchestrator and pin state are snapshots for this panel visit.
4407
4497
  // Actions (including p/P) reopen the panel and read fresh state.
4408
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);
4409
4521
  return ctx.ui.custom<ProfilesPanelResult>(
4410
4522
  (tui, theme, keybindings, done) => {
4411
4523
  const panel = new ProfilesPanel(
@@ -4421,6 +4533,8 @@ async function showProfilesPanel(
4421
4533
  () => tui.requestRender(),
4422
4534
  () => readProfilePinStatus(ctx.cwd),
4423
4535
  () => sessionBoundName,
4536
+ discoveredAgentNames,
4537
+ unmatchedKeys,
4424
4538
  () => liveOrchestratorSnapshot(ctx, live)?.entry,
4425
4539
  status,
4426
4540
  );
@@ -5293,7 +5407,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
5293
5407
  },
5294
5408
  input: {
5295
5409
  type: "string",
5296
- 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). 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.",
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.",
5297
5411
  },
5298
5412
  outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
5299
5413
  inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
@@ -5564,7 +5678,7 @@ function parseReviewControllerParameters(value: unknown): ReviewControllerParame
5564
5678
  };
5565
5679
  for (const key of ["changeName", "idempotencyKey", "transition", "input", "outputPath", "inputPath", "operationId", "lineageIds", "acknowledgeUntrustedBundleSource", "workspaceRoot"] as const) {
5566
5680
  const optional = value[key];
5567
- 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)) {
5568
5682
  parameters.input = serializeReviewJsonArgument(optional);
5569
5683
  continue;
5570
5684
  }
@@ -5792,21 +5906,18 @@ const REVIEW_MODE_DISABLED_OUTCOME = "review-mode-disabled";
5792
5906
  // to make the global opt-in first when needed, then clear this clone override.
5793
5907
  // Pi never mutates the operator's global gentle-ai state automatically.
5794
5908
  //
5795
- // The default branch changed with the pinned v2.4.0 runtime, which made
5796
- // receipt-driven development opt-in. It used to be unreachable as a reason for
5797
- // reviews being off — an all-sources-unset install resolved to ON with source
5798
- // `default` — so naming a continuation for it would have been a guess, and
5799
- // gentle-ai returned an empty scope to say exactly that. v2.4.0 resolves the
5800
- // same install to OFF with source `default`, which makes it the most common
5801
- // refusal there is: every install that never opted in. gentle-ai answers
5802
- // `global` for it now, not because default is a global opinion but because
5803
- // global is the only scope that can turn reviews on at all, and Pi answers the
5804
- // same. Leaving this undefined would hand the single most common state a dead
5805
- // 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.
5806
5917
  function reviewModeContinuation(source: NativeReviewModeSource): string | undefined {
5807
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.";
5808
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.";
5809
- 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.";
5810
5921
  }
5811
5922
 
5812
5923
  // Names the situation before the mechanism, then the mechanism, mirroring
@@ -7459,11 +7570,24 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
7459
7570
  // its provider through the live model registry the caller supplies, or it is
7460
7571
  // refused before materialize ever runs (validateReviewerSelectionConfiguration
7461
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.
7462
7579
  function reviewHostRelaySelection(lens: string | undefined, config: AgentModelConfig): { selection?: string; thinking?: string; routingKey: string } {
7463
7580
  const routingKey = lens === undefined || lens.length === 0 ? "review capture" : lens.startsWith("review-") ? lens : `review-${lens}`;
7464
7581
  const entry = config[routingKey];
7465
- const model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
7466
- 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
+ }
7467
7591
  return {
7468
7592
  ...(model === undefined ? {} : { selection: model }),
7469
7593
  ...(thinking === undefined ? {} : { thinking }),
@@ -9630,6 +9754,7 @@ export const __testing = {
9630
9754
  guardedCommandTitle,
9631
9755
  loadRuntimeGuardrailsConfig,
9632
9756
  isReversibleYoloCommand,
9757
+ evaluateSuperYoloCommand,
9633
9758
  yoloConfiguredRestriction,
9634
9759
  buildGentlePrompt,
9635
9760
  nativeStatusUnsupported,
@@ -9700,6 +9825,9 @@ export interface GentleAiRuntimeDependencies {
9700
9825
  telemetryExecFileAdapter?: ExecFileAdapter;
9701
9826
  /** Test-only inference seam; production always uses the active session model. */
9702
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 };
9703
9831
  }
9704
9832
 
9705
9833
  export function createGentleAiExtension(dependencies: GentleAiRuntimeDependencies = {}): (pi: ExtensionAPI) => void {
@@ -10358,7 +10486,7 @@ function createGentleAiExtensionForTesting(
10358
10486
  // and forwards only systemPromptOptions, so the harness is delivered
10359
10487
  // through the mutable appendSystemPrompt section instead of a replacement.
10360
10488
  appendSystemPromptOnce(event.systemPromptOptions, `${gentlePrompt}${reviewContractPrompt}`);
10361
- updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx));
10489
+ updateYoloPrompt(event.systemPromptOptions, isPrimarySession && await yolo.active(ctx), isPrimarySession && await yolo.superActive(ctx));
10362
10490
  updateAutoPrompt(event.systemPromptOptions, isPrimarySession && await yolo.mode(ctx) === "auto");
10363
10491
  return undefined;
10364
10492
  });
@@ -10429,14 +10557,23 @@ function createGentleAiExtensionForTesting(
10429
10557
  } catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
10430
10558
  };
10431
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
+ });
10432
10568
  pi.on("tool_result", async (event, ctx) => {
10433
- // Harness roadmap R1: the primary session's measured context is checked
10434
- // against the backstop on every tool result; the first result at or past
10435
- // it carries one notice, after any other patch below.
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.
10436
10573
  const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
10437
10574
  const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
10438
10575
  let notice: string | undefined;
10439
- try { notice = primary ? contextBackstopSignal(sessionKey, ctx.getContextUsage?.()) : undefined; } catch { notice = undefined; }
10576
+ try { notice = primary ? contextBackstopSignal(sessionKey, contextBackstopMeasure(sessionKey, () => ctx.getContextUsage?.())) : undefined; } catch { notice = undefined; }
10440
10577
  const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
10441
10578
  // Harness roadmap T4: a parent commit reports the branch's authored
10442
10579
  // changed lines so delivery slicing reads a number, not an estimate.
@@ -10455,7 +10592,11 @@ function createGentleAiExtensionForTesting(
10455
10592
  return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
10456
10593
  }
10457
10594
  const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
10458
- 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";
10459
10600
  const sensitivePathDenied = evaluateSensitivePathTool(
10460
10601
  event.toolName,
10461
10602
  event.input,
@@ -10501,13 +10642,17 @@ function createGentleAiExtensionForTesting(
10501
10642
  if (childDenied) return childDenied;
10502
10643
  }
10503
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.
10504
10649
  const permissionMode = primaryToolCall ? await yolo.mode(ctx) : "normal";
10505
10650
  // A superseded lookup returns Normal for display, not permission to skip Auto.
10506
10651
  if (primaryToolCall && modeEpoch !== yolo.epoch()) {
10507
10652
  return { block: true, reason: "Permission mode changed during tool admission. This action was not run; retry under the current mode." };
10508
10653
  }
10509
10654
  const autoGate = permissionMode === "auto" ? gate : undefined;
10510
- return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate);
10655
+ return await confirmCommand(command, ctx, pi.events, herdrLifecycle, yoloActive, autoGate, superYoloActive);
10511
10656
  });
10512
10657
 
10513
10658
  for (const owner of ["delegation", "review"] as const) {
@@ -10526,7 +10671,7 @@ function createGentleAiExtensionForTesting(
10526
10671
  }
10527
10672
 
10528
10673
  pi.registerCommand("gentle:models", {
10529
- description: "Configure global per-agent models for el Gentleman.",
10674
+ description: "Configure global agent models and choose a profile orchestrator.",
10530
10675
  handler: async (_args, ctx) => {
10531
10676
  await handleModelsCommand(ctx, pi);
10532
10677
  },