gentle-pi 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -4,7 +4,7 @@ import { Type } from "typebox";
4
4
  import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
5
5
  import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
6
6
  import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
7
- import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
7
+ import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, nativeRootSpelling, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
8
8
  import { canonicalWriterRoot } from "../lib/writer-surfaces.ts";
9
9
  import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
10
10
  import { MESSAGING_REASON_MAX_UTF8_BYTES, MESSAGING_REASON_MIN_CHARACTERS, normalizeMessagingReason, SessionMessagingGrants } from "../lib/session-messaging-grants.ts";
@@ -20,21 +20,23 @@ import { JOB_GLYPH, JOB_NOTICE_TYPE, jobDetails, jobNoticeText, monitorEventsTex
20
20
  import { createMonitorController, mergeMonitorBatches, type MonitorNotice } from "../lib/background-monitor.ts";
21
21
  import { JobsView } from "../lib/jobs-view.ts";
22
22
  import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY } from "../lib/jobs-sidebar-state.ts";
23
- import { Text, type TUI } from "@earendil-works/pi-tui";
23
+ import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
24
24
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
25
- import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
25
+ import { sidebarPart, VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
26
+ import { NativePointerRegion } from "../lib/native-pointer-region.ts";
26
27
  import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
27
28
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
28
29
  import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
29
- import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
30
- import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt } from "../lib/session-profile-freeze.ts";
30
+ import { AGENT_MODE, PROFILE_SOURCE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, unmatchedProfileKeys, withPinnedModelProfiles, type AgentDefinition, type AgentMode, type ProfileSource } from "../lib/agents-config.ts";
31
+ import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt, type InheritedProfileOptions, type SessionProfileResolution } from "../lib/session-profile-freeze.ts";
32
+ import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
31
33
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
32
34
  import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
33
35
  import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
