@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
@@ -82,6 +82,13 @@ export interface PruneResult {
82
82
  pruned: PrunedNode[];
83
83
  dryRun: boolean;
84
84
  }
85
+ /** Hard-delete ONE node: drop its row (the edges→nodes FK, ON DELETE CASCADE,
86
+ * GCs its edges) and remove its on-disk `nodes/<id>/` dir. The single-node
87
+ * analogue of {@link pruneNodes}, for IMMEDIATE reaping — e.g. an empty node the
88
+ * user closed or detached from. Pure persisted-state removal: the caller MUST
89
+ * have already torn down the broker engine + viewer (this does not signal any
90
+ * process). Best-effort dir removal after the row delete, mirroring pruneNodes. */
91
+ export declare function deleteNode(nodeId: string): void;
85
92
  /** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
86
93
  * `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
87
94
  * node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
@@ -4,8 +4,8 @@
4
4
  // Source-of-truth split: a node's meta.json is canonical for its own fields;
5
5
  // the db row is a queryable index re-derivable from it. The subscribes_to edges
6
6
  // are db-authoritative (mutable, many-writers — what WAL is for).
7
- import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync, } from 'node:fs';
8
- import { join } from 'node:path';
7
+ import { existsSync, readFileSync, renameSync, readdirSync, rmSync, openSync, writeSync, fsyncSync, closeSync, } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
9
  import { openDb } from './db.js';
10
10
  import { isPidAlive } from './pid.js';
11
11
  import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, } from './paths.js';
@@ -40,14 +40,53 @@ function readMeta(nodeId) {
40
40
  // Legacy metas may still carry runtime fields on disk; toIdentity-on-read is
41
41
  // unnecessary (callers go through getNode, which overlays the row), but the
42
42
  // raw parse is typed as identity — extra props are ignored.
43
- return JSON.parse(readFileSync(p, 'utf8'));
43
+ //
44
+ // Tolerate an EMPTY or unparseable meta.json by treating it as MISSING (null)
45
+ // rather than throwing. A bare write-temp-then-rename without fsync can leave a
46
+ // 0-byte meta.json on an NFS-backed home after the holding process/sandbox dies
47
+ // (see writeMeta + the Hearth M0 cold-boot bug); a single corrupt node must not
48
+ // crash rebuildIndex/revive for the whole canvas. Recovery (re-seed from a
49
+ // durable snapshot) is the caller's job.
50
+ const raw = readFileSync(p, 'utf8');
51
+ if (raw.trim() === '')
52
+ return null;
53
+ try {
54
+ return JSON.parse(raw);
55
+ }
56
+ catch {
57
+ return null;
58
+ }
44
59
  }
45
- /** Serialize ONLY the identity subset → meta.json never holds runtime fields. */
60
+ /** Serialize ONLY the identity subset → meta.json never holds runtime fields.
61
+ *
62
+ * Durable write: fsync the temp file before the rename, then fsync the parent
63
+ * directory so the rename itself is on disk. A bare writeFileSync+rename can
64
+ * leave a 0-byte meta.json on an NFS-backed home (Hearth's durable Volume mounts
65
+ * `local_lock=none,noac`): when meta.json is held open, the rename silly-renames
66
+ * the open inode and the new content is never flushed, so a sandbox
67
+ * recreate-from-template loses it. meta.json is written rarely (birth, polymorph,
68
+ * session-id capture, naming) so the per-write fsync cost is negligible.
69
+ * Bug: Hearth M0 cold-boot — 0-byte meta.json → rebuild-index/revive JSON.parse(''). */
46
70
  function writeMeta(meta) {
47
71
  const p = nodeMetaPath(meta.node_id);
48
72
  const tmp = `${p}.tmp`;
49
- writeFileSync(tmp, JSON.stringify(toIdentity(meta), null, 2));
73
+ const data = JSON.stringify(toIdentity(meta), null, 2);
74
+ const fd = openSync(tmp, 'w');
75
+ try {
76
+ writeSync(fd, data);
77
+ fsyncSync(fd);
78
+ }
79
+ finally {
80
+ closeSync(fd);
81
+ }
50
82
  renameSync(tmp, p);
83
+ const dirFd = openSync(dirname(p), 'r');
84
+ try {
85
+ fsyncSync(dirFd);
86
+ }
87
+ finally {
88
+ closeSync(dirFd);
89
+ }
51
90
  }
