gentle-pi 4.1.0 → 4.3.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 (249) hide show
  1. package/README.md +352 -128
  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/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,6 +1,6 @@
1
1
  import { statSync } from "node:fs";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { activitySummary, ActivityPublisher, createActivityColumns, metadataTransport } from "../lib/herdr-activity.ts";
3
+ import { activitySummary, ActivityPublisher, createActivityColumns, herdrDetected, metadataTransport } from "../lib/herdr-activity.ts";
4
4
  import { replayTodo, type TodoState } from "../lib/shell-todo.ts";
5
5
 
6
6
  interface ActivityOptions {
@@ -15,8 +15,7 @@ interface ActivityOptions {
15
15
  export function createHerdrActivityExtension(options: ActivityOptions = {}) {
16
16
  return (pi: ExtensionAPI): void => {
17
17
  const env = options.env ?? process.env;
18
- if (env.GENTLE_PI_AGENTS_CHILD === "1" || env.HERDR_ENV !== "1" ||
19
- !env.HERDR_PANE_ID?.trim() || !env.HERDR_SOCKET_PATH?.trim()) return;
18
+ if (env.GENTLE_PI_AGENTS_CHILD === "1" || !herdrDetected(env)) return;
20
19
  const isSocket = options.isSocket ?? ((path: string) => {
21
20
  try { return statSync(path).isSocket(); } catch { return false; }
22
21
  });
@@ -6,13 +6,13 @@ import { existsSync, realpathSync, statSync } from "node:fs";
6
6
  import { lstat, readFile } from "node:fs/promises";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { profilesFilePath, profileRoleEntries, readProfilesFileResult } from "../lib/agent-profiles.ts";
9
- import { readSessionProfileBinding } from "../lib/session-profile-binding.ts";
10
- import { resolveProfilePin } from "../lib/agent-profile-pin.ts";
9
+ import { resolveSessionProfile, sessionProfileLabel, sessionProfileModelProfiles } from "../lib/session-profile-freeze.ts";
11
10
  import * as os from "node:os";
12
11
  import { dirname, join, resolve } from "node:path";
13
12
  import { buildShellHeaderModel, renderShellBar, renderShellBelowInputFloat, renderShellBottomOnlyBar, renderShellHeaderChrome, renderShellSidebarBar, keepNativeWorkingRow, shellEnabled, shellHeaderUsageHit, shellJobsCount, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
14
13
  import { CHANGE_STATUS, DIFF_MAX_BYTES, DIFF_MAX_LINES, DIFF_TOO_LARGE, WorktreeChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type FileRevision, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
15
14
  import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
15
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
16
16
  import { SessionWorktreeRegistry, resolveSessionWorktree, worktreeGitEnvironment, type WorktreeResolver, type WorktreeIdentity } from "../lib/session-worktree-registry.ts";
17
17
  import { CARD_STYLE, CARD_TONE, cardStyle, renderCard, setCardStyle, type Card, type CardTheme } from "../lib/shell-card.ts";
18
18
  import { resolveCardStyle, writeCardStyle } from "../lib/card-style-policy.ts";
@@ -22,6 +22,8 @@ import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
22
22
  import { discoverYoloUiAdapter, YOLO_DISPLAY, type YoloDisplay, type YoloUiAdapter } from "../lib/yolo-session-policy.ts";
23
23
  import { VisualCustomizeView, type CustomizeCategory, type CustomizeRow, type ProfileActions } from "../lib/visual-customize-view.ts";
24
24
  import { deleteVisualProfile, getVisualProfile, listVisualProfiles, resetVisualProfiles, saveVisualProfile } from "../lib/visual-profiles.ts";
25
+ import { gaugePaintFor } from "../lib/gauge-theme.ts";
26
+ import type { GaugePaint } from "../lib/shell-gauge.ts";
25
27
  import { sourcePalettePreview } from "../lib/theme-customization.ts";
26
28
  import { DEFAULT_VISUAL_SETTINGS, DENSITY, HEADER_PLACEMENT, STATUS_PLACEMENT, VISUAL_SECTION_KEYS, resolveVisualSettings, writeVisualSettings } from "../lib/visual-customization-policy.ts";
27
29
  import { BANNER_COLORS, DEFAULT_BANNER_CONFIG, readBannerConfig, readBannerConfigForEdit, writeBannerConfig } from "./startup-banner.ts";
@@ -32,7 +34,6 @@ import { oddPhaseRegistry } from "../lib/odd-phase.ts";
32
34
  import { inferOddPhase } from "../lib/odd-phase-inference.ts";
33
35
  import { isInteractiveMode } from "../lib/rpc-host.ts";
34
36
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
35
- import { bundledGentlePiConfigHome } from "../lib/gentle-shell-bundled.ts";
36
37
  import { isNanAdaptiveReasoningModel } from "../lib/nan-provider.ts";
37
38
  import { resolveAnimationPolicy, writeAnimationPolicy, type AnimationPolicy } from "../lib/animation-policy.ts";
38
39
  import { resolveVimPolicy, writeVimPolicy, type VimPolicy } from "../lib/vim-policy.ts";
@@ -181,7 +182,7 @@ export type ActiveProfileReader = (() => string | undefined) & {
181
182
  // Unbound reads retain the global file-identity cache, including atomic replacements.
182
183
  // Bound reads are snapshots: no filesystem or Git work is done during a frame.
183
184
  export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env): ActiveProfileReader {
184
- const path = profilesFilePath(bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"));
185
+ const path = profilesFilePath(gentlePiConfigHome(env));
185
186
  let fingerprint: string | undefined;
186
187
  let name: string | undefined;
187
188
  let bound = false;
@@ -208,23 +209,18 @@ export function createActiveProfileReader(env: NodeJS.ProcessEnv = process.env):
208
209
  const reader = (() => bound ? effective : global()) as ActiveProfileReader;
209
210
  reader.refresh = () => {
210
211
  if (!bound) return false;
211
- // gentle-shell#1064 slice 1: a parent-session profile binding outranks
212
- // both the pin layers and the global active profile, with the same
213
- // "name (scope)" spelling the pin uses. The shared precedence rule lives
214
- // in one place: session → p (local pin) → P (repo declaration) → global.
215
- const session = sessionId === undefined ? undefined : readSessionProfileBinding(sessionId);
216
- if (session !== undefined) {
217
- const next = `${session.name} (session)`;
218
- const changed = next !== effective;
219
- effective = next;
220
- return changed;
221
- }
222
- const pin = identity && cwd ? resolveProfilePin({
212
+ // gentle-shell#1064: the status shows the profile launches use, through
213
+ // the same single rule (explicit Enter binding → profile frozen at
214
+ // startup → live p → P → G only in follow mode), with the existing
215
+ // spelling: "name (session)", "name (local)", "name (repo)", or the bare
216
+ // global name.
217
+ const next = cwd === undefined ? global() : sessionProfileLabel(resolveSessionProfile({
218
+ sessionId,
223
219
  cwd,
224
- configHome: bundledGentlePiConfigHome(env) ?? env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai"),
225
- resolveWorktree: () => identity!,
226
- }) : undefined;
227
- const next = pin ? `${pin.profile} (${pin.source})` : global();
220
+ configHome: gentlePiConfigHome(env),
221
+ resolveWorktree: () => identity,
222
+ env,
223
+ }));
228
224
  const changed = next !== effective;
229
225
  effective = next;
230
226
  return changed;
@@ -340,6 +336,16 @@ export function buildShellBarModel(
340
336
  };
341
337
  }
342
338
 
339
+ // Gauge-specific theme roles (issue #2222): resolved from the installed theme
340
+ // source and memoized by gauge-theme.ts, so the render hot path only stats the
341
+ // theme file. Undefined — and therefore today's shared roles — whenever the theme
342
+ // source is unavailable.
343
+ function gaugePaint(ctx: ExtensionContext): GaugePaint | undefined {
344
+ const themeName = ctx.ui.theme.name;
345
+ const source = ctx.ui.theme.sourcePath ?? ctx.ui.getAllThemes?.().find((item) => item?.name === themeName)?.path;
346
+ return gaugePaintFor(typeof source === "string" ? source : undefined);
347
+ }
348
+
343
349
  export function createShellBarComponent(
344
350
  pi: ExtensionAPI,
345
351
  ctx: ExtensionContext,
@@ -356,7 +362,7 @@ export function createShellBarComponent(
356
362
  });
357
363
  return {
358
364
  render(width: number) {
359
- return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width, presentation?.());
365
+ return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width, presentation?.(), gaugePaint(ctx));
360
366
  },
361
367
  invalidate() {},
362
368
  dispose() {
@@ -1229,8 +1235,6 @@ const CHANGES_SHORTCUT_DEFAULT = "alt+g";
1229
1235
  const CHANGES_POLL_DEFAULT_MS = 2000;
1230
1236
  const GIT_TIMEOUT_MS = 5000;
1231
1237
  const COMMANDS_COMMAND_NAME = "gentle:commands";
1232
- const OVERLAY_HEIGHT_RATIO = 0.8;
1233
- const OVERLAY_MIN_ROWS = 8;
1234
1238
 
1235
1239
  export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile, maxBuffer = Infinity): GitRunner {
1236
1240
  // Pi exec cannot replace the inherited environment. Use argv directly and
@@ -1335,6 +1339,8 @@ interface OverlayDeps {
1335
1339
  }
1336
1340
 
1337
1341
  // The overlay polls each root's Git state while it is open; diffs load lazily.
1342
+ // Like Gentle Agents it takes the whole terminal, and the native fullscreen
1343
+ // wrapper keeps mouse dispatch on the view while it owns the keyboard.
1338
1344
  async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
1339
1345
  let host: ExternalEditorHost | undefined;
1340
1346
  let view: WorktreeChangesView | undefined;
@@ -1352,16 +1358,18 @@ async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Pro
1352
1358
  host = tui;
1353
1359
  view = new WorktreeChangesView(deps.worktrees(), {
1354
1360
  theme,
1355
- rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
1361
+ rows: () => Math.max(0, tui.terminal.rows),
1356
1362
  loadDiff: deps.loadDiff,
1357
1363
  onOpen: (root, file) => close({ root, file }),
1358
1364
  onRefresh: () => void refresh().catch(() => {}),
1359
1365
  onClose: () => close(null),
1360
1366
  requestRender: () => tui.requestRender(),
1361
1367
  });
1362
- return view;
1368
+ const interaction = createNativeFullscreenInteraction({ keyboardTarget: view, requestRender: () => tui.requestRender() });
1369
+ interaction.addChild(view);
1370
+ return interaction;
1363
1371
  },
1364
- { overlay: true, overlayOptions: { width: "92%", anchor: "center" } },
1372
+ { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } },
1365
1373
  );
1366
1374
  if (!chosen || !host) return;
1367
1375
  if (!openInExternalEditor(host, chosen.file.path, process.env, spawnSync, chosen.root)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
@@ -1578,19 +1586,20 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1578
1586
  // store and the profile reader cannot drift onto two different stores.
1579
1587
  const usageConfigHome = gentlePiConfigHome(env);
1580
1588
  const usageFetchTimeout = usageFetchTimeoutMs(env);
1581
- // The subagent routing in force for this session: a session binding first
1582
- // (gentle-shell#1064 slice 1), then a repository pin, then the global active
1583
- // profile. Only the profile's own role entries count; the reserved
1584
- // orchestrator key is not a route.
1589
+ // The subagent routing in force for this session, through the same single
1590
+ // rule launches use (gentle-shell#1064): explicit Enter binding, then the
1591
+ // profile frozen at startup, then the live p → P → G only in follow mode.
1592
+ // Only the profile's own role entries count; the reserved orchestrator key
1593
+ // is not a route.
1585
1594
  const activeRoutingModels = (ctx: ExtensionContext): Array<string | undefined> => {
1586
- const session = readSessionProfileBinding(ctx.sessionManager?.getSessionId?.())?.modelProfiles;
1587
- if (session) return [...profileRoleEntries(session).map(([, entry]) => entry.model)];
1588
- const pin = resolveProfilePin({ cwd: ctx.cwd, configHome: usageConfigHome, resolveWorktree: deps.resolveWorktree });
1589
- const config = pin ? pin.modelProfiles : undefined;
1590
- if (config) return [...profileRoleEntries(config).map(([, entry]) => entry.model)];
1591
- const store = readProfilesFileResult(profilesFilePath(usageConfigHome));
1592
- const active = store.status === "valid" && store.file.active !== undefined ? store.file.profiles[store.file.active] : undefined;
1593
- return active ? profileRoleEntries(active).map(([, entry]) => entry.model) : [];
1595
+ const config = sessionProfileModelProfiles(resolveSessionProfile({
1596
+ sessionId: ctx.sessionManager?.getSessionId?.(),
1597
+ cwd: ctx.cwd,
1598
+ configHome: usageConfigHome,
1599
+ resolveWorktree: deps.resolveWorktree,
1600
+ env,
1601
+ }));
1602
+ return config ? profileRoleEntries(config).map(([, entry]) => entry.model) : [];
1594
1603
  };
1595
1604
  // A bare model id names a provider only when exactly one provider in the
1596
1605
  // registry carries that id; anything else stays untargeted rather than guessed.
@@ -1743,6 +1752,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1743
1752
  onClose: () => withOverlayRepaint(tui, done)(null),
1744
1753
  requestRender: () => tui.requestRender(),
1745
1754
  scope: () => ({ providers: usageScope, failed: new Set(usageFailures) }),
1755
+ gauge: () => gaugePaint(ctx),
1746
1756
  });
1747
1757
  // Open promptly: the panel draws whatever the store already holds
1748
1758
  // while the forced refresh runs underneath it, and repaints when
@@ -1929,7 +1939,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1929
1939
  const statusOwner = () => narrowStatusOwner({ mode: (tui as TUI & { mode?: string }).mode, columns: tui.terminal?.columns ?? 0, statusPlacement: visualSettings.statusPlacement, headerPlacement: visualSettings.headerPlacement });
1930
1940
  const statusCardVisible = () => Boolean(tui.terminal && tui.terminal.columns >= SIDEBAR_BREAKPOINT && sidebarState(tui).ownsHost?.() && visualSettings.statusPlacement !== "hidden");
1931
1941
  const belowFloat = (width: number, statuses: boolean) => (tui as TUI & { mode?: string }).mode === "fullscreen"
1932
- ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }), ...(statusCardVisible() ? { jobs: undefined } : {}) }, theme, width, usageShortcutKey, visualSettings, tracker.model)
1942
+ ? renderShellBelowInputFloat({ ...footerModel(), ...(statuses ? {} : { statuses: [] }), ...(statusCardVisible() ? { jobs: undefined } : {}) }, theme, width, usageShortcutKey, visualSettings, tracker.model, gaugePaint(ctx))
1933
1943
  : undefined;
