@crouton-kit/crouter 0.3.32 → 0.3.33

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 (440) hide show
  1. package/README.md +8 -0
  2. package/dist/build-root.js +1 -0
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
  6. package/dist/builtin-memory/crouter-development/personas.md +3 -4
  7. package/dist/builtin-memory/crouter-development/plugins.md +18 -18
  8. package/dist/builtin-memory/design.md +4 -1
  9. package/dist/builtin-memory/development.md +4 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
  12. package/dist/builtin-memory/internal/storage-tiers.md +5 -5
  13. package/dist/builtin-memory/planning.md +4 -1
  14. package/dist/builtin-memory/product.md +80 -0
  15. package/dist/builtin-memory/spec.md +4 -1
  16. package/dist/builtin-personas/advisor/PERSONA.md +10 -0
  17. package/dist/builtin-personas/design/PERSONA.md +2 -2
  18. package/dist/builtin-personas/design/orchestrator.md +3 -3
  19. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  20. package/dist/builtin-personas/developer/orchestrator.md +2 -2
  21. package/dist/builtin-personas/explore/PERSONA.md +3 -3
  22. package/dist/builtin-personas/explore/orchestrator.md +4 -2
  23. package/dist/builtin-personas/general/PERSONA.md +1 -1
  24. package/dist/builtin-personas/general/orchestrator.md +1 -1
  25. package/dist/builtin-personas/orchestration-kernel.md +7 -14
  26. package/dist/builtin-personas/plan/PERSONA.md +1 -1
  27. package/dist/builtin-personas/plan/orchestrator.md +2 -2
  28. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
  29. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
  30. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
  31. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
  32. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
  33. package/dist/builtin-personas/product/PERSONA.md +18 -0
  34. package/dist/builtin-personas/product/orchestrator.md +14 -0
  35. package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
  36. package/dist/builtin-personas/review/PERSONA.md +1 -1
  37. package/dist/builtin-personas/review/orchestrator.md +1 -1
  38. package/dist/builtin-personas/runtime-base.md +5 -0
  39. package/dist/builtin-personas/spec/PERSONA.md +1 -1
  40. package/dist/builtin-personas/spec/orchestrator.md +2 -2
  41. package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
  42. package/dist/builtin-views/canvas/core.mjs +82 -1
  43. package/dist/builtin-views/canvas/tui.mjs +9 -5
  44. package/dist/builtin-views/canvas/web.jsx +3 -2
  45. package/dist/builtin-views/chat/core.mjs +725 -0
  46. package/dist/builtin-views/chat/text.mjs +101 -0
  47. package/dist/builtin-views/chat/tui.mjs +368 -0
  48. package/dist/builtin-views/chat/web.jsx +367 -0
  49. package/dist/builtin-views/prompt-review/core.mjs +863 -0
  50. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  51. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  52. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  53. package/dist/builtin-views/settings/core.mjs +397 -0
  54. package/dist/builtin-views/settings/text.mjs +40 -0
  55. package/dist/builtin-views/settings/tui.mjs +95 -0
  56. package/dist/builtin-views/settings/web.jsx +167 -0
  57. package/dist/cli.js +16 -3
  58. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
  59. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
  60. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
  61. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
  62. package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
  63. package/dist/clients/attach/attach-cmd.d.ts +18 -0
  64. package/dist/clients/attach/attach-cmd.js +1723 -795
  65. package/dist/clients/attach/canvas-panels.js +2 -2
  66. package/dist/clients/attach/chat-view.d.ts +10 -0
  67. package/dist/clients/attach/chat-view.js +23 -0
  68. package/dist/clients/attach/config-load.d.ts +8 -4
  69. package/dist/clients/attach/config-load.js +2 -0
  70. package/dist/clients/attach/extension-dialogs.d.ts +2 -3
  71. package/dist/clients/attach/extension-dialogs.js +7 -9
  72. package/dist/clients/attach/graph-overlay.js +3 -2
  73. package/dist/clients/attach/input-controller.d.ts +4 -3
  74. package/dist/clients/attach/input-controller.js +24 -3
  75. package/dist/clients/attach/slash-commands.d.ts +7 -7
  76. package/dist/clients/attach/slash-commands.js +38 -14
  77. package/dist/clients/attach/titled-editor.js +18 -14
  78. package/dist/clients/attach/view-socket.d.ts +2 -1
  79. package/dist/clients/attach/view-socket.js +27 -8
  80. package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
  81. package/dist/clients/web/__tests__/source-cache.test.js +32 -0
  82. package/dist/clients/web/dev-server.js +1 -0
  83. package/dist/clients/web/events.js +9 -11
  84. package/dist/clients/web/server.d.ts +4 -3
  85. package/dist/clients/web/server.js +138 -31
  86. package/dist/clients/web/source-cache.d.ts +10 -0
  87. package/dist/clients/web/source-cache.js +57 -0
  88. package/dist/clients/web/web-cmd.js +28 -9
  89. package/dist/commands/__tests__/human.test.js +30 -28
  90. package/dist/commands/canvas-history/search.js +1 -1
  91. package/dist/commands/canvas-history.js +5 -8
  92. package/dist/commands/canvas-issue.d.ts +2 -0
  93. package/dist/commands/canvas-issue.js +148 -0
  94. package/dist/commands/canvas-prune.js +19 -0
  95. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  96. package/dist/commands/canvas-rebuild-index.js +57 -0
  97. package/dist/commands/canvas-snapshot.d.ts +2 -0
  98. package/dist/commands/canvas-snapshot.js +48 -0
  99. package/dist/commands/canvas-tmux-spread.d.ts +2 -0
  100. package/dist/commands/canvas-tmux-spread.js +188 -0
  101. package/dist/commands/canvas.d.ts +1 -0
  102. package/dist/commands/canvas.js +16 -1
  103. package/dist/commands/chord.js +143 -48
  104. package/dist/commands/daemon.js +3 -3
  105. package/dist/commands/human/prompts.d.ts +0 -1
  106. package/dist/commands/human/prompts.js +39 -54
  107. package/dist/commands/human/queue.d.ts +12 -1
  108. package/dist/commands/human/queue.js +468 -73
  109. package/dist/commands/human/shared.d.ts +3 -4
  110. package/dist/commands/human/shared.js +3 -3
  111. package/dist/commands/human.js +11 -10
  112. package/dist/commands/memory/find.js +3 -2
  113. package/dist/commands/memory/list.js +4 -3
  114. package/dist/commands/memory/read.js +3 -3
  115. package/dist/commands/memory/shared.d.ts +1 -4
  116. package/dist/commands/memory/shared.js +6 -9
  117. package/dist/commands/memory/write.js +3 -3
  118. package/dist/commands/memory.js +4 -7
  119. package/dist/commands/node-context.d.ts +2 -0
  120. package/dist/commands/node-context.js +172 -0
  121. package/dist/commands/node-snapshot.d.ts +2 -0
  122. package/dist/commands/node-snapshot.js +123 -0
  123. package/dist/commands/node.js +228 -52
  124. package/dist/commands/pkg/plugin-inspect.js +6 -6
  125. package/dist/commands/pkg/plugin-manage.js +1 -1
  126. package/dist/commands/pkg/plugin.js +2 -2
  127. package/dist/commands/pkg.js +2 -2
  128. package/dist/commands/push.d.ts +2 -0
  129. package/dist/commands/push.js +79 -11
  130. package/dist/commands/revive.js +74 -1
  131. package/dist/commands/search/answer.d.ts +1 -0
  132. package/dist/commands/search/answer.js +50 -0
  133. package/dist/commands/search/contents.d.ts +1 -0
  134. package/dist/commands/search/contents.js +96 -0
  135. package/dist/commands/search/exa.d.ts +53 -0
  136. package/dist/commands/search/exa.js +155 -0
  137. package/dist/commands/search/puremd.d.ts +11 -0
  138. package/dist/commands/search/puremd.js +53 -0
  139. package/dist/commands/search/web.d.ts +1 -0
  140. package/dist/commands/search/web.js +65 -0
  141. package/dist/commands/search.d.ts +2 -0
  142. package/dist/commands/search.js +24 -0
  143. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  144. package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
  145. package/dist/commands/sys/config.js +59 -10
  146. package/dist/commands/sys/doctor.js +56 -4
  147. package/dist/commands/sys/prompt-review.d.ts +1 -0
  148. package/dist/commands/sys/prompt-review.js +178 -0
  149. package/dist/commands/sys/promptstudio.d.ts +2 -0
  150. package/dist/commands/sys/promptstudio.js +65 -0
  151. package/dist/commands/sys/settings.d.ts +2 -0
  152. package/dist/commands/sys/settings.js +16 -0
  153. package/dist/commands/sys/sync.js +311 -159
  154. package/dist/commands/sys/sysprompt.d.ts +1 -0
  155. package/dist/commands/sys/sysprompt.js +86 -0
  156. package/dist/commands/sys.js +9 -5
  157. package/dist/commands/view-new.js +2 -2
  158. package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
  159. package/dist/core/__tests__/artifact-paths.test.js +44 -0
  160. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  161. package/dist/core/__tests__/broker-preflight.test.js +85 -0
  162. package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
  163. package/dist/core/__tests__/canvas.test.js +37 -1
  164. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  165. package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
  166. package/dist/core/__tests__/child-death-wake.test.js +11 -2
  167. package/dist/core/__tests__/close.test.js +39 -1
  168. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  169. package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
  170. package/dist/core/__tests__/context-intro.test.js +48 -14
  171. package/dist/core/__tests__/daemon-boot.test.js +182 -9
  172. package/dist/core/__tests__/editor-label.test.d.ts +1 -0
  173. package/dist/core/__tests__/editor-label.test.js +26 -0
  174. package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
  175. package/dist/core/__tests__/fault-marker.test.js +112 -0
  176. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
  177. package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
  178. package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
  179. package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
  180. package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
  181. package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
  182. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  183. package/dist/core/__tests__/helpers/harness.js +2 -2
  184. package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
  185. package/dist/core/__tests__/human-surface-target.test.js +1 -1
  186. package/dist/core/__tests__/kickoff.test.js +18 -19
  187. package/dist/core/__tests__/memory-resolver.test.js +12 -1
  188. package/dist/core/__tests__/migration.test.js +49 -14
  189. package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
  190. package/dist/core/__tests__/model-ladders.test.js +160 -0
  191. package/dist/core/__tests__/node-env.test.d.ts +1 -0
  192. package/dist/core/__tests__/node-env.test.js +26 -0
  193. package/dist/core/__tests__/push-final-guard.test.js +7 -1
  194. package/dist/core/__tests__/relaunch-root.test.js +9 -9
  195. package/dist/core/__tests__/revive.test.js +36 -6
  196. package/dist/core/__tests__/spawn-root.test.js +24 -1
  197. package/dist/core/__tests__/stop-guard.test.js +10 -0
  198. package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
  199. package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
  200. package/dist/core/__tests__/tmux-surface.test.js +5 -1
  201. package/dist/core/__tests__/unknown-path.test.js +19 -19
  202. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  203. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  204. package/dist/core/artifact.d.ts +2 -33
  205. package/dist/core/artifact.js +27 -87
  206. package/dist/core/bootstrap.js +2 -0
  207. package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
  208. package/dist/core/canvas/browse/app.js +87 -17
  209. package/dist/core/canvas/browse/model.d.ts +10 -1
  210. package/dist/core/canvas/browse/model.js +37 -1
  211. package/dist/core/canvas/browse/render.js +35 -33
  212. package/dist/core/canvas/canvas.d.ts +7 -0
  213. package/dist/core/canvas/canvas.js +54 -5
  214. package/dist/core/canvas/db.js +35 -8
  215. package/dist/core/canvas/labels.d.ts +6 -8
  216. package/dist/core/canvas/labels.js +8 -11
  217. package/dist/core/canvas/nav-model.d.ts +5 -4
  218. package/dist/core/canvas/nav-model.js +31 -17
  219. package/dist/core/canvas/paths.d.ts +18 -1
  220. package/dist/core/canvas/paths.js +31 -2
  221. package/dist/core/canvas/render.d.ts +25 -0
  222. package/dist/core/canvas/render.js +96 -13
  223. package/dist/core/canvas/status-glyph.d.ts +35 -0
  224. package/dist/core/canvas/status-glyph.js +104 -0
  225. package/dist/core/canvas/types.d.ts +2 -2
  226. package/dist/core/config.js +114 -16
  227. package/dist/core/fault-classifier.d.ts +41 -0
  228. package/dist/core/fault-classifier.js +110 -0
  229. package/dist/core/feed/inbox.d.ts +15 -0
  230. package/dist/core/feed/inbox.js +40 -14
  231. package/dist/core/frontmatter.d.ts +1 -11
  232. package/dist/core/frontmatter.js +1 -55
  233. package/dist/core/hearth/config.d.ts +2 -0
  234. package/dist/core/hearth/config.js +75 -0
  235. package/dist/core/hearth/guest-env.d.ts +2 -0
  236. package/dist/core/hearth/guest-env.js +11 -0
  237. package/dist/core/hearth/index.d.ts +5 -0
  238. package/dist/core/hearth/index.js +5 -0
  239. package/dist/core/hearth/provider.d.ts +21 -0
  240. package/dist/core/hearth/provider.js +10 -0
  241. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  242. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  243. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  244. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  245. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  246. package/dist/core/hearth/providers/blaxel.js +208 -0
  247. package/dist/core/hearth/providers/types.d.ts +72 -0
  248. package/dist/core/hearth/providers/types.js +1 -0
  249. package/dist/core/hearth/registry.d.ts +15 -0
  250. package/dist/core/hearth/registry.js +179 -0
  251. package/dist/core/hearth/types.d.ts +121 -0
  252. package/dist/core/hearth/types.js +1 -0
  253. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  254. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  255. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  256. package/dist/core/host-exports/builtins.js +68 -0
  257. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  258. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  259. package/dist/core/io.js +1 -1
  260. package/dist/core/log.d.ts +9 -0
  261. package/dist/core/log.js +113 -0
  262. package/dist/core/memory-resolver.d.ts +8 -8
  263. package/dist/core/memory-resolver.js +15 -28
  264. package/dist/core/personas/index.d.ts +4 -4
  265. package/dist/core/personas/index.js +2 -2
  266. package/dist/core/personas/loader.d.ts +51 -0
  267. package/dist/core/personas/loader.js +54 -50
  268. package/dist/core/personas/resolve.d.ts +43 -27
  269. package/dist/core/personas/resolve.js +336 -94
  270. package/dist/core/runtime/auth-reload.d.ts +7 -0
  271. package/dist/core/runtime/auth-reload.js +69 -0
  272. package/dist/core/runtime/bearings.d.ts +35 -15
  273. package/dist/core/runtime/bearings.js +305 -44
  274. package/dist/core/runtime/branded-host.d.ts +4 -2
  275. package/dist/core/runtime/branded-host.js +66 -4
  276. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  277. package/dist/core/runtime/broker.js +343 -18
  278. package/dist/core/runtime/close.js +37 -29
  279. package/dist/core/runtime/connectivity.d.ts +12 -0
  280. package/dist/core/runtime/connectivity.js +73 -0
  281. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  282. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  283. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  284. package/dist/core/runtime/fault-recovery.js +74 -0
  285. package/dist/core/runtime/fault.d.ts +25 -0
  286. package/dist/core/runtime/fault.js +176 -0
  287. package/dist/core/runtime/front-door.d.ts +1 -1
  288. package/dist/core/runtime/front-door.js +2 -2
  289. package/dist/core/runtime/host.d.ts +10 -0
  290. package/dist/core/runtime/host.js +71 -9
  291. package/dist/core/runtime/kickoff.js +31 -45
  292. package/dist/core/runtime/launch.d.ts +32 -18
  293. package/dist/core/runtime/launch.js +148 -43
  294. package/dist/core/runtime/model-swap.d.ts +18 -0
  295. package/dist/core/runtime/model-swap.js +95 -0
  296. package/dist/core/runtime/naming.d.ts +7 -0
  297. package/dist/core/runtime/naming.js +61 -9
  298. package/dist/core/runtime/nodes.js +6 -1
  299. package/dist/core/runtime/persona.js +16 -17
  300. package/dist/core/runtime/placement.d.ts +36 -31
  301. package/dist/core/runtime/placement.js +149 -64
  302. package/dist/core/runtime/promote.d.ts +2 -0
  303. package/dist/core/runtime/promote.js +23 -3
  304. package/dist/core/runtime/recycle.js +6 -4
  305. package/dist/core/runtime/reset.d.ts +5 -2
  306. package/dist/core/runtime/reset.js +2 -2
  307. package/dist/core/runtime/revive.d.ts +4 -0
  308. package/dist/core/runtime/revive.js +27 -9
  309. package/dist/core/runtime/roadmap.d.ts +1 -1
  310. package/dist/core/runtime/roadmap.js +1 -1
  311. package/dist/core/runtime/spawn.d.ts +2 -2
  312. package/dist/core/runtime/spawn.js +30 -6
  313. package/dist/core/runtime/stop-guard.js +6 -1
  314. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  315. package/dist/core/runtime/tmux-chrome.js +1 -1
  316. package/dist/core/runtime/tmux.d.ts +29 -6
  317. package/dist/core/runtime/tmux.js +134 -80
  318. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  319. package/dist/core/runtime/view-socket-client.js +203 -0
  320. package/dist/core/substrate/index.d.ts +1 -1
  321. package/dist/core/substrate/index.js +1 -1
  322. package/dist/core/substrate/on-read.js +16 -4
  323. package/dist/core/substrate/render.d.ts +10 -13
  324. package/dist/core/substrate/render.js +56 -51
  325. package/dist/core/tui/host.js +83 -18
  326. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  327. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  328. package/dist/core/view/contract.d.ts +20 -0
  329. package/dist/core/view/stream-local.d.ts +3 -0
  330. package/dist/core/view/stream-local.js +184 -0
  331. package/dist/core/view/transport-cache.d.ts +8 -0
  332. package/dist/core/view/transport-cache.js +38 -0
  333. package/dist/core/view/transport-local.js +4 -0
  334. package/dist/core/view/transport.d.ts +7 -1
  335. package/dist/daemon/crtrd.d.ts +3 -19
  336. package/dist/daemon/crtrd.js +139 -178
  337. package/dist/daemon/manage.d.ts +18 -1
  338. package/dist/daemon/manage.js +54 -9
  339. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  340. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  341. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  342. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  343. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  344. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  345. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  346. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  347. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  348. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  349. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  350. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  351. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  352. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  353. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  354. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  355. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  356. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  357. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  358. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  359. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  360. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  361. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  362. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  363. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  364. package/dist/hearth/wake-proxy/auth.js +128 -0
  365. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  366. package/dist/hearth/wake-proxy/config.js +151 -0
  367. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  368. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  369. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  370. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  371. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  372. package/dist/hearth/wake-proxy/home.js +297 -0
  373. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  374. package/dist/hearth/wake-proxy/main.js +134 -0
  375. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  376. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  377. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  378. package/dist/hearth/wake-proxy/proxy.js +716 -0
  379. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  380. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  381. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  382. package/dist/hearth/wake-proxy/redact.js +17 -0
  383. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  384. package/dist/hearth/wake-proxy/server.js +142 -0
  385. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  386. package/dist/hearth/wake-proxy/state.js +342 -0
  387. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  388. package/dist/hearth/wake-proxy/types.js +1 -0
  389. package/dist/index.js +3 -2
  390. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  391. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  392. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  393. package/dist/pi-extensions/canvas-nav.js +15 -8
  394. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  395. package/dist/pi-extensions/canvas-stophook.js +78 -11
  396. package/dist/types.d.ts +39 -20
  397. package/dist/types.js +38 -13
  398. package/dist/web/runtime.js +141 -42
  399. package/dist/web/transport-http.js +7 -5
  400. package/dist/web/transport-stream.d.ts +3 -0
  401. package/dist/web/transport-stream.js +204 -0
  402. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  403. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  404. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  405. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  406. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  407. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  408. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  409. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  410. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  411. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  412. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  413. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  414. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  415. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  416. package/dist/web-client/index.html +3 -2
  417. package/package.json +32 -10
  418. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  419. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  420. package/dist/core/skill-sync/builtins.js +0 -112
  421. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  422. package/dist/core/skill-sync/claude-plugins.js +0 -71
  423. package/dist/core/skill-sync/engine.d.ts +0 -42
  424. package/dist/core/skill-sync/engine.js +0 -633
  425. package/dist/core/skill-sync/manifest.d.ts +0 -64
  426. package/dist/core/skill-sync/manifest.js +0 -181
  427. package/dist/core/skill-sync/profile.d.ts +0 -76
  428. package/dist/core/skill-sync/profile.js +0 -173
  429. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  430. package/dist/core/skill-sync/snapshot.js +0 -120
  431. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  432. package/dist/pi-extensions/canvas-commands.js +0 -113
  433. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  434. package/dist/pi-extensions/canvas-resume.js +0 -83
  435. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  436. package/dist/pi-extensions/canvas-view.js +0 -76
  437. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  438. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  439. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  440. /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  // server.ts — the unified `crtr web serve` host: the crouter web UI server.