52
91
  // ---------------------------------------------------------------------------
53
92
  // row index — identity columns are a derived projection of meta; runtime
@@ -356,6 +395,16 @@ export function rebuildIndex() {
356
395
  recordSpawn(meta.node_id, prov);
357
396
  }
358
397
  }
398
+ /** Hard-delete ONE node: drop its row (the edges→nodes FK, ON DELETE CASCADE,
399
+ * GCs its edges) and remove its on-disk `nodes/<id>/` dir. The single-node
400
+ * analogue of {@link pruneNodes}, for IMMEDIATE reaping — e.g. an empty node the
401
+ * user closed or detached from. Pure persisted-state removal: the caller MUST
402
+ * have already torn down the broker engine + viewer (this does not signal any
403
+ * process). Best-effort dir removal after the row delete, mirroring pruneNodes. */
404
+ export function deleteNode(nodeId) {
405
+ openDb().prepare('DELETE FROM nodes WHERE node_id = ?').run(nodeId);
406
+ rmSync(nodeDir(nodeId), { recursive: true, force: true });
407
+ }
359
408
  /** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
360
409
  * `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
361
410
  * node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
@@ -44,15 +44,27 @@ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
44
44
  CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
45
45
  `);
46
46
  }
47
+ /** The node row's current columns, used to make additive migrations safe to
48
+ * re-run against a partially applied schema. */
49
+ function nodeColumns(db) {
50
+ return new Set(db.prepare('PRAGMA table_info(nodes)').all().map((r) => r.name));
51
+ }
52
+ function addNodeColumnIfMissing(db, columns, name, ddl) {
53
+ if (columns.has(name))
54
+ return;
55
+ db.exec(ddl);
56
+ columns.add(name);
57
+ }
47
58
  /** v2 — additive runtime columns the keystone (Phase 2) will make
48
59
  * authoritative: `intent, pi_pid, window, tmux_session`. `status` already
49
60
  * lives in the baseline row, so it is NOT re-added here. All four default to
50
61
  * NULL, so nothing observes a behavior change until a later phase reads them. */
51
62
  function addRuntimeColumns(db) {
52
- db.exec(`ALTER TABLE nodes ADD COLUMN intent TEXT;`);
53
- db.exec(`ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;`);
54
- db.exec(`ALTER TABLE nodes ADD COLUMN window TEXT;`);
55
- db.exec(`ALTER TABLE nodes ADD COLUMN tmux_session TEXT;`);
63
+ const columns = nodeColumns(db);
64
+ addNodeColumnIfMissing(db, columns, 'intent', `ALTER TABLE nodes ADD COLUMN intent TEXT;`);
65
+ addNodeColumnIfMissing(db, columns, 'pi_pid', `ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;`);
66
+ addNodeColumnIfMissing(db, columns, 'window', `ALTER TABLE nodes ADD COLUMN window TEXT;`);
67
+ addNodeColumnIfMissing(db, columns, 'tmux_session', `ALTER TABLE nodes ADD COLUMN tmux_session TEXT;`);
56
68
  }
57
69
  /** v3 — DATA backfill (keystone, Phase 2). The runtime fields
58
70
  * (`intent, pi_pid, window, tmux_session`) become authoritative in the row;
@@ -190,7 +202,8 @@ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
190
202
  * Defaults NULL — nothing reads it until the placement layer lands, so this
191
203
  * observes no behavior change. Additive, forward-only. */
192
204
  function addPaneColumn(db) {
193
- db.exec(`ALTER TABLE nodes ADD COLUMN pane TEXT;`);
205
+ const columns = nodeColumns(db);
206
+ addNodeColumnIfMissing(db, columns, 'pane', `ALTER TABLE nodes ADD COLUMN pane TEXT;`);
194
207
  }
195
208
  /** v6 — the `focuses` table: durable, PLURAL on-screen viewports, one row per
196
209
  * viewport (Q7 widens canvas.db from "topology" to "topology + focuses"). Each
@@ -250,7 +263,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_wakeups_deadline ON wakeups(node_id) WHERE
250
263
  * Defaults NULL ⇒ `'tmux'` (every existing node is tmux-hosted); no data
251
264
  * backfill. Additive, forward-only. */
252
265
  function addHostKindColumn(db) {
253
- db.exec(`ALTER TABLE nodes ADD COLUMN host_kind TEXT;`);
266
+ const columns = nodeColumns(db);
267
+ addNodeColumnIfMissing(db, columns, 'host_kind', `ALTER TABLE nodes ADD COLUMN host_kind TEXT;`);
254
268
  }