34
36
  import { AgentRunner, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
35
37
  import { parseChildPackageInjection } from "../lib/child-package-injection.ts";
36
38
  import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
37
- import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
39
+ import { ActiveSessionClient, ActiveSessionListener, SessionPresenceError, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
38
40
  import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
39
41
  import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
40
42
  import { inheritedUnsafeGitEnvironmentKeys } from "../lib/review-repository.ts";
@@ -49,6 +51,7 @@ import { discoverOrchestrators } from "../lib/orchestrator-discovery.ts";
49
51
  import { consultPublishedMetadata, unavailableMetadata, type MetadataReceipt } from "../lib/orchestrator-consultation.ts";
50
52
  import { HelperCostPermission } from "../lib/orchestrator-helper-consent.ts";
51
53
  import { OrchestratorStateCache } from "../lib/orchestrator-state.ts";
54
+ import { TODO_PROGRESS_EVENT, type TodoProgress } from "../lib/shell-todo.ts";
52
55
  import { normalizeTaskSubject } from "../lib/orchestrator-alias.ts";
53
56
  import { decodeWorkDescriptor } from "../lib/orchestrator-work.ts";
54
57
  import { validateWorkFilter, searchPublishedWork } from "../lib/orchestrator-work-search.ts";
@@ -56,8 +59,8 @@ import { OrchestratorScopeCache, type RepositoryFact } from "../lib/orchestrator
56
59
  import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
57
60
  import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
58
61
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
59
- import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
60
- import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
62
+ import { AGENTS_GLYPH, agentsCardTone, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
63
+ import { CARD_TONE, panelHeaderRow, renderCard } from "../lib/shell-card.ts";
61
64
  import { openInExternalEditor } from "./gentle-shell.ts";
62
65
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
63
66
  import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
@@ -186,6 +189,44 @@ function childExtensionRequest(env: NodeJS.ProcessEnv, curated: string[] | undef
186
189
  return curated && curated.length > 0 ? { extensionPaths: [...curated] } : {};
187
190
  }
188
191
 
192
+ // gentle-shell#2180: a short, stable token for the profile layer that supplied
193
+ // the launch's model, recorded next to the model in the TaskRecord so every
194
+ // task card shows which layer won and stale disk edits below a frozen session
195
+ // become visible. Derived from the resolution the request already performed;
196
+ // the only profile reads are the pin/declaration lookups in the paths where
197
+ // `sessionProfileRoutingAt` itself reads the target's own layers without
198
+ // carrying the origin (a foreign target under a session frozen without a
199
+ // profile, and `follow`/child launches), so no layer is resolved twice to route.
200
+ function modelSourceToken(
201
+ source: ProfileSource,
202
+ resolution: SessionProfileResolution,
203
+ routingTarget: InheritedProfileOptions & { foreignRepository?: boolean },
204
+ ): string | undefined {
205
+ if (source === PROFILE_SOURCE.DEFINITION) return "definition";
206
+ // Unresolved means no layer supplied a model at all: the card already shows
207
+ // `default`, and naming a layer next to it would only be noise.
208
+ if (source === PROFILE_SOURCE.UNRESOLVED) return undefined;
209
+ if (source !== PROFILE_SOURCE.PROFILE) return "default";
210
+ if (resolution.kind === "explicit") return "binding";
211
+ if (resolution.kind === "inherited") {
212
+ if (resolution.profile !== undefined) return `frozen:${resolution.profile.origin}`;
213
+ // Frozen without a profile: a foreign target keeps only its own pin
214
+ // layers, so a winning pin there is named from the same read
215
+ // `sessionProfileRoutingAt` just performed; otherwise the session keeps
216
+ // the materialized config routing.
217
+ if (routingTarget.foreignRepository === true) {
218
+ const pin = resolveProfilePin(routingTarget);
219
+ return pin === undefined ? "profile" : `pin:${pin.source}`;
220
+ }
221
+ return "profile";
222
+ }
223
+ // `follow` and live child launches read the target's own defaults
224
+ // (pin → declaration → global active); name the layer the same read chose.
225
+ const pin = resolveProfilePin(routingTarget);
226
+ if (pin !== undefined) return `pin:${pin.source}`;
227
+ return resolveUnversionedProjectProfile(routingTarget.cwd, routingTarget.configHome) !== undefined ? "declaration" : "profile";
228
+ }
229
+
189
230
  export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
190
231
  const root = join(agentHome, "gentle-agents");
191
232
  return { sessions: join(root, "sessions"), transcripts: join(root, "transcripts") };
@@ -521,7 +562,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
521
562
  };
522
563
  return PresencePublisher.start({ profile: agentHome, sessionId, label: labelSource(), labelSource, activity: [] });
523
564
  };
524
- const publishActivity = () => {
565
+ const publishActivityNow = (deferred = false) => {
525
566
  if (!sessions) return;
526
567
  try {
527
568
  if (!presence || presence.error) {
@@ -540,14 +581,81 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
540
581
  return data?.sessionId === activeSessionId() && typeof data.root === "string" && typeof data.evidence === "string" ? [data.root] : [];
541
582
  });
542
583
  presence?.updateDiscovery(transport.listener.record, { workspace: sessions.getCwd(), tasks, registered,
543
- scope: scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
584
+ // A deferred (render-cadence trailing) publish must never resolve Git:
585
+ // its timer can land while a peer metadata query is being served, and
586
+ // that contract adds no caller Git probes. It reuses the snapshot the
587
+ // last event-time publication projected; the next event-time publish
588
+ // refreshes the facts.
589
+ scope: deferred ? scopeCache.cachedScope() : scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
544
590
  }
545
591
  } catch { presence?.dispose(); presence = undefined; }
546
592
  };
593
+ // Coalesced like requestRender: the first call publishes immediately so an
594
+ // idle→working transition or a resumed session never waits, and every call
595
+ // inside the window collapses into one trailing publish, so a burst of child
596
+ // deltas costs one publication instead of one per event.
597
+ let activityPublishQueued = false;
598
+ let activityPublishTrailing = false;
599
+ let cancelActivityPublish: (() => void) | undefined;
600
+ const resetActivityPublish = () => {
601
+ cancelActivityPublish?.();
602
+ cancelActivityPublish = undefined;
603
+ activityPublishQueued = false;
604
+ activityPublishTrailing = false;
605
+ };
606
+ const flushDeferredPublish = () => {
607
+ cancelActivityPublish = undefined;
608
+ activityPublishQueued = false;
609
+ if (!activityPublishTrailing) return;
610
+ activityPublishTrailing = false;
611
+ publishActivityNow(true);
612
+ };
613
+ const publishActivity = () => {
614
+ if (activityPublishQueued) {
615
+ activityPublishTrailing = true;
616
+ return;
617
+ }
618
+ publishActivityNow();
619
+ activityPublishQueued = true;
620
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
621
+ };
622
+ // Discrete semantic events publish immediately so peers and readers see them
623
+ // promptly (a consult binds to the published snapshot); the coalescing window
624
+ // stays armed so a child-delta burst that follows still collapses into one
625
+ // trailing publish.
626
+ const publishActivityPrompt = () => {
627
+ publishActivityNow();
628
+ if (!activityPublishQueued) {
629
+ activityPublishQueued = true;
630
+ cancelActivityPublish = deps.schedule(flushDeferredPublish, RENDER_COALESCE_MS);
631
+ }
632
+ };
547
633
  const unsubscribeScope = pi.events.on(SESSION_WORKTREE_CHANGED, (event) => {
548
634
  if ((event as { sessionId?: string } | undefined)?.sessionId !== sessions?.getSessionId()) return;
549
635
  scopeCache.clear();
550
- publishActivity();
636
+ publishActivityPrompt();
637
+ });
638
+ // The todo list is the session's progress: publish it for peers so the model
639
+ // spends no turn on orchestrator_session_id. Merged into the owner's curated
640
+ // state; a publication that cannot be made never fails the todo write.
641
+ // Counts only: a todo title is private work text, never published to peers.
642
+ // The last automatic line survives an owner publication that omits `progress`.
643
+ let automaticProgress: { sessionId: string; text: string } | undefined;
644
+ const unsubscribeTodoProgress = pi.events.on(TODO_PROGRESS_EVENT, (event) => {
645
+ const progress = event as Partial<TodoProgress> | undefined;
646
+ if (!sessions || progress?.sessionId !== sessions.getSessionId() || typeof progress.done !== "number" || typeof progress.total !== "number") return;
647
+ const text = `${progress.done}/${progress.total} done`;
648
+ try {
649
+ const owned = stateCache.get(sessions);
650
+ // A state the owner withdrew stays withdrawn until the owner publishes again.
651
+ if (owned?.state === null) return;
652
+ const { progress: previous, ...curated } = owned?.state ?? {};
653
+ // A cleared list has no progress to report: drop the line, publish nothing new.
654
+ if (progress.total === 0 && previous === undefined) return;
655
+ stateCache.publish(sessions, progress.total === 0 ? curated : { ...curated, progress: text }, (type, data) => pi.appendEntry(type, data));
656
+ automaticProgress = progress.total === 0 ? undefined : { sessionId: progress.sessionId, text };
657
+ publishActivity();
658
+ } catch { /* stale session or over the state budget: keep the previous publication */ }
551
659
  });
552
660
  let worktrees: SessionWorktreeRegistry | undefined;
553
661
  let worktreeManager: ExtensionContext["sessionManager"] | undefined;
@@ -594,6 +702,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
594
702
  clearTaskMetrics();
595
703
  unsubscribeMetrics();
596
704
  unsubscribeScope();
705
+ unsubscribeTodoProgress();
597
706
  scopeCache.clear();
598
707
  });