2
2
  //
3
- // ONE long-running server process on 127.0.0.1 that serves THREE concerns on one
4
- // origin (the §2 unified server):
3
+ // ONE long-running server process on the bound host that serves THREE concerns
4
+ // on one origin (the §2 unified server):
5
5
  // • GET /* → the shell SPA (static dist/web-client/, or Vite
6
6
  // middleware in --dev) + SPA fallback.
7
7
  // • POST /__crtr/source → the source + command bridge: decode a SourceRequest
@@ -21,23 +21,28 @@
21
21
  // ALREADY-running broker's `view.sock`; if the broker is not running the WS is
22
22
  // closed with a clear reason. The relay NEVER launches an engine. ALL writes go
23
23
  // through the bridge running `crtr` SUBPROCESSES — this process is never the
24
- // sanctioned writer; the CLI it shells out to is.
24
+ // sanctioned writer; the CLI it shells out to is. The sanctioned exception is a
25
+ // relay fault record written directly to the node when the broker socket fails.
25
26
  //
26
27
  // One `view.sock` connection per browser-WS (N browsers of one node = N broker
27
28
  // clients) — the broker fans out natively, so there is no bridge-side fan-out.
28
29
  import { createServer as createHttpServer } from 'node:http';
29
- import { createConnection } from 'node:net';
30
+ import { createConnection, isIP } from 'node:net';
30
31
  import { createReadStream, existsSync, statSync } from 'node:fs';
