@crouton-kit/crouter 0.3.32 → 0.3.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/README.md +8 -0
  2. package/dist/build-root.js +1 -0
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
  6. package/dist/builtin-memory/crouter-development/personas.md +3 -4
  7. package/dist/builtin-memory/crouter-development/plugins.md +18 -18
  8. package/dist/builtin-memory/design.md +4 -1
  9. package/dist/builtin-memory/development.md +4 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
  12. package/dist/builtin-memory/internal/storage-tiers.md +5 -5
  13. package/dist/builtin-memory/planning.md +4 -1
  14. package/dist/builtin-memory/product.md +80 -0
  15. package/dist/builtin-memory/spec.md +4 -1
  16. package/dist/builtin-personas/advisor/PERSONA.md +10 -0
  17. package/dist/builtin-personas/design/PERSONA.md +2 -2
  18. package/dist/builtin-personas/design/orchestrator.md +3 -3
  19. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  20. package/dist/builtin-personas/developer/orchestrator.md +2 -2
  21. package/dist/builtin-personas/explore/PERSONA.md +3 -3
  22. package/dist/builtin-personas/explore/orchestrator.md +4 -2
  23. package/dist/builtin-personas/general/PERSONA.md +1 -1
  24. package/dist/builtin-personas/general/orchestrator.md +1 -1
  25. package/dist/builtin-personas/orchestration-kernel.md +7 -14
  26. package/dist/builtin-personas/plan/PERSONA.md +1 -1
  27. package/dist/builtin-personas/plan/orchestrator.md +2 -2
  28. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
  29. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
  30. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
  31. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
  32. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
  33. package/dist/builtin-personas/product/PERSONA.md +18 -0
  34. package/dist/builtin-personas/product/orchestrator.md +14 -0
  35. package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
  36. package/dist/builtin-personas/review/PERSONA.md +1 -1
  37. package/dist/builtin-personas/review/orchestrator.md +1 -1
  38. package/dist/builtin-personas/runtime-base.md +5 -0
  39. package/dist/builtin-personas/spec/PERSONA.md +1 -1
  40. package/dist/builtin-personas/spec/orchestrator.md +2 -2
  41. package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
  42. package/dist/builtin-views/canvas/core.mjs +82 -1
  43. package/dist/builtin-views/canvas/tui.mjs +9 -5
  44. package/dist/builtin-views/canvas/web.jsx +3 -2
  45. package/dist/builtin-views/chat/core.mjs +725 -0
  46. package/dist/builtin-views/chat/text.mjs +101 -0
  47. package/dist/builtin-views/chat/tui.mjs +368 -0
  48. package/dist/builtin-views/chat/web.jsx +367 -0
  49. package/dist/builtin-views/prompt-review/core.mjs +863 -0
  50. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  51. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  52. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  53. package/dist/builtin-views/settings/core.mjs +397 -0
  54. package/dist/builtin-views/settings/text.mjs +40 -0
  55. package/dist/builtin-views/settings/tui.mjs +95 -0
  56. package/dist/builtin-views/settings/web.jsx +167 -0
  57. package/dist/cli.js +16 -3
  58. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
  59. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
  60. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
  61. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
  62. package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
  63. package/dist/clients/attach/attach-cmd.d.ts +18 -0
  64. package/dist/clients/attach/attach-cmd.js +1723 -795
  65. package/dist/clients/attach/canvas-panels.js +2 -2
  66. package/dist/clients/attach/chat-view.d.ts +10 -0
  67. package/dist/clients/attach/chat-view.js +23 -0
  68. package/dist/clients/attach/config-load.d.ts +8 -4
  69. package/dist/clients/attach/config-load.js +2 -0
  70. package/dist/clients/attach/extension-dialogs.d.ts +2 -3
  71. package/dist/clients/attach/extension-dialogs.js +7 -9
  72. package/dist/clients/attach/graph-overlay.js +3 -2
  73. package/dist/clients/attach/input-controller.d.ts +4 -3
  74. package/dist/clients/attach/input-controller.js +24 -3
  75. package/dist/clients/attach/slash-commands.d.ts +7 -7
  76. package/dist/clients/attach/slash-commands.js +38 -14
  77. package/dist/clients/attach/titled-editor.js +18 -14
  78. package/dist/clients/attach/view-socket.d.ts +2 -1
  79. package/dist/clients/attach/view-socket.js +27 -8
  80. package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
  81. package/dist/clients/web/__tests__/source-cache.test.js +32 -0
  82. package/dist/clients/web/dev-server.js +1 -0
  83. package/dist/clients/web/events.js +9 -11
  84. package/dist/clients/web/server.d.ts +4 -3
  85. package/dist/clients/web/server.js +138 -31
  86. package/dist/clients/web/source-cache.d.ts +10 -0
  87. package/dist/clients/web/source-cache.js +57 -0
  88. package/dist/clients/web/web-cmd.js +28 -9
  89. package/dist/commands/__tests__/human.test.js +30 -28
  90. package/dist/commands/canvas-history/search.js +1 -1
  91. package/dist/commands/canvas-history.js +5 -8
  92. package/dist/commands/canvas-issue.d.ts +2 -0
  93. package/dist/commands/canvas-issue.js +148 -0
  94. package/dist/commands/canvas-prune.js +19 -0
  95. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  96. package/dist/commands/canvas-rebuild-index.js +57 -0
  97. package/dist/commands/canvas-snapshot.d.ts +2 -0
  98. package/dist/commands/canvas-snapshot.js +48 -0
  99. package/dist/commands/canvas-tmux-spread.d.ts +2 -0
  100. package/dist/commands/canvas-tmux-spread.js +188 -0
  101. package/dist/commands/canvas.d.ts +1 -0
  102. package/dist/commands/canvas.js +16 -1
  103. package/dist/commands/chord.js +143 -48
  104. package/dist/commands/daemon.js +3 -3
  105. package/dist/commands/human/prompts.d.ts +0 -1
  106. package/dist/commands/human/prompts.js +39 -54
  107. package/dist/commands/human/queue.d.ts +12 -1
  108. package/dist/commands/human/queue.js +468 -73
  109. package/dist/commands/human/shared.d.ts +3 -4
  110. package/dist/commands/human/shared.js +3 -3
  111. package/dist/commands/human.js +11 -10
  112. package/dist/commands/memory/find.js +3 -2
  113. package/dist/commands/memory/list.js +4 -3
  114. package/dist/commands/memory/read.js +3 -3
  115. package/dist/commands/memory/shared.d.ts +1 -4
  116. package/dist/commands/memory/shared.js +6 -9
  117. package/dist/commands/memory/write.js +3 -3
  118. package/dist/commands/memory.js +4 -7
  119. package/dist/commands/node-context.d.ts +2 -0
  120. package/dist/commands/node-context.js +172 -0
  121. package/dist/commands/node-snapshot.d.ts +2 -0
  122. package/dist/commands/node-snapshot.js +123 -0
  123. package/dist/commands/node.js +228 -52
  124. package/dist/commands/pkg/plugin-inspect.js +6 -6
  125. package/dist/commands/pkg/plugin-manage.js +1 -1
  126. package/dist/commands/pkg/plugin.js +2 -2
  127. package/dist/commands/pkg.js +2 -2
  128. package/dist/commands/push.d.ts +2 -0
  129. package/dist/commands/push.js +79 -11
  130. package/dist/commands/revive.js +74 -1
  131. package/dist/commands/search/answer.d.ts +1 -0
  132. package/dist/commands/search/answer.js +50 -0
  133. package/dist/commands/search/contents.d.ts +1 -0
  134. package/dist/commands/search/contents.js +96 -0
  135. package/dist/commands/search/exa.d.ts +53 -0
  136. package/dist/commands/search/exa.js +155 -0
  137. package/dist/commands/search/puremd.d.ts +11 -0
  138. package/dist/commands/search/puremd.js +53 -0
  139. package/dist/commands/search/web.d.ts +1 -0
  140. package/dist/commands/search/web.js +65 -0
  141. package/dist/commands/search.d.ts +2 -0
  142. package/dist/commands/search.js +24 -0
  143. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  144. package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
  145. package/dist/commands/sys/config.js +59 -10
  146. package/dist/commands/sys/doctor.js +56 -4
  147. package/dist/commands/sys/prompt-review.d.ts +1 -0
  148. package/dist/commands/sys/prompt-review.js +178 -0
  149. package/dist/commands/sys/promptstudio.d.ts +2 -0
  150. package/dist/commands/sys/promptstudio.js +65 -0
  151. package/dist/commands/sys/settings.d.ts +2 -0
  152. package/dist/commands/sys/settings.js +16 -0
  153. package/dist/commands/sys/sync.js +311 -159
  154. package/dist/commands/sys/sysprompt.d.ts +1 -0
  155. package/dist/commands/sys/sysprompt.js +86 -0
  156. package/dist/commands/sys.js +9 -5
  157. package/dist/commands/view-new.js +2 -2
  158. package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
  159. package/dist/core/__tests__/artifact-paths.test.js +44 -0
  160. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  161. package/dist/core/__tests__/broker-preflight.test.js +85 -0
  162. package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
  163. package/dist/core/__tests__/canvas.test.js +37 -1
  164. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  165. package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
  166. package/dist/core/__tests__/child-death-wake.test.js +11 -2
  167. package/dist/core/__tests__/close.test.js +39 -1
  168. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  169. package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
  170. package/dist/core/__tests__/context-intro.test.js +48 -14
  171. package/dist/core/__tests__/daemon-boot.test.js +182 -9
  172. package/dist/core/__tests__/editor-label.test.d.ts +1 -0
  173. package/dist/core/__tests__/editor-label.test.js +26 -0
  174. package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
  175. package/dist/core/__tests__/fault-marker.test.js +112 -0
  176. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
  177. package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
  178. package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
  179. package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
  180. package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
  181. package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
  182. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  183. package/dist/core/__tests__/helpers/harness.js +2 -2
  184. package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
  185. package/dist/core/__tests__/human-surface-target.test.js +1 -1
  186. package/dist/core/__tests__/kickoff.test.js +18 -19
  187. package/dist/core/__tests__/memory-resolver.test.js +12 -1
  188. package/dist/core/__tests__/migration.test.js +49 -14
  189. package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
  190. package/dist/core/__tests__/model-ladders.test.js +160 -0
  191. package/dist/core/__tests__/node-env.test.d.ts +1 -0
  192. package/dist/core/__tests__/node-env.test.js +26 -0
  193. package/dist/core/__tests__/push-final-guard.test.js +7 -1
  194. package/dist/core/__tests__/relaunch-root.test.js +9 -9
  195. package/dist/core/__tests__/revive.test.js +36 -6
  196. package/dist/core/__tests__/spawn-root.test.js +24 -1
  197. package/dist/core/__tests__/stop-guard.test.js +10 -0
  198. package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
  199. package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
  200. package/dist/core/__tests__/tmux-surface.test.js +5 -1
  201. package/dist/core/__tests__/unknown-path.test.js +19 -19
  202. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  203. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  204. package/dist/core/artifact.d.ts +2 -33
  205. package/dist/core/artifact.js +27 -87
  206. package/dist/core/bootstrap.js +2 -0
  207. package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
  208. package/dist/core/canvas/browse/app.js +87 -17
  209. package/dist/core/canvas/browse/model.d.ts +10 -1
  210. package/dist/core/canvas/browse/model.js +37 -1
  211. package/dist/core/canvas/browse/render.js +35 -33
  212. package/dist/core/canvas/canvas.d.ts +7 -0
  213. package/dist/core/canvas/canvas.js +54 -5
  214. package/dist/core/canvas/db.js +35 -8
  215. package/dist/core/canvas/labels.d.ts +6 -8
  216. package/dist/core/canvas/labels.js +8 -11
  217. package/dist/core/canvas/nav-model.d.ts +5 -4
  218. package/dist/core/canvas/nav-model.js +31 -17
  219. package/dist/core/canvas/paths.d.ts +18 -1
  220. package/dist/core/canvas/paths.js +31 -2
  221. package/dist/core/canvas/render.d.ts +25 -0
  222. package/dist/core/canvas/render.js +96 -13
  223. package/dist/core/canvas/status-glyph.d.ts +35 -0
  224. package/dist/core/canvas/status-glyph.js +104 -0
  225. package/dist/core/canvas/types.d.ts +2 -2
  226. package/dist/core/config.js +114 -16
  227. package/dist/core/fault-classifier.d.ts +41 -0
  228. package/dist/core/fault-classifier.js +110 -0
  229. package/dist/core/feed/inbox.d.ts +15 -0
  230. package/dist/core/feed/inbox.js +40 -14
  231. package/dist/core/frontmatter.d.ts +1 -11
  232. package/dist/core/frontmatter.js +1 -55
  233. package/dist/core/hearth/config.d.ts +2 -0
  234. package/dist/core/hearth/config.js +75 -0
  235. package/dist/core/hearth/guest-env.d.ts +2 -0
  236. package/dist/core/hearth/guest-env.js +11 -0
  237. package/dist/core/hearth/index.d.ts +5 -0
  238. package/dist/core/hearth/index.js +5 -0
  239. package/dist/core/hearth/provider.d.ts +21 -0
  240. package/dist/core/hearth/provider.js +10 -0
  241. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  242. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  243. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  244. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  245. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  246. package/dist/core/hearth/providers/blaxel.js +208 -0
  247. package/dist/core/hearth/providers/types.d.ts +72 -0
  248. package/dist/core/hearth/providers/types.js +1 -0
  249. package/dist/core/hearth/registry.d.ts +15 -0
  250. package/dist/core/hearth/registry.js +179 -0
  251. package/dist/core/hearth/types.d.ts +121 -0
  252. package/dist/core/hearth/types.js +1 -0
  253. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  254. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  255. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  256. package/dist/core/host-exports/builtins.js +68 -0
  257. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  258. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  259. package/dist/core/io.js +1 -1
  260. package/dist/core/log.d.ts +9 -0
  261. package/dist/core/log.js +113 -0
  262. package/dist/core/memory-resolver.d.ts +8 -8
  263. package/dist/core/memory-resolver.js +15 -28
  264. package/dist/core/personas/index.d.ts +4 -4
  265. package/dist/core/personas/index.js +2 -2
  266. package/dist/core/personas/loader.d.ts +51 -0
  267. package/dist/core/personas/loader.js +54 -50
  268. package/dist/core/personas/resolve.d.ts +43 -27
  269. package/dist/core/personas/resolve.js +336 -94
  270. package/dist/core/runtime/auth-reload.d.ts +7 -0
  271. package/dist/core/runtime/auth-reload.js +69 -0
  272. package/dist/core/runtime/bearings.d.ts +35 -15
  273. package/dist/core/runtime/bearings.js +305 -44
  274. package/dist/core/runtime/branded-host.d.ts +4 -2
  275. package/dist/core/runtime/branded-host.js +66 -4
  276. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  277. package/dist/core/runtime/broker.js +343 -18
  278. package/dist/core/runtime/close.js +37 -29
  279. package/dist/core/runtime/connectivity.d.ts +12 -0
  280. package/dist/core/runtime/connectivity.js +73 -0
  281. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  282. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  283. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  284. package/dist/core/runtime/fault-recovery.js +74 -0
  285. package/dist/core/runtime/fault.d.ts +25 -0
  286. package/dist/core/runtime/fault.js +176 -0
  287. package/dist/core/runtime/front-door.d.ts +1 -1
  288. package/dist/core/runtime/front-door.js +2 -2
  289. package/dist/core/runtime/host.d.ts +10 -0
  290. package/dist/core/runtime/host.js +71 -9
  291. package/dist/core/runtime/kickoff.js +31 -45
  292. package/dist/core/runtime/launch.d.ts +32 -18
  293. package/dist/core/runtime/launch.js +148 -43
  294. package/dist/core/runtime/model-swap.d.ts +18 -0
  295. package/dist/core/runtime/model-swap.js +95 -0
  296. package/dist/core/runtime/naming.d.ts +7 -0
  297. package/dist/core/runtime/naming.js +61 -9
  298. package/dist/core/runtime/nodes.js +6 -1
  299. package/dist/core/runtime/persona.js +16 -17
  300. package/dist/core/runtime/placement.d.ts +36 -31
  301. package/dist/core/runtime/placement.js +149 -64
  302. package/dist/core/runtime/promote.d.ts +2 -0
  303. package/dist/core/runtime/promote.js +23 -3
  304. package/dist/core/runtime/recycle.js +6 -4
  305. package/dist/core/runtime/reset.d.ts +5 -2
  306. package/dist/core/runtime/reset.js +2 -2
  307. package/dist/core/runtime/revive.d.ts +4 -0
  308. package/dist/core/runtime/revive.js +27 -9
  309. package/dist/core/runtime/roadmap.d.ts +1 -1
  310. package/dist/core/runtime/roadmap.js +1 -1
  311. package/dist/core/runtime/spawn.d.ts +2 -2
  312. package/dist/core/runtime/spawn.js +30 -6
  313. package/dist/core/runtime/stop-guard.js +6 -1
  314. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  315. package/dist/core/runtime/tmux-chrome.js +1 -1
  316. package/dist/core/runtime/tmux.d.ts +29 -6
  317. package/dist/core/runtime/tmux.js +134 -80
  318. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  319. package/dist/core/runtime/view-socket-client.js +203 -0
  320. package/dist/core/substrate/index.d.ts +1 -1
  321. package/dist/core/substrate/index.js +1 -1
  322. package/dist/core/substrate/on-read.js +16 -4
  323. package/dist/core/substrate/render.d.ts +10 -13
  324. package/dist/core/substrate/render.js +56 -51
  325. package/dist/core/tui/host.js +83 -18
  326. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  327. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  328. package/dist/core/view/contract.d.ts +20 -0
  329. package/dist/core/view/stream-local.d.ts +3 -0
  330. package/dist/core/view/stream-local.js +184 -0
  331. package/dist/core/view/transport-cache.d.ts +8 -0
  332. package/dist/core/view/transport-cache.js +38 -0
  333. package/dist/core/view/transport-local.js +4 -0
  334. package/dist/core/view/transport.d.ts +7 -1
  335. package/dist/daemon/crtrd.d.ts +3 -19
  336. package/dist/daemon/crtrd.js +139 -178
  337. package/dist/daemon/manage.d.ts +18 -1
  338. package/dist/daemon/manage.js +54 -9
  339. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  340. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  341. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  342. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  343. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  344. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  345. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  346. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  347. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  348. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  349. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  350. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  351. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  352. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  353. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  354. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  355. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  356. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  357. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  358. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  359. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  360. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  361. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  362. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  363. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  364. package/dist/hearth/wake-proxy/auth.js +128 -0
  365. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  366. package/dist/hearth/wake-proxy/config.js +151 -0
  367. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  368. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  369. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  370. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  371. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  372. package/dist/hearth/wake-proxy/home.js +297 -0
  373. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  374. package/dist/hearth/wake-proxy/main.js +134 -0
  375. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  376. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  377. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  378. package/dist/hearth/wake-proxy/proxy.js +716 -0
  379. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  380. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  381. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  382. package/dist/hearth/wake-proxy/redact.js +17 -0
  383. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  384. package/dist/hearth/wake-proxy/server.js +142 -0
  385. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  386. package/dist/hearth/wake-proxy/state.js +342 -0
  387. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  388. package/dist/hearth/wake-proxy/types.js +1 -0
  389. package/dist/index.js +3 -2
  390. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  391. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  392. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  393. package/dist/pi-extensions/canvas-nav.js +15 -8
  394. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  395. package/dist/pi-extensions/canvas-stophook.js +78 -11
  396. package/dist/types.d.ts +39 -20
  397. package/dist/types.js +38 -13
  398. package/dist/web/runtime.js +141 -42
  399. package/dist/web/transport-http.js +7 -5
  400. package/dist/web/transport-stream.d.ts +3 -0
  401. package/dist/web/transport-stream.js +204 -0
  402. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  403. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  404. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  405. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  406. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  407. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  408. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  409. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  410. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  411. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  412. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  413. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  414. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  415. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  416. package/dist/web-client/index.html +3 -2
  417. package/package.json +32 -10
  418. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  419. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  420. package/dist/core/skill-sync/builtins.js +0 -112
  421. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  422. package/dist/core/skill-sync/claude-plugins.js +0 -71
  423. package/dist/core/skill-sync/engine.d.ts +0 -42
  424. package/dist/core/skill-sync/engine.js +0 -633
  425. package/dist/core/skill-sync/manifest.d.ts +0 -64
  426. package/dist/core/skill-sync/manifest.js +0 -181
  427. package/dist/core/skill-sync/profile.d.ts +0 -76
  428. package/dist/core/skill-sync/profile.js +0 -173
  429. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  430. package/dist/core/skill-sync/snapshot.js +0 -120
  431. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  432. package/dist/pi-extensions/canvas-commands.js +0 -113
  433. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  434. package/dist/pi-extensions/canvas-resume.js +0 -83
  435. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  436. package/dist/pi-extensions/canvas-view.js +0 -76
  437. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  438. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  439. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  440. /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
