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
@@ -25,7 +25,7 @@ type LayoutRoot = Component & { [NODE]?: () => LayoutNode };
25
25
  type Host = TUI & { mode?: string; layoutRoot?: LayoutRoot };
26
26
  type SidebarCache = { revision: number };
27
27
  type RailHit = { key: string; component: Component; startY: number; height: number; width: number };
28
- type SectionCacheEntry = { component: Component; digest: string | undefined; revision: number; contentWidth: number; theme: ShellBarTheme; lines: string[] };
28
+ type SectionCacheEntry = { component: Component; digest: string | undefined; revision: number; contentWidth: number; theme: ShellBarTheme; themeSignature: string; lines: string[] };
29
29
  type SidebarPresentation = { scrollTop: number; output: LayoutNode };
30
30
  type PreparedRail = {
31
31
  revision: number;
@@ -35,6 +35,7 @@ type PreparedRail = {
35
35
  cardStyle: CardStyle;
36
36
  root: LayoutRoot;
37
37
  theme: ShellBarTheme;
38
+ themeSignature: string;
38
39
  parts: Array<[string, SidebarRail]>;
39
40
  digests: Array<string | undefined>;
40
41
  contentWidth: number;
@@ -74,6 +75,20 @@ function railDigest(rail: SidebarRail): string | undefined {
74
75
  }
75
76
  }
76
77
 
78
+ // Pi passes extensions one live theme proxy, so identity never changes on a
79
+ // theme switch, and the switch only calls ui.invalidate(), which does not reach
80
+ // the rail inside the layout tree. How the current theme paints a few roles is
81
+ // what changes, so the memo keys on that: a digest-only section (Status,
82
+ // Agents, Todos) would otherwise keep the previous theme's colors.
83
+ const THEME_SIGNATURE_ROLES = ["accent", "border", "borderAccent", "muted", "dim", "text", "success", "warning", "error"];
84
+ function themeSignature(theme: ShellBarTheme): string {
85
+ try {
86
+ return THEME_SIGNATURE_ROLES.map((role) => theme.fg(role, "·")).join("") + theme.bold("·");
87
+ } catch {
88
+ return "";
89
+ }
90
+ }
91
+
77
92
  export const STATUS_OWNER = { HEADER: "header", BOTTOM: "bottom" } as const;
78
93
  export type StatusOwner = (typeof STATUS_OWNER)[keyof typeof STATUS_OWNER];
79
94
  export interface StatusOwnerInput {
@@ -217,9 +232,13 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
217
232
  }
218
233
  const style = cardStyle();
219
234
  const parts = [...state.parts.entries()];
235
+ // Digest closures read live session state, so they must be evaluated once
236
+ // per frame and stored for the next comparison (issue #1681): the sections
237
+ // loop below reuses this pass instead of calling each digest a second time.
220
238
  const digests = parts.map(([, rail]) => railDigest(rail));
239
+ const signature = themeSignature(theme);
221
240
  const unchanged = prepared?.revision === cache.revision &&
222
- prepared.width === width && prepared.mode === host.mode && prepared.headerPlacement === headerPlacement() && prepared.cardStyle === style && prepared.root === root && prepared.theme === theme &&
241
+ prepared.width === width && prepared.mode === host.mode && prepared.headerPlacement === headerPlacement() && prepared.cardStyle === style && prepared.root === root && prepared.theme === theme && prepared.themeSignature === signature &&
223
242
  prepared.parts.length === parts.length && prepared.parts.every(([key, part], index) => parts[index]?.[0] === key && parts[index]?.[1] === part) &&
224
243
  prepared.digests.length === digests.length && prepared.digests.every((digest, index) => digest === digests[index]);
225
244
  if (unchanged) {
@@ -228,6 +247,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
228
247
  return prepared.active;
229
248
  }
230
249
  try {
250
+ const digestByPart = new Map(parts.map(([key], index) => [key, digests[index]]));
231
251
  // The header is a full-width sibling row, not a rail section: it reads
232
252
  // the terminal width (minus the rail's right inset), never the
233
253
  // 50-column rail's content width.
@@ -241,16 +261,16 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
241
261
  sectionCache.delete(key);
242
262
  return { key, component, lines: [] as string[] };
243
263
  }
244
- const digest = railDigest(component);
264
+ const digest = digestByPart.get(key);
245
265
  const existing = sectionCache.get(key);
246
- const reusable = existing?.component === component && existing.contentWidth === contentWidth && existing.theme === theme &&
266
+ const reusable = existing?.component === component && existing.contentWidth === contentWidth && existing.theme === theme && existing.themeSignature === signature &&
247
267
  (digest !== undefined ? existing.digest === digest : existing.digest === undefined && existing.revision === cache.revision);
248
268
  const lines = reusable ? existing.lines : (() => {
249
269
  const rendered = [...(component.render(contentWidth - RAIL_PADDING * 2) ?? [])];
250
270
  while (rendered.length && rendered[rendered.length - 1]?.trim() === "") rendered.pop();
251
271
  return rendered;
252
272
  })();
253
- sectionCache.set(key, { component, digest, revision: cache.revision, contentWidth, theme, lines });
273
+ sectionCache.set(key, { component, digest, revision: cache.revision, contentWidth, theme, themeSignature: signature, lines });
254
274
  return { key, component, lines };
255
275
  }).filter((section) => section.component !== undefined && section.lines.length > 0) as Array<{ key: string; component: Component; lines: string[] }>;