31
32
  import { fileURLToPath } from 'node:url';
32
33
  import { dirname, extname, join, normalize, resolve } from 'node:path';
33
34
  import { WebSocketServer } from 'ws';
34
- import { nodeDir } from '../../core/canvas/paths.js';
35
+ import { viewSocketPath } from '../../core/canvas/paths.js';
35
36
  import { getNode } from '../../core/canvas/index.js';
36
37
  import { BROKER_READ_CAPS, CLIENT_READ_CAPS, FrameDecoder, FrameOverflowError, } from '../../core/runtime/broker-protocol.js';
38
+ import { classify } from '../../core/fault-classifier.js';
39
+ import { clearFault, recordFault } from '../../core/runtime/fault.js';
37
40
  import { createLocalTransport } from '../../core/view/transport-local.js';
41
+ import { createBurstCoalescingTransport } from '../../core/view/transport-cache.js';
38
42
  import { runSourceRequest } from '../../core/view/bridge.js';
39
43
  import { startEventHub } from './events.js';
40
44
  import { createDevServer } from './dev-server.js';
45
+ import { SOURCE_CACHE_TTL_MS, isCoalescableSourceRequest } from './source-cache.js';
41
46
  const HERE = dirname(fileURLToPath(import.meta.url));
42
47
  /** Candidate dirs to serve the shell SPA bundle from, in priority order: an
43
48
  * explicit override, the compiled-module-relative `dist/web-client/` (what
@@ -94,11 +99,46 @@ function nodeIdFromPath(pathname) {
94
99
  return null; // malformed percent-encoding — treat as no match
95
100
  }
96
101
  }
97
- /** A node id is used to build `nodeDir(id)/view.sock`, so it MUST NOT contain a
98
- * path separator or `..` (e.g. a decoded `..%2f..` would escape `nodes/`). */
102
+ /** A node id is used to build the broker socket path via `viewSocketPath(id)`, so
103
+ * it MUST NOT contain a path separator or `..` (e.g. a decoded `..%2f..`). */
99
104
  function isSafeNodeId(id) {
100
105
  return id !== '' && !/[/\\]/.test(id) && !id.includes('..');
101
106
  }