599
708
  let stopAllConfirmation: Promise<void> | undefined;
@@ -673,7 +782,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
673
782
  await closeStartupTransport();
674
783
  return;
675
784
  }
676
- publishActivity();
785
+ // Publish immediately, not on the coalescing trailing edge: this is the
786
+ // session's first discovery publication, and a peer metadata query binds
787
+ // to the activation only through it — a 400 ms lag here makes a prompt
788
+ // consult read publication-unknown-or-stale.
789
+ publishActivityPrompt();
677
790
  } catch {
678
791
  if (activeSessionTransport?.generation === generation) activeSessionTransport = undefined;
679
792
  await closeStartupTransport();
@@ -1502,7 +1615,11 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1502
1615
  ctx.ui.notify(`Could not read the task's session: ${error instanceof Error ? error.message : String(error)}`, "warning");
1503
1616
  return;
1504
1617
  }
1505
- if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1618
+ try {
1619
+ if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
1620
+ } catch (error) {
1621
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
1622
+ }
1506
1623
  };
1507
1624
 
1508
1625
  const writeTranscript = async (task: TaskRecord): Promise<string> => {
@@ -1534,24 +1651,80 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1534
1651
  host?.requestRender();
1535
1652
  });
1536
1653
  tickClock();
1537
- // Agents is not a rail part: the fullscreen sidebar only suppresses
1538
- // bottom components registered through sidebarPart, and this widget is
1539
- // the only Agents surface in every mode, so it stays a plain component.
1654
+ // Agents follows Todos: the fullscreen rail shows it when the terminal is
1655
+ // wide enough, and sidebarPart suppresses the above-editor copy only
1656
+ // while the rail owns the host; narrower terminals keep the widget.
1540
1657
  ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
