@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
@@ -0,0 +1,160 @@
1
+ // Run: node --import tsx/esm --test src/core/__tests__/model-ladders.test.ts
2
+ //
3
+ // The runtime model ladder is user-configurable: config.json can override the
4
+ // provider default, the per-provider ladder cells, and persona-strength picks.
5
+ // This test verifies both the merge shape and the cached sync read invalidation
6
+ // on config edits.
7
+ import { test, before, after, beforeEach } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { readConfig } from '../config.js';
13
+ import { buildLaunchSpec, equivalentOtherProviderModel, normalizeModel } from '../runtime/launch.js';
14
+ let home;
15
+ const crtrRoot = () => join(home, '.crouter');
16
+ const configFile = () => join(crtrRoot(), 'config.json');
17
+ function writeConfig(config) {
18
+ mkdirSync(crtrRoot(), { recursive: true });
19
+ writeFileSync(configFile(), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
20
+ }
21
+ before(() => {
22
+ home = mkdtempSync(join(tmpdir(), 'crtr-model-ladders-'));
23
+ process.env['HOME'] = home;
24
+ });
25
+ beforeEach(() => {
26
+ rmSync(crtrRoot(), { recursive: true, force: true });
27
+ });
28
+ after(() => {
29
+ rmSync(home, { recursive: true, force: true });
30
+ delete process.env['HOME'];
31
+ });
32
+ test('config merge keeps ladder defaults while honoring overrides', () => {
33
+ writeConfig({
34
+ modelLadders: {
35
+ defaultProvider: 'openai',
36
+ anthropic: {
37
+ ultra: 'anthropic/custom-ultra-1',
38
+ strong: 'anthropic/custom-strong-1',
39
+ medium: 'anthropic/custom-medium-1',
40
+ light: 'anthropic/custom-light-1',
41
+ },
42
+ openai: {
43
+ ultra: 'openai/custom-ultra-1',
44
+ strong: 'openai/custom-strong-1',
45
+ medium: 'openai/custom-medium-1',
46
+ light: 'openai/custom-light-1',
47
+ },
48
+ },
49
+ personaStrengths: {
50
+ developer: 'light',
51
+ },
52
+ });
53
+ const cfg = readConfig('user');
54
+ assert.equal(cfg.modelLadders.defaultProvider, 'openai');
55
+ assert.equal(cfg.modelLadders.openai.strong, 'openai/custom-strong-1');
56
+ assert.equal(cfg.modelLadders.anthropic.light, 'anthropic/custom-light-1');
57
+ assert.equal(cfg.personaStrengths.developer, 'light');
58
+ });
59
+ test('normalizeModel and buildLaunchSpec honor config overrides, and edits invalidate the cache', () => {
60
+ writeConfig({
61
+ modelLadders: {
62
+ defaultProvider: 'openai',
63
+ anthropic: {
64
+ ultra: 'anthropic/custom-ultra-1',
65
+ strong: 'anthropic/custom-strong-1',
66
+ medium: 'anthropic/custom-medium-1',
67
+ light: 'anthropic/custom-light-1',
68
+ },
69
+ openai: {
70
+ ultra: 'openai/custom-ultra-1',
71
+ strong: 'openai/custom-strong-1',
72
+ medium: 'openai/custom-medium-1',
73
+ light: 'openai/custom-light-1',
74
+ },
75
+ },
76
+ personaStrengths: {
77
+ developer: 'light',
78
+ },
79
+ });
80
+ assert.equal(normalizeModel('strong'), 'openai/custom-strong-1', 'config default provider wins over env/default');
81
+ assert.equal(normalizeModel('opus'), 'anthropic/custom-strong-1', 'family aliases still map to anthropic');
82
+ const override = buildLaunchSpec('developer', 'base', {
83
+ lifecycle: 'terminal',
84
+ hasManager: true,
85
+ model: 'medium',
86
+ });
87
+ assert.equal(override.launch.model, 'openai/custom-medium-1', 'meta.model_override wins over personaStrengths');
88
+ const persona = buildLaunchSpec('developer', 'base', {
89
+ lifecycle: 'terminal',
90
+ hasManager: true,
91
+ });
92
+ assert.equal(persona.launch.model, 'openai/custom-light-1', 'personaStrengths wins over persona frontmatter');
93
+ writeConfig({
94
+ modelLadders: {
95
+ defaultProvider: 'anthropic',
96
+ anthropic: {
97
+ ultra: 'anthropic/custom-ultra-2',
98
+ strong: 'anthropic/custom-strong-2',
99
+ medium: 'anthropic/custom-medium-2',
100
+ light: 'anthropic/custom-light-2',
101
+ },
102
+ openai: {
103
+ ultra: 'openai/custom-ultra-2',
104
+ strong: 'openai/custom-strong-2',
105
+ medium: 'openai/custom-medium-2',
106
+ light: 'openai/custom-light-2',
107
+ },
108
+ },
109
+ personaStrengths: {
110
+ developer: 'strong',
111
+ },
112
+ });
113
+ assert.equal(normalizeModel('strong'), 'anthropic/custom-strong-2', 'a config edit invalidates the cached sync read');
114
+ const edited = buildLaunchSpec('developer', 'base', {
115
+ lifecycle: 'terminal',
116
+ hasManager: true,
117
+ });
118
+ assert.equal(edited.launch.model, 'anthropic/custom-strong-2', 'edited personaStrengths are picked up on the next launch');
119
+ });
120
+ test('equivalentOtherProviderModel maps a failed ladder model to the other provider at the same strength', () => {
121
+ writeConfig({
122
+ modelLadders: {
123
+ defaultProvider: 'anthropic',
124
+ anthropic: {
125
+ ultra: 'anthropic/custom-ultra',
126
+ strong: 'anthropic/custom-strong:high',
127
+ medium: 'anthropic/custom-medium',
128
+ light: 'anthropic/custom-light',
129
+ },
130
+ openai: {
131
+ ultra: 'openai/custom-ultra',
132
+ strong: 'openai/custom-strong:xhigh',
133
+ medium: 'openai/custom-medium',
134
+ light: 'openai/custom-light',
135
+ },
136
+ },
137
+ });
138
+ assert.deepEqual(equivalentOtherProviderModel('anthropic/custom-strong:medium'), {
139
+ fromProvider: 'anthropic',
140
+ toProvider: 'openai',
141
+ strength: 'strong',
142
+ model: 'openai/custom-strong:xhigh',
143
+ });
144
+ assert.deepEqual(equivalentOtherProviderModel('openai/custom-light'), {
145
+ fromProvider: 'openai',
146
+ toProvider: 'anthropic',
147
+ strength: 'light',
148
+ model: 'anthropic/custom-light',
149
+ });
150
+ assert.equal(equivalentOtherProviderModel('anthropic/custom-strong:medium', new Set(['anthropic'])), null);
151
+ assert.equal(equivalentOtherProviderModel('some-provider/not-in-ladder'), null);
152
+ });
153
+ test('malformed existing user config surfaces instead of falling back to defaults', () => {
154
+ mkdirSync(crtrRoot(), { recursive: true });
155
+ writeFileSync(configFile(), '{\n', 'utf8');
156
+ assert.throws(() => buildLaunchSpec('developer', 'base', {
157
+ lifecycle: 'terminal',
158
+ hasManager: true,
159
+ }), /JSON|Unexpected token|Unexpected end/);
160
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { nodeEnv } from '../runtime/nodes.js';
4
+ function meta(over = {}) {
5
+ return {
6
+ node_id: 'n',
7
+ name: 'alpha-beta',
8
+ created: '2026-01-01T00:00:00.000Z',
9
+ cwd: '/tmp/work',
10
+ kind: 'developer',
11
+ mode: 'base',
12
+ lifecycle: 'terminal',
13
+ status: 'active',
14
+ ...over,
15
+ };
16
+ }
17
+ test('nodeEnv exports CRTR_CYCLES from meta.cycles', () => {
18
+ const env = nodeEnv(meta({ cycles: 9 }));
19
+ assert.equal(env.CRTR_CYCLES, '9');
20
+ });
21
+ test('nodeEnv defaults CRTR_CYCLES to 0 when meta.cycles is absent', () => {
22
+ const withoutCycles = meta();
23
+ delete withoutCycles.cycles;
24
+ const env = nodeEnv(withoutCycles);
25
+ assert.equal(env.CRTR_CYCLES, '0');
26
+ });
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { createNode, setStatus } from '../canvas/canvas.js';
13
13
  import { closeDb } from '../canvas/db.js';
14
- import { registerPush } from '../../commands/push.js';
14
+ import { registerPush, mapFinalizeTransitionRaceError } from '../../commands/push.js';
15
15
  import { InputError } from '../io.js';
16
16
  let home;
17
17
  const prevNodeId = process.env['CRTR_NODE_ID'];
@@ -69,3 +69,9 @@ test('push final on a dead node is also caught cleanly', async () => {
69
69
  process.env['CRTR_NODE_ID'] = id;
70
70
  await assert.rejects(() => finalLeaf().run({ body: 'x' }), (e) => e instanceof InputError && e.payload.error === 'already_finalized');
71
71
  });
72
+ test('a raw finalize-transition race is normalized to already_finalized', () => {
73
+ const mapped = mapFinalizeTransitionRaceError('canceled', new Error("illegal lifecycle transition: 'finalize' from status='canceled' (node raceNode)"));
74
+ assert.ok(mapped instanceof InputError, 'the raw transition error is converted');
75
+ assert.equal(mapped?.payload.error, 'already_finalized');
76
+ assert.equal(mapped?.payload.message, 'This node is already canceled — `push final` finishes a node once and cannot finalize it again.');
77
+ });
@@ -36,7 +36,7 @@ function makeDeps(pid) {
36
36
  const deps = {
37
37
  launchBroker: (nodeId) => {
38
38
  launched.push(nodeId);
39
- return { pid };
39
+ return { pid, exited: new Promise(() => { }) };
40
40
  },
41
41
  waitForViewSocket: () => true,
42
42
  respawnViewer: () => true,
@@ -57,7 +57,7 @@ after(() => {
57
57
  rmSync(home, { recursive: true, force: true });
58
58
  delete process.env['CRTR_HOME'];
59
59
  });
60
- test('relaunchRoot parks the old root done and mints a fresh active root, reaping descendants', () => {
60
+ test('relaunchRoot parks the old root done and mints a fresh active root, reaping descendants', async () => {
61
61
  // root → child → grandchild (a parent that subscribes to its workers).
62
62
  createNode(node('root', { parent: null, lifecycle: 'resident', cwd: '/tmp/proj' }));
63
63
  createNode(node('child', { parent: 'root' }));
@@ -65,7 +65,7 @@ test('relaunchRoot parks the old root done and mints a fresh active root, reapin
65
65
  subscribe('root', 'child', true);
66
66
  subscribe('child', 'grand', true);
67
67
  const { deps, launched } = makeDeps(4242);
68
- const res = relaunchRoot('root', deps);
68
+ const res = await relaunchRoot('root', deps);
69
69
  // A fresh, DIFFERENT node id was minted and booted.
70
70
  assert.ok(res, 'relaunchRoot returns the new node id');
71
71
  const newId = res.newNodeId;
@@ -86,14 +86,14 @@ test('relaunchRoot parks the old root done and mints a fresh active root, reapin
86
86
  assert.equal(getNode('child')?.status, 'canceled', 'descendant child canceled');
87
87
  assert.equal(getNode('grand')?.status, 'canceled', 'descendant grandchild canceled');
88
88
  });
89
- test('relaunchRoot on a boot failure leaves the old root + descendants fully intact', () => {
89
+ test('relaunchRoot on a boot failure leaves the old root + descendants fully intact', async () => {
90
90
  // The mint-and-boot-FIRST ordering: a launch failure (pid=null) must occur
91
91
  // BEFORE the old root is parked or its descendants reaped.
92
92
  createNode(node('root', { parent: null, lifecycle: 'resident' }));
93
93
  createNode(node('child', { parent: 'root' }));
94
94
  subscribe('root', 'child', true);
95
95
  const { deps, launched } = makeDeps(null); // broker never starts
96
- const res = relaunchRoot('root', deps);
96
+ const res = await relaunchRoot('root', deps);
97
97
  assert.equal(res, null, 'a boot failure returns null');
98
98
  // Old root + descendant untouched: still live, ready to keep running.
99
99
  assert.equal(getNode('root')?.status, 'active', 'old root left active + untouched');
@@ -102,13 +102,13 @@ test('relaunchRoot on a boot failure leaves the old root + descendants fully int
102
102
  assert.equal(launched.length, 1, 'one broker launch was attempted');
103
103
  assert.equal(getNode(launched[0])?.status, 'dead', 'the half-born new node is crashed (dead)');
104
104
  });
105
- test('relaunchRoot is a no-op for a non-root child and an already-parked root', () => {
105
+ test('relaunchRoot is a no-op for a non-root child and an already-parked root', async () => {
106
106
  createNode(node('root', { parent: null, lifecycle: 'resident' }));
107
107
  createNode(node('child', { parent: 'root' }));
108
108
  createNode(node('done-root', { parent: null, status: 'done' }));
109
109
  const { deps, launched } = makeDeps(1);
110
- assert.equal(relaunchRoot('child', deps), null, 'a child is not a relaunchable root');
111
- assert.equal(relaunchRoot('done-root', deps), null, 'a done root is not relaunched again (double /new)');
112
- assert.equal(relaunchRoot('ghost', deps), null, 'an unknown node is a no-op');
110
+ assert.equal(await relaunchRoot('child', deps), null, 'a child is not a relaunchable root');
111
+ assert.equal(await relaunchRoot('done-root', deps), null, 'a done root is not relaunched again (double /new)');
112
+ assert.equal(await relaunchRoot('ghost', deps), null, 'an unknown node is a no-op');
113
113
  assert.deepEqual(launched, [], 'no broker was booted for any of the no-op cases');
114
114
  });
@@ -1,11 +1,11 @@
1
1
  // Run with: node --import tsx/esm --test src/core/__tests__/revive.test.ts
2
2
  //
3
3
  // HEADLESS RETARGET (foundation-spec §C.13 + §E). Covers the session-picker fix
4
- // (resume by ABSOLUTE session-file path, cwd-immune, falling back to the bare
5
- // uuid for older nodes) AND the double-revive guard that keeps two pi processes
6
- // off one session file. The argv selection is unit-tested via the pure
7
- // `resumeArgs` + `buildPiArgv`; the guard + the cycle-counter bump are now driven
8
- // against a fabricated BROKER-hosted node — ZERO real tmux, ZERO real boot.
4
+ // (resume by ABSOLUTE session-file path, cwd-immune) and the double-revive guard
5
+ // that keeps two pi processes off one session file. The argv selection is
6
+ // unit-tested via the pure `resumeArgs` + `buildPiArgv`; the guard + the
7
+ // cycle-counter bump are now driven against a fabricated BROKER-hosted node —
8
+ // ZERO real tmux, ZERO real boot.
9
9
  //
10
10
  // (1) BUG LOCKED — the double-revive guard (revive.ts:114): a node already up
11
11
  // (its engine container alive AND its pi pid live) must NOT be re-launched —
@@ -30,13 +30,14 @@
30
30
  // goes RED.
31
31
  import { test, before, after, beforeEach } from 'node:test';
32
32
  import assert from 'node:assert/strict';
33
- import { mkdtempSync, rmSync } from 'node:fs';
33
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
34
34
  import { tmpdir } from 'node:os';
35
35
  import { join } from 'node:path';
36
36
  import { spawn, spawnSync } from 'node:child_process';
37
37
  import { createNode } from '../canvas/canvas.js';
38
38
  import { closeDb } from '../canvas/db.js';
39
39
  import { buildPiArgv } from '../runtime/launch.js';
40
+ import { headlessBrokerHost } from '../runtime/host.js';
40
41
  import { resumeArgs, reviveNode } from '../runtime/revive.js';
41
42
  import { createHarness } from './helpers/harness.js';
42
43
  let home;
@@ -211,11 +212,15 @@ test('reviveNode PROCEEDS when the broker engine is DOWN (dead pid) — cycle co
211
212
  // circuit → reviveNode proceeds. It bumps cycles BEFORE the detached (fake-
212
213
  // engine) launch, so the bump is observable instantly; the throwaway broker is
213
214
  // killed by dispose().
215
+ const sessionFile = join(home, 'sessions', 'crtr-revive-proceed.jsonl');
216
+ mkdirSync(join(home, 'sessions'), { recursive: true });
217
+ writeFileSync(sessionFile, '{}\n', 'utf8');
214
218
  const M = h.fabricateBrokerNode({
215
219
  status: 'active',
216
220
  intent: null,
217
221
  pi_pid: deadPid(),
218
222
  pi_session_id: 'uuid-1',
223
+ pi_session_file: sessionFile,
219
224
  });
220
225
  assert.equal(h.node(M).cycles ?? 0, 0, 'fabricated at cycle 0 (no revive yet)');
221
226
  reviveNode(M, { resume: true });
@@ -225,3 +230,28 @@ test('reviveNode PROCEEDS when the broker engine is DOWN (dead pid) — cycle co
225
230
  await h.dispose();
226
231
  }
227
232
  });
233
+ test('reviveNode crashes and clears pid when launch/preflight throws', async () => {
234
+ const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-fail' });
235
+ const originalLaunch = headlessBrokerHost.launch;
236
+ try {
237
+ const sessionFile = join(home, 'sessions', 'crtr-revive-fail.jsonl');
238
+ mkdirSync(join(home, 'sessions'), { recursive: true });
239
+ writeFileSync(sessionFile, '{}\n', 'utf8');
240
+ const M = h.fabricateBrokerNode({
241
+ status: 'active',
242
+ intent: null,
243
+ pi_pid: deadPid(),
244
+ pi_session_id: 'uuid-2',
245
+ pi_session_file: sessionFile,
246
+ });
247
+ headlessBrokerHost.launch = (_nodeId, _inv, _opts) => { throw new Error('boom'); };
248
+ assert.throws(() => reviveNode(M, { resume: true }), /refusing to revive/i);
249
+ const after = h.node(M);
250
+ assert.equal(after.status, 'dead', 'failed revive marks the node dead');
251
+ assert.equal(after.pi_pid, null, 'failed revive clears the stale pid');
252
+ }
253
+ finally {
254
+ headlessBrokerHost.launch = originalLaunch;
255
+ await h.dispose();
256
+ }
257
+ });
@@ -10,7 +10,8 @@ import { createNode, getNode, getRow, subscribersOf } from '../canvas/canvas.js'
10
10
  import { closeDb } from '../canvas/db.js';
11
11
  import { nodeDir } from '../canvas/paths.js';
12
12
  import { spawnNode } from '../runtime/nodes.js';
13
- import { resolveSpawner } from '../runtime/spawn.js';
13
+ import { spawnChild, resolveSpawner } from '../runtime/spawn.js';
14
+ import { headlessBrokerHost } from '../runtime/host.js';
14
15
  let home;
15
16
  function spawner(id) {
16
17
  return {
@@ -81,3 +82,25 @@ test('unknown parent: spawnNode throws and mints NO node dir / row (validate bef
81
82
  assert.equal(getRow(orphanId), null, 'no index row written for the orphan');
82
83
  assert.equal(existsSync(nodeDir(orphanId)), false, 'no node dir scaffolded for the orphan');
83
84
  });
85
+ test('spawnChild crashes the node when launch/preflight throws', async () => {
86
+ const parentId = 'A';
87
+ createNode(spawner(parentId));
88
+ const originalLaunch = headlessBrokerHost.launch;
89
+ let launchedNodeId = null;
90
+ try {
91
+ headlessBrokerHost.launch = (nodeId, _inv, _opts) => {
92
+ launchedNodeId = nodeId;
93
+ throw new Error('boom');
94
+ };
95
+ await assert.rejects(async () => spawnChild({ kind: 'developer', cwd: '/tmp/work', parent: parentId, prompt: 'task' }), /refusing to launch/i);
96
+ assert.ok(launchedNodeId !== null, 'the launch hook saw the spawned node id');
97
+ const after = getNode(launchedNodeId);
98
+ assert.equal(after.status, 'dead', 'failed spawn marks the node dead');
99
+ assert.equal(after.pi_pid, null, 'failed spawn clears the stale pid');
100
+ }
101
+ finally {
102
+ headlessBrokerHost.launch = originalLaunch;
103
+ closeDb();
104
+ rmSync(home, { recursive: true, force: true });
105
+ }
106
+ });
@@ -80,3 +80,13 @@ test('pushedFinal → finished; askedHuman → escalated (both short-circuit bef
80
80
  assert.deepEqual(evaluateStop('t', { pushedFinal: true, askedHuman: false }), { action: 'allow', reason: 'finished' });
81
81
  assert.deepEqual(evaluateStop('t', { pushedFinal: false, askedHuman: true }), { action: 'allow', reason: 'escalated' });
82
82
  });
83
+ test('already-terminal stored statuses are allowed without a stall reprompt', () => {
84
+ for (const [id, status] of [
85
+ ['doneNode', 'done'],
86
+ ['deadNode', 'dead'],
87
+ ['canceledNode', 'canceled'],
88
+ ]) {
89
+ createNode(node(id, { parent: 'mgr', lifecycle: 'terminal', status }));
90
+ assert.deepEqual(evaluateStop(id, noSignals), { action: 'allow', reason: 'finished' });
91
+ }
92
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/stranded-relaunch.test.ts
2
+ //
3
+ // BUG-REGRESSION (observed 2026-06-15 on the live canvas). A broker-hosted node
4
+ // whose row carries pi_pid=NULL but pi_session_id SET was read by
5
+ // handleNodeLiveness as "a relaunch in flight — leave it" UNCONDITIONALLY, with
6
+ // no timeout. reviveNode clears pi_pid right after launch and the fresh engine
7
+ // re-records it within a tick or two — but a relaunch whose broker DIED in that
8
+ // gap (after the pid-clear, before re-record) never re-records, so the node sat
9
+ // 'active' with no engine FOREVER and its parent waited on a dead child. Six such
10
+ // stranded resident roots had accumulated and the daemon would never recover or
11
+ // reap any of them.
12
+ //
13
+ // THE FIX — give the pid==null + session-set case the SAME REVIVE_GRACE_MS boot
14
+ // grace as the never-booted case: a healthy relaunch always re-records its pid
15
+ // well inside the grace (and the pid-alive branch clears the clock); if the grace
16
+ // ELAPSES with still no pid, the relaunch is dead → grace-revive RESUME on the
17
+ // saved session. The 20s grace doubles as the double-spawn guard.
18
+ //
19
+ // WHY MODEL-LEVEL, NOT TMUX CHROME — the stranding lives entirely in pure daemon
20
+ // logic (the pid==null branch of handleNodeLiveness + the unhealthySince clock).
21
+ // A fabricated broker row carrying pi_pid=null + a recorded pi_session_id
22
+ // reproduces the exact stranded state with no process at all; h.tick(now) walks a
23
+ // deterministic clock across the grace boundary and getNode().cycles (reviveNode
24
+ // bumps cycles + transition('revive') BEFORE the detached spawn) makes the revive
25
+ // observable instantly.
26
+ //
27
+ // HOW IT FAILS IF THE BUG REGRESSES — restore the unconditional
28
+ // `if (meta.pi_session_id != null) return;` and the PAST-grace tick never revives
29
+ // → cycles stays 0 → the final assert goes RED.
30
+ import { test } from 'node:test';
31
+ import assert from 'node:assert/strict';
32
+ import { createHarness } from './helpers/harness.js';
33
+ // REVIVE_GRACE_MS is 20_000 (crtrd.ts). Offsets well inside / well past it keep
34
+ // the test robust to the exact value without importing it.
35
+ const NOW = 7_000_000;
36
+ const WITHIN_GRACE = NOW + 10_000; // < 20s after first-observed-null → still pending
37
+ const PAST_GRACE = NOW + 25_000; // > 20s after first-observed-null → revive
38
+ test('stranded relaunch: pid=null + session set is revived once the boot grace elapses, not left active forever', { timeout: 30_000 }, async () => {
39
+ const h = await createHarness({ headless: true, sessionPrefix: 'crtr-stranded' });
40
+ try {
41
+ // The EXACT stranded state: a broker row that booted once (pi_session_id
42
+ // captured) whose relaunch then died after the pid-clear — pi_pid NULL,
43
+ // intent null (so it is neither the refresh nor the idle-release path).
44
+ const B = h.fabricateBrokerNode({
45
+ kind: 'developer',
46
+ status: 'active',
47
+ intent: null,
48
+ pi_pid: null,
49
+ pi_session_id: 'sess-stranded-B',
50
+ });
51
+ const b0 = h.node(B);
52
+ assert.equal(b0.host_kind, 'broker', 'B is broker-hosted (paneless)');
53
+ assert.equal(b0.pi_pid ?? null, null, 'row pi_pid is null (relaunch never re-recorded)');
54
+ assert.equal(b0.pi_session_id, 'sess-stranded-B', 'session is set (booted once)');
55
+ assert.equal(b0.cycles ?? 0, 0, 'no revive yet — cycles at 0');
56
+ // --- TICK 1 @ NOW: first observation of null pid → start the boot grace
57
+ // clock, do NOT revive (a healthy relaunch re-records its pid here). ---
58
+ await h.tick(NOW);
59
+ assert.equal(h.node(B).cycles ?? 0, 0, 'first tick: no revive — grace clock started');
60
+ // --- TICK 2 @ NOW+10s (WITHIN grace): still pending. This is the
61
+ // double-spawn guard — a healthy relaunch would have re-recorded by now. ---
62
+ await h.tick(WITHIN_GRACE);
63
+ assert.equal(h.node(B).cycles ?? 0, 0, 'within REVIVE_GRACE_MS: still no revive (healthy relaunch grace, not a strand yet)');
64
+ // --- TICK 3 @ NOW+25s (PAST grace): the relaunch is dead → grace-revive
65
+ // RESUME on the saved session; reviveNode bumps cycles immediately. ---
66
+ await h.tick(PAST_GRACE);
67
+ assert.equal(h.node(B).cycles ?? 0, 1, 'past REVIVE_GRACE_MS: the stranded relaunch is revived (cycles bumped) — NOT left active forever');
68
+ }
69
+ finally {
70
+ await h.dispose();
71
+ }
72
+ });
@@ -68,7 +68,11 @@ test('§2.2 driver invariant: every placement verb in tmux.ts passes an explicit
68
68
  found++;
69
69
  const verb = m[1];
70
70
  const arr = enclosingArray(src, m.index);
71
- assert.ok(arr.includes(`'-t'`), `tmux verb '${verb}' is invoked WITHOUT an explicit -t target a latent ` +
71
+ // The invariant is "name a specific tmux object, never fall back to the
72
+ // GLOBAL current one". `-t` targets a destination; `break-pane` (and the
73
+ // deleted swap/join/move-pane) instead name their SOURCE with `-s` — which
74
+ // satisfies the invariant identically. Accept either explicit target flag.
75
+ assert.ok(arr.includes(`'-t'`) || arr.includes(`'-s'`), `tmux verb '${verb}' is invoked WITHOUT an explicit -t/-s target — a latent ` +
72
76
  `instance of the unbidden-window bug (§2.2). Offending args array:\n${arr}`);
73
77
  }
74
78
  // Sanity: the driver really does contain placement verbs (so a refactor that
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // The `next` road sign must name a command that actually exists: the FULL path
5
5
  // to the deepest matched node, not just its local name. A prior bug emitted
6
- // `crtr find -h` (dropping the `skill` parent) when `crtr skill find bogus` was
6
+ // `crtr inspect -h` (dropping the `pkg` parent) when `crtr pkg inspect bogus` was
7
7
  // invoked, sending the caller to a nonexistent top-level command.
8
8
  import { test, describe } from 'node:test';
9
9
  import assert from 'node:assert/strict';
@@ -15,25 +15,25 @@ const leaf = defineLeaf({
15
15
  help: { name: 'search', summary: 'search', output: [], outputKind: 'object', effects: ['None. Read-only.'] },
16
16
  run: async () => ({}),
17
17
  });
18
- const findBranch = defineBranch({
19
- name: 'find',
20
- description: 'find',
18
+ const inspectBranch = defineBranch({
19
+ name: 'inspect',
20
+ description: 'inspect',
21
21
  whenToUse: 'x',
22
- help: { name: 'find', summary: 'find' },
22
+ help: { name: 'inspect', summary: 'inspect' },
23
23
  children: [leaf],
24
24
  });
25
- const skillBranch = defineBranch({
26
- name: 'skill',
27
- description: 'skill',
25
+ const pkgBranch = defineBranch({
26
+ name: 'pkg',
27
+ description: 'pkg',
28
28
  whenToUse: 'x',
29
- help: { name: 'skill', summary: 'skill' },
30
- rootEntry: { concept: 'skill', desc: 'skill', useWhen: 'x' },
31
- children: [findBranch],
29
+ help: { name: 'pkg', summary: 'pkg' },
30
+ rootEntry: { concept: 'pkg', desc: 'pkg', useWhen: 'x' },
31
+ children: [inspectBranch],
32
32
  });
33
33
  const root = defineRoot({
34
34
  tagline: 'test runtime',
35
35
  globals: [],
36
- subtrees: [skillBranch],
36
+ subtrees: [pkgBranch],
37
37
  });
38
38
  function nextHint(...tokens) {
39
39
  const { node, path, remaining } = walk(root, tokens);
@@ -44,15 +44,15 @@ describe('unknown-path error: recovery hint names the full valid path', () => {
44
44
  test('root-level unknown points at `crtr -h`', () => {
45
45
  assert.match(nextHint('bogus'), /Run `crtr -h`/);
46
46
  });
47
- test('one-level unknown points at `crtr skill -h`', () => {
48
- assert.match(nextHint('skill', 'bogus'), /Run `crtr skill -h`/);
47
+ test('one-level unknown points at `crtr pkg -h`', () => {
48
+ assert.match(nextHint('pkg', 'bogus'), /Run `crtr pkg -h`/);
49
49
  });
50
- test('two-level unknown points at `crtr skill find -h`, not `crtr find -h`', () => {
51
- const hint = nextHint('skill', 'find', 'bogus');
52
- assert.match(hint, /Run `crtr skill find -h`/);
53
- assert.doesNotMatch(hint, /Run `crtr find -h`/);
50
+ test('two-level unknown points at `crtr pkg inspect -h`, not `crtr inspect -h`', () => {
51
+ const hint = nextHint('pkg', 'inspect', 'bogus');
52
+ assert.match(hint, /Run `crtr pkg inspect -h`/);
53
+ assert.doesNotMatch(hint, /Run `crtr inspect -h`/);
54
54
  });
55
55
  test('valid children of the matched node are listed', () => {
56
- assert.match(nextHint('skill', 'find', 'bogus'), /Valid children: search\./);
56
+ assert.match(nextHint('pkg', 'inspect', 'bogus'), /Valid children: search\./);
57
57
  });
58
58
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/yield-ensures-daemon.test.ts
2
+ //
3
+ // Regression: `crtr node yield` timed out into "broker gone" instead of reviving
4
+ // in the same pane (observed 2026-06-18). The yield arms intent='refresh' and
5
+ // the broker shuts down expecting the DAEMON to relaunch it — but requestYield
6
+ // never ensured the daemon was running, unlike the spawn/recycle paths. With a
7
+ // dead daemon, the broker exited and nothing revived it; the viewer redialed its
8
+ // view.sock for ~30s and reported "broker gone". This locks in that requestYield
9
+ // ensures the supervisor before arming the refresh.
10
+ import { test, before, beforeEach, after } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { mkdtempSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { createNode, getNode } from '../canvas/canvas.js';
16
+ import { closeDb } from '../canvas/db.js';
17
+ import { requestYield } from '../runtime/promote.js';
18
+ let home;
19
+ function mkOrchestrator(id) {
20
+ const meta = {
21
+ node_id: id,
22
+ name: id,
23
+ created: new Date().toISOString(),
24
+ cwd: '/tmp/work',
25
+ kind: 'general',
26
+ mode: 'orchestrator', // already an orchestrator → yield skips the promote path
27
+ lifecycle: 'terminal',
28
+ status: 'active', // LIVE, so transition('yield') is legal
29
+ };
30
+ createNode(meta);
31
+ }
32
+ before(() => {
33
+ home = mkdtempSync(join(tmpdir(), 'crtr-yield-ensure-'));
34
+ process.env['CRTR_HOME'] = home;
35
+ });
36
+ beforeEach(() => {
37
+ closeDb();
38
+ rmSync(home, { recursive: true, force: true });
39
+ });
40
+ after(() => {
41
+ closeDb();
42
+ rmSync(home, { recursive: true, force: true });
43
+ delete process.env['CRTR_HOME'];
44
+ });
45
+ test('requestYield ensures the daemon before arming intent=refresh', () => {
46
+ mkOrchestrator('n1');
47
+ let ensured = 0;
48
+ const res = requestYield('n1', {}, { ensure: () => { ensured += 1; } });
49
+ // The supervisor that performs the refresh-revive must have been ensured...
50
+ assert.equal(ensured, 1, 'requestYield must ensure the daemon is running');
51
+ // ...and the yield must still arm the refresh.
52
+ assert.equal(res.willRefresh, true);
53
+ assert.equal(getNode('n1')?.intent, 'refresh');
54
+ });