107
+ function isLoopbackHost(host) {
108
+ const lower = host.toLowerCase();
109
+ const ipVersion = isIP(host);
110
+ if (lower === 'localhost' || lower === '::1')
111
+ return true;
112
+ if (ipVersion === 4 && host.startsWith('127.'))
113
+ return true;
114
+ if (lower === '::ffff:127.0.0.1')
115
+ return true;
116
+ if (ipVersion === 6 && lower.startsWith('::ffff:127.'))
117
+ return true;
118
+ return false;
119
+ }
120
+ function hostForUrl(host) {
121
+ return host.includes(':') ? `[${host}]` : host;
122
+ }
123
+ function requestTokens(req) {
124
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
125
+ const tokens = [];
126
+ const queryToken = url.searchParams.get('token');
127
+ if (queryToken !== null && queryToken !== '')
128
+ tokens.push(queryToken);
129
+ const authorization = req.headers.authorization;
130
+ if (typeof authorization === 'string') {
131
+ const match = /^Bearer\s+(.+)$/.exec(authorization);
132
+ if (match !== null && match[1] !== '')
133
+ tokens.push(match[1]);
134
+ }
135
+ return tokens;
136
+ }
137
+ function requestTokenMatches(req, secret) {
138
+ if (secret === undefined)
139
+ return false;
140
+ return requestTokens(req).some((token) => token === secret);
141
+ }
102
142
  /** A WS close reason is capped at 123 UTF-8 bytes by the protocol. */
103
143
  function clampReason(reason) {
104
144
  const buf = Buffer.from(reason, 'utf8');
@@ -192,10 +232,15 @@ function bridgeConnection(ws, nodeId) {
192
232
  }
193
233
  return;
194
234
  }
195
- const sockPath = join(nodeDir(nodeId), 'view.sock');
235
+ clearFault(nodeId, { link: 'browser↔relay' });
236
+ const sockPath = viewSocketPath(nodeId);
196
237
  const socket = createConnection(sockPath);
197
238
  const decoder = new FrameDecoder(CLIENT_READ_CAPS);
198
239
  let closed = false;
240
+ let browserFaultRecorded = false;
241
+ socket.on('connect', () => {
242
+ clearFault(nodeId, { link: 'relay↔broker' });
243
+ });
199
244
  const teardown = (wsCode, reason) => {
200
245
  if (closed)
201
246
  return;
@@ -234,12 +279,62 @@ function bridgeConnection(ws, nodeId) {
234
279
  }
235
280
  });
236
281
  socket.on('error', (err) => {
282
+ const classification = classify('relay↔broker', err);
283
+ recordFault(nodeId, {
284
+ link: 'relay↔broker',
285
+ op: 'bridge socket',
286
+ kind: classification.kind,
287
+ retry: { disposition: 'manual' },
288
+ message: err.message,
289
+ });
237
290
  const reason = err.code === 'ECONNREFUSED' || err.code === 'ENOENT'
238
291
  ? `no running broker for ${nodeId}`
239
292
  : `view socket error for ${nodeId}: ${err.message}`;
240
293
  teardown(1011, reason);
241
294
  });