- // Tests for the <crtr-context> bearings preamble:
2
- // 1. Worker and orchestrator bearings carry the `<knowledge>` block
3
- // (substrate knowledge docs + node-local docs as a file tree).
1
+ // Tests for the <crtr-bearings> preamble:
2
+ // 1. Worker and orchestrator bearings carry the `<memory kind="knowledge">`
3
+ // block (substrate knowledge docs + node-local docs as a file tree).
4
4
  // Orchestrators add the across-cycles framing; promotion delivers that
5
5
  // same orchestrator context-dir framing to a node that spawned base.
6
6
  // 2. canvas-context-intro injects the block as its own session message at
@@ -20,9 +20,14 @@ import { personaDrift } from '../runtime/persona.js';
20
20
  import { memoryDir } from '../runtime/memory.js';
21
21
  import registerCanvasContextIntro, { buildContextIntro, renderContextMessage, CONTEXT_INTRO_CUSTOM_TYPE, } from '../../pi-extensions/canvas-context-intro.js';
22
22
  let home;
23
+ let prevAgentDir;
23
24
  before(() => {
24
25
  home = mkdtempSync(join(tmpdir(), 'crtr-ctxintro-'));
25
26
  process.env['CRTR_HOME'] = home;
27
+ // Isolate pi's global agent dir to an empty path so buildProjectContextBlock
28
+ // can't pick up the dev machine's real ~/.pi/agent/AGENTS.md (hermeticity).
29
+ prevAgentDir = process.env['PI_CODING_AGENT_DIR'];
30
+ process.env['PI_CODING_AGENT_DIR'] = join(home, 'no-agent-dir');
26
31
  });