256
276
  // The header carries the brand once it is active; the banner is the
@@ -276,7 +296,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
276
296
  // Height is owned by the native ScrollView, never by the transcript.
277
297
  const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
278
298
  headerLines = headerActive && headerPlacement() === "top" ? preparedHeaderLines : [];
279
- prepared = { revision: cache.revision, width, mode: host.mode, headerPlacement: headerPlacement(), cardStyle: style, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
299
+ prepared = { revision: cache.revision, width, mode: host.mode, headerPlacement: headerPlacement(), cardStyle: style, root, theme, themeSignature: signature, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
280
300
  state.active = active;
281
301
  return active;
282
302
  } catch {
package/lib/shell-todo.ts CHANGED
@@ -90,6 +90,21 @@ export interface TodoRenderOptions {
90
90
  export const TODO_DETAILS_KEY = "gentleTodo";
91
91
  export const TODO_TOOL_NAME = "todo";
92
92
  export const TODO_GLYPH = "❀";
93
+ /** Emitted on pi.events after each successful todo write; gentle-agents publishes it as session progress. */
94
+ export const TODO_PROGRESS_EVENT = "gentle-pi:todo:progress";
95
+
96
+ export interface TodoProgress {
97
+ sessionId: string;
98
+ done: number;
99
+ total: number;
100
+ /** Title of the first in-progress task, or null when none is in progress. */
101
+ current: string | null;
102
+ }
103
+
104
+ export function todoProgress(state: TodoState, sessionId: string): TodoProgress {
105
+ const current = state.tasks.find((task) => task.status === TODO_STATUS.IN_PROGRESS)?.title ?? null;
106
+ return { sessionId, done: countStatus(state, TODO_STATUS.DONE), total: state.tasks.length, current };
107
+ }
93
108
  const STATUS_ALIASES: Record<string, TodoStatus> = {
94
109
  completed: TODO_STATUS.DONE,
95
110
  complete: TODO_STATUS.DONE,
@@ -261,13 +276,14 @@ export function replayTodo(entries: readonly unknown[]): TodoState {
261
276
  // gentle-shell#1367, #1662: the system prompt is a cached prefix for every
262
277
  // provider, so the block it carries is byte-stable for the whole session:
263
278
  // rules only, no task content and no counters. The current list travels as a
264
- // request-local message at the end of the conversation (todoStateMessage),
265
- // where a change costs only the tail of the request, never the prefix.
279
+ // hidden transcript message appended when it changes (todoStateMessage,
280
+ // gentle-shell#2195), so requests stay append-only and a change costs only
281
+ // the tail of the conversation, never the prefix.
266
282
  const TODO_PROMPT_BLOCK = [
267
283
  "## Todo list",
268
- "Keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
284
+ "A small task (Task Size) uses no todo list: do it and report. For tracked or multi-step work, keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
269
285
  "A blocked task waits on something outside the list: do not work on it until its condition holds. When the plan changes, mark the tasks it made obsolete dropped instead of leaving them pending or marking them done.",
270
- "While tasks stay open, the current list arrives as the last message of each request; it is not repeated here.",
286
+ "While tasks stay open, the current list arrives as a hidden conversation message whenever it changes; the latest one is current, and it is not repeated here.",
271
287
  ].join("\n");
272
288
 
273
289
  /** The byte-stable rules block appended to every system prompt. */
@@ -275,7 +291,10 @@ export function todoPromptBlock(): string {
275
291
  return TODO_PROMPT_BLOCK;
276
292
  }
277
293
 
278
- /** The open-task snapshot delivered as a request-local message; undefined when nothing is open. */
294
+ /** Persisted once an open list closes, so an earlier snapshot never reads as current work (#2195). */
295
+ export const TODO_CLOSED_STATE = "Current todo list: no open tasks.";
296
+
297
+ /** The open-task snapshot delivered as a hidden transcript message; undefined when nothing is open. */
279
298
  export function todoStateMessage(state: TodoState): string | undefined {
280
299
  if (todoSummary(state).open === 0) return undefined;
281
300
  const lines = state.tasks.map((task, index) => `${index + 1}. [${task.status}] ${task.title}${task.note ? ` — ${task.note}` : ""}`);
@@ -1,5 +1,6 @@
1
1
  import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
2
2
  import { renderUsagePanel, type ActiveProvider, type UsagePanelScope, type UsageSourceRegistry, type UsageStore, type UsageTheme } from "./shell-usage.ts";
3
+ import type { GaugePaint } from "./shell-gauge.ts";
3
4
  import { paintHoverable } from "./shell-hover.ts";
4
5
 
5
6
  // Gentle Shell subscriptions overlay: a framed panel over the usage store.
@@ -16,6 +17,9 @@ export interface UsageViewDeps {
16
17
  // provider the store holds; with it, exactly the targeted providers, in
17
18
  // scope order, explaining the ones without data.
18
19
  scope?(): UsagePanelScope | undefined;
20
+ // Optional: gauge-specific theme roles resolved from the installed theme
21
+ // source; without it the panel paints with the shared roles.
22
+ gauge?(): GaugePaint | undefined;
19
23
  onRefresh(): Promise<void>;
20
24
  onClose(): void;
21
25
  requestRender(): void;
@@ -106,7 +110,7 @@ export class UsageView {
106
110
  const inner = width - 2;
107
111
  const title = this.refreshing ? REFRESHING : TITLE;
108
112
  const top = theme.fg(FRAME_ROLE, "╭─ ") + theme.fg(TITLE_ROLE, title) + theme.fg(FRAME_ROLE, ` ${rule(inner - visibleWidth(title) - 3)}╮`);
109
- const body = renderUsagePanel(this.store.all(), theme, inner - 2, this.deps.now(), this.deps.active(), this.deps.registry?.(), this.deps.scope?.()).map(
113
+ const body = renderUsagePanel(this.store.all(), theme, inner - 2, this.deps.now(), this.deps.active(), this.deps.registry?.(), this.deps.scope?.(), this.deps.gauge?.()).map(
110
114
  (line) => `${theme.fg(FRAME_ROLE, "│")} ${fit(line, inner - 2)} ${theme.fg(FRAME_ROLE, "│")}`,
111
115
  );
112
116
  const hints = KEYS.map(([key, label]) => ({ key, label, text: `${key} ${label}`, action: (key === "r" ? "refresh" : "close") as HintAction }));
@@ -1,6 +1,6 @@
1
1
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
2
  import { parseModelRef } from "./agents-config.ts";
3
- import { paintGauge } from "./shell-gauge.ts";
3
+ import { paintGauge, type GaugePaint } from "./shell-gauge.ts";
4
4
 
5
5
  // Gentle Shell subscription usage: the rate-limit windows each connected
6
6
  // provider reports. Codex sends them as SSE headers and through its usage
@@ -514,16 +514,16 @@ export function accountIdFromToken(token: string): string | undefined {
514
514
  }
515
515
  }
516
516
 
517
- function paintMeter(percent: number, cells: number, theme: UsageTheme): string {
518
- return paintGauge(percent, theme, cells);
517
+ function paintMeter(percent: number, cells: number, theme: UsageTheme, paint?: GaugePaint): string {
518
+ return paintGauge(percent, theme, cells, paint);
519
519
  }
520
520
 
521
- export function renderUsageBar(usage: ProviderUsage, theme: UsageTheme, activeModelId?: string): string | undefined {
521
+ export function renderUsageBar(usage: ProviderUsage, theme: UsageTheme, activeModelId?: string, paint?: GaugePaint): string | undefined {
522
522
  const main = selectUsageLimit(usage, activeModelId);
523
523
  const [first, ...rest] = main?.windows ?? [];
524
524
  if (!first) return undefined;
525
525
  // An unlabeled window prints as the name, the meter and the percentage.
526
- const head = [theme.fg(ROLE.LABEL, main.name), ...(first.label.length === 0 ? [] : [theme.fg(ROLE.LABEL, first.label)]), paintMeter(first.usedPercent, 8, theme), theme.fg(ROLE.PERCENT, `${Math.round(first.usedPercent)}%`)].join(" ");
526
+ const head = [theme.fg(ROLE.LABEL, main.name), ...(first.label.length === 0 ? [] : [theme.fg(ROLE.LABEL, first.label)]), paintMeter(first.usedPercent, 8, theme, paint), theme.fg(ROLE.PERCENT, `${Math.round(first.usedPercent)}%`)].join(" ");
527
527
  const tail = rest.map((window) => `${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.LABEL, window.label)} ${theme.fg(ROLE.PERCENT, `${Math.round(window.usedPercent)}%`)}`);
528
528
  return [head, ...tail].join(" ");
529
529
  }
@@ -574,7 +574,7 @@ export interface UsagePanelScope {
574
574
  // snapshot keeps that snapshot and says so beside it, and a provider recorded
575
575
  // under a previous profile's routing is never presented as current scope after
576
576
  // a profile switch.
577
- export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, width: number, now: number, active?: ActiveProvider, registry?: UsageSourceRegistry, scope?: UsagePanelScope): string[] {
577
+ export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, width: number, now: number, active?: ActiveProvider, registry?: UsageSourceRegistry, scope?: UsagePanelScope, paint?: GaugePaint): string[] {
578
578
  const activeUsage = active ? usages.find((usage) => usage.provider === active.provider) : undefined;
579
579
  const byProvider = new Map(usages.map((usage) => [usage.provider, usage] as const));
580
580
  const rows = scope
@@ -607,7 +607,7 @@ export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, wid
607
607
  const percent = `${Math.round(row.window.usedPercent)}%`.padStart(4);
608
608
  const reset = formatReset(row.window.resetAt, now);
609
609
  const tail = reset.length > 0 ? ` ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, reset)}` : "";
610
- lines.push(` ${theme.fg(ROLE.LABEL, row.name.padEnd(nameWidth))} ${paintMeter(row.window.usedPercent, PANEL_METER_CELLS, theme)} ${theme.fg(ROLE.PERCENT, percent)}${tail}`);
610
+ lines.push(` ${theme.fg(ROLE.LABEL, row.name.padEnd(nameWidth))} ${paintMeter(row.window.usedPercent, PANEL_METER_CELLS, theme, paint)} ${theme.fg(ROLE.PERCENT, percent)}${tail}`);
611
611
  }
612
612
  // A failure after a good snapshot never hides either fact: the last good
613
613
  // snapshot stays on its rows and the failed refresh says so beside it.
@@ -0,0 +1,30 @@
1
+ import { namesExistingFeatureDocumentTask } from "./bounded-writer-admission.ts";
2
+ import { contextBackstopReached, type MeasuredContext } from "./context-backstop.ts";
3
+
4
+ // odd/tasks/verifier-admission.md: the Verification rule reserves the independent verifier for a change that names a
5
+ // Task Size high-risk item, yet a bench run still launched it on a small, not high-risk task. In the primary session a
6
+ // verifier launch is admitted only when its task or context declares a named high-risk item, carries a task of an
7
+ // existing feature document (the same lines writer admission accepts), or the parent's measured context reaches the
8
+ // context backstop. The agent catalog has no generic verifier alias, so only `gentle-ai-verify` is gated.
9
+ const VERIFIER_NAMES = ["gentle-ai-verify"];
10
+ // A declaration that starts a line (optionally as a list item) or a sentence (after `.`, `!` or `?` and a space) with
11
+ // `Risk:` whose first entry is `item N` with N in 1-6, for example `Risk: item 3 (shared parser)`, `- Risk: item 2,
12
+ // item 3 (reason)` or `... on top of ebf3f35. Risk: item 1 (migration)`. `Risk: none` and a mention inside a
13
+ // sentence ("Do not write Risk: item 3") do not start one.
14
+ const HIGH_RISK_ITEM_LINE = /(?:^[ \t]*(?:[-*+][ \t]+)?|[.!?][ \t]+)Risk:[ \t]*item[ \t]+[1-6](?!\d)/m;
15
+ export const VERIFIER_RISK_REJECTION =
16
+ "An independent verifier runs only for a change that declares a named high-risk item (`Risk: item N` from the Task Size list in the task). This change declares none: run its focused test and suite inline yourself now; do not retry the launch, and never add a Risk line or create a feature document only to get a verifier admitted.";
17
+
18
+ export function rejectUnriskedVerifierLaunch(
19
+ input: unknown,
20
+ options: { root: string; contextUsage: MeasuredContext | undefined; env?: Readonly<Record<string, string | undefined>> },
21
+ ): { block: true; reason: string } | undefined {
22
+ if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
23
+ const record = input as Record<string, unknown>;
24
+ if (typeof record.agent !== "string" || !VERIFIER_NAMES.includes(record.agent)) return undefined;
25
+ const values = [record.task, record.context];
26
+ if (values.some(value => typeof value === "string" && HIGH_RISK_ITEM_LINE.test(value))) return undefined;
27
+ if (namesExistingFeatureDocumentTask(values, options.root)) return undefined;
28
+ if (contextBackstopReached(options.contextUsage, options.env)) return undefined;
29
+ return { block: true, reason: VERIFIER_RISK_REJECTION };
30
+ }
@@ -32,6 +32,8 @@ interface PrivateEditor {
32
32
  onChange?: (text: string) => void;
33
33
  pastes: Map<number, string>;
34
34
  pasteCounter: number;
35
+ /** Host-owned atomic spans (gentle-shell#2464 image chips) that NORMAL motions must not split. */
36
+ atomicSpans?(line: string): Array<{ start: number; end: number }>;
35
37
  undoStack: Stack;
36
38
  history: string[];
37
39
  historyIndex: number;
@@ -151,12 +153,13 @@ function highlightRenderedRow(row: string, chunk: LayoutLine, left: number, righ
151
153
  return result;
152
154
  }
153
155
 
154
- function boundaries(line: string, pastes: Map<number, string>): number[] {
156
+ function boundaries(line: string, editor: Pick<PrivateEditor, "pastes" | "atomicSpans">): number[] {
155
157
  const units = [...segmenter.segment(line)].map((part) => part.index);
156
- for (const match of line.matchAll(marker)) {
157
- if (!pastes.has(Number(match[1]))) continue;
158
- const start = match.index;
159
- const end = start + match[0].length;
158
+ const spans = [...line.matchAll(marker)]
159
+ .filter((match) => editor.pastes.has(Number(match[1])))
160
+ .map((match) => ({ start: match.index, end: match.index + match[0].length }));
161
+ if (typeof editor.atomicSpans === "function") spans.push(...editor.atomicSpans(line));
162
+ for (const { start, end } of spans) {
160
163
  for (let i = units.length - 1; i >= 0; i--) {
161
164
  if (units[i]! > start && units[i]! < end) units.splice(i, 1);
162
165
  }
@@ -177,7 +180,7 @@ function assertUniquePastes(editor: PrivateEditor): void {
177
180
  function assertPosition(editor: PrivateEditor, pos: Position): void {
178
181
  assertUniquePastes(editor);
179
182
  const line = editor.state.lines[pos.line];
180
- if (line === undefined || !Number.isInteger(pos.col) || !boundaries(line, editor.pastes).includes(pos.col)) {
183
+ if (line === undefined || !Number.isInteger(pos.col) || !boundaries(line, editor).includes(pos.col)) {
181
184
  throw new Error("Cursor must be on a valid grapheme or paste boundary");
182
185
  }
183
186
  }
@@ -265,7 +268,7 @@ export function createVimEditorAdapter(value: unknown, version: string, editorCl
265
268
  },
266
269
  motionBoundaries(): number[][] {
267
270
  assertUniquePastes(editor);
268
- return editor.state.lines.map((line) => boundaries(line, editor.pastes));
271
+ return editor.state.lines.map((line) => boundaries(line, editor));
269
272
  },
270
273
  move(pos: Position): void {
271
274
  assertPosition(editor, pos);
@@ -277,7 +280,7 @@ export function createVimEditorAdapter(value: unknown, version: string, editorCl
277
280
  moveByGraphemes(delta: number): void {
278
281
  if (!Number.isInteger(delta)) throw new Error("Invalid motion");
279
282
  const positions = editor.state.lines.flatMap((line, index) =>
280
- boundaries(line, editor.pastes).map((col) => ({ line: index, col })));
283
+ boundaries(line, editor).map((col) => ({ line: index, col })));
281
284
  const current = editor.getCursor();
282
285
  const at = positions.findIndex((p) => p.line === current.line && p.col === current.col);
283
286
  if (at < 0) throw new Error("Invalid cursor boundary");
@@ -0,0 +1,188 @@
1
+ // Pinned artifacts for the optional local voice dictation (S1-S3). Nothing here is
2
+ // fetched or resolved at runtime: every version, size and digest below was read
3
+ // from the public npm registry and Hugging Face metadata for the exact versions,
4
+ // and the tarball layouts were inspected for the facts the comments state.
5
+ // Installation lives in voice-install.ts; this module is data only.
6
+
7
+ export type VoiceTarget = "darwin-x64" | "darwin-arm64" | "linux-x64" | "linux-arm64" | "win32-x64";
8
+
9
+ export const VOICE_TARGETS: readonly VoiceTarget[] = ["darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64", "win32-x64"];
10
+
11
+ // A redirect hop is followed only when its URL is HTTPS on this origin and its
12
+ // path starts with the prefix. Anything else ends the download.
13
+ export interface VoiceRedirectRule {
14
+ origin: string;
15
+ pathPrefix: string;
16
+ }
17
+
18
+ export interface VoiceNpmPackage {
19
+ name: string;
20
+ version: string;
21
+ license: string;
22
+ tarball: string;
23
+ // The packed tarball size in bytes and its registry `dist.integrity`.
24
+ size: number;
25
+ integrity: string;
26
+ // Registry `dist.unpackedSize` and `dist.fileCount`: hard limits for extraction.
27
+ unpackedSize: number;
28
+ fileCount: number;
29
+ // Paths relative to the package root that must be present after extraction.
30
+ requiredFiles: string[];
31
+ }
32
+
33
+ export interface VoiceRuntimeManifest {
34
+ // The installed directory name; also part of the install marker.
35
+ id: string;
36
+ target: VoiceTarget;
37
+ packages: VoiceNpmPackage[];
38
+ redirects: VoiceRedirectRule[];
39
+ }
40
+
41
+ export interface VoiceModelFile {
42
+ name: string;
43
+ size: number;
44
+ sha256: string;
45
+ }
46
+
47
+ export interface VoiceModelManifest {
48
+ id: string;
49
+ revision: string;
50
+ // Files are fetched from `${baseUrl}/${name}`.
51
+ baseUrl: string;
52
+ files: VoiceModelFile[];
53
+ redirects: VoiceRedirectRule[];
54
+ }
55
+
56
+ const SHERPA_VERSION = "1.13.8";
57
+
58
+ const npmTarball = (name: string, version: string) => `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`;
59
+
60
+ // sherpa-onnx-node holds only JavaScript. Its addon-static-import.js loads
61
+ // `../sherpa-onnx-<platform>-<arch>/sherpa-onnx.node` (Windows spelled `win`), so the
62
+ // platform package must be installed as a sibling directory under node_modules.
63
+ export const SHERPA_ONNX_NODE: VoiceNpmPackage = {
64
+ name: "sherpa-onnx-node",
65
+ version: SHERPA_VERSION,
66
+ license: "Apache-2.0",
67
+ tarball: npmTarball("sherpa-onnx-node", SHERPA_VERSION),
68
+ size: 11954,
69
+ integrity: "sha512-MsDMBdhLFTZ1GwvcGSSQhnS7g/EA8OMH6IYysCVUOM7j8Icty9KRc0E6YT1A5fWBsZwRfKOeh88QC95aRvS8ag==",
70
+ unpackedSize: 61221,
71
+ fileCount: 19,
72
+ requiredFiles: ["package.json", "sherpa-onnx.js", "addon.js", "addon-static-import.js", "non-streaming-asr.js"],
73
+ };
74
+
75
+ function sherpaPlatform(target: VoiceTarget, size: number, integrity: string, unpackedSize: number, fileCount: number): VoiceNpmPackage {
76
+ const name = `sherpa-onnx-${target.replace(/^win32-/, "win-")}`;
77
+ return {
78
+ name,
79
+ version: SHERPA_VERSION,
80
+ license: "Apache-2.0",
81
+ tarball: npmTarball(name, SHERPA_VERSION),
82
+ size,
83
+ integrity,
84
+ unpackedSize,
85
+ fileCount,
86
+ requiredFiles: ["package.json", "sherpa-onnx.node"],
87
+ };
88
+ }
89
+
90
+ // Each platform package bundles sherpa-onnx.node, the sherpa-onnx C/C++ API
91
+ // libraries and onnxruntime, all built for the named architecture.
92
+ export const SHERPA_ONNX_PLATFORM_PACKAGES: Readonly<Record<VoiceTarget, VoiceNpmPackage>> = {
93
+ "darwin-x64": sherpaPlatform("darwin-x64", 11191481, "sha512-7BLRpjM6w4f9W46/nmkmq8lEKUayhebvcpslCVQ+6QN2uReYlZEMDZlSpXMjme+hUFrPfRz8P3UNq8ep/4d19g==", 37678686, 7),
94
+ "darwin-arm64": sherpaPlatform("darwin-arm64", 10047754, "sha512-FPNgJMgnWVl/KhRTIhG3KL3A4Om63Rn4YKXc9/uHY7SzLcvqLJLc/h7UBWJwduXvv7K18t5NpxHR6XgXn4sjWw==", 34089738, 7),
95
+ "linux-x64": sherpaPlatform("linux-x64", 11089653, "sha512-6plnhjagsSeTntCgnlag86hWbs/uZE9Crms1LgOb68/1nKsIQjMd+WG519m+aPwT6TrsBOiEMzrx41t8sL5L5g==", 33381117, 7),
96
+ "linux-arm64": sherpaPlatform("linux-arm64", 13910679, "sha512-Tlg7a70b/Wge3OF8IgTHF9jhSVCsLyKQKhwc4BsJ5A+dL/SrFtGBjzuHp4XeLhiiOT7afCxX5PdSn/D4c8Lnuw==", 40306856, 7),
97
+ "win32-x64": sherpaPlatform("win32-x64", 8894875, "sha512-oZF1c9VPOKtMwn83Bboc5XSWL+76BRoyB3eUuVnCknBKxwSULZU2Foia9VHWzU+n4I12rPsP6z6H9Rp1hD9o8g==", 23453246, 8),
98
+ };
99
+
100
+ // node-cpal has no dependencies or install scripts and needs Node >= 22. Its
101
+ // tarball ships every prebuilt binary; the default backend loads
102
+ // bin/<platform>-<arch>/index.node through facade.js. The five binary headers were
103
+ // inspected (not loaded) and each is built for its own architecture. Published
104
+ // from saeta-eth/node-cpal tag v1.0.0, commit 20e2ac8d1922de7c4af50431e2ddc21a421cf764.
105
+ // 0.1.1 is not used: its darwin-x64 and linux-arm64 binaries were built for the
106
+ // other architecture.
107
+ const NODE_CPAL_VERSION = "1.0.0";
108
+
109
+ export const NODE_CPAL: VoiceNpmPackage = {
110
+ name: "node-cpal",
111
+ version: NODE_CPAL_VERSION,
112
+ license: "ISC",
113
+ tarball: npmTarball("node-cpal", NODE_CPAL_VERSION),
114
+ size: 7723259,
115
+ integrity: "sha512-d71jIzKDjw1HepqaJQMPd+/k3ZLqYZWhLAv6a8aafIGnYoA1QWlShA5iB5CgijIJtQxq9PdQAk99JQKspqlnTA==",
116
+ unpackedSize: 20363834,
117
+ fileCount: 40,
118
+ requiredFiles: ["package.json", "index.js", "facade.js", "cpal-values.js"],
119
+ };
120
+
121
+ // The oldest glibc the pinned Linux native binaries accept.
122
+ export const VOICE_LINUX_MIN_GLIBC = "2.34";
123
+
124
+ export function voiceRuntimeManifest(target: VoiceTarget): VoiceRuntimeManifest {
125
+ const packages = [
126
+ SHERPA_ONNX_NODE,
127
+ SHERPA_ONNX_PLATFORM_PACKAGES[target],
128
+ { ...NODE_CPAL, requiredFiles: [...NODE_CPAL.requiredFiles, `bin/${target}/index.node`] },
129
+ ];
130
+ // The id names both native versions, so a runtime installed with other pins
131
+ // lives in another directory and is never reported as this one.
132
+ // npm serves these tarballs directly; no redirect is expected or followed.
133
+ return { id: `sherpa-onnx-${SHERPA_VERSION}-node-cpal-${NODE_CPAL_VERSION}-${target}`, target, packages, redirects: [] };
134
+ }
135
+
136
+ const PARAKEET_REPO = "csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8";
137
+ const PARAKEET_REVISION = "2bda32ec70b097a55adaa07d9a7173915b43cc78";
138
+
139
+ // The int8 sherpa-onnx export of NVIDIA Parakeet TDT 0.6B v3 at a fixed commit.
140
+ // Observed redirects: tokens.txt answers 307 with a same-origin resolve-cache path,
141
+ // the LFS/Xet files answer 302 to us.aws.cdn.hf.co/xet-bridge-us/. Other CDN
142
+ // regions were not observed, so they are refused rather than guessed.
143
+ export const PARAKEET_MODEL_MANIFEST: VoiceModelManifest = {
144
+ id: `parakeet-tdt-0.6b-v3-int8-${PARAKEET_REVISION.slice(0, 12)}`,
145
+ revision: PARAKEET_REVISION,
146
+ baseUrl: `https://huggingface.co/${PARAKEET_REPO}/resolve/${PARAKEET_REVISION}`,
147
+ files: [
148
+ { name: "encoder.int8.onnx", size: 652184281, sha256: "acfc2b4456377e15d04f0243af540b7fe7c992f8d898d751cf134c3a55fd2247" },
149
+ { name: "decoder.int8.onnx", size: 11845275, sha256: "179e50c43d1a9de79c8a24149a2f9bac6eb5981823f2a2ed88d655b24248db4e" },
150
+ { name: "joiner.int8.onnx", size: 6355277, sha256: "3164c13fc2821009440d20fcb5fdc78bff28b4db2f8d0f0b329101719c0948b3" },
151
+ { name: "tokens.txt", size: 93939, sha256: "d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d" },
152
+ ],
153
+ redirects: [
154
+ { origin: "https://huggingface.co", pathPrefix: `/api/resolve-cache/models/${PARAKEET_REPO}/${PARAKEET_REVISION}/` },
155
+ { origin: "https://us.aws.cdn.hf.co", pathPrefix: "/xet-bridge-us/" },
156
+ ],
157
+ };
158
+
159
+ export interface VoiceNotice {
160
+ component: string;
161
+ license: string;
162
+ source: string;
163
+ attribution?: string;
164
+ // What is known to be missing from the distributed artifact, stated plainly.
165
+ unresolved?: string;
166
+ }
167
+
168
+ export const VOICE_NOTICES: readonly VoiceNotice[] = [
169
+ {
170
+ component: "parakeet-tdt-0.6b-v3",
171
+ license: "CC-BY-4.0",
172
+ source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
173
+ attribution: "Parakeet TDT 0.6B v3 by NVIDIA, licensed under CC-BY-4.0; int8 sherpa-onnx export by csukuangfj.",
174
+ unresolved: `The ${PARAKEET_REPO} export has no license file or model card license of its own; the license is taken from the upstream NVIDIA model.`,
175
+ },
176
+ {
177
+ component: "sherpa-onnx-node",
178
+ license: "Apache-2.0",
179
+ source: "https://github.com/k2-fsa/sherpa-onnx",
180
+ unresolved: "The sherpa-onnx npm tarballs ship no LICENSE or NOTICE file, and the platform packages bundle onnxruntime (MIT) without its license text.",
181
+ },
182
+ {
183
+ component: "node-cpal",
184
+ license: "ISC",
185
+ source: "https://github.com/saeta-eth/node-cpal",
186
+ unresolved: "The node-cpal@1.0.0 tarball ships no LICENSE or NOTICE file, and its prebuilt binaries link Rust crates whose notices are not included.",
187
+ },
188
+ ];