1934
1944
  // Match sidebarPart's live paint ownership, including an unavailable rail.
1935
1945
  const footerSuppressed = () => {
@@ -1940,7 +1950,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1940
1950
  groupedChangesOwner = (width) => belowFloat(width, !footerSuppressed()) !== undefined;
1941
1951
  const bottomBar = {
1942
1952
  ...bottom,
1943
- render: (width: number) => belowFloat(width, true)?.rows ?? (statusOwner() === STATUS_OWNER.BOTTOM ? renderShellBottomOnlyBar(footerModel(), theme, width, usageShortcutKey, visualSettings) : bottom.render(width)),
1953
+ render: (width: number) => belowFloat(width, true)?.rows ?? (statusOwner() === STATUS_OWNER.BOTTOM ? renderShellBottomOnlyBar(footerModel(), theme, width, usageShortcutKey, visualSettings, undefined, gaugePaint(ctx)) : bottom.render(width)),
1944
1954
  handleMouse(event: Parameters<NonNullable<SidebarRail["handleMouse"]>>[0]) {
1945
1955
  if (footerSuppressed() || event.type !== "click" || event.button !== "left") return undefined;
1946
1956
  const chrome = belowFloat(event.width, true);
@@ -1957,7 +1967,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
1957
1967
  // The header row carries everything that ticks every frame (model,
1958
1968
  // effort, context, cost, usage) plus session identity and the jobs count
1959
1969
  // when Status is absent. Opaque statuses and working state stay excluded.
1960
- const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel(), !statusCardVisible() && (statusOwner() === STATUS_OWNER.HEADER || visualSettings.statusPlacement === "hidden")), theme, width, usageShortcutKey, visualSettings);
1970
+ const headerBar = (width: number) => renderShellHeaderChrome(buildShellHeaderModel(footerModel(), !statusCardVisible() && (statusOwner() === STATUS_OWNER.HEADER || visualSettings.statusPlacement === "hidden")), theme, width, usageShortcutKey, visualSettings, gaugePaint(ctx));
1961
1971
  const disposeHeader = sidebarHeader(tui, {
1962
1972
  digest: () => JSON.stringify([footerModel(), tracker.model, visualSettings, cardStyle()]),
1963
1973
  render: (width) => headerBar(width).rows,
@@ -2261,7 +2271,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2261
2271
  });
2262
2272
  for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2263
2273
  category,
2264
- label: () => `Session permissions: ${mode.toUpperCase()}${permissionDisplay === mode ? " (current)" : ""}`,
2274
+ label: () => `Session permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDisplay === mode ? " (current)" : ""}`,
2265
2275
  preview: () => ({ title: "Permissions · current session", sample: mode === "auto"
2266
2276
  ? "independent session-model action checks · extra inference cost · hard safety and native consent remain"
2267
2277
  : mode === "yolo" ? "ordinary scoped delivery permission · destructive confirmations remain" : "normal checks · overrides an elevated global default" }),
@@ -2273,7 +2283,7 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
2273
2283
  });