1541
1658
  host = tui;
1542
1659
  sidebarTui = tui;
1543
- return {
1544
- render(width: number) {
1545
- if (!agentsVisible) return [];
1546
- const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
1547
- return lines.length === 0 ? [] : [...lines, ""];
1548
- },
1549
- invalidate() {},
1550
- };
1660
+ return sidebarPart(tui, "agents", agentsCard(tui, theme, false), agentsCard(tui, theme, true));
1661
+ });
1662
+ };
1663
+
1664
+ const toggleCollapsed = () => {
1665
+ collapsed = !collapsed;
1666
+ if (sidebarTui) invalidateSidebar(sidebarTui);
1667
+ host?.requestRender();
1668
+ };
1669
+
1670
+ // One surface of the Agents card. The rail is natively scrollable, so it
1671
+ // lists every task; the above-editor widget keeps its row budget. Only the
1672
+ // header row is the clickable collapse control, exactly like Todos.
1673
+ const agentsCard = (tui: TUI, theme: Parameters<typeof renderAgentsCard>[1], rail: boolean): Component & { dispose(): void; digest(): string } => {
1674
+ let hovered = false;
1675
+ let headerRow = 0;
1676
+ const card: Component = {
1677
+ render(width: number) {
1678
+ if (!agentsVisible) return [];
1679
+ const tasks = visibleTasks();
1680
+ const now = deps.now();
1681
+ headerRow = panelHeaderRow(theme, width, agentsCardTone(tasks, now));
1682
+ const lines = renderAgentsCard(tasks, theme, width, now, { collapsed, collapseKey, maxRows: rail ? undefined : widgetRows(tui.terminal?.rows), viewKey, collapseControl: true, hovered });
1683
+ return lines.length === 0 || rail ? lines : [...lines, ""];
1684
+ },
1685
+ invalidate() {
1686
+ hovered = false;
1687
+ },
1688
+ };
1689
+ const region = new NativePointerRegion(card, {
1690
+ onHover(event) {
1691
+ const next = event.y === headerRow;
1692
+ if (next === hovered) return { handled: true };
1693
+ hovered = next;
1694
+ return { handled: true, render: true };
1695
+ },
1696
+ onLeave() {
1697
+ if (!hovered) return;
1698
+ hovered = false;
1699
+ host?.requestRender();
1700
+ },
1701
+ onClick(event) {
1702
+ if (event.button !== "left" || event.y !== headerRow) return undefined;
1703
+ toggleCollapsed();
1704
+ return { handled: true, render: true };
1705
+ },
1551
1706
  });
1707
+ return {
1708
+ // The rail memo repaints a part only when its digest moves: task
1709
+ // state, the local controls, and the elapsed clock (to the second)
1710
+ // only while a task is still running, so an idle or finished card
1711
+ // never forces a rail rebuild every second.
1712
+ digest: () => {
1713
+ const tasks = visibleTasks();
1714
+ const clock = tasks.some((task) => !isFinished(task.status)) ? Math.floor(deps.now() / 1000) : 0;
1715
+ return JSON.stringify([agentsVisible, collapsed, hovered, tui.terminal?.rows, clock, tasks.map((task) => [task.id, task.status, task.tokens, task.cost, task.label, task.lastStep, task.error])]);
1716
+ },
1717
+ render: (width) => region.render(width),
1718
+ handleMouse: (event) => region.handleMouse(event),
1719
+ invalidate: () => region.invalidate(),
1720
+ dispose: () => region.dispose(),
1721
+ };
1552
1722
  };
