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
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ // Voice dictation worker (S1-S3). lib/voice-session.ts runs this file as its own
3
+ // Node process, so capture callbacks, the blocking recognizer load and the decode
4
+ // never run inside Gentle AI. It speaks JSON lines: stdin receives one `start`
5
+ // request, then `stop` or `cancel`; stdout carries `recording`, `transcribing`,
6
+ // and one final `result` or `error`. Errors carry a closed code only, never a
7
+ // message, path or native error text.
8
+ //
9
+ // Capture starts as soon as the runtime files pass validation and node-cpal is
10
+ // loaded, so the first words are recorded. The model digests are then verified
11
+ // while recording, by streamed hashing that never blocks capture for long.
12
+ // sherpa-onnx and the model are loaded only after every digest matched and stop
13
+ // arrived; their synchronous load would stall capture callbacks. A mismatch
14
+ // found while recording ends the session with model-invalid at once.
15
+ //
16
+ // The optional native packages are loaded only after the files they come from
17
+ // pass validation, and only from the roots in the start request, which the
18
+ // parent computes from its pinned manifests. The validation refuses links, special
19
+ // files, foreign owners and digest mismatches; it does not make the install
20
+ // tamper-proof against the same user, who can replace the files at any time.
21
+
22
+ const { createHash } = require("node:crypto");
23
+ const { createReadStream } = require("node:fs");
24
+ const { lstat, readdir } = require("node:fs/promises");
25
+ const path = require("node:path");
26
+ const { addAbortSignal } = require("node:stream");
27
+
28
+ const SCHEMA = "gentle.voice/v1";
29
+ const TARGETS = ["darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64", "win32-x64"];
30
+ const MODEL_ROLES = ["encoder", "decoder", "joiner", "tokens"];
31
+ // The only runtime files ever loaded; both must be listed as required files.
32
+ const SHERPA_ENTRY = "node_modules/sherpa-onnx-node/non-streaming-asr.js";
33
+ const CPAL_ENTRY = "node_modules/node-cpal/index.js";
34
+ const MAX_INPUT_LINE = 64 * 1024;
35
+ const MAX_TREE_ENTRIES = 256;
36
+ const MAX_TEXT = 16 * 1024;
37
+ const TARGET_RATE = 16000;
38
+ // Less than a tenth of a second of audio is treated as no audio at all.
39
+ const MIN_SECONDS = 0.1;
40
+ const SAFE_SEGMENT = /^[A-Za-z0-9@_][A-Za-z0-9._@+-]*$/;
41
+ // Integer formats are scaled to [-1, 1]; unsigned formats are centred first.
42
+ const SAMPLE_FORMATS = {
43
+ f32: { offset: 0, scale: 1 },
44
+ f64: { offset: 0, scale: 1 },
45
+ i8: { offset: 0, scale: 128 },
46
+ i16: { offset: 0, scale: 32768 },
47
+ i24: { offset: 0, scale: 8388608 },
48
+ i32: { offset: 0, scale: 2147483648 },
49
+ u8: { offset: 128, scale: 128 },
50
+ u16: { offset: 32768, scale: 32768 },
51
+ u32: { offset: 2147483648, scale: 2147483648 },
52
+ };
53
+
54
+ class WorkerFailure extends Error {
55
+ constructor(code) {
56
+ super(code);
57
+ this.code = code;
58
+ }
59
+ }
60
+
61
+ const fail = (code) => {
62
+ throw new WorkerFailure(code);
63
+ };
64
+
65
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66
+ const hasExactKeys = (value, keys) => isRecord(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
67
+ const isSafeRelative = (value) => typeof value === "string" && value.split("/").every((segment) => segment !== "." && segment !== ".." && SAFE_SEGMENT.test(segment));
68
+ const isInteger = (value, min, max) => Number.isSafeInteger(value) && value >= min && value <= max;
69
+
70
+ function parseStart(message) {
71
+ if (!hasExactKeys(message, ["schema", "type", "target", "maxRecordingMs", "numThreads", "runtime", "model"])) fail("invalid-request");
72
+ if (message.schema !== SCHEMA || message.type !== "start" || !TARGETS.includes(message.target)) fail("invalid-request");
73
+ if (!isInteger(message.maxRecordingMs, 1000, 600000) || !isInteger(message.numThreads, 1, 8)) fail("invalid-request");
74
+ const { runtime, model } = message;
75
+ if (!hasExactKeys(runtime, ["root", "files"]) || !hasExactKeys(model, ["root", "files"])) fail("invalid-request");
76
+ if (typeof runtime.root !== "string" || !path.isAbsolute(runtime.root) || typeof model.root !== "string" || !path.isAbsolute(model.root)) fail("invalid-request");
77
+ const runtimeFiles = runtime.files;
78
+ if (!Array.isArray(runtimeFiles) || runtimeFiles.length > MAX_TREE_ENTRIES || !runtimeFiles.every(isSafeRelative)) fail("invalid-request");
79
+ if (!runtimeFiles.includes(SHERPA_ENTRY) || !runtimeFiles.includes(CPAL_ENTRY)) fail("invalid-request");
80
+ if (!hasExactKeys(model.files, MODEL_ROLES)) fail("invalid-request");
81
+ for (const role of MODEL_ROLES) {
82
+ const file = model.files[role];
83
+ const valid = hasExactKeys(file, ["name", "size", "sha256"]) && isSafeRelative(file.name) && !file.name.includes("/") && isInteger(file.size, 1, Number.MAX_SAFE_INTEGER) && /^[0-9a-f]{64}$/.test(file.sha256);
84
+ if (!valid) fail("invalid-request");
85
+ }
86
+ return message;
87
+ }
88
+
89
+ // Walks an installed component and returns its regular files by relative path.
90
+ // Anything that is not a plain directory or regular file, is owned by another
91
+ // user, or is writable by everyone makes the whole component invalid. Ownership
92
+ // and modes are not checked on Windows, where Node reports neither.
93
+ async function readTree(root, uid, code) {
94
+ const files = new Map();
95
+ let entries = 0;
96
+ const check = (info) => {
97
+ if (uid !== undefined && (info.uid !== uid || (info.mode & 0o002) !== 0)) fail(code);
98
+ };
99
+ const rootInfo = await lstat(root).catch(() => fail(code));
100
+ if (!rootInfo.isDirectory()) fail(code);
101
+ check(rootInfo);
102
+ const walk = async (directory, prefix) => {
103
+ for (const name of await readdir(directory)) {
104
+ if (++entries > MAX_TREE_ENTRIES) fail(code);
105
+ const relative = prefix ? `${prefix}/${name}` : name;
106
+ const info = await lstat(path.join(directory, name));
107
+ check(info);
108
+ if (info.isDirectory()) await walk(path.join(directory, name), relative);
109
+ else if (info.isFile()) files.set(relative, info);
110
+ else fail(code);
111
+ }
112
+ };
113
+ await walk(root, "").catch((error) => {
114
+ throw error instanceof WorkerFailure ? error : new WorkerFailure(code);
115
+ });
116
+ return files;
117
+ }
118
+
119
+ // Hashes a file chunk by chunk, so capture callbacks keep running in between.
120
+ function sha256(openRead, file, signal) {
121
+ return new Promise((resolve, reject) => {
122
+ const hash = createHash("sha256");
123
+ addAbortSignal(signal, openRead(file))
124
+ .on("data", (chunk) => hash.update(chunk))
125
+ .on("error", reject)
126
+ .on("end", () => resolve(hash.digest("hex")));
127
+ });
128
+ }
129
+
130
+ async function validateRuntime(runtime, uid) {
131
+ const files = await readTree(runtime.root, uid, "runtime-invalid");
132
+ for (const required of runtime.files) if (!files.has(required)) fail("runtime-invalid");
133
+ }
134
+
135
+ async function validateModel(model, uid, openRead, signal) {
136
+ const files = await readTree(model.root, uid, "model-invalid");
137
+ for (const role of MODEL_ROLES) {
138
+ const pin = model.files[role];
139
+ if (files.get(pin.name)?.size !== pin.size) fail("model-invalid");
140
+ }
141
+ // The digests are checked on every session: the marker only records sizes.
142
+ for (const role of MODEL_ROLES) {
143
+ if (signal.aborted) fail("model-invalid");
144
+ const pin = model.files[role];
145
+ const digest = await sha256(openRead, path.join(model.root, pin.name), signal).catch(() => fail("model-invalid"));
146
+ if (digest !== pin.sha256) fail("model-invalid");
147
+ }
148
+ }
149
+
150
+ // Prefers 32-bit float input; otherwise keeps the device default when it is a
151
+ // format this worker can scale. Rates and channel counts stay at the default.
152
+ function chooseInputConfig(convenience, deviceId) {
153
+ const defaults = convenience.getDefaultInputConfig(deviceId);
154
+ const formats = convenience.getSupportedFormats(deviceId);
155
+ if (!isRecord(defaults) || !isInteger(defaults.sampleRate, 8000, 384000) || !isInteger(defaults.channels, 1, 64)) fail("unsupported-input");
156
+ const sampleFormat = Array.isArray(formats) && formats.includes("f32") ? "f32" : defaults.sampleFormat;
157
+ if (!Object.hasOwn(SAMPLE_FORMATS, sampleFormat)) fail("unsupported-input");
158
+ return { sampleRate: defaults.sampleRate, channels: defaults.channels, sampleFormat };
159
+ }
160
+
161
+ // Collects interleaved device frames as mono samples at the device rate, up to
162
+ // a fixed number of samples. Returns false once the limit is reached.
163
+ class MonoRecorder {
164
+ constructor(config, maxSamples) {
165
+ this.config = config;
166
+ this.format = SAMPLE_FORMATS[config.sampleFormat];
167
+ this.maxSamples = maxSamples;
168
+ this.chunks = [];
169
+ this.length = 0;
170
+ }
171
+
172
+ append(data) {
173
+ const { channels } = this.config;
174
+ const { offset, scale } = this.format;
175
+ const frames = Math.min(Math.floor(data.length / channels), this.maxSamples - this.length);
176
+ const mono = new Float32Array(frames);
177
+ for (let frame = 0; frame < frames; frame++) {
178
+ let sum = 0;
179
+ for (let channel = 0; channel < channels; channel++) sum += (Number(data[frame * channels + channel]) - offset) / scale;
180
+ const value = sum / channels;
181
+ mono[frame] = Number.isFinite(value) ? Math.max(-1, Math.min(1, value)) : 0;
182
+ }
183
+ this.chunks.push(mono);
184
+ this.length += frames;
185
+ return this.length < this.maxSamples;
186
+ }
187
+
188
+ samples() {
189
+ const all = new Float32Array(this.length);
190
+ let at = 0;
191
+ for (const chunk of this.chunks) {
192
+ all.set(chunk, at);
193
+ at += chunk.length;
194
+ }
195
+ this.chunks = [];
196
+ return all;
197
+ }
198
+ }
199
+
200
+ // Converts to 16 kHz. Downsampling averages each output sample's input window,
201
+ // a box filter that keeps most aliasing out of the speech band; upsampling
202
+ // interpolates linearly.
203
+ function resample(input, fromRate) {
204
+ if (fromRate === TARGET_RATE) return input;
205
+ const step = fromRate / TARGET_RATE;
206
+ const output = new Float32Array(Math.floor(input.length / step));
207
+ for (let i = 0; i < output.length; i++) {
208
+ const start = i * step;
209
+ if (step > 1) {
210
+ const from = Math.floor(start);
211
+ const to = Math.min(input.length, Math.max(from + 1, Math.floor(start + step)));
212
+ let sum = 0;
213
+ for (let j = from; j < to; j++) sum += input[j];
214
+ output[i] = sum / (to - from);
215
+ } else {
216
+ const j = Math.floor(start);
217
+ const next = input[Math.min(j + 1, input.length - 1)];
218
+ output[i] = input[j] + (next - input[j]) * (start - j);
219
+ }
220
+ }
221
+ return output;
222
+ }
223
+
224
+ class WorkerSession {
225
+ constructor(io, exit) {
226
+ this.io = io;
227
+ this.exit = exit;
228
+ this.state = "waiting";
229
+ this.stream = undefined;
230
+ this.recorder = undefined;
231
+ this.request = undefined;
232
+ // Settles once every model digest matched; rejects with model-invalid.
233
+ this.modelVerified = undefined;
234
+ this.verification = new AbortController();
235
+ this.truncated = false;
236
+ }
237
+
238
+ send(message) {
239
+ this.io.write(`${JSON.stringify({ schema: SCHEMA, ...message })}\n`);
240
+ }
241
+
242
+ // Ends the session once: closes capture, reports at most one final message.
243
+ async end(final) {
244
+ if (this.state === "ended") return;
245
+ this.state = "ended";
246
+ this.verification.abort();
247
+ await this.closeStream();
248
+ if (final) this.send(final);
249
+ this.exit(final?.type === "error" ? 1 : 0);
250
+ }
251
+
252
+ async closeStream() {
253
+ const stream = this.stream;
254
+ this.stream = undefined;
255
+ if (stream) await Promise.resolve(stream.close()).catch(() => {});
256
+ }
257
+
258
+ async handle(message) {
259
+ if (this.state === "waiting") return this.begin(parseStart(message));
260
+ if (!hasExactKeys(message, ["schema", "type"]) || message.schema !== SCHEMA) fail("invalid-request");
261
+ if (message.type === "cancel") return this.end(undefined);
262
+ if (message.type === "stop") {
263
+ if (this.state === "recording") return this.transcribe();
264
+ if (this.state === "loading") fail("invalid-request");
265
+ return undefined;
266
+ }
267
+ fail("invalid-request");
268
+ }
269
+
270
+ async begin(request) {
271
+ this.state = "loading";
272
+ const { io } = this;
273
+ if (request.target !== `${io.platform}-${io.arch}`) fail("runtime-invalid");
274
+ // A cancel can arrive during any await; each step re-checks before going on.
275
+ await validateRuntime(request.runtime, io.uid);
276
+ if (this.state !== "loading") return;
277
+ this.request = request;
278
+ let cpal;
279
+ try {
280
+ cpal = io.loadModule(this.fromRuntime(CPAL_ENTRY));
281
+ } catch {
282
+ fail("runtime-load");
283
+ }
284
+ await this.openCapture(cpal?.convenience, request.maxRecordingMs);
285
+ }
286
+
287
+ // Starts in the same step that enters recording, so no stop can see a
288
+ // session without a pending verification.
289
+ verifyModel() {
290
+ this.modelVerified = validateModel(this.request.model, this.io.uid, this.io.openRead ?? createReadStream, this.verification.signal);
291
+ // While recording, a failed verification ends the session at once: the
292
+ // audio could never be transcribed. Later, transcribe() reports it.
293
+ this.modelVerified.catch((error) => {
294
+ if (this.state === "recording") void this.fail(error);
295
+ });
296
+ }
297
+
298
+ fromRuntime(entry) {
299
+ return path.join(this.request.runtime.root, ...entry.split("/"));
300
+ }
301
+
302
+ // Loads sherpa-onnx and the recognizer; only called once the model verified.
303
+ loadRecognizer() {
304
+ const { request } = this;
305
+ const fromModel = (role) => path.join(request.model.root, request.model.files[role].name);
306
+ let asr;
307
+ try {
308
+ asr = this.io.loadModule(this.fromRuntime(SHERPA_ENTRY));
309
+ } catch {
310
+ fail("runtime-load");
311
+ }
312
+ try {
313
+ return new asr.OfflineRecognizer({
314
+ featConfig: { sampleRate: TARGET_RATE, featureDim: 80 },
315
+ modelConfig: {
316
+ transducer: { encoder: fromModel("encoder"), decoder: fromModel("decoder"), joiner: fromModel("joiner") },
317
+ tokens: fromModel("tokens"),
318
+ numThreads: request.numThreads,
319
+ provider: "cpu",
320
+ modelType: "nemo_transducer",
321
+ },
322
+ });
323
+ } catch {
324
+ fail("model-load");
325
+ }
326
+ }
327
+
328
+ async openCapture(convenience, maxRecordingMs) {
329
+ if (!isRecord(convenience)) fail("runtime-load");
330
+ let device;
331
+ try {
332
+ device = convenience.getDefaultInputDevice();
333
+ } catch {
334
+ fail("no-input-device");
335
+ }
336
+ if (!isRecord(device) || typeof device.deviceId !== "string" || device.deviceId.length === 0) fail("no-input-device");
337
+ let config;
338
+ try {
339
+ config = chooseInputConfig(convenience, device.deviceId);
340
+ } catch (error) {
341
+ throw error instanceof WorkerFailure ? error : new WorkerFailure("unsupported-input");
342
+ }
343
+ this.recorder = new MonoRecorder(config, Math.ceil((config.sampleRate * maxRecordingMs) / 1000));
344
+ try {
345
+ // The native layer holds the stream weakly; this session keeps it alive.
346
+ this.stream = await convenience.createInputStream({
347
+ deviceId: device.deviceId,
348
+ config,
349
+ onData: (data) => this.onData(data),
350
+ onError: () => void this.fail(new WorkerFailure("capture")),
351
+ autoStart: true,
352
+ queueCapacityBuffers: 64,
353
+ });
354
+ } catch {
355
+ fail("capture");
356
+ }
357
+ if (this.state !== "loading") return this.closeStream();
358
+ this.state = "recording";
359
+ this.send({ type: "recording" });
360
+ this.verifyModel();
361
+ }
362
+
363
+ onData(data) {
364
+ if (this.state !== "recording" || !ArrayBuffer.isView(data)) return;
365
+ if (!this.recorder.append(data)) {
366
+ this.truncated = true;
367
+ void this.transcribe().catch((error) => this.fail(error));
368
+ }
369
+ }
370
+
371
+ async transcribe() {
372
+ this.state = "transcribing";
373
+ this.send({ type: "transcribing" });
374
+ await this.closeStream();
375
+ if (this.state !== "transcribing") return;
376
+ // Nothing from sherpa-onnx or the model is loaded before every digest matched.
377
+ if (!this.modelVerified) fail("model-invalid");
378
+ await this.modelVerified;
379
+ if (this.state !== "transcribing") return;
380
+ const { sampleRate } = this.recorder.config;
381
+ const captured = this.recorder.samples();
382
+ if (captured.length < sampleRate * MIN_SECONDS) fail("empty-audio");
383
+ const samples = resample(captured, sampleRate);
384
+ const recognizer = this.loadRecognizer();
385
+ let text;
386
+ try {
387
+ const stream = recognizer.createStream();
388
+ stream.acceptWaveform({ sampleRate: TARGET_RATE, samples });
389
+ recognizer.decode(stream);
390
+ text = recognizer.getResult(stream)?.text;
391
+ } catch {
392
+ fail("decode");
393
+ }
394
+ if (typeof text !== "string") fail("decode");
395
+ await this.end({ type: "result", text: text.trim().slice(0, MAX_TEXT), truncated: this.truncated });
396
+ }
397
+
398
+ fail(error) {
399
+ const code = error instanceof WorkerFailure ? error.code : "internal";
400
+ return this.end({ type: "error", code });
401
+ }
402
+ }
403
+
404
+ // Runs one dictation session. `io` supplies stdin, stdout, exit, the module
405
+ // loader, an optional file reader (`openRead`, fs.createReadStream by default)
406
+ // and the process facts, so tests can drive the worker in-process with
407
+ // fake modules; the real entry point below passes Node's own.
408
+ // Resolves after the session has ended and `io.exit` was called once.
409
+ function runVoiceWorker(io) {
410
+ return new Promise((resolve) => {
411
+ let buffered = "";
412
+ const session = new WorkerSession(io, (code) => {
413
+ io.input.removeListener("data", onData);
414
+ io.input.removeListener("end", onEnd);
415
+ io.exit(code);
416
+ resolve();
417
+ });
418
+ // Messages are handled as they arrive, not queued: a cancel must reach the
419
+ // session while it is still validating or loading.
420
+ const dispatch = (handler) => {
421
+ try {
422
+ void Promise.resolve(handler()).catch((error) => session.fail(error));
423
+ } catch (error) {
424
+ void session.fail(error);
425
+ }
426
+ };
427
+ const onData = (chunk) => {
428
+ buffered += String(chunk);
429
+ let newline;
430
+ while ((newline = buffered.indexOf("\n")) !== -1) {
431
+ const line = buffered.slice(0, newline);
432
+ buffered = buffered.slice(newline + 1);
433
+ dispatch(() => {
434
+ let message;
435
+ try {
436
+ message = line.length <= MAX_INPUT_LINE ? JSON.parse(line) : undefined;
437
+ } catch {
438
+ message = undefined;
439
+ }
440
+ return session.handle(message);
441
+ });
442
+ }
443
+ if (buffered.length > MAX_INPUT_LINE) {
444
+ buffered = "";
445
+ dispatch(() => fail("invalid-request"));
446
+ }
447
+ };
448
+ // A closed stdin means the parent is gone: stop capturing and leave.
449
+ const onEnd = () => dispatch(() => session.end(undefined));
450
+ io.input.on("data", onData);
451
+ io.input.on("end", onEnd);
452
+ if (!isInteger(Number(String(io.nodeVersion).split(".")[0]), 22, Number.MAX_SAFE_INTEGER)) {
453
+ void session.end({ type: "error", code: "node-version" });
454
+ }
455
+ });
456
+ }
457
+
458
+ module.exports = { runVoiceWorker };
459
+
460
+ if (require.main === module) {
461
+ const io = {
462
+ input: process.stdin,
463
+ write: (line) => process.stdout.write(line),
464
+ exit: (code) => process.stdout.write("", () => process.exit(code)),
465
+ loadModule: (file) => require(file),
466
+ platform: process.platform,
467
+ arch: process.arch,
468
+ nodeVersion: process.versions.node,
469
+ uid: typeof process.getuid === "function" ? process.getuid() : undefined,
470
+ };
471
+ process.on("uncaughtException", () => {
472
+ process.stdout.write(`${JSON.stringify({ schema: SCHEMA, type: "error", code: "internal" })}\n`, () => process.exit(1));
473
+ });
474
+ process.stdin.setEncoding("utf8");
475
+ void runVoiceWorker(io);
476
+ }
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: gentle-design
3
+ description: "Trigger: design a screen or page, mockup, landing page, UI directions, artboards, try a different approach, design system sync. Write self-contained HTML design directions under design/<slug>/."
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: gentleman-programming
7
+ version: "1.0"
8
+ ---
9
+
10
+ ## Activation Contract
11
+
12
+ Use this skill by default whenever the user asks, in plain words, for a UI design, mockup, screen, landing page, or visual directions, or iterates on artboards under `design/`. Do not use it to implement production code in the project's stack.
13
+
14
+ The `/gentle:design` commands are optional shortcuts: every action also works from a plain request, through this skill and the `gentle_design` tool (`list`, `open`, `stop`, `implement`, `handoff`).
15
+
16
+ ## Hard Rules
17
+
18
+ - Every artboard is one self-contained HTML file: inline CSS, JS, and SVG; system font stacks only. Never reference external URLs, CDNs, remote fonts, images, or scripts.
19
+ - Use realistic content for the audience; no lorem ipsum or placeholder names.
20
+ - Accessible markup: semantic elements and landmarks, WCAG AA contrast, visible focus styles, `prefers-reduced-motion` respected for any motion.
21
+ - When `design/system.md` exists, read it first and use only its tokens and component rules; run the Self-check from "Design system sync" before finishing any artboard.
22
+ - Ask at most one clarifying question, and only when the brief lacks the goal or the content.
23
+ - Iterating edits only the targeted artboard. Never rewrite the others.
24
+
25
+ ## Decision Gates
26
+
27
+ | Situation | Action |
28
+ | --- | --- |
29
+ | New brief | Write N clearly different directions (3 by default, or the number asked) to a new `design/<slug>/` |
30
+ | Feedback on one artboard | Edit that file only; keep its manifest entry, update `tradeoff` if it changed |
31
+ | "Save and try a different approach" | Add a new artboard with the next `NN`; keep previous ones; append it to the manifest |
32
+ | User wants to tune values (spacing, color, type scale) | Add sliders or controls in the artboard plus a "Copy as prompt" button that copies the chosen values as a prompt |
33
+ | `design/<slug>/` already exists for a new brief | Pick a new slug; never overwrite another design |
34
+ | A message carries design comments, or the user asks to apply them | Follow "Canvas comments" below |
35
+ | `/gentle:design sync`, or the user asks to capture the project's design system | Follow "Design system sync" below |
36
+ | The user asks to show, stop, or list designs, or to implement an artboard | Call the `gentle_design` tool (`open`, `stop`, `list`, or `implement` with `<slug>/<artboard>`); follow the handoff text `implement` returns, and report progress with `action "handoff"`: `status: "in_progress"` when you start, `status: "implemented"` when done, with `previewUrl` set to the running app's loopback URL when one is running |
37
+
38
+ ## Before designing
39
+
40
+ Run this gate before every new brief: call the `gentle_design` tool with `action: "list"`, then:
41
+
42
+ | `list` reports | Action |
43
+ | --- | --- |
44
+ | `system.exists` false and `designSources` present | Run "Design system sync" first, tell the user in one line that you synced the project's design system, then design with it |
45
+ | No `designSources` | Skip the sync and design freely |
46
+ | `system.staleSources` present | Name those sources, say `design/system.md` may be outdated, and offer to sync again before designing; re-sync only with the user's OK, and never use the stale tokens silently |
47
+
48
+ ## Execution Steps
49
+
50
+ 1. Take the brief: goal, layout, content, audience.
51
+ 2. Choose a kebab-case slug from the brief.
52
+ 3. Make the directions differ in structure or visual strategy, not only in color. Label each with the tradeoff it makes.
53
+ 4. Write `design/<slug>/NN-<name>.html` per artboard (`01-…`, `02-…`; kebab-case name).
54
+ 5. Write `design/<slug>/design.json`:
55
+
56
+ ```json
57
+ {
58
+ "title": "Pricing page",
59
+ "brief": "Pricing page for a note-taking app aimed at students",
60
+ "feature": "odd/tasks/pricing-page.md",
61
+ "artboards": [
62
+ { "file": "01-comparison-table.html", "title": "Comparison table", "tradeoff": "Dense and scannable; weaker emotional pull", "width": 1440, "height": 900 },
63
+ { "file": "02-single-plan-hero.html", "title": "Single plan hero", "tradeoff": "Strong focus; hides plan differences", "width": 1440, "height": 900 }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ 6. Set `width`/`height` to the artboard's intended viewport in CSS pixels; keep the manifest in sync with the files on every change. Fields: `title`, `brief`, optional `feature`, and `artboards[]` with `file`, `title`, `tradeoff`, `width`, `height`.
69
+ 7. ODD link: when the design comes from a tracked ODD feature (an existing `odd/tasks/<feature>.md` for the current work), set `feature` to that path and append one entry to that document's `## Log` naming `design/<slug>/` and its artboards. When an artboard of that design is handed off to implement, the Log entry also names the chosen artboard (the one handed off; `list` reports it as `Chosen`) and that the others are alternatives. For small untracked work, omit `feature` and leave the design standalone; never create a feature document just to link it.
70
+ 8. After writing or changing artboards, call the `gentle_design` tool with `action: "open"` and the slug, unless the user said not to. When a canvas tab is already open it refreshes that tab instead of opening a new one, so call it after every change. Report the URL it returns.
71
+
72
+ ## Design system sync
73
+
74
+ 1. Read only, and never edit project source. Sources: CSS custom properties (`:root`, `@theme`, `[data-theme]`), the Tailwind config or v4 `@theme`, component library folders, existing token files, and the fonts they declare.
75
+ 2. Write `design/system.md`: colors (token name, value, usage), typography, spacing, radius, shadows, and component rules. Each entry names its source `path`. Record only values found in the sources.
76
+ 3. Write `design/system.html`: one self-contained artboard, 1440 px wide, that renders those tokens (swatches, type scale, spacing, radius, shadows) and the core components, under the same no-external-URL rules. The canvas shows it as the first board of every design.
77
+ 4. When no design sources exist, say so and propose a minimal starter system for the user to accept; never present invented values as project tokens.
78
+ 5. Self-check, for every artboard you finish while `design/system.md` exists: list the colors and font families the artboard uses, compare them with the system tokens, and fix every deviation before you report.
79
+
80
+ ## Canvas comments
81
+
82
+ Comments from the canvas are stored in `design/<slug>/comments.json`; the session also receives them as messages that name each comment id.
83
+
84
+ 1. Read the open comments with the `gentle_design_comments` tool (`action: "list"`, optional `slug` and `file`). Without the tool, read `design/<slug>/comments.json` and use the entries whose `status` is `"open"`.
85
+ 2. Apply each comment by editing only the artboard in its `file`; use `selector` and `excerpt` to find the element. When a comment has a `screenshot`, look at it first: it arrives attached to the comment message, or open `design/<slug>/<screenshot>` with the read tool. It shows what the user saw, often a transient animation or transition state that the static artboard does not show.
86
+ 3. Mark each applied comment resolved with the tool (`action: "resolve"`, `slug`, `ids`). Without the tool, set its `status` to `"resolved"` and `resolvedAt` to the current ISO time, keeping every other entry and field as is. Leave comments you did not apply open, and say why.
87
+
88
+ The file format, for agents that only have file access:
89
+
90
+ ```json
91
+ {
92
+ "version": 1,
93
+ "comments": [
94
+ { "id": "k3x9p2ab", "file": "02-bold.html", "selector": "body > header > h1", "excerpt": "Ship faster", "comment": "Make the headline shorter.", "createdAt": "2026-10-26T10:00:00.000Z", "status": "open", "sentAt": null, "resolvedAt": null },
95
+ { "id": "m7q2w9cd", "file": "02-bold.html", "selector": "#cta", "excerpt": "Start", "comment": "The button jumps during the hover transition.", "screenshot": ".screenshots/m7q2w9cd.png", "createdAt": "2026-10-26T10:05:00.000Z", "status": "open", "sentAt": null, "resolvedAt": null }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ `screenshot` is optional: a path relative to the design folder, under `.screenshots/`, present only when the user attached an image.
101
+
102
+ Never delete or reorder entries, and keep the file valid JSON: the canvas refuses to overwrite a file it cannot parse.
103
+
104
+ ## Output Contract
105
+
106
+ Return: the slug, each artboard file with its title and tradeoff, which files changed in this turn, the comment ids resolved in this turn, and any open question. Give the canvas URL from step 8, or mention that `/gentle:design open <slug>` shows the canvas when you did not open it. For a sync, return the source paths read, the token counts per group, and whether the system is a proposed starter; after an automatic sync from "Before designing", say it ran and report it the same way.
107
+
108
+ ## References
109
+
110
+ - `design/system.md` in the project, when present — the design tokens to follow.
@@ -17,7 +17,7 @@ When asked who or what you are, answer as el Gentleman: a Pi-specific coding-age
17
17
  - Size every task by the orchestrator's Task Size section: understood, contained risk, and resumable from the original request plus `git diff`. Counts of files, commands, tests, or fixes never decide it. Track only large work in its task document and mirror.
18
18
  - For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, relevant alternate cases, then REFACTOR and record evidence. Test presence alone does not establish applicability. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, state why and run proportionate ordinary functional or structural verification. Never invent RED/GREEN, skip checks, or require a chat/TUI toggle.
19
19
  - Keep one parent session responsible for orchestration; child subagents should receive concrete phase work and must not spawn more subagents.
20
- - Parent-only delegation triggers fire one mechanism at a time: an open decision (ask), understanding beyond the evidence budget (explore), high risk (independent verifier), a large task (track), a writer reason (parallel units or context), tooling/worktree incidents, or a parent context past the context backstop.
20
+ - Parent-only delegation triggers fire one mechanism at a time: an open decision (ask), understanding beyond the evidence budget (explore), a named high-risk item (independent verifier, through assess `escalate`), a large task (track), a writer reason (parallel units or context), tooling/worktree incidents, or a parent context past the context backstop.
21
21
  - Parallel writers only with disjoint Allowed edit surfaces (runtime-enforced) or isolated worktrees.
22
22
  - Forecast review workload before large changes; ask before producing oversized or multi-area diffs.
23
23
  - Keep dangerous-command safety independent and authoritative.
@@ -45,9 +45,9 @@ Hard delegation triggers:
45
45
 
46
46
  - **Evidence-budget rule**: read inline only when the evidence fits one parallel batch of at most 3 calls, ~10k tokens (grep and line ranges, never whole large files). When understanding needs more reading or more than ~5 sequential lookups, delegate one explorer that returns a handoff of at most ~2k tokens with `path:line` evidence. Never force delegation for a small targeted question; do not re-read what the handoff covered beyond one spot check.
47
47
  - **Writer rule**: a writer only for a reason (parallel units launched together, or context); size and file count never fire it.
48
- - **Verification rule**: a high-risk change gets an independent verifier; otherwise the change's own focused test and suite run inline.
48
+ - **Verification rule**: a change that touches a named high-risk item gets an independent verifier (assess `escalate`); otherwise the change's own focused test and suite run inline.
49
49
  - **Incident rule**: after wrong cwd, accidental worktree/repo mutation, merge recovery, confusing test command, or environment workaround, diagnose separately.
50
- - **Context backstop**: when the parent context passes ~150k tokens, pause and delegate the next bounded unit of work to a non-review subagent. Keep command output bounded (counts, `--stat`, `tail`).
50
+ - **Context backstop**: when the runtime signals the context backstop (measured parent context past the smaller of an absolute cap and a fraction of the model's context window), pause and delegate the next bounded unit that needs several steps to a non-review subagent; small edits stay inline. Keep command output bounded (counts, `--stat`, `tail`).
51
51
 
52
52
  ## Review Lens Selection
53
53
 
@@ -18,7 +18,7 @@ Use it for:
18
18
  - Turning a large change into chained or stacked PRs.
19
19
  - Keeping reviewer cognitive load healthy.
20
20
  - Applying SDD tasks without accidentally producing a PR above 400 changed lines.
21
- - Closing an ODD task with a work-unit commit.
21
+ - Committing ODD work in reviewable slices of about 400 authored changed lines.
22
22
 
23
23
  ## Critical Rules
24
24
 
@@ -80,11 +80,13 @@ Each SDD work unit should map cleanly to a commit or PR with:
80
80
 
81
81
  ## ODD Relationship
82
82
 
83
- Every ODD task closes with at least one work-unit commit:
83
+ ODD commits work units in reviewable slices of about 400 authored changed lines, not one commit per task:
84
84
 
85
+ - A slice holds one or more whole tasks with their tests and docs; the last slice closes the feature.
86
+ - A unit whose Risk line names a high-risk item gets its own commit, so native review takes it alone.
85
87
  - The native review candidate is that commit, or the PR slice it belongs to when review is deferred, evaluated against the previous reviewed boundary.
86
88
  - The running authored line count from work-unit commits feeds the same delivery-strategy vocabulary as SDD.
87
- - The feature document records the commit identity and, once a delivery strategy applies, the slice boundaries.
89
+ - The feature document records the commit that holds each task and, once a delivery strategy applies, the slice boundaries.
88
90
 
89
91
  ## Commands
90
92
 
@@ -27,6 +27,7 @@ import {
27
27
  profileExportPath,
28
28
  profileRoleEntries,
29
29
  profileRoutingRows,
30
+ unmatchedRoutingNotes,
30
31
  profilesFilePath,
31
32
  readProfileOrchestrator,
32
33
  readProfilesFileResult,
@@ -779,3 +780,26 @@ test("buildProfileListItems marks the session-bound profile ahead of the pinned
779
780
  const unbound = buildProfileListItems(file, "work");
780
781
  assert.equal(unbound.find((item) => item.id === "personal")?.label, "personal", "no session marker without a binding");
781
782
  });
783
+
784
+ // Issue #2196 (PR #2233 maintainer decision): the panel must not render a role
785
+ // key that names no discovered agent as a valid-looking row; it flags it with a
786
+ // did-you-mean suggestion instead of silently ignoring it at launch.
787
+ test("unmatchedRoutingNotes flags routing rows whose key names no discovered agent", () => {
788
+ const rows = [
789
+ { agent: "explore", model: "nan/glm5.3", thinking: "inherit" },
790
+ { agent: "worker", model: "inherit", thinking: "inherit" },
791
+ { agent: "gentle-ai-verify", model: "openai/alpha", thinking: "high" },
792
+ ];
793
+ const notes = unmatchedRoutingNotes(rows, ["gentle-ai-explore", "gentle-ai-verify", "review-risk"]);
794
+ assert.equal(notes.get("explore"), "did you mean gentle-ai-explore?");
795
+ assert.equal(notes.get("worker"), "no matching agent");
796
+ assert.equal(notes.get("gentle-ai-verify"), undefined);
797
+ assert.equal(notes.size, 2);
798
+ assert.deepEqual(
799
+ unmatchedRoutingNotes(rows, ["gentle-ai-explore", "gentle-ai-worker", "gentle-ai-verify"]),
800
+ new Map([
801
+ ["explore", "did you mean gentle-ai-explore?"],
802
+ ["worker", "did you mean gentle-ai-worker?"],
803
+ ]),
804
+ );
805
+ });