2274
2284
  for (const mode of ["normal", "auto", "yolo"] as const) rows.push({
2275
2285
  category,
2276
- label: () => `Default permissions: ${mode.toUpperCase()}${permissionDefault === mode ? " (saved)" : ""}`,
2286
+ label: () => `Default permissions: ${mode.toUpperCase()}${mode === "auto" ? " (beta)" : ""}${permissionDefault === mode ? " (saved)" : ""}`,
2277
2287
  preview: () => ({ title: "Permissions · user-global default", sample: `New primary sessions start in ${mode.toUpperCase()}; current session stays unchanged. ${mode === "auto" ? "Additional classifier inference uses the session model; usage is separate." : "Independent safety and consent remain."}` }),
2278
2288
  action: async () => {
2279
2289
  if (closed) return;
@@ -13,9 +13,13 @@ import {
13
13
  staleTurns,
14
14
  TODO_DETAILS_KEY,
15
15
  TODO_GLYPH,
16
+ TODO_PROGRESS_EVENT,
16
17
  TODO_TOOL_NAME,
17
18
  todoCardTone,
19
+ TODO_CLOSED_STATE,
20
+ todoProgress,
18
21
  todoPromptBlock,
22
+ todoStateMessage,
19
23
  todoSummary,
20
24
  type TodoParams,
21
25
  type TodoState,
@@ -24,10 +28,13 @@ import {
24
28
  // Gentle Todo: the task list the model keeps while it works, drawn as a
25
29
  // Gentle Shell card above the editor. Three things keep it current that a
26
30
  // static tool description cannot: `write` replaces the whole list in one
27
- // call, each run's system prompt carries the open tasks and the rules, and
28
- // a list that goes untouched while tasks stay open is marked stale for both
29
- // the human and the model. Long runs also receive one request-local reminder
30
- // after several tool-use turns, without waking the agent or changing tasks.
31
+ // call, each run's system prompt carries a byte-stable rules block while the
32
+ // open tasks reach the model as a hidden transcript message appended when the
33
+ // list changes (so list changes never invalidate the cached prompt prefix and
34
+ // requests stay append-only, gentle-shell#1662, #2195), and
35
+ // a list that goes untouched while tasks stay open is marked stale for the
36
+ // human. Long runs also receive one request-local reminder after several
37
+ // tool-use turns, without waking the agent or changing tasks.
31
38
 
32
39
  const WIDGET_KEY = "gentle-todo";
33
40
  const STATUS_ENUM = ["pending", "in_progress", "blocked", "done", "dropped"];
@@ -84,6 +91,10 @@ interface TodoSession {
84
91
  collapsed: boolean;
85
92
  /** A finished list stays on screen for the turn it finished in, then clears. */
86
93
  clearOnNextTurn: boolean;
94
+ /** The last state snapshot sent to the transcript. */
95
+ lastSent: string | undefined;
96
+ /** A snapshot queued during the current turn; Pi appends it at turn_end. */
97
+ pending: string | undefined;
87
98
  ui: ExtensionContext["ui"] | undefined;
88
99
  host: { requestRender(): void } | undefined;
89
100
  tui: TUI | undefined;
@@ -104,12 +115,21 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
104
115
  const key = sessionKey(ctx);
105
116
  let current = sessions.get(key);
106
117
  if (!current) {
107
- current = { state: emptyTodo(), turn: 0, toolTurnsSinceWrite: 0, reminded: false, wroteThisTurn: false, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
118
+ current = { state: emptyTodo(), turn: 0, toolTurnsSinceWrite: 0, reminded: false, wroteThisTurn: false, collapsed: false, clearOnNextTurn: false, lastSent: undefined, pending: undefined, ui: undefined, host: undefined, tui: undefined };
108
119
  sessions.set(key, current);
109
120
  }
110
121
  return current;
111
122
  };
112
123
 
124
+ // While the agent streams, Pi appends this hidden message at turn_end, after
125
+ // the tool results and before the next model request; it never starts a turn.
126
+ const persistState = (current: TodoSession, state: string) => {
127
+ if (!pi.sendMessage) return;
128
+ pi.sendMessage({ customType: "gentle-todo-state", content: state, display: false }, { triggerTurn: false });
129
+ current.lastSent = state;
130
+ current.pending = state;
131
+ };
132
+
113
133
  const toggle = (current: TodoSession) => {
114
134
  current.collapsed = !current.collapsed;
115
135
  if (current.tui) invalidateSidebar(current.tui);
@@ -262,6 +282,11 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
262
282
  current.toolTurnsSinceWrite = 0;
263
283
  current.reminded = false;
264
284
  current.wroteThisTurn = true;
285
+ // A closed list persists the closing marker, so the last open
286
+ // snapshot in the transcript never reads as current work.
287
+ const state = todoStateMessage(current.state) ?? (current.lastSent === undefined ? undefined : TODO_CLOSED_STATE);
288
+ if (state !== undefined && state !== current.lastSent) persistState(current, state);
289
+ pi.events?.emit(TODO_PROGRESS_EVENT, todoProgress(current.state, ctx.sessionManager.getSessionId()));
265
290
  }
266
291
  if (current.tui) invalidateSidebar(current.tui);
267
292
  }
@@ -281,7 +306,9 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
281
306
  });
282
307
  }
283
308
 
284
- for (const [key, lines] of [["ctrl+shift+up", -3], ["ctrl+shift+down", 3]] as const) {
309
+ // Pi binds ctrl+shift+up/down to prompt navigation (tui.altScreen.*Prompt) and
310
+ // alt+up/down to dequeue and model reordering; alt+shift+up/down are free.
311
+ for (const [key, lines] of [["alt+shift+up", -3], ["alt+shift+down", 3]] as const) {
285
312
  pi.registerShortcut(key, {
286
313
  description: `Scroll the Todo list ${lines < 0 ? "up" : "down"}`,
287
314
  handler: async (ctx) => session(ctx).scrollTodo?.(lines),
@@ -298,6 +325,8 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
298
325
  current.toolTurnsSinceWrite = 0;
299
326
  current.reminded = false;
300
327
  current.wroteThisTurn = false;
328
+ current.lastSent = undefined;
329
+ current.pending = undefined;
301
330
  current.ui = ctx.hasUI ? ctx.ui : undefined;
302
331
  show(current);
303
332
  });
@@ -315,11 +344,10 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
315
344
  current.clearOnNextTurn = false;
316
345
  show(current);
317
346
  }
318
- const block = todoPromptBlock(current.state, staleTurns(current.state, current.turn));
319
- if (!block) return undefined;
320
347
  // gentle-shell#1485: pi-claude-bridge drops a handler-returned
321
- // systemPrompt, so the open-tasks block goes through appendSystemPrompt.
322
- appendSystemPromptOnce(event.systemPromptOptions, block);
348
+ // systemPrompt, so the rules block goes through appendSystemPrompt. It is
349
+ // the same bytes on every turn, list or no list (gentle-shell#1662).
350
+ appendSystemPromptOnce(event.systemPromptOptions, todoPromptBlock());
323
351
  return undefined;
324
352
  });
325
353
 
@@ -331,18 +359,34 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
331
359
  current.toolTurnsSinceWrite += 1;
332
360
  }