255
269
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
256
270
  * `user_version` tracks how many have been applied. Append only. */
@@ -284,8 +298,21 @@ const handles = new Map();
284
298
  export function openDb() {
285
299
  const path = canvasDbPath();
286
300
  const existing = handles.get(path);
287
- if (existing)
288
- return existing;
301
+ if (existing) {
302
+ try {
303
+ existing.prepare('PRAGMA database_list').all();
304
+ return existing;
305
+ }
306
+ catch {
307
+ try {
308
+ existing.close();
309
+ }
310
+ catch {
311
+ /* ignore */
312
+ }
313
+ handles.delete(path);
314
+ }
315
+ }
289
316
  ensureHome();
290
317
  const db = new DatabaseSync(path);
291
318
  db.exec('PRAGMA journal_mode = WAL;');
@@ -15,13 +15,11 @@ export interface NamedNode {
15
15
  * message). Never empty. */
16
16
  export declare function fullName(node: NamedNode): string;
17
17
  /** The pi session display name — the editor label in the top-left. Format is
18
- * `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
19
- * plus the pi-generated description of its first task (see fullName) and
20
- * `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
21
- * fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
22
- * cycle working the auth refactor. The name segment is omitted while it
23
- * collapses to the bare kind (a bare root before its first message is named).
18
+ * `<fullName> <kind> (<mode>)`, where `<fullName>` is the node's handle plus
19
+ * the pi-generated description of its first task (see fullName). The name
20
+ * segment is omitted while it collapses to the bare kind (a bare root before
21
+ * its first message is named), so the fallback is just `<kind> (<mode>)`.
24
22
  * Recomputed from meta on every revive (and pushed live via pi.setSessionName
25
- * when a bare root is named mid-session), so a base→orchestrator polymorph, a
26
- * fresh cycle, or a first-message naming all update the label. */
23
+ * when a bare root is named mid-session), so a base→orchestrator polymorph or
24
+ * a first-message naming update the label immediately. */
27
25
  export declare function editorLabel(meta: NodeMeta): string;
@@ -19,18 +19,15 @@ export function fullName(node) {
19
19
  return combined !== '' ? combined : node.name;
20
20
  }
21
21
  /** The pi session display name — the editor label in the top-left. Format is
22
- * `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
23
- * plus the pi-generated description of its first task (see fullName) and
24
- * `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
25
- * fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
26
- * cycle working the auth refactor. The name segment is omitted while it
27
- * collapses to the bare kind (a bare root before its first message is named).
22
+ * `<fullName> <kind> (<mode>)`, where `<fullName>` is the node's handle plus
23
+ * the pi-generated description of its first task (see fullName). The name
24
+ * segment is omitted while it collapses to the bare kind (a bare root before
25
+ * its first message is named), so the fallback is just `<kind> (<mode>)`.
28
26
  * Recomputed from meta on every revive (and pushed live via pi.setSessionName
29
- * when a bare root is named mid-session), so a base→orchestrator polymorph, a
30
- * fresh cycle, or a first-message naming all update the label. */
27
+ * when a bare root is named mid-session), so a base→orchestrator polymorph or
28
+ * a first-message naming update the label immediately. */
31
29
  export function editorLabel(meta) {
32
- const base = `${meta.kind} (${meta.mode})`;
33
30
  const full = fullName(meta);
34
- const cycle = meta.cycles ?? 0;
35
- return full !== '' && full !== meta.kind ? `${base} ${full} ${cycle}` : `${base} ${cycle}`;
31
+ const base = `${meta.kind} (${meta.mode})`;
32
+ return full !== '' && full !== meta.kind ? `${full} ${base}` : base;
36
33
  }
@@ -18,8 +18,9 @@ export declare const RED = "\u001B[31m";
18
18
  export declare const YELLOW = "\u001B[33m";
19
19
  export declare const CYAN = "\u001B[36m";
20
20
  export declare const GRAY = "\u001B[90m";
21
- /** Status glyph colored by state: active green, idle dim, done cyan, dead red. */
22
- export declare function coloredGlyph(node: NodeMeta | null): string;
21
+ /** Overlay-aware glyph for a node: hanging outranks streaming ⟳, which
22
+ * outranks the base status glyph. */
23
+ export declare function nodeGlyph(node: NodeMeta | null): string;
23
24
  /** Visible width, ignoring ANSI escapes. */