1553
1723
 
1554
1724
  const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
1725
+ // Issue #2400: parent sessions already told about model_profiles keys that
1726
+ // match no agent and suggest none, so the notice fires once per session.
1727
+ const unmatchedKeysNotifiedSessions = new Set<string>();
1555
1728
 
1556
1729
  const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
1557
1730
  if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
@@ -1615,7 +1788,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1615
1788
  // same-clone registry. Never use a caller-injected resolver for this identity.
1616
1789
  const identity = resolveSessionWorktree(selectedRoot, parentCwd);
1617
1790
  const canonicalParent = resolveSessionWorktree(parentCwd, parentCwd);
1618
- if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || selectedRoot !== identity.root || !isAbsolute(selectedRoot) || realpathSync(selectedRoot) !== selectedRoot) throw new Error("repository_root must select a canonical independent Git repository.");
1791
+ const spelling = nativeRootSpelling(selectedRoot);
1792
+ if (!identity || (canonicalParent && identity.commonDir === canonicalParent.commonDir) || spelling !== identity.root || !isAbsolute(spelling) || realpathSync(spelling) !== spelling) throw new Error("repository_root must select a canonical independent Git repository.");
1619
1793
  await foreignGrants.authorize(ctx, identity, { continuation: resume !== undefined, signal });
1620
1794
  foreignIdentity = identity;
1621
1795
  foreignParent = canonicalParent;
@@ -1641,14 +1815,17 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1641
1815
  // included; only `follow` reads the defaults of the target. The profile is
1642
1816
  // resolved here, at task-request creation, so queued and running children
1643
1817
  // keep the routing frozen into their requests even if the session rebinds.
1818
+ // The routing target this launch reads (#1064), reused to name the winning
1819
+ // layer (#2180) so both answers come from one resolution.
1820
+ const routingTarget = {
1821
+ cwd: target ?? parentCwd,
1822
+ configHome: profileConfigHome,
1823
+ resolveWorktree: pinIdentity,
1824
+ foreignRepository: foreign,
1825
+ };
1644
1826
  const config = withPinnedModelProfiles(
1645
1827
  loadAgentsConfig(roots(ctx)),
1646
- sessionProfileRoutingAt(sessionProfile, {
1647
- cwd: target ?? parentCwd,
1648
- configHome: profileConfigHome,
1649
- resolveWorktree: pinIdentity,
1650
- foreignRepository: foreign,
1651
- }),
1828
+ sessionProfileRoutingAt(sessionProfile, routingTarget),
1652
1829
  );