333
361
  current.wroteThisTurn = false;
362
+ // Pi flushes the queued snapshot right after this event.
363
+ current.pending = undefined;
334
364
  });
335
365
 
336
366
  pi.on("context", (event, ctx) => {
337
367
  const current = session(ctx);
338
- if (current.reminded || current.toolTurnsSinceWrite < REMIND_AFTER_TOOL_TURNS) return undefined;
339
- if (!current.state.tasks.some((task) => task.status === "pending" || task.status === "in_progress")) return undefined;
340
- current.reminded = true;
341
- // Context transforms are request-local: no transcript entry, UI
342
- // notification, continuation or provider-specific wake is needed.
343
- return {
344
- messages: [...event.messages, { role: "custom" as const, customType: "gentle-todo-reminder", content: TODO_REMINDER, display: false, timestamp: Date.now() }],
345
- };
368
+ // gentle-shell#1662, #2195: providers cache the request prefix up to the
369
+ // last message (Anthropic) or send only the delta after the previous input
370
+ // (openai-codex), so a trailing message that the next request drops breaks
371
+ // both. The list is persisted when it changes; this transform only covers a
372
+ // transcript that lacks the current snapshot (compaction, branch navigation,
373
+ // sessions recorded earlier): it trails this one request and persists the
374
+ // snapshot once, so the following requests are append-only again.
375
+ const trailing: Array<{ role: "custom"; customType: string; content: string; display: false; timestamp: number }> = [];
376
+ const last = [...event.messages].reverse().find((message) => (message as { customType?: string }).customType === "gentle-todo-state") as { content?: unknown } | undefined;
377
+ const seen = typeof last?.content === "string" ? last.content : undefined;
378
+ const state = todoStateMessage(current.state) ?? (seen === undefined || seen === TODO_CLOSED_STATE ? undefined : TODO_CLOSED_STATE);
379
+ if (state !== undefined && seen !== state) {
380
+ trailing.push({ role: "custom", customType: "gentle-todo-state", content: state, display: false, timestamp: Date.now() });
381
+ if (current.pending !== state) persistState(current, state);
382
+ }
383
+ const actionable = current.state.tasks.some((task) => task.status === "pending" || task.status === "in_progress");
384
+ if (!current.reminded && current.toolTurnsSinceWrite >= REMIND_AFTER_TOOL_TURNS && actionable) {
385
+ current.reminded = true;
386
+ trailing.push({ role: "custom", customType: "gentle-todo-reminder", content: TODO_REMINDER, display: false, timestamp: Date.now() });
387
+ }
388
+ if (trailing.length === 0) return undefined;
389
+ return { messages: [...event.messages, ...trailing] };
346
390
  });
347
391
 
348
392
  pi.on("tool_execution_end", (event, ctx) => {
@@ -795,7 +795,7 @@ export function createQuietToolRenderer(
795
795
  safeResult,
796
796
  options,
797
797
  theme,
798
- sanitizedRenderContext(renderContext) as any,
798
+ { ...sanitizedRenderContext(renderContext), lastComponent: undefined } as any,
799
799
  ), false);
800
800
  }
801
801
  let output = formatToolResultOutput(toolName, safeResult, {