24
25
  export declare function visibleWidth(s: string): number;
25
26
  /** Truncate to `max` VISIBLE columns: escape sequences are copied through
@@ -151,8 +152,8 @@ export declare function computeDefaultExpanded(root: string, self: string): Set<
151
152
  export declare function buildGraphModel(self: string, folds: FoldState, expand?: ReadonlySet<string>): FlatRow[];
152
153
  /** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
153
154
  * (human-watched) node → a coloured background bar; SELF → bold name. The
154
- * cursor outranks the attached tint when both land on the same row. Running
155
- * is signaled by the dot glyph alone ( green = active engine). */
155
+ * cursor outranks the attached tint when both land on the same row. The
156
+ * overlay-aware glyph alone carries the live signal (status / / ). */
156
157
  export declare function renderGraphRow(r: FlatRow, isCursor: boolean, focused: ReadonlySet<string>, activeBelow: ReadonlyMap<string, number>, asks: Record<string, number>): string;
157
158
  /** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
158
159
  * MAX_WIDGET_LINES — anything past that pi truncates itself, eating our own
@@ -15,6 +15,11 @@ import { execFileSync } from 'node:child_process';
15
15
  import { existsSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { getNode, subscribersOf, subscriptionsOf, jobDir, listFocuses } from './index.js';
18
+ import { isPidAlive } from './pid.js';
19
+ import { isBusy } from '../runtime/busy.js';
20
+ import { readFault } from '../runtime/fault.js';
21
+ import { activeFaultForDisplay } from './render.js';
22
+ import { resolveNodeVisual, faultSummary } from './status-glyph.js';
18
23
  // ---------------------------------------------------------------------------
19
24
  // Tuning constants
20
25
  // ---------------------------------------------------------------------------
@@ -27,9 +32,9 @@ export const VIEWPORT_FALLBACK_ROWS = 30;
27
32
  // ANSI styling. pi renders embedded escapes in widget lines and measures width
28
33
  // ANSI-aware, so raw escapes are safe and need no pi-tui dependency. The cursor
29
34
  // (selected row) uses a theme-agnostic ATTRIBUTE (reverse), so it reads under
30
- // NO_COLOR; the attached-row tint is a background COLOUR, while the dot glyph
31
- // (●/○/✓/✗) carries the running signal independently, even where colour is
32
- // stripped.
35
+ // NO_COLOR; the attached-row tint is a background COLOUR, while the overlay-
36
+ // aware glyph (●/○/✓/✗/⟳/⚠) carries the live signal independently, even where
37
+ // colour is stripped.
33
38
  // ---------------------------------------------------------------------------
34
39
  export const ESC = '\x1b[';
35
40
  export const RESET = `${ESC}0m`;
@@ -45,18 +50,25 @@ export const RED = `${ESC}31m`;
45
50
  export const YELLOW = `${ESC}33m`;
46
51
  export const CYAN = `${ESC}36m`;
47
52
  export const GRAY = `${ESC}90m`;
48
- /** Status glyph colored by state: active green, idle dim, done cyan, dead red. */
49
- export function coloredGlyph(node) {
53
+ function hangingFor(node) {
54
+ return activeFaultForDisplay(node, readFault(node.node_id));
55
+ }
56
+ function streamingFor(node) {
57
+ if (node.status !== 'active' && node.status !== 'idle')
58
+ return false;
59
+ if (!isPidAlive(node.pi_pid))
60
+ return false;
61
+ // `isBusy` (the canonical mid-turn marker) AND-ed with the live pid above —
62
+ // a stale marker from a crashed pi fails the pid check, never the existsSync.
63
+ return isBusy(node.node_id);
64
+ }
65
+ /** Overlay-aware glyph for a node: hanging ⚠ outranks streaming ⟳, which
66
+ * outranks the base status glyph. */
67
+ export function nodeGlyph(node) {
50
68
  if (node === null)
51
69
  return '?';
52
- switch (node.status) {
53
- case 'active': return `${GREEN}●${RESET}`;
54
- case 'idle': return `${GRAY}○${RESET}`;
55
- case 'done': return `${CYAN}✓${RESET}`;
56
- case 'dead': return `${RED}✗${RESET}`;
57
- case 'canceled': return `${YELLOW}⊘${RESET}`;
58
- default: return '?';
59
- }
70
+ const v = resolveNodeVisual(node.status, { streaming: streamingFor(node), hanging: hangingFor(node) });
71
+ return `${ESC}${v.color}m${v.glyph}${RESET}`;
60
72
  }
61
73
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
62
74
  /** Visible width, ignoring ANSI escapes. */
@@ -451,8 +463,8 @@ export function buildGraphModel(self, folds, expand) {
451
463
  }
452
464
  /** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ATTACHED
453
465
  * (human-watched) node → a coloured background bar; SELF → bold name. The
454
- * cursor outranks the attached tint when both land on the same row. Running
455
- * is signaled by the dot glyph alone ( green = active engine). */
466
+ * cursor outranks the attached tint when both land on the same row. The
467
+ * overlay-aware glyph alone carries the live signal (status / / ). */
456
468
  export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
457
469
  const wrap = (line, attached) => isCursor ? fillBar(line, fillWidth(), REVERSE)
458
470
  : attached ? fillBar(line, fillWidth(), BG_ATTACHED)
@@ -462,7 +474,7 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
462
474
  return wrap(line, false);
463
475
  }