1653
1830
  // Drift is measured against the defaults of the session's own directory,
1654
1831
  // where the profile was frozen, never against a delegated target.
@@ -1663,6 +1840,31 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1663
1840
  if (drift !== undefined) ctx.ui.notify(drift, "info");
1664
1841
  }
1665
1842
  const profile = resolveAgentProfile(agent, config);
1843
+ // Issue #2196 (PR #2233 maintainer decision): a model_profiles key that
1844
+ // names no discovered agent was silently ignored while the child inherited
1845
+ // the orchestrator model. When a configured key suggests this agent, name
1846
+ // it at launch; resolution stays exact-match, so an omitted role keeps
1847
+ // inheriting and a matched key never warns. Keys are checked against every
1848
+ // discovered agent, so a key naming another agent is never reported here.
1849
+ if (ctx.hasUI) {
1850
+ const unmatched = unmatchedProfileKeys(config.modelProfiles, discoverAgents(roots(ctx)).agents);
1851
+ const nearMisses = unmatched.filter((entry) => entry.suggestion === agent.name);
1852
+ if (nearMisses.length > 0) {
1853
+ ctx.ui.notify(nearMisses.map((entry) => `"${entry.key}" did not match any agent, did you mean ${agent.name}?`).join(" "), "warning");
1854
+ }
1855
+ // Issue #2400: a key with no suggestion never names the launched agent,
1856
+ // so the filter above kept it silent. The session's first launch lists
1857
+ // every such key once; later launches in the same session stay quiet.
1858
+ const unsuggested = unmatched.filter((entry) => entry.suggestion === undefined);
1859
+ const parentSession = ctx.sessionManager.getSessionId() ?? "";
1860
+ if (unsuggested.length > 0 && !unmatchedKeysNotifiedSessions.has(parentSession)) {
1861
+ unmatchedKeysNotifiedSessions.add(parentSession);
1862
+ ctx.ui.notify(unsuggested.map((entry) => `"${sanitizeTerminalText(entry.key)}" did not match any agent and was ignored.`).join(" "), "warning");
1863
+ }
1864
+ }
1865
+ // #2180: the winning layer is named from the resolution and routing this
1866
+ // request already performed, never by re-resolving them.
1867
+ const modelSource = modelSourceToken(profile.source.model, sessionProfile, routingTarget);
1666
1868
  if (admittedModel !== undefined) {
1667
1869
  const model = profile.model ?? ctx.model;
1668
1870
  const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
@@ -1703,6 +1905,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1703
1905
  } } : {}),
1704
1906
  ...(target === undefined || foreign ? {} : { onLaunch: () => { registry.register(target, "subagent:spawn"); } }),
1705
1907
  model: profile.model,
1908
+ ...(modelSource === undefined ? {} : { modelSource }),
1706
1909
  thinking: profile.thinking,
1707
1910
  sessionDir,
1708
1911
  resumeSessionPath: resume,
@@ -1769,7 +1972,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1769
1972
  if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
1770
1973
  ownedTaskIds.add(task.id);
1771
1974
  publishWork?.(task.id);
1772
- publishActivity(); // Admission's summary notification precedes runtime ownership.
1975
+ publishActivityPrompt(); // Admission's summary notification precedes runtime ownership.
1773
1976
  store.subscribe(task.id, () => { publishActivity(); requestRender(); });
1774
1977
  if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
1775
1978
  // A tool call aborted by the host (a human interrupting the turn, a timeout)
