@crouton-kit/crouter 0.3.31 → 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 (443) 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 +9 -3
  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 +37 -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/__tests__/empty-node-guard.test.d.ts +1 -0
  230. package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
  231. package/dist/core/feed/feed.js +8 -0
  232. package/dist/core/feed/inbox.d.ts +15 -0
  233. package/dist/core/feed/inbox.js +40 -14
  234. package/dist/core/frontmatter.d.ts +1 -11
  235. package/dist/core/frontmatter.js +1 -55
  236. package/dist/core/hearth/config.d.ts +2 -0
  237. package/dist/core/hearth/config.js +75 -0
  238. package/dist/core/hearth/guest-env.d.ts +2 -0
  239. package/dist/core/hearth/guest-env.js +11 -0
  240. package/dist/core/hearth/index.d.ts +5 -0
  241. package/dist/core/hearth/index.js +5 -0
  242. package/dist/core/hearth/provider.d.ts +21 -0
  243. package/dist/core/hearth/provider.js +10 -0
  244. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  245. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  246. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  247. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  248. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  249. package/dist/core/hearth/providers/blaxel.js +208 -0
  250. package/dist/core/hearth/providers/types.d.ts +72 -0
  251. package/dist/core/hearth/providers/types.js +1 -0
  252. package/dist/core/hearth/registry.d.ts +15 -0
  253. package/dist/core/hearth/registry.js +179 -0
  254. package/dist/core/hearth/types.d.ts +121 -0
  255. package/dist/core/hearth/types.js +1 -0
  256. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  257. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  258. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  259. package/dist/core/host-exports/builtins.js +68 -0
  260. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  261. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  262. package/dist/core/io.js +1 -1
  263. package/dist/core/log.d.ts +9 -0
  264. package/dist/core/log.js +113 -0
  265. package/dist/core/memory-resolver.d.ts +8 -8
  266. package/dist/core/memory-resolver.js +15 -28
  267. package/dist/core/personas/index.d.ts +4 -4
  268. package/dist/core/personas/index.js +2 -2
  269. package/dist/core/personas/loader.d.ts +51 -0
  270. package/dist/core/personas/loader.js +54 -50
  271. package/dist/core/personas/resolve.d.ts +43 -27
  272. package/dist/core/personas/resolve.js +336 -94
  273. package/dist/core/runtime/auth-reload.d.ts +7 -0
  274. package/dist/core/runtime/auth-reload.js +69 -0
  275. package/dist/core/runtime/bearings.d.ts +35 -15
  276. package/dist/core/runtime/bearings.js +305 -44
  277. package/dist/core/runtime/branded-host.d.ts +4 -2
  278. package/dist/core/runtime/branded-host.js +66 -4
  279. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  280. package/dist/core/runtime/broker.js +343 -18
  281. package/dist/core/runtime/close.js +37 -29
  282. package/dist/core/runtime/connectivity.d.ts +12 -0
  283. package/dist/core/runtime/connectivity.js +73 -0
  284. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  285. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  286. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  287. package/dist/core/runtime/fault-recovery.js +74 -0
  288. package/dist/core/runtime/fault.d.ts +25 -0
  289. package/dist/core/runtime/fault.js +176 -0
  290. package/dist/core/runtime/front-door.d.ts +1 -1
  291. package/dist/core/runtime/front-door.js +2 -2
  292. package/dist/core/runtime/host.d.ts +10 -0
  293. package/dist/core/runtime/host.js +71 -9
  294. package/dist/core/runtime/kickoff.js +31 -45
  295. package/dist/core/runtime/launch.d.ts +32 -18
  296. package/dist/core/runtime/launch.js +148 -43
  297. package/dist/core/runtime/model-swap.d.ts +18 -0
  298. package/dist/core/runtime/model-swap.js +95 -0
  299. package/dist/core/runtime/naming.d.ts +7 -0
  300. package/dist/core/runtime/naming.js +61 -9
  301. package/dist/core/runtime/nodes.js +6 -1
  302. package/dist/core/runtime/persona.js +16 -17
  303. package/dist/core/runtime/placement.d.ts +36 -31
  304. package/dist/core/runtime/placement.js +149 -64
  305. package/dist/core/runtime/promote.d.ts +2 -0
  306. package/dist/core/runtime/promote.js +23 -3
  307. package/dist/core/runtime/recycle.js +6 -4
  308. package/dist/core/runtime/reset.d.ts +5 -2
  309. package/dist/core/runtime/reset.js +2 -2
  310. package/dist/core/runtime/revive.d.ts +4 -0
  311. package/dist/core/runtime/revive.js +27 -9
  312. package/dist/core/runtime/roadmap.d.ts +1 -1
  313. package/dist/core/runtime/roadmap.js +1 -1
  314. package/dist/core/runtime/spawn.d.ts +2 -2
  315. package/dist/core/runtime/spawn.js +30 -6
  316. package/dist/core/runtime/stop-guard.js +6 -1
  317. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  318. package/dist/core/runtime/tmux-chrome.js +1 -1
  319. package/dist/core/runtime/tmux.d.ts +29 -6
  320. package/dist/core/runtime/tmux.js +134 -80
  321. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  322. package/dist/core/runtime/view-socket-client.js +203 -0
  323. package/dist/core/substrate/index.d.ts +1 -1
  324. package/dist/core/substrate/index.js +1 -1
  325. package/dist/core/substrate/on-read.js +16 -4
  326. package/dist/core/substrate/render.d.ts +10 -13
  327. package/dist/core/substrate/render.js +56 -51
  328. package/dist/core/tui/host.js +83 -18
  329. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  330. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  331. package/dist/core/view/contract.d.ts +20 -0
  332. package/dist/core/view/stream-local.d.ts +3 -0
  333. package/dist/core/view/stream-local.js +184 -0
  334. package/dist/core/view/transport-cache.d.ts +8 -0
  335. package/dist/core/view/transport-cache.js +38 -0
  336. package/dist/core/view/transport-local.js +4 -0
  337. package/dist/core/view/transport.d.ts +7 -1
  338. package/dist/daemon/crtrd.d.ts +3 -19
  339. package/dist/daemon/crtrd.js +139 -178
  340. package/dist/daemon/manage.d.ts +18 -1
  341. package/dist/daemon/manage.js +54 -9
  342. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  343. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  344. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  345. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  346. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  347. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  348. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  349. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  350. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  351. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  352. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  353. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  354. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  355. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  356. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  357. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  358. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  359. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  360. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  361. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  362. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  363. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  364. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  365. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  366. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  367. package/dist/hearth/wake-proxy/auth.js +128 -0
  368. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  369. package/dist/hearth/wake-proxy/config.js +151 -0
  370. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  371. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  372. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  373. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  374. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  375. package/dist/hearth/wake-proxy/home.js +297 -0
  376. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  377. package/dist/hearth/wake-proxy/main.js +134 -0
  378. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  379. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  380. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  381. package/dist/hearth/wake-proxy/proxy.js +716 -0
  382. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  383. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  384. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  385. package/dist/hearth/wake-proxy/redact.js +17 -0
  386. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  387. package/dist/hearth/wake-proxy/server.js +142 -0
  388. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  389. package/dist/hearth/wake-proxy/state.js +342 -0
  390. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  391. package/dist/hearth/wake-proxy/types.js +1 -0
  392. package/dist/index.js +3 -2
  393. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  394. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  395. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  396. package/dist/pi-extensions/canvas-nav.js +15 -8
  397. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  398. package/dist/pi-extensions/canvas-stophook.js +78 -11
  399. package/dist/types.d.ts +39 -20
  400. package/dist/types.js +38 -13
  401. package/dist/web/runtime.js +141 -42
  402. package/dist/web/transport-http.js +7 -5
  403. package/dist/web/transport-stream.d.ts +3 -0
  404. package/dist/web/transport-stream.js +204 -0
  405. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  406. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  407. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  408. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  409. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  410. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  411. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  412. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  413. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  414. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  415. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  416. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  417. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  418. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  419. package/dist/web-client/index.html +3 -2
  420. package/package.json +32 -10
  421. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  422. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  423. package/dist/core/skill-sync/builtins.js +0 -112
  424. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  425. package/dist/core/skill-sync/claude-plugins.js +0 -71
  426. package/dist/core/skill-sync/engine.d.ts +0 -42
  427. package/dist/core/skill-sync/engine.js +0 -633
  428. package/dist/core/skill-sync/manifest.d.ts +0 -64
  429. package/dist/core/skill-sync/manifest.js +0 -181
  430. package/dist/core/skill-sync/profile.d.ts +0 -76
  431. package/dist/core/skill-sync/profile.js +0 -173
  432. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  433. package/dist/core/skill-sync/snapshot.js +0 -120
  434. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  435. package/dist/pi-extensions/canvas-commands.js +0 -113
  436. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  437. package/dist/pi-extensions/canvas-resume.js +0 -83
  438. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  439. package/dist/pi-extensions/canvas-view.js +0 -76
  440. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  441. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  442. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  443. /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,10 +1,10 @@