464
476
  const node = cNode(r.id);
465
- const dot = coloredGlyph(node);
477
+ const dot = nodeGlyph(node);
466
478
  const rawName = navLabel(node, r.id);
467
479
  const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
468
480
  const kind = `${DIM}${node?.kind ?? ''}${RESET}`;
@@ -473,7 +485,9 @@ export function renderGraphRow(r, isCursor, focused, activeBelow, asks) {
473
485
  const expandable = r.hasKids && r.collapsed;
474
486
  const caret = !isCursor && expandable ? `${DIM}▸${RESET} ` : ' ';
475
487
  const fold = expandable ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
476
- const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${fold}${askBadge(r.id, asks)}${activityCell(r.id, node)}`;
488
+ const fault = node !== null ? activeFaultForDisplay(node, readFault(r.id)) : null;
489
+ const faultSuffix = fault !== null ? ` · ${faultSummary(fault)}` : '';
490
+ const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${cycleBadge(node)}${childBadge(node)}${liveBelowBadge(node, activeBelow)}${fold}${askBadge(r.id, asks)}${activityCell(r.id, node)}${faultSuffix}`;
477
491
  return wrap(line, isAttached(r.id, node, focused));
478
492
  }
479
493
  /** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
@@ -1,9 +1,26 @@
1
1
  /** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
2
2
  * Nested under the `.crouter` scope root so the whole runtime lives in one
3
3
  * visible top-level dir; `canvas/` keeps node-graph runtime state separate from
4
- * durable user content (skills/plugins/marketplaces/config) at the scope root. */
4
+ * durable user content (memory/plugins/marketplaces/config) at the scope root. */
5
5
  export declare function crtrHome(): string;
6
+ /** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
7
+ * the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
8
+ * (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
9
+ * other node data under `crtrHome()`. This split exists for the Hearth guest:
10
+ * node data (meta.json/sessions, plain-file writes) stays on the durable NFS
11
+ * Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL locks would hang on
12
+ * NFS (`local_lock=none`) — so it must live on local disk. Parent dir is
13
+ * created on use so an unset-default and an override both just work. */
6
14
  export declare function canvasDbPath(): string;
15
+ /** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
16
+ * the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
17
+ * `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
18
+ * Sibling of `CRTR_CANVAS_DB`, and exists for the same Hearth-guest reason: an
19
+ * AF_UNIX socket cannot be bound on the durable NFS Volume (`bind()` → EACCES,
20
+ * even where plain file writes into the same dir succeed), so the broker socket
21
+ * must live on local disk. Unset (the default) is unchanged for normal
22
+ * local-disk hosts. Parent dir is created on use so both paths just work. */
23
+ export declare function viewSocketPath(nodeId: string): string;
7
24
  export declare function nodesRoot(): string;
8
25
  export declare function nodeDir(nodeId: string): string;
9
26
  export declare function contextDir(nodeId: string): string;
@@ -13,22 +13,51 @@
13
13
  //
14
14
  // `CRTR_HOME` overrides the root (used by tests and isolated runs).
15
15
  import { homedir } from 'node:os';
16
- import { join } from 'node:path';
16
+ import { dirname, join } from 'node:path';
17
17
  import { mkdirSync } from 'node:fs';
18
18
  import { CRTR_DIR_NAME } from '../../types.js';