@@ -1842,7 +2045,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1842
2045
  pi.registerTool({
1843
2046
  name: "orchestrator_session_id",
1844
2047
  label: "Orchestrator session ID",
1845
- description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. For work beyond small direct tasks, publish subject and current/next status here; refresh at task changes/completion. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
2048
+ description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Publishing state.work with an explicit topic and no subject also refreshes Current from that topic; keep work.topic current when the task changes. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. Progress is published automatically from the todo list; publish state here only for objective, decisions, blockers, or work, never just to report progress. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
1846
2049
  ...orchestratorToolRenderers("session", expandHint),
1847
2050
  parameters: { type: "object", additionalProperties: false, properties: {
1848
2051
  subject: { type: "string", maxLength: 120, description: "Optional short task subject; updates task aliases independently of human names. Also names an unnamed Pi session." },
@@ -1864,12 +2067,25 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1864
2067
  if (!transport) return text("Error: session messaging is not ready.", { error: "not ready" });
1865
2068
  const { subject, state } = params as { subject?: unknown; state?: unknown };
1866
2069
  const declared = normalizeTaskSubject(subject);
1867
- if (state !== undefined || declared) stateCache.publish(ctx.sessionManager, state, (type, data) => pi.appendEntry(type, data), Date.now(), declared);
2070
+ // An owner object without `progress` keeps the automatic todo progress still on
2071
+ // display; null and an explicit `progress` win, and an over-budget merge does not fail the call.
2072
+ const shown = stateCache.get(ctx.sessionManager)?.state?.progress;
2073
+ const merged = state && typeof state === "object" && !Array.isArray(state) && !Object.hasOwn(state, "progress")
2074
+ && automaticProgress?.sessionId === ctx.sessionManager.getSessionId() && shown === automaticProgress.text
2075
+ ? { ...state, progress: shown } : undefined;
2076
+ const append = (type: string, data: unknown) => pi.appendEntry(type, data);
2077
+ if (merged) {
2078
+ try { stateCache.publish(ctx.sessionManager, merged, append, Date.now(), declared); }
2079
+ catch (error) {
2080
+ if ((error as Error).message !== "invalid-published-state") throw error;
2081
+ stateCache.publish(ctx.sessionManager, state, append, Date.now(), declared);
2082
+ }
2083
+ } else if (state !== undefined || declared) stateCache.publish(ctx.sessionManager, state, append, Date.now(), declared);
1868
2084
  if (activeTransportFor(ctx) !== transport) throw new Error("stale-published-state");
1869
2085
  if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
1870
2086
  const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
1871
2087
  const aliases = stateCache.get(ctx.sessionManager)?.aliases;
1872
- if (state !== undefined || declared) publishActivity();
2088
+ if (state !== undefined || declared) publishActivityPrompt();
1873
2089
  presence?.refreshLabel();
1874
2090
  return text(`Active session ID: ${transport.sessionId}\nCurrent alias (CURRENT TASK): ${aliases?.currentAlias ?? "unknown"}\nInitial alias (FIRST TASK): ${aliases?.initialAlias ?? "unknown"}\nSession name: ${alias || "unnamed"}`,
1875
2091
  { gentleAgents: { senderSessionId: transport.sessionId, alias, sessionName: alias, initialAlias: aliases?.initialAlias ?? null, currentAlias: aliases?.currentAlias ?? null } });
@@ -1977,17 +2193,24 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1977
2193
  ? `repository: ${fact.root} · clone: ${fact.cloneHash} · Git resolved at: ${fact.resolvedAt} (${fact.source})`
1978
2194
  : "repository: unknown";
1979
2195
  const rows = peers.map(peer => {
1980
- const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : " · context: unknown";
2196
+ const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : peer.legacyLabel !== undefined
2197
+ ? ` · ${peer.legacyLabel || "unnamed"} · context: legacy (this session predates discovery publication; restart it to publish its workspace and repository)`
2198
+ : " · context: unknown";
1981
2199
  const tasks = peer.tasks?.map(task => `\n - ${task.label || task.id} [${task.status}] · launch workspace: ${task.workspace || "unknown"} · ${repository(peer.scope?.tasks.find(t => t.id === task.id)?.repository)}`).join("") ?? "";
1982
2200
  const registered = peer.scope?.registered.map(fact => `\n registered: ${repository(fact)}`).join("") ?? "";
1983
2201
  const gaps = peer.scope && !peer.scope.complete ? `\n (scope incomplete: ${peer.scope.omittedTasks} tasks, ${peer.scope.omittedRegistered} registered roots omitted)` : "";
1984
2202
  const catalog = peer.catalog ? `\n recorded catalog (not Git identity): ${JSON.stringify(peer.catalog)}` : "\n recorded catalog: unknown";
1985
2203
  const note = params.recipient_session_id ? `\n owner-curated recorded state (not consent or owner reply; authority none): ${peer.state ? JSON.stringify(peer.state) : "unknown"}` : "";
1986
- return `- ${peer.sessionId}${context} · ${repository(peer.scope?.host)} · metadata: ${peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
2204
+ // A legacy peer never publishes its scope: say so instead of reporting an unknown.
2205
+ const legacy = peer.legacyLabel !== undefined;
2206
+ return `- ${peer.sessionId}${context} · ${legacy ? "repository: not published" : repository(peer.scope?.host)} · metadata: ${legacy ? "legacy" : peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
1987
2207
  });
1988
2208
  return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
1989
- } catch {
1990
- return text("Error: session discovery is unavailable.", { error: "unavailable" });
2209
+ } catch (error) {
2210
+ // SessionPresenceError codes are a fixed enum with no record content,
2211
+ // so surfacing the reason is safe and keeps diagnosis possible.
2212
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2213
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
1991
2214
  }
1992
2215
  },
1993
2216
  });
@@ -2045,8 +2268,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2045
2268
  recipient = candidates[index].sessionId;
2046
2269
  activation = candidates[index];
2047
2270
  }