242
- socket.on('close', () => teardown(1000, `broker for ${nodeId} closed`));
295
+ socket.on('close', (hadError) => {
296
+ if (closed || hadError)
297
+ return;
298
+ clearFault(nodeId, { link: 'relay↔broker' });
299
+ teardown(1000, 'broker socket closed');
300
+ });
301
+ ws.on('close', (code, reason) => {
302
+ const reasonText = reason.toString('utf8');
303
+ if (closed) {
304
+ if (code === 1000)
305
+ clearFault(nodeId, { link: 'browser↔relay' });
306
+ return;
307
+ }
308
+ if (code === 1000) {
309
+ clearFault(nodeId, { link: 'browser↔relay' });
310
+ }
311
+ else if (!browserFaultRecorded) {
312
+ browserFaultRecorded = true;
313
+ const classification = classify('browser↔relay', { code, reason: reasonText });
314
+ recordFault(nodeId, {
315
+ link: 'browser↔relay',
316
+ op: 'browser websocket',
317
+ kind: classification.kind,
318
+ retry: { disposition: 'manual' },
319
+ message: reasonText || `browser relay closed (${code})`,
320
+ });
321
+ }
322
+ teardown(code, reasonText || `browser relay closed (${code})`);
323
+ });
324
+ ws.on('error', () => {
325
+ if (!browserFaultRecorded) {
326
+ browserFaultRecorded = true;
327
+ const classification = classify('browser↔relay', { code: 1006, reason: 'browser ws error' });
328
+ recordFault(nodeId, {
329
+ link: 'browser↔relay',
330
+ op: 'browser websocket',
331
+ kind: classification.kind,
332
+ retry: { disposition: 'manual' },
333
+ message: 'browser ws error',
334
+ });
335
+ }
336
+ teardown(1011, 'browser ws error');
337
+ });
243
338
  // WS → socket: re-frame each message as one newline-terminated JSON line.
244
339
  // VERBATIM (no parse) — the broker validates and bounds its own reads.
245
340
  ws.on('message', (data) => {
@@ -259,34 +354,43 @@ function bridgeConnection(ws, nodeId) {
259
354
  /* dead socket — its 'close' drives teardown */
260
355
  }
261
356
  });
262
- ws.on('close', () => teardown(1000, 'browser closed'));
263
- ws.on('error', () => teardown(1011, 'browser ws error'));
264
357
  }