19
19
  /** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
20
20
  * Nested under the `.crouter` scope root so the whole runtime lives in one
21
21
  * visible top-level dir; `canvas/` keeps node-graph runtime state separate from
22
- * durable user content (skills/plugins/marketplaces/config) at the scope root. */
22
+ * durable user content (memory/plugins/marketplaces/config) at the scope root. */
23
23
  export function crtrHome() {
24
24
  const override = process.env['CRTR_HOME'];
25
25
  return override !== undefined && override !== ''
26
26
  ? override
27
27
  : join(homedir(), CRTR_DIR_NAME, 'canvas');
28
28
  }
29
+ /** Absolute path to the SQLite canvas db file. Defaults to `canvas.db` under
30
+ * the canvas home, but `CRTR_CANVAS_DB` overrides ONLY this file's location
31
+ * (its `-wal`/`-shm` siblings follow it) while leaving `nodesRoot()` and all
32
+ * other node data under `crtrHome()`. This split exists for the Hearth guest:
33
+ * node data (meta.json/sessions, plain-file writes) stays on the durable NFS
34
+ * Volume via `CRTR_HOME`, but canvas.db's SQLite fcntl/WAL locks would hang on
35
+ * NFS (`local_lock=none`) — so it must live on local disk. Parent dir is
36
+ * created on use so an unset-default and an override both just work. */
29
37
  export function canvasDbPath() {
38
+ const override = process.env['CRTR_CANVAS_DB'];
39
+ if (override !== undefined && override !== '') {
40
+ mkdirSync(dirname(override), { recursive: true });
41
+ return override;
42
+ }
30
43
  return join(crtrHome(), 'canvas.db');
31
44
  }
45
+ /** Absolute path to a node's broker `view.sock`. Defaults to `view.sock` under
46
+ * the node dir, but `CRTR_SOCK_DIR` overrides the PARENT dir (the socket becomes
47
+ * `<CRTR_SOCK_DIR>/<nodeId>.sock`) while node data stays under `crtrHome()`.
48
+ * Sibling of `CRTR_CANVAS_DB`, and exists for the same Hearth-guest reason: an
49
+ * AF_UNIX socket cannot be bound on the durable NFS Volume (`bind()` → EACCES,
50
+ * even where plain file writes into the same dir succeed), so the broker socket
51
+ * must live on local disk. Unset (the default) is unchanged for normal
52
+ * local-disk hosts. Parent dir is created on use so both paths just work. */
53
+ export function viewSocketPath(nodeId) {
54
+ const override = process.env['CRTR_SOCK_DIR'];
55
+ if (override !== undefined && override !== '') {
56
+ mkdirSync(override, { recursive: true });
57
+ return join(override, `${nodeId}.sock`);
58
+ }
59
+ return join(nodeDir(nodeId), 'view.sock');
60
+ }
32
61
  export function nodesRoot() {
33
62
  return join(crtrHome(), 'nodes');
34
63
  }
@@ -1,4 +1,9 @@
1
+ import { type Fault } from '../runtime/fault.js';
1
2
  import type { NodeStatus, Lifecycle } from './types.js';
3
+ export declare function activeFaultForDisplay<T extends {
4
+ status: NodeStatus;
5
+ pi_pid?: number | null;
6
+ }>(node: T, fault: Fault | null): Fault | null;
2
7
  /**
3
8
  * Render the subscription sub-DAG rooted at `rootId` as an ASCII tree.
4
9
  * The root is the first line (no connector prefix); children are indented.
@@ -59,6 +64,13 @@ export interface DashboardRow {
59
64
  * live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
60
65
  * only (dormant rows are always false). */
61
66
  streaming?: boolean;
67
+ /** Set when the node is parked on an active fault. The otherwise-invisible
68
+ * "stuck, awaiting recovery" window. Mutually exclusive with `streaming`
69
+ * (hanging means the turn ended, so `busy` is gone); when set,
70
+ * dashboardRowsAll forces `streaming:false`. Computed on the cheap boot path
71
+ * for every node, then suppressed only for stale daemon-owned provider faults
72
+ * whose broker pid is gone. */
73
+ hanging?: Fault | null;
62
74
  /** True when a viewer (focus row) is attached to the node — i.e. someone has it
63
75
  * open on screen. Set on the cheap boot path from the one listFocuses() query. */
64
76
  viewed?: boolean;