27
32
  beforeEach(() => {
28
33
  closeDb();
@@ -33,6 +38,10 @@ after(() => {
33
38
  rmSync(home, { recursive: true, force: true });
34
39
  delete process.env['CRTR_HOME'];
35
40
  delete process.env['CRTR_NODE_ID'];
41
+ if (prevAgentDir === undefined)
42
+ delete process.env['PI_CODING_AGENT_DIR'];
43
+ else
44
+ process.env['PI_CODING_AGENT_DIR'] = prevAgentDir;
36
45
  });
37
46
  test('worker bearings: base framing + <knowledge> block, NO across-cycles framing', () => {
38
47
  // Bug-regression: review finding M1 — buildContextBearings renders the
@@ -44,23 +53,49 @@ test('worker bearings: base framing + <knowledge> block, NO across-cycles framin
44
53
  mkdirSync(dir, { recursive: true });
45
54
  writeFileSync(join(dir, 'test-ref.md'), '---\nkind: knowledge\nwhen-and-why-to-read: When testing, this reference should be read because it is a regression fixture\nsystem-prompt-visibility: preview\n---\nTest body.\n');
46
55
  const block = buildContextIntro(meta.node_id);
47
- assert.match(block, new RegExp(`<crtr-context dir="${contextDir(meta.node_id)}">`));
56
+ assert.match(block, /<crtr-bearings>/, 'opens with the bearings section');
57
+ assert.ok(block.includes(contextDir(meta.node_id)), 'names the context-dir path inline (no dir= attribute)');
58
+ assert.doesNotMatch(block, /<crtr-context/, 'no legacy <crtr-context dir=…> wrapper');
59
+ assert.match(block, /## Your context directory/, 'context dir is its own markdown subsection');
48
60
  assert.match(block, /shared document store, not a task tracker/, 'base = shared docs, not tasks');
49
- // Knowledge content renders ONLY as the <knowledge> file-tree block.
50
- assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
61
+ // The catalog is a sibling <memory kind="knowledge"> block, never nested
62
+ // under anything dir-scoped.
63
+ assert.match(block, /<memory kind="knowledge">/, 'knowledge catalog present as a memory block');
51
64
  // Per-store stanza headers (label · dir) never appear.
52
65
  assert.doesNotMatch(block, /user-global · /, 'no user-global label·dir stanza');
53
66
  assert.doesNotMatch(block, /node-local · /, 'no node-local label·dir stanza');
54
67
  // No (empty) placeholder marker.
55
68
  assert.doesNotMatch(block, /\(empty\)/, 'no (empty) placeholder');
56
- assert.match(block, /<knowledge>/, '<knowledge> block present');
57
69
  assert.match(block, /\nknowledge\n/, 'tree headed by the `knowledge` root label');
58
70
  // The node-local preview-rung doc renders as a tree entry with a `# read when:`
59
71
  // routing line (verbatim previewLine output).
60
72
  assert.match(block, /test-ref {2}# read when: When testing, this reference should be read because it is a regression fixture\./, 'node-local preview doc renders its routing line');
73
+ assert.match(block, /<project_context>/, 'project_context now rides the bearings even without project files');
74
+ assert.match(block, /<environment cwd="\/tmp\/work">/, 'environment block is always present');
75
+ assert.match(block, /Directory listing: unavailable \(cwd missing or unreadable\)\./, 'missing cwd degrades the directory listing');
76
+ assert.match(block, /Git: not a git repository\./, 'missing cwd degrades git info');
61
77
  // A terminal worker must NOT carry the orchestrator across-cycles framing.
62
78
  assert.doesNotMatch(block, /refresh cycles/, 'no across-cycles framing for a terminal worker');
63
- assert.match(block, /<\/crtr-context>/);
79
+ assert.match(block, /<\/crtr-bearings>/);
80
+ });
81
+ test('project instructions (AGENTS.md/CLAUDE.md) ride the bearings, not the system prompt', () => {
82
+ // pi's <project_context> is suppressed in the system prompt (broker.ts
83
+ // noContextFiles) and re-rendered into the first-message bearings here. The
84
+ // discovery walks the node cwd ancestry, so a CLAUDE.md in the node's cwd must
85
+ // surface inside a <project_context> block AFTER the <crtr-bearings> block.
86
+ const proj = mkdtempSync(join(tmpdir(), 'crtr-proj-'));
87
+ writeFileSync(join(proj, 'CLAUDE.md'), 'PROJECT_MARKER: build then commit.\n');
88
+ const meta = spawnNode({ kind: 'general', cwd: proj, parent: null });
89
+ const block = buildContextIntro(meta.node_id);
90
+ assert.match(block, /<project_context>/, 'project_context block present in the bearings');
91
+ assert.match(block, new RegExp(`<project_instructions path="${join(proj, 'CLAUDE.md')}">`), 'the project CLAUDE.md is rendered with its path');
92
+ assert.match(block, /PROJECT_MARKER: build then commit\./, 'project file content rides the message');
93
+ assert.match(block, /<environment cwd=".*crtr-proj-.*">/, 'environment block is appended inside project_context');
94
+ assert.match(block, /Directory:\n CLAUDE\.md\n/, 'environment block includes an ls-style listing');
95
+ assert.match(block, /Git: not a git repository\./, 'git snapshot degrades cleanly for a non-repo');
96
+ assert.match(block, /<environment[\s\S]*<\/environment>\n<\/project_context>$/, 'environment block closes project_context');
97
+ assert.ok(block.indexOf('</crtr-bearings>') < block.indexOf('<project_context>'), 'project_context follows the crtr-bearings block');
98
+ rmSync(proj, { recursive: true, force: true });
64
99
  });
65
100
  test('orchestrator bearings: across-cycles framing + node-local substrate docs ride into <knowledge>; a non-substrate .md file is never inlined', () => {
66
101
  // Bug-regression: review finding M1 — buildContextBearings renders <knowledge>.
@@ -78,21 +113,20 @@ test('orchestrator bearings: across-cycles framing + node-local substrate docs r
78
113
  const block = buildContextIntro(meta.node_id);
79
114
  assert.match(block, /shared document store, not a task tracker/, 'still carries the base framing');
80
115
  assert.match(block, /refresh cycles/, 'orchestrator gets the across-cycles framing');
81
- assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
82
- assert.match(block, /<knowledge>/, 'knowledge block present');
116
+ assert.match(block, /<memory kind="knowledge">/, 'knowledge catalog present as a memory block');
83
117
  assert.match(block, /flaky-build {2}# read when: When the build flakes, this reference should be read because the first run fails\./, 'node-local doc renders as a tree entry with its routing line');
84
118
  // The non-substrate file never renders: no header line, no pointer line, no path.
85
119
  assert.ok(!block.includes('# memory index'), 'the index header comment is NOT inlined');
86
120
  assert.ok(!block.includes('- [Flaky build](flaky-build.md)'), 'index pointer lines are NOT inlined');
87
121
  assert.ok(!block.includes(legacyIndexPath), 'no absolute index (MEMORY.md) path');
88
122
  assert.ok(!block.includes('node-local · '), 'no label·dir stanza header');
89
- assert.match(block, /<\/crtr-context>/);
123
+ assert.match(block, /<\/crtr-bearings>/);
90
124
  });
91
125
  test('orchestrator bearings: no per-store stanzas or (empty) markers; a rung-none node-local doc still surfaces as a bare-name tree entry', () => {
92
126
  // Bug-regression: review findings M1 + M6 — the <knowledge> block carries no
93
127
  // per-store `label · dir` stanzas or (empty) markers, and node-local docs are
94
128
  // NOT filtered on rung: a migrated node-local doc defaults
95
- // system-prompt-visibility: none and must still ride into <knowledge> as
129
+ // system-prompt-visibility: none and must still ride into the knowledge memory block as
96
130
  // its bare name (floored to the `name` rung; never its body).
97
131
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
98
132
  promote(meta.node_id); // flip to orchestrator mode
@@ -147,7 +181,7 @@ test('session_start injects the block as the first message of an empty session',
147
181
  assert.equal(pi.sent.length, 1, 'one message injected');
148
182
  assert.equal(pi.sent[0].customType, CONTEXT_INTRO_CUSTOM_TYPE);
149
183
  assert.equal(pi.sent[0].display, true);
150
- assert.match(pi.sent[0].content, /<crtr-context dir=/);
184
+ assert.match(pi.sent[0].content, /<crtr-bearings>/);
151
185
  });
152
186
  test('session_start is idempotent across resume (skips if already in history)', () => {
153
187
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
@@ -177,7 +211,7 @@ test('renderer registered for the customType; collapsed hides body, ctrl+o expan
177
211
  registerCanvasContextIntro(pi);
178
212
  const renderer = pi.renderers[CONTEXT_INTRO_CUSTOM_TYPE];
179
213
  assert.ok(renderer, 'message renderer registered for crtr-context');
180
- const body = '<crtr-context dir="/x">\nshared document store\n</crtr-context>';
214
+ const body = '<crtr-bearings>\nshared document store\n</crtr-bearings>';
181
215
  const message = { customType: CONTEXT_INTRO_CUSTOM_TYPE, content: body };
182
216
  const theme = {}; // no fg → plain text, easy to assert
183
217
  // Collapsed (default): a single stub line, NONE of the body.
@@ -1,13 +1,18 @@
1
1
  import { test, before, after, beforeEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { spawnSync } from 'node:child_process';
6
+ import { spawnSync, spawn } from 'node:child_process';
7
+ import { createServer } from 'node:net';
7
8
  import { createNode, getNode, subscribe } from '../canvas/canvas.js';
8
9
  import { closeDb } from '../canvas/db.js';
9
10
  import { readInboxSince } from '../feed/inbox.js';
10
- import { superviseTick } from '../../daemon/crtrd.js';
11
+ import { superviseTick, readPidfile } from '../../daemon/crtrd.js';
12
+ import { verifyDaemonStartup } from '../../daemon/manage.js';
13
+ import { waitForBrokerViewSocket } from '../runtime/placement.js';
14
+ import { viewSocketPath } from '../canvas/paths.js';
15
+ import { signBrandedHostArtifacts, brandedHostWorks } from '../runtime/branded-host.js';
11
16
  import { markBusy, clearBusy } from '../runtime/busy.js';
12
17
  let home;
13
18
  function node(id, over = {}) {
@@ -104,13 +109,181 @@ test('a crashed booted child is revivable, not reaped, and raises no false boot-
104
109
  assert.equal(readInboxSince('P2').length, 0, 'no false boot-failure / child-death push on a crash (wake relocated to push/close)');
105
110
  clearBusy('C2');
106
111
  });
107
- // A still-booting node whose window is alive must be left untouched — boot is
108
- // slow, and an alive window means pi may still be coming up.
109
- test('a node with a live window is left alone even before it boots', async () => {
110
- // No tmux_session/window → treated as an inline root and skipped entirely,
111
- // which exercises the "no placement → not daemon-managed" guard rather than a
112
- // false reap. (We avoid depending on a real live tmux window in the test env.)
112
+ // An unplaced active node is not daemon-managed; the daemon leaves it untouched
113
+ // rather than treating the missing broker/session fields as a boot failure.
114
+ test('an unplaced active node is left alone before it boots', async () => {
113
115
  createNode(node('S', { status: 'active', pi_session_id: null }));
114
116
  await superviseTick();
115
117
  assert.equal(getNode('S').status, 'active', 'unplaced active node is not reaped');
116
118
  });
119
+ // Daemon start guard: spawnDaemon must not report success before the daemon is
120
+ // plausibly alive and has written its pidfile. This is a pure bounded poll over
121
+ // injected clocks/probes, so we can lock it down deterministically without a
122
+ // real daemon process.
123
+ test('verifyDaemonStartup waits for pidfile + live pid, then reports success for the spawned pid', async () => {
124
+ let now = 0;
125
+ let pidfileReady = false;
126
+ let pidAliveReady = false;
127
+ let slept = 0;
128
+ const existingPid = await verifyDaemonStartup(42, 50, {
129
+ now: () => now,
130
+ readPidfile: () => (pidfileReady ? 42 : null),
131
+ isPidAlive: () => pidAliveReady,
132
+ sleepMs: (ms) => {
133
+ slept += ms;
134
+ now += ms;
135
+ if (now >= 20)
136
+ pidfileReady = true;
137
+ if (now >= 30)
138
+ pidAliveReady = true;
139
+ },
140
+ });
141
+ assert.equal(existingPid, null, 'the spawned daemon owns the pidfile when startup succeeds');
142
+ assert.equal(pidfileReady, true, 'pidfile must have appeared before success');
143
+ assert.equal(pidAliveReady, true, 'pid must have been alive before success');
144
+ assert.equal(slept > 0, true, 'the guard must actually poll');
145
+ });
146
+ // Regression for the cold-guest recreate blocker: the production startup window
147
+ // must tolerate a slower cold start than the old 500ms bound.
148
+ test('verifyDaemonStartup default window tolerates a slower cold start', async () => {
149
+ let now = 0;
150
+ let pidfileReady = false;
151
+ let pidAliveReady = false;
152
+ const existingPid = await verifyDaemonStartup(42, undefined, {
153
+ now: () => now,
154
+ readPidfile: () => (pidfileReady ? 42 : null),
155
+ isPidAlive: () => pidAliveReady,
156
+ sleepMs: (ms) => {
157
+ now += ms;
158
+ if (now >= 600)
159
+ pidfileReady = true;
160
+ if (now >= 700)
161
+ pidAliveReady = true;
162
+ },
163
+ });
164
+ assert.equal(existingPid, null, 'the production startup window must allow a cold guest to settle');
165
+ });
166
+ // A concurrent start race is success when another live daemon already owns the
167
+ // pidfile; the loser must return that existing pid instead of timing out.
168
+ test('verifyDaemonStartup returns the existing live daemon when another pid won the startup race', async () => {
169
+ let now = 0;
170
+ let slept = 0;
171
+ const existingPid = await verifyDaemonStartup(42, 50, {
172
+ now: () => now,
173
+ readPidfile: () => 7,
174
+ isPidAlive: (pid) => pid === 7,
175
+ sleepMs: (ms) => {
176
+ slept += ms;
177
+ now += ms;
178
+ },
179
+ });
180
+ assert.equal(existingPid, 7, 'the live pidfile owner must be reported back');
181
+ assert.equal(slept, 0, 'no polling is needed once another live daemon is visible');
182
+ });
183
+ // The guard stays bounded: if the daemon never becomes ready, it throws instead
184
+ // of claiming "started" forever.
185
+ test('verifyDaemonStartup throws after its tiny startup window expires', async () => {
186
+ let now = 0;
187
+ await assert.rejects(async () => verifyDaemonStartup(99, 25, {
188
+ now: () => now,
189
+ readPidfile: () => null,
190
+ isPidAlive: () => false,
191
+ sleepMs: (ms) => {
192
+ now += ms;
193
+ },
194
+ }), /did not become ready within 25ms/);
195
+ });
196
+ // REGRESSION (bug #102): the verifier polls with a real async sleep, NOT a
197
+ // synchronous Atomics.wait, so the event loop stays free to dispatch the spawned
198
+ // child's `exit` event. We spawn a REAL child that exits immediately and let the
199
+ // real wall clock + real setTimeout sleep drive the loop: the exit event fires
200
+ // mid-wait, childExited() flips non-null, and the verifier fails fast with the
201
+ // exit reason — well inside the 2s window. If the loop blocked the event loop
202
+ // (the old bug), the exit event would never fire and this would time out with the
203
+ // generic message instead.
204
+ test('verifyDaemonStartup throws when the spawned daemon exits before it can become ready', async () => {
205
+ let exited = null;
206
+ const child = spawn(process.execPath, ['-e', 'process.exit(1)'], { stdio: 'ignore' });
207
+ child.once('exit', (code, signal) => {
208
+ exited = { code, signal };
209
+ });
210
+ const started = Date.now();
211
+ await assert.rejects(async () => verifyDaemonStartup(77, 2_000, {
212
+ readPidfile: () => null,
213
+ isPidAlive: () => false,
214
+ childExited: () => exited,
215
+ }), /exited before becoming ready \(with exit code 1\)/);
216
+ assert.ok(Date.now() - started < 1_500, 'must fail fast on the real child exit, not run out the window');
217
+ });
218
+ test('the daemon honours an explicit CRTR_PIDFILE override', () => {
219
+ const home = mkdtempSync(join(tmpdir(), 'crtr-pidfile-home-'));
220
+ const pidfile = join(home, 'crtrd.pid');
221
+ const prevHome = process.env['CRTR_HOME'];
222
+ const prevPidfile = process.env['CRTR_PIDFILE'];
223
+ process.env['CRTR_HOME'] = mkdtempSync(join(tmpdir(), 'crtr-home-'));
224
+ process.env['CRTR_PIDFILE'] = pidfile;
225
+ writeFileSync(pidfile, '321');
226
+ try {
227
+ assert.equal(readPidfile(), 321, 'the daemon must read its pidfile from CRTR_PIDFILE, not CRTR_HOME');
228
+ }
229
+ finally {
230
+ if (prevHome === undefined)
231
+ delete process.env['CRTR_HOME'];
232
+ else
233
+ process.env['CRTR_HOME'] = prevHome;
234
+ if (prevPidfile === undefined)
235
+ delete process.env['CRTR_PIDFILE'];
236
+ else
237
+ process.env['CRTR_PIDFILE'] = prevPidfile;
238
+ }
239
+ });
240
+ // REGRESSION (bug #102 follow-up): with NO exit promise, the readiness loop must
241
+ // still observe a real `connect`. A buggy `?? Promise.resolve('retry')` race lost
242
+ // to the probe instantly, so live-broker callers (focus on an already-alive node,
243
+ // canvas-tmux-spread) could never return true and stalled the full 30s deadline.
244
+ test('waitForBrokerViewSocket returns true on a live socket when given no exit promise', async () => {
245
+ // CRTR_SOCK_DIR override keeps the AF_UNIX path short — the temp CRTR_HOME path
246
+ // would blow past macOS's 104-byte sun_path limit (listen EINVAL).
247
+ const sockDir = mkdtempSync('/tmp/crtr-sock-');
248
+ process.env['CRTR_SOCK_DIR'] = sockDir;
249
+ const nodeId = 'lb';
250
+ const sockPath = viewSocketPath(nodeId);
251
+ const server = createServer();
252
+ await new Promise((resolve) => server.listen(sockPath, () => resolve()));
253
+ try {
254
+ const started = Date.now();
255
+ const ready = await waitForBrokerViewSocket(nodeId);
256
+ assert.equal(ready, true, 'a live accepting socket must report ready even without an exit promise');
257
+ assert.ok(Date.now() - started < 1_000, 'readiness must resolve promptly, not stall the full deadline');
258
+ }
259
+ finally {
260
+ await new Promise((resolve) => server.close(() => resolve()));
261
+ delete process.env['CRTR_SOCK_DIR'];
262
+ rmSync(sockDir, { recursive: true, force: true });
263
+ }
264
+ });
265
+ test('waitForBrokerViewSocket returns false as soon as the real broker process exits', async () => {
266
+ const child = spawn(process.execPath, ['-e', 'process.exit(1)'], { stdio: 'ignore' });
267
+ const exited = new Promise((resolve) => {
268
+ child.once('exit', (code, signal) => {
269
+ resolve({ code, signal });
270
+ });
271
+ });
272
+ const started = Date.now();
273
+ const ready = await waitForBrokerViewSocket('dead-broker-for-readiness-test', exited);
274
+ assert.equal(ready, false, 'an exited broker must fail readiness');
275
+ assert.ok(Date.now() - started < 1000, 'the readiness guard must fail fast instead of waiting for the socket deadline');
276
+ });
277
+ test('signBrandedHostArtifacts signs the copied libnode before the branded binary', () => {
278
+ const signed = [];
279
+ signBrandedHostArtifacts('/tmp/crouter', '/tmp/libnode.dylib', (path) => signed.push(path));
280
+ assert.deepEqual(signed, ['/tmp/libnode.dylib', '/tmp/crouter']);
281
+ });
282
+ test('brandedHostWorks health-checks if a binary is executable', () => {
283
+ // Issue #102: the branded host binary can be corrupted or killed by macOS
284
+ // before exec, causing spawn() to fail silently. The health-check detects this.
285
+ const goodBin = process.execPath; // a real, executable binary
286
+ const badBin = '/nonexistent/crouter-broken';
287
+ assert.equal(brandedHostWorks(goodBin), true, 'health-check passes for a real executable binary');
288
+ assert.equal(brandedHostWorks(badBin), false, 'health-check fails for a nonexistent binary');
289
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { editorLabel } from '../canvas/labels.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: 'orchestrator',
12
+ lifecycle: 'resident',
13
+ status: 'active',
14
+ cycles: 7,
15
+ ...over,
16
+ };
17
+ }
18
+ test('editorLabel orders full name before kind and omits the cycle number', () => {
19
+ const label = editorLabel(meta());
20
+ assert.equal(label, 'alpha-beta developer (orchestrator)');
21
+ assert.equal(label.includes(String(meta().cycles)), false);
22
+ });
23
+ test('editorLabel falls back to kind when fullName collapses to kind or empty', () => {
24
+ assert.equal(editorLabel(meta({ name: 'developer', kind: 'developer', mode: 'base' })), 'developer (base)');
25
+ assert.equal(editorLabel(meta({ name: '', kind: 'developer', mode: 'base' })), 'developer (base)');
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/fault-marker.test.ts
2
+ //
3
+ // BUG-REGRESSION: active-fault visibility. When a node's engine exhausts its
4
+ // retry budget on a rate-limit / overloaded / connection error, the broker
5
+ // stays alive and the node is indistinguishable from a healthy dormant node for
6
+ // up to the fault retry quiet window.
7
+ // The active-fault MARKER makes that invisible window visible on the canvas-graph
8
+ // views. Two legs:
9
+ // M1 — classify: each kind + the fallback (the heuristic, order-sensitive:
10
+ // rate-limit/overloaded checked before connection).
11
+ // M2 — the marker round-trip through dashboardRowsAll: record → the row shows
12
+ // hanging (streaming forced false); clear → gone; a DEAD pid → not hanging
13
+ // (the pid-AND gate makes a stale marker from a crashed broker harmless).
14
+ import { test } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { createHeadlessHarness } from './helpers/harness.js';
18
+ import { classify } from '../fault-classifier.js';
19
+ import { clearFault, recordFault, isModelNotFoundError } from '../runtime/fault.js';
20
+ import { dashboardRowsAll } from '../canvas/render.js';
21
+ // ---------------------------------------------------------------------------
22
+ // M1 — the heuristic classifier.
23
+ // ---------------------------------------------------------------------------
24
+ test('M1 classify: each kind + fallback, order-sensitive', () => {
25
+ // rate-limit.
26
+ assert.equal(classify('pi→provider', 'Rate limit exceeded').kind, 'rate-limit');
27
+ assert.equal(classify('pi→provider', 'HTTP 429 Too Many Requests').kind, 'rate-limit');
28
+ assert.equal(classify('pi→provider', 'you have exceeded your quota').kind, 'rate-limit');
29
+ // overloaded.
30
+ assert.equal(classify('pi→provider', 'Overloaded').kind, 'overloaded');
31
+ assert.equal(classify('pi→provider', 'Error 529: server is temporarily unavailable').kind, 'overloaded');
32
+ assert.equal(classify('pi→provider', '503 Service Unavailable').kind, 'overloaded');
33
+ assert.equal(classify('pi→provider', 'the server busy, try again').kind, 'overloaded');
34
+ // connection.
35
+ assert.equal(classify('pi→provider', 'Connection error.').kind, 'connection');
36
+ assert.equal(classify('pi→provider', 'ECONNRESET').kind, 'connection');
37
+ assert.equal(classify('pi→provider', 'fetch failed').kind, 'connection');
38
+ assert.equal(classify('pi→provider', 'request timed out').kind, 'connection');
39
+ assert.equal(classify('viewer↔broker', { code: 'ENOENT' }).kind, 'connection');
40
+ assert.equal(classify('viewer↔broker', { code: 1011, reason: 'no running broker for n1' }).kind, 'connection');
41
+ // ORDER: a 429/529 whose text ALSO mentions connection/network must NOT be
42
+ // swallowed by the connection match (rate-limit/overloaded are checked first).
43
+ assert.equal(classify('pi→provider', '429 rate limit on this network connection').kind, 'rate-limit');
44
+ assert.equal(classify('pi→provider', '529 overloaded — connection to upstream lost').kind, 'overloaded');
45
+ // fallback.
46
+ assert.equal(classify('pi→provider', 'some unrecognized engine failure').kind, 'other');
47
+ assert.equal(classify('pi→provider', 'some unrecognized engine failure').disposition, 'fatal');
48
+ assert.equal(classify('pi→provider', '').kind, 'other');
49
+ });
50
+ // ---------------------------------------------------------------------------
51
+ // BUG-REGRESSION: a 404 not_found (configured model decommissioned/unavailable)
52
+ // must be detected so the broker fails the node over to the strong-anthropic
53
+ // ladder model instead of wedging on the same dead model forever. The verbatim
54
+ // text below is the engine error a node hit when its `anthropic/ultra` ladder
55
+ // cell pointed at a model the account could not reach.
56
+ // ---------------------------------------------------------------------------
57
+ test('isModelNotFoundError detects the provider 404 not_found that triggers strong-anthropic fallback', () => {
58
+ const verbatim = 'Error: 404 {"type":"error","error":{"type":"not_found_error","message":"Claude Fable 5 is not available. Please use Opus 4.8. Learn more: https://www.anthropic.com/news/fable-mythos-access"},"request_id":"req_011CcHgqfwD2Hjg193Ym1Sys"}';
59
+ assert.equal(isModelNotFoundError(verbatim), true);
60
+ assert.equal(isModelNotFoundError('Error: 404 model not found'), true);
61
+ assert.equal(isModelNotFoundError('the requested model does not exist'), true);
62
+ assert.equal(isModelNotFoundError('HTTP 429 Too Many Requests'), false);
63
+ assert.equal(isModelNotFoundError('503 Service Unavailable'), false);
64
+ assert.equal(isModelNotFoundError('Connection error.'), false);
65
+ assert.equal(isModelNotFoundError('Error: 404 unexpected endpoint /v1/foo'), false);
66
+ assert.equal(isModelNotFoundError(''), false);
67
+ });
68
+ // ---------------------------------------------------------------------------
69
+ // M2 — the marker round-trip through dashboardRowsAll.
70
+ // ---------------------------------------------------------------------------
71
+ test('M2 dashboardRowsAll surfaces a hanging marker (pid-gated); clear removes it', async () => {
72
+ const h = await createHeadlessHarness({ sessionPrefix: 'crtr-esm' });
73
+ try {
74
+ // A LIVE node (the test process pid is definitely alive).
75
+ const liveId = h.fabricateBrokerNode({ kind: 'developer', status: 'active', pi_pid: process.pid });
76
+ // A node whose recorded pid is DEAD (the pid-gate must read it not-hanging).
77
+ // A child we spawn-then-let-exit gives a pid that is reliably gone.
78
+ const dead = spawnSync('true', [], { stdio: 'ignore' });
79
+ const deadPid = dead.pid ?? 2147483646; // `true` has already exited
80
+ const deadId = h.fabricateBrokerNode({ kind: 'developer', status: 'active', pi_pid: deadPid });
81
+ const find = (id) => dashboardRowsAll().find((r) => r.node_id === id);
82
+ // No marker yet → not hanging.
83
+ assert.equal(find(liveId)?.hanging ?? null, null, 'no marker → not hanging');
84
+ // Mark the live node → its row shows hanging, classified, streaming forced off.
85
+ recordFault(liveId, {
86
+ link: 'pi→provider',
87
+ op: 'generation turn',
88
+ kind: classify('pi→provider', 'Rate limit exceeded (429)').kind,
89
+ retry: { disposition: 'auto', by: 'daemon' },
90
+ message: 'Rate limit exceeded (429)',
91
+ });
92
+ const row = find(liveId);
93
+ assert.notEqual(row?.hanging ?? null, null, 'marked live node → hanging set');
94
+ assert.equal(row?.hanging?.kind, 'rate-limit', 'kind classified from the message');
95
+ assert.equal(row?.streaming, false, 'hanging forces streaming false (mutually exclusive)');
96
+ // A DEAD pid + a marker → NOT hanging (the pid-AND gate).
97
+ recordFault(deadId, {
98
+ link: 'pi→provider',
99
+ op: 'generation turn',
100
+ kind: classify('pi→provider', 'Connection error.').kind,
101
+ retry: { disposition: 'auto', by: 'daemon' },
102
+ message: 'Connection error.',
103
+ });
104
+ assert.equal(find(deadId)?.hanging ?? null, null, 'dead pid → marker ignored (pid gate)');
105
+ // Clear → gone.
106
+ clearFault(liveId);
107
+ assert.equal(find(liveId)?.hanging ?? null, null, 'cleared marker → not hanging');
108
+ }
109
+ finally {
110
+ await h.dispose();
111
+ }
112
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/fault-retry-rewind.test.ts
2
+ //
3
+ // BUG-REGRESSION: broker-owned fault retries rewind the session tree before
4
+ // re-sending the recovery nudge, so the active branch keeps exactly one nudge
5
+ // and the first failed attempt stays on an abandoned sibling branch.
6
+ import { test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { createHeadlessHarness } from './helpers/harness.js';
11
+ import { CONNECTION_FAULT_RECOVERY_BODY } from '../runtime/fault-recovery-nudge.js';
12
+ const TASK = 'fault-retry rewind regression task';
13
+ function messageText(message) {
14
+ if (typeof message.content === 'string')
15
+ return message.content;
16
+ const content = message.content ?? [];
17
+ for (const part of content) {
18
+ if (typeof part?.text === 'string' && part.text !== '')
19
+ return part.text;
20
+ }
21
+ return '';
22
+ }
23
+ function flatten(nodes) {
24
+ const out = [];
25
+ const walk = (node) => {
26
+ out.push(node);
27
+ for (const child of node.children)
28
+ walk(child);
29
+ };
30
+ for (const node of nodes)
31
+ walk(node);
32
+ return out;
33
+ }
34
+ function pathToLeaf(nodes, leafId) {
35
+ if (leafId === null)
36
+ return [];
37
+ const byId = new Map(nodes.map((node) => [node.entry.id, node]));
38
+ const path = [];
39
+ let cur = byId.get(leafId);
40
+ while (cur !== undefined) {
41
+ path.push(cur);
42
+ cur = cur.entry.parentId !== null ? byId.get(cur.entry.parentId) : undefined;
43
+ }
44
+ return path.reverse();
45
+ }
46
+ function readSnapshot(home, nodeId) {
47
+ const path = join(home, 'nodes', nodeId, 'fake-pi.tree.json');
48
+ return JSON.parse(readFileSync(path, 'utf8'));
49
+ }
50
+ function agentEndCount(home, nodeId) {
51
+ const path = join(home, 'nodes', nodeId, 'fake-pi.events.jsonl');
52
+ try {
53
+ return readFileSync(path, 'utf8')
54
+ .split('\n')
55
+ .filter((line) => line.trim() !== '')
56
+ .filter((line) => {
57
+ try {
58
+ return JSON.parse(line).event === 'agent_end';
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }).length;
64
+ }
65
+ catch {
66
+ return 0;
67
+ }
68
+ }
69
+ test('broker fault retries rewind before replaying the recovery nudge', { timeout: 30_000 }, async () => {
70
+ const prevProbe = process.env['CRTR_FAULT_RETRY_PROBE'];
71
+ const prevFails = process.env['CRTR_FAKE_ENGINE_PROMPT_FAILS'];
72
+ process.env['CRTR_FAULT_RETRY_PROBE'] = 'always';
73
+ process.env['CRTR_FAKE_ENGINE_PROMPT_FAILS'] = '2';
74
+ const h = await createHeadlessHarness({ sessionPrefix: 'crtr-fault-rewind' });
75
+ try {
76
+ const root = h.spawnRoot('fault-retry rewind root');
77
+ const nodeId = await h.spawnHeadlessChild(root, TASK);
78
+ await h.waitFor(() => (agentEndCount(h.home, nodeId) >= 2 ? true : null), { timeoutMs: 20_000, label: 'second failed attempt to settle' });
79
+ const snapshot = await h.waitFor(() => {
80
+ const fresh = readSnapshot(h.home, nodeId);
81
+ const flat = flatten(fresh.tree);
82
+ const path = pathToLeaf(flat, fresh.leafId);
83
+ const pathIds = new Set(path.map((node) => node.entry.id));
84
+ const nudgeOnPath = path.filter((node) => node.entry.message.role === 'user' && messageText(node.entry.message) === CONNECTION_FAULT_RECOVERY_BODY);
85
+ if (nudgeOnPath.length !== 1)
86
+ return null;
87
+ const nudgeNode = nudgeOnPath[0];
88
+ const failedAttempt = flat.find((node) => node.entry.message.role === 'assistant'
89
+ && node.entry.message.stopReason === 'error'
90
+ && messageText(node.entry.message) === TASK);
91
+ if (failedAttempt === undefined)
92
+ return null;
93
+ if (pathIds.has(failedAttempt.entry.id))
94
+ return null;
95
+ if (failedAttempt.entry.parentId !== nudgeNode.entry.parentId)
96
+ return null;
97
+ return fresh;
98
+ }, { timeoutMs: 20_000, label: 'rewound active branch snapshot' });
99
+ const flat = flatten(snapshot.tree);
100
+ const path = pathToLeaf(flat, snapshot.leafId);
101
+ const pathMessages = path.map((node) => node.entry.message);
102
+ assert.equal(pathMessages.filter((message) => message.role === 'user' && messageText(message) === CONNECTION_FAULT_RECOVERY_BODY).length, 1, 'the active branch carries exactly one recovery nudge');
103
+ const failedAttempt = flat.find((node) => node.entry.message.role === 'assistant'
104
+ && node.entry.message.stopReason === 'error'
105
+ && messageText(node.entry.message) === TASK);
106
+ assert.ok(failedAttempt !== undefined, 'the first failed attempt is still present in the tree');
107
+ assert.ok(!path.some((node) => node.entry.id === failedAttempt.entry.id), 'the first failed attempt is not on the active path');
108
+ const nudgeNode = path.find((node) => node.entry.message.role === 'user' && messageText(node.entry.message) === CONNECTION_FAULT_RECOVERY_BODY);
109
+ assert.ok(nudgeNode !== undefined, 'the active branch includes the recovery nudge');
110
+ assert.equal(failedAttempt.entry.parentId, nudgeNode.entry.parentId, 'the failed attempt is a sibling branch of the active nudge');
111
+ }
112
+ finally {
113
+ if (prevProbe === undefined)
114
+ delete process.env['CRTR_FAULT_RETRY_PROBE'];
115
+ else
116
+ process.env['CRTR_FAULT_RETRY_PROBE'] = prevProbe;
117
+ if (prevFails === undefined)
118
+ delete process.env['CRTR_FAKE_ENGINE_PROMPT_FAILS'];
119
+ else
120
+ process.env['CRTR_FAKE_ENGINE_PROMPT_FAILS'] = prevFails;
121
+ await h.dispose();
122
+ }
123
+ });