265
- /** Build + start the unified web server. Binds 127.0.0.1 only (remote access is
266
- * out of scope — §9). Resolves once the server is listening. Async because
267
- * --dev awaits a Vite middleware server before listening. */
358
+ /** Build + start the unified web server. Binds the requested host/port and
359
+ * resolves once the server is listening. Async because --dev awaits a Vite
360
+ * middleware server before listening. */
268
361
  export async function startWebServer(opts) {
269
362
  const host = opts.host ?? '127.0.0.1';
363
+ const token = opts.token !== undefined && opts.token !== '' ? opts.token : undefined;
270
364
  const dev = opts.dev ?? false;
365
+ const tokenRequired = !isLoopbackHost(host);
366
+ if (tokenRequired && (token === undefined || token === '')) {
367
+ throw new Error(`crtr web serve --host ${host} requires --token for remote-capable endpoints`);
368
+ }
271
369
  const clientDir = resolveClientDir();
272
370
  // The bridge runs view sources + command verbs locally, in the cwd crtr was
273
371
  // invoked from (a git-pr view inspects THIS repo; a `crtr node new` lands here).
274
- const transport = createLocalTransport({ cwd: process.cwd() });
372
+ const transport = createBurstCoalescingTransport(createLocalTransport({ cwd: process.cwd() }), {
373
+ ttlMs: SOURCE_CACHE_TTL_MS,
374
+ shouldCache: isCoalescableSourceRequest,
375
+ });
275
376
  // The SSE change lane — one hub, fanned to every connected EventSource.
276
377
  const eventHub = startEventHub();
277
- // Same-origin allowlist (M1): a browser bypasses same-origin policy on both
278
- // WebSockets AND cross-origin POSTs, so any web page the user visits could
279
- // otherwise open ws://127.0.0.1:PORT/node/<id> and DRIVE their agents, OR POST
280
- // /__crtr/source to run ARBITRARY exec. The same gate guards all three write/
281
- // drive surfaces (WS upgrade, bridge POST, SSE GET). Populated once we know the
282
- // bound port. A request with NO Origin (a CLI/curl/test client, not a browser)
283
- // is allowed — the threat model is foreign browser pages, which always send it.
378
+ // Same-origin allowlist: tokenless localhost pages keep the legacy guard,
379
+ // while a valid relay token can authorize remote Hearth route origins that would
380
+ // otherwise be rejected by originAllowed().
284
381
  let allowedOrigins = new Set();
285
382
  const originAllowed = (origin) => {
286
383
  if (origin === undefined || origin === '')
287
384
  return true;
288
385
  return allowedOrigins.has(origin);
289
386
  };
387
+ const requestAllowed = (req) => {
388
+ if (requestTokenMatches(req, token))
389
+ return true;
390
+ if (tokenRequired)
391
+ return false;
392
+ return originAllowed(req.headers.origin);
393
+ };
290
394
  // Assigned before listen() when --dev; the request handler closes over it.
291
395
  let vite;
292
396
  const httpServer = createHttpServer((req, res) => {
@@ -294,15 +398,16 @@ export async function startWebServer(opts) {
294
398
  const url = new URL(req.url ?? '/', 'http://127.0.0.1');
295
399
  const pathname = url.pathname;
296
400
  // The source + command bridge: POST /__crtr/source → run the SourceRequest
297
- // through the local transport. Origin-gated (same M1 gate as the WS relay):
298
- // arbitrary exec from a foreign browser page is exactly what M1 prevents.
401
+ // through the local transport. Token-or-origin gated (same guard as the WS
402
+ // relay): arbitrary exec from a foreign browser page is exactly what the
403
+ // remote binding guard prevents.
299
404
  if (pathname === '/__crtr/source') {
300
405
  if (req.method !== 'POST') {
301
406
  res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8', allow: 'POST' });
302
407
  res.end('method not allowed\n');
303
408
  return;
304
409
  }
305
- if (!originAllowed(req.headers.origin)) {
410
+ if (!requestAllowed(req)) {
306
411
  res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
307
412
  res.end('forbidden\n');
308
413
  return;
@@ -319,15 +424,16 @@ export async function startWebServer(opts) {
319
424
  });
320
425
  return;
321
426
  }
322
- // The SSE change-invalidation lane: GET /__crtr/events. Origin-gated too —
323
- // a foreign page should not even learn the user's graph is mutating.
427
+ // The SSE change-invalidation lane: GET /__crtr/events. Token-or-origin
428
+ // gated too — a foreign page should not even learn the user's graph is
429
+ // mutating.
324
430
  if (pathname === '/__crtr/events') {
325
431
  if (req.method !== 'GET') {
326
432
  res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8', allow: 'GET' });
327
433
  res.end('method not allowed\n');
328
434
  return;
329
435
  }
330
- if (!originAllowed(req.headers.origin)) {
436
+ if (!requestAllowed(req)) {
331
437
  res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
332
438
  res.end('forbidden\n');
333
439
  return;
@@ -383,7 +489,7 @@ export async function startWebServer(opts) {
383
489
  socket.destroy();
384
490
  return;
385
491
  }
386
- if (!originAllowed(req.headers.origin)) {
492
+ if (!requestAllowed(req)) {
387
493
  socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
388
494
  socket.destroy();
389
495
  return;
@@ -418,9 +524,10 @@ export async function startWebServer(opts) {
418
524
  });
419
525
  const addr = httpServer.address();
420
526
  const port = typeof addr === 'object' && addr !== null ? addr.port : opts.port;
421
- const url = `http://${host}:${port}`;
527
+ const hostUrl = hostForUrl(host);
528
+ const url = `http://${hostUrl}:${port}`;
422
529
  allowedOrigins = new Set([
423
- `http://${host}:${port}`,
530
+ `http://${hostUrl}:${port}`,
424
531
  `http://127.0.0.1:${port}`,
425
532
  `http://localhost:${port}`,
426
533
  ]);
@@ -0,0 +1,10 @@
1
+ import type { SourceRequest } from '../../core/view/contract.js';
2
+ /** TTL for coalesced source reads. Must comfortably exceed the SSE debounce
3
+ * (events.ts, 150 ms) so a single mutation's fan-out collapses to one
4
+ * subprocess per distinct read, while staying short enough that the next
5
+ * poll/SSE refreshes promptly. */
6
+ export declare const SOURCE_CACHE_TTL_MS = 500;
7
+ /** True iff `req` is an idempotent `crtr … --json` read safe to burst-coalesce.
8
+ * Anything carrying stdin or a cwd override, any non-exec request, and any
9
+ * command not on the {@link CACHEABLE_READS} allowlist is rejected. */
10
+ export declare function isCoalescableSourceRequest(req: SourceRequest): boolean;
@@ -0,0 +1,57 @@
1
+ // source-cache.ts — which `/__crtr/source` requests the host may burst-coalesce.
2
+ //
3
+ // The web client has no data API: every read is a `crtr … --json` subprocess
4
+ // POSTed to /__crtr/source, and one SSE invalidation fans out to N independent
5
+ // React subscribers that each re-issue their own read. On NFS-backed canvas
6
+ // state each such subprocess is expensive, so an SSE burst spawns a storm of
7
+ // duplicate reads. The host wraps the transport in a burst-coalescing decorator
8
+ // (transport-cache.ts) keyed on JSON.stringify(req); this predicate decides
9
+ // which requests are safe to share/cache within a short TTL.
10
+ //
11
+ // ONLY idempotent, side-effect-free `crtr` reads belong here. Writes
12
+ // (node new / canvas revive / node msg), file reads, http requests, and any
13
+ // request carrying stdin or an overridden cwd are NEVER cacheable — caching
14
+ // those would drop a mutation or serve a stale cross-cwd read. Target-dependent
15
+ // reads (node snapshot/inspect for a specific id) ARE cacheable because the id
16
+ // is part of the request and therefore part of the coalescing key, so distinct
17
+ // ids never collide.
18
+ /** TTL for coalesced source reads. Must comfortably exceed the SSE debounce
19
+ * (events.ts, 150 ms) so a single mutation's fan-out collapses to one
20
+ * subprocess per distinct read, while staying short enough that the next
21
+ * poll/SSE refreshes promptly. */
22
+ export const SOURCE_CACHE_TTL_MS = 500;
23
+ /** The literal `crtr … --json` read commands the web client re-issues on every
24
+ * SSE burst. Each entry is matched positionally against the exec args; an
25
+ * `'*'` placeholder matches exactly one arbitrary arg (a target id). */
26
+ const CACHEABLE_READS = [
27
+ ['canvas', 'snapshot', '--json'],
28
+ ['human', 'list', '--json'],
29
+ ['view', 'list', '--json'],
30
+ ['node', 'snapshot', '*', '--json'],
31
+ ['node', 'inspect', 'show', '*', '--json'],
32
+ ];
33
+ function argsMatch(pattern, args) {
34
+ if (pattern.length !== args.length)
35
+ return false;
36
+ for (let i = 0; i < pattern.length; i++) {
37
+ if (pattern[i] === '*')
38
+ continue;
39
+ if (pattern[i] !== args[i])
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ /** True iff `req` is an idempotent `crtr … --json` read safe to burst-coalesce.
45
+ * Anything carrying stdin or a cwd override, any non-exec request, and any
46
+ * command not on the {@link CACHEABLE_READS} allowlist is rejected. */
47
+ export function isCoalescableSourceRequest(req) {
48
+ if (req.kind !== 'exec')
49
+ return false;
50
+ if (req.bin !== 'crtr')
51
+ return false;
52
+ if (req.stdin !== undefined)
53
+ return false;
54
+ if (req.cwd !== undefined)
55
+ return false;
56
+ return CACHEABLE_READS.some((pattern) => argsMatch(pattern, req.args));
57
+ }
@@ -3,7 +3,7 @@
3
3
  // Starts the ONE long-running unified web server (server.ts): the crouter web UI
4
4
  // host. It serves the shell SPA, the source + command bridge (POST
5
5
  // /__crtr/source), the SSE change lane (GET /__crtr/events), and a VERBATIM
6
- // browser ⇄ broker relay on `ws://127.0.0.1:PORT/node/<id>` (the browser becomes
6
+ // browser ⇄ broker relay on `ws://HOST:PORT/node/<id>` (the browser becomes
7
7
  // the SAME protocol peer as `crtr attach`). The node is selected by the URL, so
8
8
  // there is NO positional node here.
9
9
  //
@@ -37,10 +37,10 @@ function openBrowser(url) {
37
37
  const webServeLeaf = defineLeaf({
38
38
  name: 'serve',
39
39
  description: 'start the crouter web UI server — the shell, the source/command bridge, and a browser ⇄ broker relay',
40
- whenToUse: 'you want the crouter web UI: a browser shell to watch the canvas and drive headless nodes, instead of a terminal pane. It starts ONE long-running local server (127.0.0.1) that serves the shell SPA, the source + command bridge (POST /__crtr/source — views\' reads AND `crtr` command writes), an SSE change-invalidation lane (GET /__crtr/events), and a VERBATIM browser ⇄ broker relay on /node/<id> (the browser becomes the same protocol peer as `crtr attach`: one controller drives, extra viewers follow read-only). The relay NEVER starts an engine — each node must already have a running headless broker (focus or revive it first); all writes flow through `crtr` subprocesses the bridge runs. Pass --dev for Vite middleware (HMR) while iterating on the shell. The server runs until ctrl+c; the node is picked in the browser via the URL, not on the command line.',
40
+ whenToUse: 'you want the crouter web UI: a browser shell to watch the canvas and drive headless nodes, instead of a terminal pane. It starts ONE long-running server (default 127.0.0.1) that serves the shell SPA, the source + command bridge (POST /__crtr/source — views\' reads AND `crtr` command writes), an SSE change-invalidation lane (GET /__crtr/events), and a VERBATIM browser ⇄ broker relay on /node/<id> (the browser becomes the same protocol peer as `crtr attach`: one controller drives, extra viewers follow read-only). If you bind a non-loopback host, pass --token so the remote-capable bridge, SSE, and relay endpoints can authorize the page. The relay NEVER starts an engine — each node must already have a running headless broker (focus or revive it first); all writes flow through `crtr` subprocesses the bridge runs. Pass --dev for Vite middleware (HMR) while iterating on the shell. The server runs until ctrl+c; the node is picked in the browser via the URL, not on the command line.',
41
41
  help: {
42
42
  name: 'web serve',
43
- summary: 'start the long-running crouter web UI server on 127.0.0.1: serves the shell SPA, the POST /__crtr/source bridge, the GET /__crtr/events SSE lane, and a VERBATIM browser ⇄ node-broker relay (browser picks the node via /node/<id>)',
43
+ summary: 'start the long-running crouter web UI server: serves the shell SPA, the POST /__crtr/source bridge, the GET /__crtr/events SSE lane, and a VERBATIM browser ⇄ node-broker relay (browser picks the node via /node/<id>)',
44
44
  params: [
45
45
  {
46
46
  kind: 'flag',
@@ -50,6 +50,21 @@ const webServeLeaf = defineLeaf({
50
50
  default: DEFAULT_PORT,
51
51
  constraint: `TCP port to bind on 127.0.0.1. Default ${DEFAULT_PORT}.`,
52
52
  },
53
+ {
54
+ kind: 'flag',
55
+ name: 'host',
56
+ type: 'string',
57
+ required: false,
58
+ default: '127.0.0.1',
59
+ constraint: 'TCP host to bind. Default 127.0.0.1. Non-loopback binds require --token.',
60
+ },
61
+ {
62
+ kind: 'flag',
63
+ name: 'token',
64
+ type: 'string',
65
+ required: false,
66
+ constraint: 'Bearer token for remote-capable bridge, SSE, and broker relay requests. Required for non-loopback binds.',
67
+ },
53
68
  {
54
69
  kind: 'flag',
55
70
  name: 'open',
@@ -70,17 +85,21 @@ const webServeLeaf = defineLeaf({
70
85
  output: [],
71
86
  outputKind: 'object',
72
87
  effects: [
73
- 'Binds a long-running HTTP+WebSocket server on 127.0.0.1:<port> and keeps serving until interrupted (ctrl+c / SIGTERM). Safe to run non-interactively (daemon-like): it prints the URL and serves.',
74
- 'Serves the shell SPA (static dist/web-client/, or Vite middleware under --dev), runs the POST /__crtr/source bridge (exec/file/http in this cwd), streams the GET /__crtr/events SSE change lane, and on a browser ws://127.0.0.1:<port>/node/<id> connection opens that node\'s running broker view.sock and relays frames VERBATIM both directions.',
88
+ 'Binds a long-running HTTP+WebSocket server on the requested host and port and keeps serving until interrupted (ctrl+c / SIGTERM). Safe to run non-interactively (daemon-like): it prints the URL and serves.',
89
+ 'Serves the shell SPA (static dist/web-client/, or Vite middleware under --dev), runs the POST /__crtr/source bridge (exec/file/http in this cwd), streams the GET /__crtr/events SSE change lane, and on a browser ws://HOST:<port>/node/<id> connection opens that node\'s running broker view.sock and relays frames VERBATIM both directions. Non-loopback binds require a token so the remote Hearth route is not an unauthenticated command surface.',
75
90
  'The relay NEVER spawns pi and NEVER writes any session — it only opens a socket to an already-running broker; if the broker is not running, the browser connection is closed with a clear reason. All graph mutations go through `crtr` subprocesses the bridge runs.',
76
91
  ],
77
92
  },
78
93
  run: async (input) => {
79
94
  const port = input['port'] ?? DEFAULT_PORT;
95
+ const host = input['host'] ?? '127.0.0.1';
96
+ const tokenRaw = input['token'];
97
+ const token = tokenRaw !== undefined && tokenRaw !== '' ? tokenRaw : undefined;
80
98
  const open = input['open'] ?? false;
81
99
  const dev = input['dev'] ?? false;
82
- const server = await startWebServer({ port, dev });
100
+ const server = await startWebServer({ port, host, token, dev });
83
101
  const nodeUrlShape = `${server.url}/node/<id>`;
102
+ const browserUrl = token !== undefined ? `${server.url}?token=${encodeURIComponent(token)}` : server.url;
84
103
  process.stdout.write(`crtr web serving on ${server.url}${dev ? ' (--dev: Vite middleware)' : ''}\n` +
85
104
  ` open a node: ${nodeUrlShape}\n` +
86
105
  ` bridge: POST ${server.url}/__crtr/source\n` +
@@ -88,7 +107,7 @@ const webServeLeaf = defineLeaf({
88
107
  ` shell assets: ${dev ? 'Vite middleware (HMR)' : server.clientDir}\n` +
89
108
  ` (ctrl+c to stop)\n`);
90
109
  if (open)
91
- openBrowser(server.url);
110
+ openBrowser(browserUrl);
92
111
  // Long-running: resolve only on a shutdown signal, tearing down cleanly.
93
112
  await new Promise((resolveShutdown) => {
94
113
  const shutdown = () => {
@@ -108,12 +127,12 @@ export function registerWeb() {
108
127
  rootEntry: {
109
128
  concept: 'the crouter web UI server — one host serving the browser shell, the source/command bridge, an SSE change lane, and a VERBATIM relay to nodes\' running brokers',
110
129
  desc: 'serve the crouter web UI for the canvas and headless nodes',
111
- useWhen: 'you want the crouter web UI in a browser instead of a tmux pane: watch the canvas and drive headless nodes. It runs one long-lived local server that serves the shell SPA, runs view sources + `crtr` command writes over POST /__crtr/source, streams canvas-change invalidations over GET /__crtr/events (SSE), and relays a browser connection VERBATIM to a node\'s already-running broker on /node/<id> — same protocol peer as `crtr attach`. The node is selected in the browser via the URL; the relay never starts an engine, so each node must already have a running headless broker.',
130
+ useWhen: 'you want the crouter web UI in a browser instead of a tmux pane: watch the canvas and drive headless nodes. It runs one long-lived server that serves the shell SPA, runs view sources + `crtr` command writes over POST /__crtr/source, streams canvas-change invalidations over GET /__crtr/events (SSE), and relays a browser connection VERBATIM to a node\'s already-running broker on /node/<id> — same protocol peer as `crtr attach`. The node is selected in the browser via the URL; if the server is bound off-loopback, the page must carry a token so the relay and bridge can authorize.',
112
131
  },
113
132
  help: {
114
133
  name: 'web',
115
134
  summary: 'serve the crouter web UI: the shell SPA, the source/command bridge, the SSE change lane, and a VERBATIM relay to headless nodes\' brokers',
116
- model: '`serve` starts the ONE long-running unified server on 127.0.0.1 (--port, default 7878; --open to launch the browser; --dev for Vite middleware/HMR). It serves the shell SPA (static dist/web-client/ shipped, Vite middleware in --dev), the POST /__crtr/source bridge (views\' reads AND `crtr` command writes via the local exec/file/http transport), the GET /__crtr/events SSE change-invalidation lane, and on a browser ws://127.0.0.1:PORT/node/<id> connection opens that node\'s broker view.sock and relays frames VERBATIM both ways — the browser is the same protocol peer as `crtr attach` (one controller drives, extra viewers are read-only; arbitration + dialogs ride the same frames). The relay NEVER spawns pi or writes a session: the node must already have a running headless broker (focus or revive it first); all graph mutations flow through `crtr` subprocesses the bridge runs. The server keeps running until ctrl+c; the node is chosen in the browser via the URL, not on the command line.',
135
+ model: '`serve` starts the ONE long-running unified server on the bound host (--port, default 7878; --host defaults to 127.0.0.1; --token is required for non-loopback binds; --open to launch the browser; --dev for Vite middleware/HMR). It serves the shell SPA (static dist/web-client/ shipped, Vite middleware in --dev), the POST /__crtr/source bridge (views\' reads AND `crtr` command writes via the local exec/file/http transport), the GET /__crtr/events SSE change-invalidation lane, and on a browser ws://HOST:PORT/node/<id> connection opens that node\'s broker view.sock and relays frames VERBATIM both ways — the browser is the same protocol peer as `crtr attach` (one controller drives, extra viewers are read-only; arbitration + dialogs ride the same frames). The relay NEVER spawns pi or writes a session: the node must already have a running headless broker (focus or revive it first); all graph mutations flow through `crtr` subprocesses the bridge runs. The server keeps running until ctrl+c; the node is chosen in the browser via the URL, not on the command line.',
117
136
  },
118
137
  children: [webServeLeaf],
119
138
  });
@@ -10,6 +10,8 @@ import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { randomBytes } from 'node:crypto';
12
12
  import { parseArgv } from '../../core/command.js';
13
+ import { renderLeafArgv } from '../../core/help.js';
14
+ import { humanAsk } from '../human/prompts.js';
13
15
  import { humanCancel } from '../human/queue.js';
14
16
  import { createNode, getNode, closeDb } from '../../core/canvas/index.js';
15
17
  // ---------------------------------------------------------------------------
@@ -23,34 +25,6 @@ function tmpJson(obj) {
23
25
  return p;
24
26
  }
25
27
  // ---------------------------------------------------------------------------
26
- // human approve — positional title + optional flags
27
- // ---------------------------------------------------------------------------
28
- describe('human approve: params', () => {
29
- const params = [
30
- { kind: 'positional', name: 'title', type: 'string', required: true, constraint: 'The question.' },
31
- { kind: 'flag', name: 'subtitle', type: 'string', required: false, constraint: 'One-line context.' },
32
- { kind: 'flag', name: 'body', type: 'string', required: false, constraint: 'Markdown body.' },
33
- ];
34
- test('parses positional title', async () => {
35
- const result = await parseArgv(params, ['Deploy to prod?']);
36
- assert.equal(result['title'], 'Deploy to prod?');
37
- });
38
- test('parses title with optional flags', async () => {
39
- const result = await parseArgv(params, ['Deploy?', '--subtitle', 'v1.2.3', '--body', 'LGTM']);
40
- assert.equal(result['title'], 'Deploy?');
41
- assert.equal(result['subtitle'], 'v1.2.3');
42
- assert.equal(result['body'], 'LGTM');
43
- });
44
- test('flags absent → undefined', async () => {
45
- const result = await parseArgv(params, ['Deploy?']);
46
- assert.equal(result['subtitle'], undefined);
47
- assert.equal(result['body'], undefined);
48
- });
49
- test('missing title throws missing_parameter', async () => {
50
- await assert.rejects(() => parseArgv(params, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
51
- });
52
- });
53
- // ---------------------------------------------------------------------------
54
28
  // human review — positional file (path) + optional --output
55
29
  // ---------------------------------------------------------------------------
56
30
  describe('human review: params', () => {
@@ -172,6 +146,34 @@ describe('human ask: params', () => {
172
146
  });
173
147
  });
174
148
  // ---------------------------------------------------------------------------
149
+ // human ask — shorthand question + strict --context-file deck
150
+ // ---------------------------------------------------------------------------
151
+ describe('human ask: shorthand', () => {
152
+ test('help documents both the bare question and --context-file forms', () => {
153
+ const out = renderLeafArgv(humanAsk.help);
154
+ assert.match(out, /QUESTION/);
155
+ assert.match(out, /--context-file PATH/);
156
+ assert.match(out, /crtr human ask "<question>"/);
157
+ assert.match(out, /crtr human ask --context-file deck\.json/);
158
+ });
159
+ test('parses a positional question into input.question', async () => {
160
+ const result = await parseArgv(humanAsk.help.params ?? [], ['Should we ship today?']);
161
+ assert.equal(result['question'], 'Should we ship today?');
162
+ assert.equal(result['wait'], false);
163
+ });
164
+ test('parses --context-file into input.deck while keeping the strict deck path', async () => {
165
+ const deck = {
166
+ title: 'Choose a path',
167
+ interactions: [
168
+ { id: 'q', title: 'Pick one', options: [{ id: 'yes', label: 'Yes' }] },
169
+ ],
170
+ };
171
+ const path = tmpJson(deck);
172
+ const result = await parseArgv(humanAsk.help.params ?? [], ['--context-file', path]);
173
+ assert.deepEqual(result['deck'], deck);
174
+ });
175
+ });
176
+ // ---------------------------------------------------------------------------
175
177
  // human cancel — positional job_id + optional --reason
176
178
  // ---------------------------------------------------------------------------
177
179
  describe('human cancel: params', () => {
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { buildCorpus, } from '../../core/canvas/index.js';
4
4
  const TYPES = ['report', 'doc', 'roadmap', 'meta'];
5
- const NODE_KINDS = ['developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
5
+ const NODE_KINDS = ['advisor', 'developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
6
6
  const STATUSES = ['active', 'idle', 'done', 'dead', 'canceled'];
7
7
  const SORTS = ['relevance', 'recency', 'oldest'];
8
8
  const REL_UNITS = {
@@ -9,15 +9,12 @@ import { defineBranch } from '../core/command.js';
9
9
  import { searchLeaf } from './canvas-history/search.js';
10
10
  import { readLeaf } from './canvas-history/read.js';
11
11
  import { showLeaf } from './canvas-history/show.js';
12
- import { callerCwd, corpusStats } from '../core/canvas/index.js';
13
- /** The bounded per-cwd `<corpus>` aggregate for the branch -h pre-empts a
14
- * `search` call just to gauge the corpus (cli-design §15: aggregate, never
15
- * enumerate). Soft-fails to omission if the db is unreachable. */
12
+ /** A tiny per-cwd `<corpus>` marker for branch -h — just enough context to
13
+ * anchor the help text without scanning nodes or files. Uses only the injected
14
+ * node cwd env (or the process cwd outside a node), so help stays cheap. */
16
15
  function corpusBlock() {
17
- const cwd = callerCwd();
18
- const s = corpusStats(cwd);
19
- const span = s.span !== null ? `${s.span.from} → ${s.span.to}` : 'n/a';
20
- return `<corpus cwd="${cwd}" nodes="${s.nodes}" reports="${s.reports}" docs="${s.docs}" span="${span}"/>`;
16
+ const cwd = process.env['CRTR_NODE_CWD'] ?? process.cwd();
17
+ return `<corpus cwd="${cwd}"/>`;
21
18
  }
22
19
  export const historyBranch = defineBranch({
23
20
  name: 'history',
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const issueNewLeaf: LeafDef;