1
1
  // Session naming — turn a node's first prompt into a short, human-readable
2
2
  // handle for the editor label.
3
3
  //
4
- // A node's editor label is `<kind> (<mode>) <name> <cycle>` (see editorLabel in
5
- // launch.ts). The `<name>` is a 3-8 word kebab-case "description" derived from
4
+ // A node's editor label is `<name> <kind> (<mode>)` (see editorLabel in
5
+ // labels.ts). The `<name>` is a 3-8 word kebab-case "description" derived from
6
6
  // the first prompt by asking pi headlessly (`pi -p`), persisted on the node's
7
- // meta so it survives revives and shows in every cycle.
7
+ // meta so it survives revives and updates the live session name.
8
8
  //
9
9
  // One entry point: generateAndPersistName — async (execFile, non-blocking).
10
10
  // Naming happens INSIDE the named node's own pi process, off the first real
@@ -73,10 +73,10 @@ const DEFAULT_NAME_MODEL = 'anthropic/claude-haiku-4-5';
73
73
  * side-effect free. Pinned to Haiku with thinking off — naming is a trivial
74
74
  * classification that never needs a reasoning budget. Override the model with
75
75
  * CRTR_NAME_MODEL. */
76
- function nameArgs(prompt) {
76
+ function piTextArgs(systemPrompt, userPrompt) {
77
77
  const override = process.env['CRTR_NAME_MODEL'];
78
78
  const model = override !== undefined && override.trim() !== '' ? override.trim() : DEFAULT_NAME_MODEL;
79
- const argv = [
79
+ return [
80
80
  '-p',
81
81
  '--no-session',
82
82
  '--no-context-files',
@@ -86,13 +86,15 @@ function nameArgs(prompt) {
86
86
  '--no-themes',
87
87
  '--no-tools',
88
88
  '--mode', 'text',
89
- // Naming is a trivial one-line classification — no thinking budget, ever.
89
+ // A trivial one-line classification — no thinking budget, ever.
90
90
  '--thinking', 'off',
91
91
  '--model', model,
92
+ '--system-prompt', systemPrompt,
93
+ userPrompt,
92
94
  ];
93
- argv.push('--system-prompt', NAME_SYSTEM_PROMPT);
94
- argv.push(nameUserPrompt(prompt));
95
- return argv;
95
+ }
96
+ function nameArgs(prompt) {
97
+ return piTextArgs(NAME_SYSTEM_PROMPT, nameUserPrompt(prompt));
96
98
  }
97
99
  /** Ask pi headlessly for a kebab-case name for `body`, async. Resolves to the
98
100
  * sanitized name, or '' on any failure (non-zero exit, timeout, empty/garbled
@@ -148,3 +150,53 @@ export function generateAndPersistName(nodeId, prompt, onNamed) {
148
150
  persist(name !== '' ? name : slugFromPrompt(body));
149
151
  });
150
152
  }
153
+ // ---------------------------------------------------------------------------
154
+ // Issue titles — turn a free-text todo/idea/bug note into a short, human-readable
155
+ // GitHub-issue title (sentence case, NOT kebab). Used by `crtr canvas issue new`.
156
+ // ---------------------------------------------------------------------------
157
+ const TITLE_SYSTEM_PROMPT = 'You write GitHub issue titles. Given a free-text note (a todo, idea, or bug ' +
158
+ 'report), reply with ONLY a concise issue title: sentence case, 4-10 words, no ' +
159
+ 'trailing period, no quotes, no prefix like "Bug:" or "Idea:", no markdown. ' +
160
+ 'Capture the gist as an actionable title. Output JUST the title, nothing else.';
161
+ function titleUserPrompt(note) {
162
+ return `<note>\n${note.slice(0, PROMPT_CAP)}\n</note>\n\nWrite a concise GitHub issue title for the note above. Output JUST the title, nothing else.`;
163
+ }
164
+ /** First non-empty line of `note`, trimmed and clamped to ~80 chars — the local
165
+ * fallback title when the headless call fails. */
166
+ function firstLineTitle(note) {
167
+ const line = (note ?? '').split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
168
+ const clamped = line.length > 80 ? `${line.slice(0, 77).trimEnd()}…` : line;
169
+ return clamped !== '' ? clamped : 'untitled';
170
+ }
171
+ /** A filesystem-safe slug for the `.todos/<slug>.md` fallback path: lowercase,
172
+ * [a-z0-9] kept, everything else → single hyphen, clamped to 8 words. */
173
+ export function titleSlug(title) {
174
+ const words = (title ?? '')
175
+ .toLowerCase()
176
+ .replace(/[^a-z0-9]+/g, '-')
177
+ .split('-')
178
+ .filter((w) => w !== '');
179
+ return words.slice(0, 8).join('-') || 'untitled';
180
+ }
181
+ /** Ask pi headlessly for a one-line issue title for `note`. Resolves to the
182
+ * generated title, or a local first-line fallback on any failure (non-zero
183
+ * exit, timeout, empty output). Async; never throws. */
184
+ export function generateIssueTitle(note) {
185
+ const body = (note ?? '').trim();
186
+ if (body === '')
187
+ return Promise.resolve('untitled');
188
+ return new Promise((resolve) => {
189
+ try {
190
+ const child = execFile('pi', piTextArgs(TITLE_SYSTEM_PROMPT, titleUserPrompt(body)), { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
191
+ if (err || typeof stdout !== 'string')
192
+ return resolve(firstLineTitle(body));
193
+ const t = stdout.trim().split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
194
+ resolve(t !== '' ? t.replace(/^["']|["']$/g, '').replace(/\.$/, '') : firstLineTitle(body));
195
+ });
196
+ child.stdin?.end(); // immediate EOF — pi -p reads stdin (see headlessName)
197
+ }
198
+ catch {
199
+ resolve(firstLineTitle(body));
200
+ }
201
+ });
202
+ }
@@ -14,7 +14,7 @@
14
14
  // is also recorded.
15
15
  import { randomBytes } from 'node:crypto';
16
16
  import { mkdirSync } from 'node:fs';
17
- import { createNode, getNode, getRow, subscribe, recordSpawn, } from '../canvas/index.js';
17
+ import { createNode, getNode, getRow, subscribe, recordSpawn, contextDir, } from '../canvas/index.js';
18
18
  import { memoryDir } from './memory.js';
19
19
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
20
20
  export function newNodeId() {
@@ -59,6 +59,11 @@ export function nodeEnv(meta) {
59
59
  CRTR_MODE: meta.mode,
60
60
  CRTR_LIFECYCLE: meta.lifecycle,
61
61
  CRTR_NODE_CWD: meta.cwd,
62
+ // The node's own context dir (canvas home), as an ABSOLUTE path. The engine
63
+ // runs with cwd = CRTR_NODE_CWD (the project), so a relative `context/`
64
+ // resolves into the REPO, not here — agents must address artifacts via this.
65
+ CRTR_CONTEXT_DIR: contextDir(meta.node_id),
66
+ CRTR_CYCLES: (meta.cycles ?? 0).toString(),
62
67
  };
63
68
  if (meta.parent)
64
69
  env['CRTR_PARENT_NODE_ID'] = meta.parent;
@@ -28,11 +28,10 @@ import { orchestratorContextNote } from './bearings.js';
28
28
  // base→orchestrator guidance (the roadmap-shaping dump) — MOVED here from
29
29
  // promote.ts so the injector is the one place that builds it.
30
30
  // ---------------------------------------------------------------------------
31
- /** Coerce a frontmatter scalar to its string form, matching the legacy
32
- * hand-rolled parser. Strings pass through; number/boolean coerce via
33
- * String(); null/undefined and non-scalars are dropped (→ null). Without this,
34
- * the `yaml` package's native scalar types would let a numeric/boolean
35
- * `roadmapSkill` be silently dropped by a `typeof === 'string'` guard. */
31
+ /** Coerce a frontmatter scalar to its string form. Strings pass through;
32
+ * number/boolean coerce via String(); null/undefined and non-scalars are
33
+ * dropped (→ null). This lets numeric/boolean `roadmapKnowledge` knowledge-doc
34
+ * pointers resolve the same way string pointers do. */
36
35
  function scalarToString(v) {
37
36
  if (typeof v === 'string')
38
37
  return v;
@@ -40,10 +39,10 @@ function scalarToString(v) {
40
39
  return String(v);
41
40
  return null;
42
41
  }
43
- /** Load a substrate doc's body text by name, or null if it can't be resolved.
44
- * Used to inline a kind's roadmap-shaping skill into the orchestration
45
- * guidance — the skill now lives as a substrate memory doc. */
46
- function loadSkillBody(name) {
42
+ /** Load a knowledge doc's body text by name, or null if it can't be resolved.
43
+ * Used to inline a kind's roadmap-shaping knowledge doc into the orchestration
44
+ * guidance. */
45
+ function loadRoadmapKnowledgeBody(name) {
47
46
  try {
48
47
  return resolveMemoryDoc(name).body.trim();
49
48
  }
@@ -52,8 +51,8 @@ function loadSkillBody(name) {
52
51
  }
53
52
  }
54
53
  /** The base→orchestrator guidance dump, specialized to the node's kind: the
55
- * shared kernel + that kind's roadmap-shaping skill + the roadmap scaffold the
56
- * node must author + the orchestrator context-dir framing + a short pointer at
54
+ * shared kernel + that kind's roadmap-shaping memory doc + the roadmap scaffold
55
+ * the node must author + the orchestrator context-dir framing + a short pointer at
57
56
  * the substrate memory flow (promotion re-informs of nothing about the stores,
58
57
  * it only adds the across-cycles relevance). The node is now a delegator whose
59
58
  * scarce resource is its own context window. (Lifecycle is left to its own
@@ -61,8 +60,8 @@ function loadSkillBody(name) {
61
60
  function orchestrationGuidance(nodeId, kind) {
62
61
  const kernel = loadKernel();
63
62
  const orch = loadPersona(kind, 'orchestrator');
64
- const roadmapSkill = scalarToString(orch?.frontmatter?.['roadmapSkill']) ?? undefined;
65
- const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
63
+ const roadmapKnowledge = scalarToString(orch?.frontmatter?.['roadmapKnowledge']) ?? undefined;
64
+ const roadmapKnowledgeBody = roadmapKnowledge ? loadRoadmapKnowledgeBody(roadmapKnowledge) : null;
66
65
  const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
67
66
  const rmPath = roadmapPath(nodeId);
68
67
  const parts = [
@@ -71,14 +70,14 @@ function orchestrationGuidance(nodeId, kind) {
71
70
  '',
72
71
  kernel,
73
72
  ];
74
- if (skillBody) {
75
- parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
73
+ if (roadmapKnowledgeBody) {
74
+ parts.push('', `--- How to shape a ${kind} roadmap (knowledge: ${roadmapKnowledge}) ---`, '', roadmapKnowledgeBody);
76
75
  }
77
76
  parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '',
78
77
  // The orchestrator framing for the context dir — the missing guidance a
79
78
  // promoted node never got at spawn (it spawned as a base worker). Same note
80
- // a born-orchestrator gets in its <crtr-context> bearings block.
81
- orchestratorContextNote(nodeId), '', 'Your three long-term memory scopes (user-global, project, node-local) already surface in your `<crtr-context>` block. What promotion changes is USE, not access: you now persist across refresh cycles, so node-local carries what THIS goal needs between your refreshes, and durable facts you commit to user-global or project outlive this node. Use `crtr memory write` to save a document; `crtr memory list` to browse; `crtr memory find` to search (see "Your long-term memory" above for the full substrate flow).', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
79
+ // a born-orchestrator gets in its <crtr-bearings> block.
80
+ orchestratorContextNote(nodeId), '', 'Your three long-term memory scopes (user-global, project, node-local) already surface in your bearings at boot. What promotion changes is USE, not access: you now persist across refresh cycles, so node-local carries what THIS goal needs between your refreshes, and durable facts you commit to user-global or project outlive this node. Use `crtr memory write` to save a document; `crtr memory list` to browse; `crtr memory find` to search (see "Your long-term memory" above for the full substrate flow).', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
82
81
  return parts.join('\n');
83
82
  }
84
83
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { type FocusRow } from '../canvas/index.js';
2
2
  export type { FocusRow };
3
- export { splitWindow, focusWindow, selectWindow, switchClient, openNodeWindow, ensureSession, paneLocation, paneExists, paneRunning, paneCurrentPath, currentTmux, inTmux, installMenuBinding, setPaneOption, getPaneOption, piCommand, respawnPaneSync, closePane, windowOfPane, renameWindow, paneOfWindow, windowAlive, listLivePanes, } from './tmux.js';
3
+ export { splitWindow, breakPane, selectLayout, selectPane, focusWindow, selectWindow, switchClient, openNodeWindow, ensureSession, paneLocation, paneExists, paneRunning, paneCurrentPath, currentTmux, inTmux, installMenuBinding, setPaneOption, getPaneOption, piCommand, respawnPaneSync, closePane, windowOfPane, renameWindow, paneOfWindow, windowAlive, listLivePanes, } from './tmux.js';
4
4
  export type { RespawnPaneOpts } from './tmux.js';
5
5
  /** The viewer a node has on screen, or null. UNIQUE(node_id) ⇒ at most one. */
6
6
  export declare function focusOf(nodeId: string): FocusRow | null;
@@ -22,17 +22,26 @@ export declare function listFocuses(): FocusRow[];
22
22
  * tmux probe, so the pane may be stale; the caller liveness-checks before
23
23
  * targeting it. */
24
24
  export declare function graphSurfaceTarget(nodeId: string): FocusRow | null;
25
- /** Synchronously wait until a broker's view.sock accepts a connection. The spawn
26
- * and focus flows are sync (the command layer calls them directly), so the async
27
- * net poll runs in a worker THREAD while this thread blocks on `Atomics.wait`
28
- * in-process, no child Node cold-start. Success proves more than file existence:
29
- * it is robust to a stale leftover socket the launching broker has not unlinked
30
- * yet, because only an accepting listener yields a `connect`. */
31
- export declare function waitForBrokerViewSocket(nodeId: string): boolean;
25
+ /** Wait until a broker's view.sock accepts a connection. The launching caller
26
+ * stays async, so the broker's real exit signal can fail fast the wait instead
27
+ * of being masked by a zombie pid. Success proves more than file existence: it
28
+ * is robust to a stale leftover socket the broker has not unlinked yet,
29
+ * because only an accepting listener yields a `connect`. */
30
+ export declare function waitForBrokerViewSocket(nodeId: string, exited?: Promise<{
31
+ code: number | null;
32
+ signal: NodeJS.Signals | null;
33
+ }>): Promise<boolean>;
32
34
  /** A reviver: ensure a node's broker ENGINE is alive (idempotent → reviveNode →
33
35
  * headlessBrokerHost.launch). Injected so placement.ts need not import revive.ts
34
36
  * (which imports placement.ts — a cycle). */
35
- export type Reviver = (nodeId: string) => void;
37
+ export type Reviver = (nodeId: string) => {
38
+ launch?: {
39
+ exited: Promise<{
40
+ code: number | null;
41
+ signal: NodeJS.Signals | null;
42
+ }>;
43
+ };
44
+ } | void;
36
45
  /** Result of a focus op. `inPlace` = navigated to an existing viewer (no new
37
46
  * pane); false = a fresh viewer pane was opened/moved. `revived` = this call
38
47
  * had to (re)launch the broker engine. */
@@ -72,32 +81,11 @@ export declare function openViewerWindow(nodeId: string, session: string, opts?:
72
81
  cwd?: string;
73
82
  besidePane?: string;
74
83
  }): FocusRow | null;
75
- /** Bring a node's ONE viewer pane forefront (§A.4). Every node is a detached
76
- * broker, so there is no engine pane to swap into the caller's viewport — focus
77
- * instead ensures the node's single `crtr attach` viewer is on screen beside (or
78
- * navigated to) the caller:
79
- * (a) ensure the broker ENGINE is alive (idempotent injected reviver) and its
80
- * view.sock is ACCEPTING (closes the cold-start race against attach's
81
- * single connect) — without this the freshly-opened viewer would exit
82
- * "no broker".
83
- * (b) `--new-pane` → always a fresh viewer beside the caller (e.g. two
84
- * different nodes side by side); drop any prior viewer first (UNIQUE).
85
- * (c) the node already has a live viewer:
86
- * - in the caller's session ⇒ navigate to it (switchClient+selectWindow),
87
- * no new pane (inPlace).
88
- * - elsewhere (e.g. a --root's viewer in another session) ⇒ MOVE it: close the old
89
- * viewer pane (the broker runs on; the fresh viewer replays full
90
- * scrollback from the broker `welcome` snapshot), drop its row, reopen
91
- * beside the caller.
92
- * (d) no live viewer ⇒ open one beside the caller and register the row.
93
- *
94
- * crtr is tmux-only: with no caller pane there is no viewport to open and no
95
- * non-tmux fallback — report not-focused. */
96
84
  export declare function focus(nodeId: string, opts: {
97
85
  pane?: string;
98
86
  newPane?: boolean;
99
87
  revive: Reviver;
100
- }): FocusResult;
88
+ }): Promise<FocusResult>;
101
89
  /** Tear a node off its viewer (close/reset/cancel teardown). The broker engine
102
90
  * is killed by its own host teardown over view.sock — this only closes the
103
91
  * on-screen viewer: kill its pane (the window collapses once its last pane goes)
@@ -112,3 +100,20 @@ export declare function tearDownNode(nodeId: string): void;
112
100
  * acts on (the Alt+C menu's `#{pane_id}`); falls back to the node's registered
113
101
  * viewer pane. No-op (false) when there is no live viewer pane to close. */
114
102
  export declare function detachToBackground(nodeId: string, pane?: string): boolean;
103
+ /** Reap `nodeId` IFF it is an empty shell (see {@link isEmptyNode}): tear down its
104
+ * broker engine, close any viewer, and hard-delete its row (edges cascade) + dir.
105
+ * The user closing or detaching from a node that never said anything wants it
106
+ * GONE, not parked as a canceled husk. Returns true when it reaped. No-op (false)
107
+ * on a node that produced output, is mid-first-turn, or no longer exists. */
108
+ export declare function reapIfEmpty(nodeId: string): boolean;
109
+ /** Sweep the whole canvas for empty shells and reap them — the bulk cleanup
110
+ * behind `crtr canvas prune --empty`, for the husks that accumulated before
111
+ * close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID), nodes
112
+ * mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a running
113
+ * engine is the daemon's domain and may have output not yet persisted, so bulk
114
+ * GC never kills one (the per-action {@link reapIfEmpty} does, since the user
115
+ * explicitly closed/detached that node). `dryRun` reports candidates without
116
+ * deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
117
+ export declare function reapEmptyNodes(opts?: {
118
+ dryRun?: boolean;
119
+ }): string[];
@@ -23,25 +23,29 @@
23
23
  // model-over-driver so the §5.1 import-lint ("only placement.ts /
24
24
  // tmux-chrome.ts import tmux.ts") holds — every other module reaches the
25
25
  // driver verbs through here.
26
- import { Worker } from 'node:worker_threads';
27
- import { join } from 'node:path';
28
- import { getNode, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, view, } from '../canvas/index.js';
29
- import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, getPaneOption, } from './tmux.js';
26
+ import { createConnection } from 'node:net';
27
+ import { getNode, deleteNode, listNodes, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, view, } from '../canvas/index.js';
28
+ import { nodeHasAssistantMessage, isNodeStreaming } from '../canvas/render.js';
29
+ import { headlessBrokerHost } from './host.js';
30
+ import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, selectLayout, selectPane, getPaneOption, } from './tmux.js';
30
31
  import { newNodeId, rootOfSpine } from './nodes.js';
31
- import { nodeDir } from '../canvas/paths.js';
32
+ import { viewSocketPath } from '../canvas/paths.js';
32
33
  import { isPidAlive } from '../canvas/pid.js';
33
34
  // Placement is the sanctioned model-over-driver (§2.1): non-placement runtime /
34
35
  // command modules that legitimately need a raw driver verb get it from here, so
35
36
  // the §5.1 lint can hold "only placement.ts / tmux-chrome.ts import tmux.ts".
36
37
  // These are the chrome + viewer verbs that survive the cut (the engine-in-pane
37
38
  // verbs — respawnPane, respawnPaneDetached, swapPaneInPlace, breakPaneToSession,
38
- // joinPane, selectLayout, setWindowOption, setRemainOnExit — are deleted).
39
+ // joinPane, setWindowOption, setRemainOnExit — are deleted). `breakPane` +
40
+ // `selectLayout` are FRESH minimal viewer-chrome helpers (not the deleted
41
+ // engine-in-pane `breakPaneToSession`): `canvas tmux-spread` breaks the caller's
42
+ // viewer pane into its own window and tiles sibling viewers beside it.
39
43
  // `piCommand` + `paneCurrentPath` are KEPT: the sessionless `crtr view` system
40
44
  // (view-cycle.ts / view-run.ts) builds its pane commands with them, and the §5.1
41
45
  // import-lint forbids those commands importing tmux.ts directly — they must reach
42
46
  // the driver through placement. (They are NOT engine-in-pane; the design's
43
47
  // "nothing keeps" classification missed the view consumers.)
44
- export { splitWindow, focusWindow, selectWindow, switchClient, openNodeWindow, ensureSession, paneLocation, paneExists, paneRunning, paneCurrentPath, currentTmux, inTmux, installMenuBinding, setPaneOption, getPaneOption, piCommand, respawnPaneSync, closePane, windowOfPane, renameWindow, paneOfWindow, windowAlive, listLivePanes, } from './tmux.js';
48
+ export { splitWindow, breakPane, selectLayout, selectPane, focusWindow, selectWindow, switchClient, openNodeWindow, ensureSession, paneLocation, paneExists, paneRunning, paneCurrentPath, currentTmux, inTmux, installMenuBinding, setPaneOption, getPaneOption, piCommand, respawnPaneSync, closePane, windowOfPane, renameWindow, paneOfWindow, windowAlive, listLivePanes, } from './tmux.js';
45
49
  // ---------------------------------------------------------------------------
46
50
  // Viewer registry reads — COMPOSE over the canvas focuses table (§2.3/§4).
47
51
  //
@@ -115,61 +119,68 @@ export function graphSurfaceTarget(nodeId) {
115
119
  // ---------------------------------------------------------------------------
116
120
  const BROKER_FOCUS_SOCKET_WAIT_MS = 30_000;
117
121
  const BROKER_FOCUS_SOCKET_RETRY_MS = 100;
118
- // The probe runs inside a worker thread (see waitForBrokerViewSocket). Async net
119
- // events drive the same poll loop; the worker reports its verdict into a shared
120
- // Int32 (0 = pending, 1 = accepted, 2 = gave up) and Atomics.notify wakes the
121
- // blocked main thread. eval:true ⇒ CommonJS, so `require` and workerData apply.
122
- const VIEW_SOCKET_PROBE_WORKER = `
123
- const net = require('node:net');
124
- const { workerData, parentPort } = require('node:worker_threads');
125
- const { buffer, sockPath, waitMs, retryMs } = workerData;
126
- const result = new Int32Array(buffer);
127
- const deadline = Date.now() + waitMs;
128
- const report = (code) => { Atomics.store(result, 0, code); Atomics.notify(result, 0); };
129
- function attempt() {
130
- let socket;
131
- let settled = false;
132
- const finish = (ok) => {
133
- if (settled) return;
134
- settled = true;
135
- if (socket !== undefined) socket.destroy();
136
- if (ok) { report(1); return; }
137
- if (Date.now() >= deadline) { report(2); return; }
138
- setTimeout(attempt, retryMs);
139
- };
140
- try {
141
- socket = net.createConnection(sockPath);
142
- } catch {
143
- finish(false);
144
- return;
145
- }
146
- socket.once('connect', () => finish(true));
147
- socket.once('error', () => finish(false));
148
- socket.setTimeout(retryMs, () => finish(false));
122
+ function sleep(ms) {
123
+ return new Promise((resolve) => {
124
+ setTimeout(resolve, ms);
125
+ });
149
126
  }
150
- attempt();
151
- void parentPort;
152
- `;
153
- /** Synchronously wait until a broker's view.sock accepts a connection. The spawn
154
- * and focus flows are sync (the command layer calls them directly), so the async
155
- * net poll runs in a worker THREAD while this thread blocks on `Atomics.wait` —
156
- * in-process, no child Node cold-start. Success proves more than file existence:
157
- * it is robust to a stale leftover socket the launching broker has not unlinked
158
- * yet, because only an accepting listener yields a `connect`. */
159
- export function waitForBrokerViewSocket(nodeId) {
160
- const sockPath = join(nodeDir(nodeId), 'view.sock');
161
- const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
162
- const result = new Int32Array(buffer);
163
- const worker = new Worker(VIEW_SOCKET_PROBE_WORKER, {
164
- eval: true,
165
- workerData: { buffer, sockPath, waitMs: BROKER_FOCUS_SOCKET_WAIT_MS, retryMs: BROKER_FOCUS_SOCKET_RETRY_MS },
127
+ function probeBrokerViewSocket(sockPath, timeoutMs) {
128
+ return new Promise((resolve) => {
129
+ let settled = false;
130
+ let socket = null;
131
+ const finish = (ok) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ if (socket !== null)
136
+ socket.destroy();
137
+ resolve(ok);
138
+ };
139
+ try {
140
+ socket = createConnection(sockPath);
141
+ }
142
+ catch {
143
+ finish(false);
144
+ return;
145
+ }
146
+ socket.once('connect', () => finish(true));
147
+ socket.once('error', () => finish(false));
148
+ socket.setTimeout(timeoutMs, () => finish(false));
166
149
  });
167
- worker.unref();
168
- // Block until the worker reports (or a hard timeout matching the old
169
- // spawnSync). 'not-equal' covers the worker finishing before we wait.
170
- Atomics.wait(result, 0, 0, BROKER_FOCUS_SOCKET_WAIT_MS + 1_000);
171
- void worker.terminate();
172
- return Atomics.load(result, 0) === 1;
150
+ }
151
+ /** Wait until a broker's view.sock accepts a connection. The launching caller
152
+ * stays async, so the broker's real exit signal can fail fast the wait instead
153
+ * of being masked by a zombie pid. Success proves more than file existence: it
154
+ * is robust to a stale leftover socket the broker has not unlinked yet,
155
+ * because only an accepting listener yields a `connect`. */
156
+ export async function waitForBrokerViewSocket(nodeId, exited) {
157
+ const sockPath = viewSocketPath(nodeId);
158
+ const deadline = Date.now() + BROKER_FOCUS_SOCKET_WAIT_MS;
159
+ const exitSeen = exited?.then(() => 'exit');
160
+ while (Date.now() <= deadline) {
161
+ const remaining = Math.max(0, deadline - Date.now());
162
+ const probe = probeBrokerViewSocket(sockPath, Math.min(BROKER_FOCUS_SOCKET_RETRY_MS, remaining)).then((ok) => (ok ? 'ready' : 'retry'));
163
+ // Race the probe against the broker's real exit ONLY when we have one. With
164
+ // no exit promise, an `?? Promise.resolve('retry')` would settle instantly
165
+ // and always beat the probe, so readiness could never observe a `connect`.
166
+ const outcome = exitSeen === undefined ? await probe : await Promise.race([probe, exitSeen]);
167
+ if (outcome === 'ready')
168
+ return true;
169
+ if (outcome === 'exit')
170
+ return false;
171
+ if (exitSeen !== undefined) {
172
+ const sleptOrExit = await Promise.race([
173
+ sleep(BROKER_FOCUS_SOCKET_RETRY_MS).then(() => 'sleep'),
174
+ exitSeen,
175
+ ]);
176
+ if (sleptOrExit === 'exit')
177
+ return false;
178
+ }
179
+ else {
180
+ await sleep(BROKER_FOCUS_SOCKET_RETRY_MS);
181
+ }
182
+ }
183
+ return false;
173
184
  }
174
185
  function newFocusId() {
175
186
  return `f-${newNodeId()}`;
@@ -263,7 +274,17 @@ export function openViewerWindow(nodeId, session, opts = {}) {
263
274
  *
264
275
  * crtr is tmux-only: with no caller pane there is no viewport to open and no
265
276
  * non-tmux fallback — report not-focused. */
266
- export function focus(nodeId, opts) {
277
+ /** After a viewer pane is opened/moved beside the caller, make it the ACTIVE
278
+ * pane and rebalance its window's panes to an even side-by-side layout — so
279
+ * picking a node (e.g. from the alt+g graph) lands the keyboard on the fresh
280
+ * viewer and the panes are evenly sized again, not halved. Best-effort. */
281
+ function surfaceNewPane(pane) {
282
+ const win = paneLocation(pane)?.window ?? null;
283
+ if (win !== null)
284
+ selectLayout(win, 'even-horizontal');
285
+ selectPane(pane);
286
+ }
287
+ export async function focus(nodeId, opts) {
267
288
  const meta = getNode(nodeId);
268
289
  if (meta === null)
269
290
  return { focused: false, session: null, inPlace: false, revived: false };
@@ -274,9 +295,9 @@ export function focus(nodeId, opts) {
274
295
  // (a) Ensure the broker engine is alive + its view.sock accepts. Capture prior
275
296
  // liveness so the result reports whether THIS focus had to launch it.
276
297
  const wasAlive = isPidAlive(meta.pi_pid ?? null);
277
- opts.revive(nodeId);
298
+ const revivedLaunch = opts.revive(nodeId);
278
299
  const revived = !wasAlive;
279
- if (!waitForBrokerViewSocket(nodeId)) {
300
+ if (!(await waitForBrokerViewSocket(nodeId, revivedLaunch?.launch?.exited))) {
280
301
  return { focused: false, session: null, inPlace: false, revived };
281
302
  }
282
303
  // focus always opens BESIDE the caller pane, so `fallbackSession` is only the
@@ -296,6 +317,8 @@ export function focus(nodeId, opts) {
296
317
  const opened = openViewerWindow(nodeId, fallbackSession, { cwd: meta.cwd, besidePane: callerPane });
297
318
  if (opened === null)
298
319
  return { focused: false, session: null, inPlace: false, revived };
320
+ if (opened.pane !== null)
321
+ surfaceNewPane(opened.pane);
299
322
  return { focused: true, session: opened.session, inPlace: false, revived };
300
323
  }
301
324
  // (c) Reuse / move the node's one live viewer — but ONLY a pane that still
@@ -315,6 +338,8 @@ export function focus(nodeId, opts) {
315
338
  // Viewer is in the caller's session — just navigate to it.
316
339
  switchClient(loc.session);
317
340
  selectWindow(loc.session, loc.window);
341
+ if (existing.pane !== null)
342
+ selectPane(existing.pane);
318
343
  return { focused: true, session: loc.session, inPlace: true, revived };
319
344
  }
320
345
  // Viewer lives elsewhere — MOVE it: close the old pane (broker runs on; the
@@ -334,6 +359,8 @@ export function focus(nodeId, opts) {
334
359
  const opened = openViewerWindow(nodeId, fallbackSession, { cwd: meta.cwd, besidePane: callerPane });
335
360
  if (opened === null)
336
361
  return { focused: false, session: null, inPlace: false, revived };
362
+ if (opened.pane !== null)
363
+ surfaceNewPane(opened.pane);
337
364
  return { focused: true, session: opened.session, inPlace: false, revived };
338
365
  }
339
366
  // ---------------------------------------------------------------------------
@@ -366,5 +393,63 @@ export function detachToBackground(nodeId, pane) {
366
393
  const f = focusByPane(viewer);
367
394
  if (f !== null)
368
395
  closeFocusRow(f.focus_id);
369
- return closePane(viewer);
396
+ const closed = closePane(viewer);
397
+ // A node the user detached from that never produced any AI output is a useless
398
+ // shell taking up graph + disk space — reap it (engine + row + dir) outright
399
+ // rather than leaving an empty broker running headless off-screen.
400
+ reapIfEmpty(nodeId);
401
+ return closed;
402
+ }
403
+ // ---------------------------------------------------------------------------
404
+ // Reaping empty nodes (no AI output) on close / detach
405
+ // ---------------------------------------------------------------------------
406
+ /** Is this node an EMPTY SHELL right now — a node whose engine never produced an
407
+ * assistant message? Such nodes are pure dead weight on the graph. A node
408
+ * actively generating its FIRST turn is NOT empty (its output just hasn't landed
409
+ * on disk yet), so streaming nodes are spared. */
410
+ function isEmptyNode(nodeId) {
411
+ const m = getNode(nodeId);
412
+ if (m === null)
413
+ return false;
414
+ if (isNodeStreaming(nodeId))
415
+ return false;
416
+ return !nodeHasAssistantMessage(m.pi_session_file);
417
+ }
418
+ /** Reap `nodeId` IFF it is an empty shell (see {@link isEmptyNode}): tear down its
419
+ * broker engine, close any viewer, and hard-delete its row (edges cascade) + dir.
420
+ * The user closing or detaching from a node that never said anything wants it
421
+ * GONE, not parked as a canceled husk. Returns true when it reaped. No-op (false)
422
+ * on a node that produced output, is mid-first-turn, or no longer exists. */
423
+ export function reapIfEmpty(nodeId) {
424
+ if (!isEmptyNode(nodeId))
425
+ return false;
426
+ headlessBrokerHost.teardown(nodeId);
427
+ tearDownNode(nodeId);
428
+ deleteNode(nodeId);
429
+ return true;
430
+ }
431
+ /** Sweep the whole canvas for empty shells and reap them — the bulk cleanup
432
+ * behind `crtr canvas prune --empty`, for the husks that accumulated before
433
+ * close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID), nodes
434
+ * mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a running
435
+ * engine is the daemon's domain and may have output not yet persisted, so bulk
436
+ * GC never kills one (the per-action {@link reapIfEmpty} does, since the user
437
+ * explicitly closed/detached that node). `dryRun` reports candidates without
438
+ * deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
439
+ export function reapEmptyNodes(opts = {}) {
440
+ const dryRun = opts.dryRun ?? false;
441
+ const selfId = process.env['CRTR_NODE_ID'] ?? '';
442
+ const reaped = [];
443
+ for (const row of listNodes()) {
444
+ const id = row.node_id;
445
+ if (id === selfId || isPidAlive(row.pi_pid) || !isEmptyNode(id))
446
+ continue;
447
+ if (!dryRun) {
448
+ headlessBrokerHost.teardown(id);
449
+ tearDownNode(id);
450
+ deleteNode(id);
451
+ }
452
+ reaped.push(id);
453
+ }
454
+ return reaped;
370
455
  }
@@ -34,4 +34,6 @@ export interface YieldResult {
34
34
  export declare function requestYield(nodeId: string, opts?: {
35
35
  kind?: string;
36
36
  model?: string;
37
+ }, deps?: {
38
+ ensure?: () => void;
37
39
  }): YieldResult;