@@ -70,6 +82,19 @@ export interface DashboardRow {
70
82
  * session (prompts + lastAssistant). Idempotency marker for the selected row. */
71
83
  previewLoaded?: boolean;
72
84
  }
85
+ /** Did this node's engine ever PRODUCE something — an assistant message with real
86
+ * text, a tool call, or non-empty reasoning? The signal that a node did work and
87
+ * is not an empty shell. A never-revived node (no session file) is trivially
88
+ * false; an assistant turn that is only an empty/aborted `thinking` stub does NOT
89
+ * count (that's a node started and killed before it did anything). CONSERVATIVE
90
+ * by design: ANY substance keeps the node, so the reap never deletes real work
91
+ * — a tool-call-only turn (no final text) still counts. Drives reap-on-close/
92
+ * detach. One file read with an early exit on the first substantive turn. */
93
+ export declare function nodeHasAssistantMessage(sessionFile: string | null | undefined): boolean;
94
+ /** {@link isStreaming} keyed by node id alone — hydrates the broker pid off the
95
+ * db row. The exported "is this node actively generating right now?" guard so a
96
+ * reap never nukes a node mid-first-turn before its output lands on disk. */
97
+ export declare function isNodeStreaming(nodeId: string): boolean;
73
98
  /** One row per node visible in the sub-DAG of `rootId` (including root). */
74
99
  export declare function dashboardRows(rootId: string): DashboardRow[];