2048
- } catch {
2049
- return text("Error: session discovery is unavailable.", { error: "unavailable" });
2271
+ } catch (error) {
2272
+ const reason = error instanceof SessionPresenceError ? error.code : "unknown";
2273
+ return text(`Error: session discovery is unavailable (${reason}).`, { error: "unavailable", reason });
2050
2274
  }
2051
2275
  }
2052
2276
  if (recipient === undefined || !validTransportSessionId(recipient)) return text("Error: recipient session ID or message is invalid.", { error: "invalid input" });
@@ -2196,11 +2420,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2196
2420
  if (collapseKey) {
2197
2421
  pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
2198
2422
  description: "Collapse or expand the agents card",
2199
- handler: async () => {
2200
- collapsed = !collapsed;
2201
- if (sidebarTui) invalidateSidebar(sidebarTui);
2202
- host?.requestRender();
2203
- },
2423
+ handler: async () => toggleCollapsed(),
2204
2424
  });
2205
2425
  }
2206
2426
 
@@ -2256,7 +2476,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2256
2476
  helperPermission.clear();
2257
2477
  if (sessions !== ctx.sessionManager) return;
2258
2478
  stateCache.load(ctx.sessionManager);
2259
- publishActivity();
2479
+ publishActivityPrompt();
2260
2480
  });
2261
2481
  pi.on("session_start", async (event, ctx) => {
2262
2482
  // gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
@@ -2298,7 +2518,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2298
2518
  if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
2299
2519
  try {
2300
2520
  presence = startPresence(ctx.sessionManager);
2301
- publishActivity();
2521
+ resetActivityPublish();
2522
+ publishActivityPrompt();
2302
2523
  } catch { presence = undefined; }
2303
2524
  void startSessionTransport(ctx);
2304
2525
  // The desktop app's own pi process: publish live subagent state through
@@ -2341,6 +2562,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2341
2562
  rpcActivityPublisher?.stop();
2342
2563
  rpcActivityPublisher = undefined;
2343
2564
  cancelClock?.();
2565
+ resetActivityPublish();
2344
2566
  for (const view of overlays) { view.handleInput("q"); view.dispose(); }
2345
2567
  overlays.clear();
2346
2568
  sessions = undefined;