75
100
  /**
@@ -20,16 +20,22 @@ import { jobDir, contextDir } from './paths.js';
20
20
  import { countAsks, asksForNodes } from './attention.js';
21
21
  import { isPidAlive } from './pid.js';
22
22
  import { listFocuses } from './focuses.js';
23
+ import { resolveNodeVisual, faultSummary } from './status-glyph.js';
24
+ import { readFault } from '../runtime/fault.js';
23
25
  // ---------------------------------------------------------------------------
24
- // Glyphs
26
+ // Active-fault overlay: stale daemon-owned provider faults are hidden once the
27
+ // broker pid is gone; manual/client faults stay visible until cleared.
25
28
  // ---------------------------------------------------------------------------
26
- const STATUS_GLYPH = {
27
- active: '●',
28
- idle: '○',
29
- done: '',
30
- dead: '✗',
31
- canceled: '⊘',
32
- };
29
+ export function activeFaultForDisplay(node, fault) {
30
+ if (fault === null)
31
+ return null;
32
+ if (fault.link === 'pi→provider' && fault.retry.by === 'daemon' && fault.retry.disposition === 'auto' && !isPidAlive(node.pi_pid))
33
+ return null;
34
+ return fault;
35
+ }
36
+ function hangingFor(node) {
37
+ return activeFaultForDisplay(node, readFault(node.node_id));
38
+ }
33
39
  function readNodeTelemetry(nodeId) {
34
40
  const path = join(jobDir(nodeId), 'telemetry.json');
35
41
  if (!existsSync(path))
@@ -57,12 +63,14 @@ function nodeLine(nodeId, indent, connector) {
57
63
  // Node id is in the db but meta.json is gone — paranoid guard.
58
64
  return `${indent}${connector}? <missing meta: ${nodeId}>`;
59
65
  }
60
- const glyph = STATUS_GLYPH[node.status] ?? '?';
66
+ const hanging = hangingFor(node);
67
+ const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
61
68
  const tel = readNodeTelemetry(nodeId);
62
69
  const ctx = fmtCtx(tel.tokens_in);
63
70
  const asks = countAsks(nodeId);
64
71
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
65
- return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`;
72
+ const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
73
+ return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
66
74
  }
67
75
  /**
68
76
  * Recursively walk the subscription sub-DAG rooted at `nodeId`, appending
@@ -112,9 +120,11 @@ export function renderTree(rootId) {
112
120
  const ctx = fmtCtx(tel.tokens_in);
113
121
  const asks = countAsks(rootId);
114
122
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
115
- const glyph = STATUS_GLYPH[node.status] ?? '?';
123
+ const hanging = hangingFor(node);
124
+ const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
125
+ const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
116
126
  const out = [];
117
- out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
127
+ out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`);
118
128
  // visited starts with root already rendered (walkTree doesn't re-emit root).
119
129
  const visited = new Set([rootId]);
120
130
  const children = subscriptionsOf(rootId);
@@ -261,6 +271,77 @@ function readSessionParts(sessionFile) {
261
271
  }
262
272
  return { prompts, lastAssistant };
263
273
  }
274
+ /** Did this node's engine ever PRODUCE something — an assistant message with real
275
+ * text, a tool call, or non-empty reasoning? The signal that a node did work and
276
+ * is not an empty shell. A never-revived node (no session file) is trivially
277
+ * false; an assistant turn that is only an empty/aborted `thinking` stub does NOT
278
+ * count (that's a node started and killed before it did anything). CONSERVATIVE
279
+ * by design: ANY substance keeps the node, so the reap never deletes real work
280
+ * — a tool-call-only turn (no final text) still counts. Drives reap-on-close/
281
+ * detach. One file read with an early exit on the first substantive turn. */
282
+ export function nodeHasAssistantMessage(sessionFile) {
283
+ if (sessionFile === undefined || sessionFile === null || sessionFile === '')
284
+ return false;
285
+ let lines;
286
+ try {
287
+ if (!existsSync(sessionFile))
288
+ return false;
289
+ lines = readFileSync(sessionFile, 'utf8').split('\n');
290
+ }
291
+ catch {
292
+ return false;
293
+ }
294
+ for (const line of lines) {
295
+ if (line === '' || (line.indexOf('"role":"assistant"') === -1 && line.indexOf('"role": "assistant"') === -1))
296
+ continue;
297
+ let rec;
298
+ try {
299
+ rec = JSON.parse(line);
300
+ }
301
+ catch {
302
+ continue;
303
+ }
304
+ if (rec.type !== 'message' || rec.message?.role !== 'assistant')
305
+ continue;
306
+ if (assistantContentIsSubstantive(rec.message.content))
307
+ return true;
308
+ }
309
+ return false;
310
+ }
311
+ /** True when an assistant message's content has at least one block that PRODUCED
312
+ * something: non-empty text, non-empty thinking, a tool call (`toolCall`/
313
+ * `tool_use`/…), or any other non-thinking/text block type. The lone non-case is
314
+ * an empty thinking stub (`[{type:'thinking',thinking:''}]`) or empty content —
315
+ * what an immediately-aborted first turn leaves behind. */
316
+ function assistantContentIsSubstantive(content) {
317
+ if (typeof content === 'string')
318
+ return content.trim() !== '';
319
+ if (!Array.isArray(content))
320
+ return false;
321
+ for (const block of content) {
322
+ if (block === null || typeof block !== 'object')
323
+ continue;
324
+ const b = block;
325
+ const t = (b.type ?? '').toLowerCase();
326
+ if (t === 'text') {
327
+ if (typeof b.text === 'string' && b.text.trim() !== '')
328
+ return true;
329
+ }
330
+ else if (t === 'thinking') {
331
+ if (typeof b.thinking === 'string' && b.thinking.trim() !== '')
332
+ return true;
333
+ }
334
+ else
335
+ return true; // toolCall / tool_use / toolResult / any other block = real output
336
+ }
337
+ return false;
338
+ }
339
+ /** {@link isStreaming} keyed by node id alone — hydrates the broker pid off the
340
+ * db row. The exported "is this node actively generating right now?" guard so a
341
+ * reap never nukes a node mid-first-turn before its output lands on disk. */
342
+ export function isNodeStreaming(nodeId) {
343
+ return isStreaming(nodeId, getNode(nodeId)?.pi_pid ?? null);
344
+ }
264
345
  /** Is the node GENUINELY mid-turn right now? The `busy` marker (touched on
265
346
  * agent_start, removed on agent_end) AND-ed with broker-pid liveness, so a stale
266
347
  * marker from a crashed pi reads false. This is the live "generating?" signal
@@ -354,6 +435,7 @@ export function dashboardRowsAll() {
354
435
  const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
355
436
  return listNodes().map((row) => {
356
437
  const live = row.status === 'active' || row.status === 'idle';
438
+ const hanging = activeFaultForDisplay(row, readFault(row.node_id));
357
439
  return {
358
440
  node_id: row.node_id,
359
441
  name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
@@ -366,7 +448,8 @@ export function dashboardRowsAll() {
366
448
  created: row.created,
367
449
  lifecycle: row.lifecycle,
368
450
  mtimeMs: sessionMtime(row.node_id, row.created),
369
- streaming: live ? isStreaming(row.node_id, row.pi_pid) : false,
451
+ streaming: hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
452
+ hanging,
370
453
  viewed: focusedNodeIds.has(row.node_id),
371
454
  };
372
455
  });