@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
@@ -8,6 +8,7 @@
8
8
  // source's parse() job.
9
9
  import { execFile } from 'node:child_process';
10
10
  import { readFile } from 'node:fs/promises';
11
+ import { openLocalStream } from './stream-local.js';
11
12
  const MAX_BUFFER = 10 * 1024 * 1024;
12
13
  export function createLocalTransport(opts = {}) {
13
14
  const baseCwd = opts.cwd ?? process.cwd();
@@ -19,6 +20,9 @@ export function createLocalTransport(opts = {}) {
19
20
  case 'http': return sendHttp(req);
20
21
  }
21
22
  },
23
+ open(req, handlers) {
24
+ return openLocalStream(req, handlers);
25
+ },
22
26
  };
23
27
  }
24
28
  function sendExec(req, baseCwd) {
@@ -1,4 +1,10 @@
1
- import type { SourceRequest, RawResponse } from './contract.js';
1
+ import type { SourceRequest, RawResponse, StreamRequest, StreamChannel, SourceError } from './contract.js';
2
+ export interface StreamHandlers {
3
+ onFrame(frame: unknown): void;
4
+ onClose(reason: string): void;
5
+ onError(err: SourceError): void;
6
+ }
2
7
  export interface Transport {
3
8
  send(req: SourceRequest): Promise<RawResponse>;
9
+ open(req: StreamRequest, handlers: StreamHandlers): StreamChannel;
4
10
  }
@@ -1,4 +1,6 @@
1
1
  import { type NodeRow } from '../core/canvas/index.js';
2
+ import { FAULT_QUIET_MS } from '../core/runtime/fault.js';
3
+ export { FAULT_QUIET_MS };
2
4
  import { isPidAlive } from '../core/canvas/pid.js';
3
5
  export declare const YIELD_STALL_GRACE_MS: number;
4
6
  /** Refresh-stall decision (§H refresh-authority; audit 2026-06-09, Bug 1 — node
@@ -17,25 +19,6 @@ export declare const YIELD_STALL_GRACE_MS: number;
17
19
  * after running `node yield` mid-turn, and a working engine must never be
18
20
  * killed), and the state has persisted past YIELD_STALL_GRACE_MS. */
19
21
  export declare function yieldStallVerdict(piPidAlive: boolean | null, intent: NodeRow['intent'], busy: boolean, stalledFor: number | null): 'leave' | 'pending' | 'kill';
20
- export declare const ERROR_STALL_QUIET_MS: number;
21
- /** The §I wedge signature, from a session-jsonl tail: the LAST assistant
22
- * message stopped with 'error' (retry budget exhausted — pi appends the error
23
- * turn and parks). Returns its errorMessage for the daemon log (the "why it
24
- * wedged" trace), or null when the last assistant turn ended any other way —
25
- * 'stop'/'length' is a healthy park, 'aborted' is a human Esc, and trailing
26
- * non-assistant lines (toolResults, an injected user message) are walked past.
27
- * The first line of a mid-file tail cut fails JSON.parse and is skipped. */
28
- export declare function trailingEngineError(tail: string | null): string | null;
29
- /** Error-stall decision (§I; gh issue #4 — node mqaeqzdu-0746b3b7, a 9h zombie
30
- * after an Anthropic outage). Pure, mirroring yieldStallVerdict; the kill side
31
- * effects live in handleErrorStall.
32
- *
33
- * 'kill' ONLY when ALL of: engine alive, NO pending intent (refresh is §H's
34
- * domain; idle-release chose dormancy), the turn is over (busy absent — a node
35
- * wedged mid-bash must be recovered by killing the subprocess, never the node),
36
- * the session file has been quiet past ERROR_STALL_QUIET_MS, and its last
37
- * assistant message stopped with 'error'. */
38
- export declare function errorStallVerdict(piPidAlive: boolean | null, intent: NodeRow['intent'], busy: boolean, quietForMs: number | null, trailingError: boolean): 'leave' | 'kill';
39
22
  export type LivenessVerdict = 'leave' | 'pending' | 'revive';
40
23
  /** Decide what to do with a node whose engine pid is DEAD, from how long it's
41
24
  * been dead. Pure — the time/revive side effects live in handleNodeLiveness;
@@ -50,6 +33,7 @@ export declare function readPidfile(): number | null;
50
33
  export { isPidAlive };
51
34
  /** True when a crtrd process is already running (pidfile exists + pid alive). */
52
35
  export declare function isDaemonRunning(): boolean;
36
+ /** Supervision tick over the daemon's current live set. */
53
37
  export declare function superviseTick(now?: number): Promise<void>;
54
38
  export interface DaemonOpts {
55
39
  /** Milliseconds between supervision polls. Default 2000. */
@@ -10,8 +10,8 @@
10
10
  // reaps a node on pane/window state. All node liveness routes through
11
11
  // handleNodeLiveness:
12
12
  // • Poll every intervalMs (default 2000ms).
13
- // • pid alive → leave; but enforce a stuck refresh-yield (§H) and a
14
- // wedged-engine error-stall (§I, see below).
13
+ // • pid alive → leave; but enforce a stuck refresh-yield and active-fault
14
+ // recovery when a parked broker is due.
15
15
  // • pid null + pi_session_id set → relaunch in flight; leave.
16
16
  // • pid null + pi_session_id null → after a boot grace, crash + surface boot
17
17
  // failure (the broker exited before session_start).
@@ -28,30 +28,39 @@
28
28
  // grace, it force-kills the engine itself (handleYieldStall) so the dead-pid
29
29
  // refresh path then revives it fresh.
30
30
  //
31
- // §I error-stall: an engine whose turn ends with stopReason 'error' (e.g. a
32
- // provider outage exhausted the SDK retry budget) is left PARKED by the
33
- // stophook ("stay alive for re-steering") — but a headless node has no human to
34
- // re-steer it, so the broker stays alive, status stays 'active', and the pid-only
35
- // liveness check reads it healthy FOREVER (gh issue #4: a 9h zombie). The daemon
36
- // is the authority here too: when a live, not-busy node with no pending intent
37
- // has a session file that is QUIET past a grace AND whose last assistant message
38
- // stopped with 'error', it kills the engine (handleErrorStall) so the ordinary
39
- // dead-pid crash path grace-revives RESUME on the saved session the exact
40
- // manual recovery (kill broker + canvas revive) that fixes these by hand.
31
+ // An engine whose turn ends with stopReason 'error' (e.g. a provider outage
32
+ // exhausted the SDK retry budget) is left parked by the stophook ("stay alive
33
+ // for re-steering") — but a headless node has no human to re-steer it, so the
34
+ // broker stays alive, status stays 'active', and the pid-only liveness check
35
+ // reads it healthy forever. The daemon is the authority here too: when a live,
36
+ // not-busy node with no pending intent has a session file that is quiet past
37
+ // the recovery window and whose last assistant message stops with 'error', the
38
+ // daemon keeps the live broker parked and retries it via the inbox-based
39
+ // fault-recovery pass so the ordinary dead-pid crash path never needs an
40
+ // out-of-band launch.
41
+ // dead-pid crash path grace-revives RESUME on the saved session — the manual
42
+ // recovery (kill broker + canvas revive) that fixes these by hand.
41
43
  //
42
44
  // Single-instance guarantee
43
- // A PID file at crtrHome()/crtrd.pid prevents double-runs. On start, if the
44
- // file exists and the recorded pid is alive, we refuse to start (exit 0).
45
- // On stop (SIGINT/SIGTERM/exit) we remove the file.
46
- import { writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, statSync, openSync, readSync, closeSync, } from 'node:fs';
47
- import { join } from 'node:path';
45
+ // A PID file prevents double-runs. On start, if the file exists and the
46
+ // recorded pid is alive, we refuse to start (exit 0). Hearth guests set
47
+ // CRTR_PIDFILE to keep this process-local on recreate; otherwise it defaults
48
+ // to crtrHome()/crtrd.pid. On stop (SIGINT/SIGTERM/exit) we remove the file.
49
+ import { writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, statSync, } from 'node:fs';
50
+ import { dirname, join } from 'node:path';
51
+ import { homedir } from 'node:os';
48
52
  import { crtrHome, nodeDir, jobDir } from '../core/canvas/paths.js';
49
53
  import { listNodes, getRow, getNode, dueWakes, consumeWake, advanceWake, listFocuses, closeFocusRow, } from '../core/canvas/index.js';
50
54
  import { transition } from '../core/runtime/lifecycle.js';
51
55
  import { isBusy } from '../core/runtime/busy.js';
56
+ import { FAULT_QUIET_MS } from '../core/runtime/fault.js';
57
+ import { logger } from '../core/log.js';
58
+ export { FAULT_QUIET_MS };
52
59
  import { isPidAlive } from '../core/canvas/pid.js';
53
60
  import { listLivePanes, tearDownNode } from '../core/runtime/placement.js';
54
61
  import { reviveNode } from '../core/runtime/revive.js';
62
+ import { isBrokerLive } from '../core/runtime/model-swap.js';
63
+ import { reloadAuthLive } from '../core/runtime/auth-reload.js';
55
64
  import { spawnChild } from '../core/runtime/spawn.js';
56
65
  import { wakeOriginFrom } from '../core/runtime/bearings.js';
57
66
  import { pushUrgent } from '../core/feed/feed.js';
@@ -78,6 +87,7 @@ async function surfaceBootFailure(meta) {
78
87
  await pushUrgent(meta.node_id, body, { from: meta.node_id });
79
88
  }
80
89
  const DEFAULT_INTERVAL_MS = 2000;
90
+ const log = logger('crtrd');
81
91
  // How long a node's broker pid may be observed dead before the daemon revives
82
92
  // it. MUST exceed worst-case broker boot time: a refresh / crash-revive
83
93
  // transiently shows a dead OLD pid for the gap between the old broker exiting
@@ -184,151 +194,77 @@ function handleYieldStall(row, pid, now) {
184
194
  }
185
195
  }
186
196
  }
187
- // §I error-stall grace: how long a node's session .jsonl must have been QUIET
188
- // (no append mtime is the clock, so it survives daemon restarts) before a
189
- // trailing-error engine is declared wedged and killed. Generous enough that an
190
- // in-flight recovery (the human re-steering, a parent's message waking a new
191
- // turn via the live inbox-watcher — both of which append and reset the clock)
192
- // always wins, while a true zombie recovers in minutes instead of never.
193
- export const ERROR_STALL_QUIET_MS = 5 * 60_000;
194
- // Per-node SIGTERM timestamps for §I kill escalation, mirroring yieldTermAt.
195
- const errorTermAt = new Map();
196
- // Memoized tail inspection per node, keyed on the session file's mtime so the
197
- // (rare) 64KB tail read happens once per file change, not once per 2s tick.
198
- const errorTailMemo = new Map();
199
- const TAIL_BYTES = 64 * 1024;
200
- /** Read the last TAIL_BYTES of a file. null on any failure (absent file reads
201
- * as "nothing to inspect", never as evidence of a wedge). */
202
- function readTail(path) {
203
- try {
204
- const size = statSync(path).size;
205
- const len = Math.min(TAIL_BYTES, size);
206
- if (len === 0)
207
- return null;
208
- const fd = openSync(path, 'r');
209
- try {
210
- const buf = Buffer.alloc(len);
211
- readSync(fd, buf, 0, len, size - len);
212
- return buf.toString('utf8');
213
- }
214
- finally {
215
- closeSync(fd);
216
- }
217
- }
218
- catch {
219
- return null;
220
- }
221
- }
222
- /** The §I wedge signature, from a session-jsonl tail: the LAST assistant
223
- * message stopped with 'error' (retry budget exhausted — pi appends the error
224
- * turn and parks). Returns its errorMessage for the daemon log (the "why it
225
- * wedged" trace), or null when the last assistant turn ended any other way —
226
- * 'stop'/'length' is a healthy park, 'aborted' is a human Esc, and trailing
227
- * non-assistant lines (toolResults, an injected user message) are walked past.
228
- * The first line of a mid-file tail cut fails JSON.parse and is skipped. */
229
- export function trailingEngineError(tail) {
230
- if (tail === null)
231
- return null;
232
- const lines = tail.split('\n');
233
- for (let i = lines.length - 1; i >= 0; i--) {
234
- const line = lines[i].trim();
235
- if (line === '')
236
- continue;
237
- let msg;
238
- try {
239
- msg = JSON.parse(line)?.message;
240
- }
241
- catch {
242
- continue; // a cut fragment (tail boundary) or non-JSON line
243
- }
244
- if (msg?.role !== 'assistant')
245
- continue;
246
- if (msg.stopReason !== 'error')
247
- return null;
248
- return typeof msg.errorMessage === 'string' && msg.errorMessage !== ''
249
- ? msg.errorMessage
250
- : 'engine error (no errorMessage recorded)';
251
- }
252
- return null;
197
+ // ---------------------------------------------------------------------------
198
+ // Auth-reload fan (one /login every live broker follows)
199
+ // ---------------------------------------------------------------------------
200
+ /** Path of the shared pi credentials file every broker reads. VENDORED from pi
201
+ * `config.getAuthPath()` (= `<agentDir>/auth.json`) the same way spawn.ts
202
+ * vendors the sessions root: a ROOT import of pi's `getAgentDir` would eager-
203
+ * load the heavy pi SDK index, so we mirror its resolution — `PI_CODING_AGENT_DIR`
204
+ * env, else `~/.pi/agent`. Re-sync on a pi SDK bump that moves auth.json. */
205
+ function piAuthPath() {
206
+ const env = process.env['PI_CODING_AGENT_DIR'];
207
+ const agentDir = env !== undefined && env !== '' ? env : join(homedir(), '.pi', 'agent');
208
+ return join(agentDir, 'auth.json');
253
209
  }
254
- /** Error-stall decision (§I; gh issue #4 — node mqaeqzdu-0746b3b7, a 9h zombie
255
- * after an Anthropic outage). Pure, mirroring yieldStallVerdict; the kill side
256
- * effects live in handleErrorStall.
210
+ // Last-observed auth.json mtime, for the daemon's change-watch. Lazily seeded on
211
+ // the first poll that sees the file (so daemon start never fans a spurious
212
+ // reload), then updated on every detected change. In-memory only — a daemon
213
+ // restart re-seeds from the current file, so at most one missed change spans the
214
+ // restart gap (re-login during a ~1s restart), which a human just redoes.
215
+ let knownAuthMtimeMs = null;
216
+ /** Watch the shared auth.json mtime and, on a change, fan a live `reload_auth`
217
+ * to every live broker so one `/login` propagates across the whole canvas. The
218
+ * broker that performed the login already self-reloaded; re-reloading it is
219
+ * harmless (authStorage.reload() is idempotent), so we don't exclude it.
257
220
  *
258
- * 'kill' ONLY when ALL of: engine alive, NO pending intent (refresh is §H's
259
- * domain; idle-release chose dormancy), the turn is over (busy absent a node
260
- * wedged mid-bash must be recovered by killing the subprocess, never the node),
261
- * the session file has been quiet past ERROR_STALL_QUIET_MS, and its last
262
- * assistant message stopped with 'error'. */
263
- export function errorStallVerdict(piPidAlive, intent, busy, quietForMs, trailingError) {
264
- if (piPidAlive !== true)
265
- return 'leave';
266
- if (intent !== null)
267
- return 'leave';
268
- if (busy)
269
- return 'leave';
270
- if (quietForMs === null || quietForMs < ERROR_STALL_QUIET_MS)
271
- return 'leave';
272
- if (!trailingError)
273
- return 'leave';
274
- return 'kill';
275
- }
276
- /** Enact an error-stall (§I): when the verdict is 'kill', SIGTERM the parked
277
- * engine (escalating to SIGKILL after KILL_ESCALATE_MS, like §H — a broker
278
- * wedged after an outage may not run graceful handlers). intent is null, so
279
- * once the pid dies the ordinary crash branch in handleNodeLiveness grace-
280
- * revives RESUME on the saved session — the conversation is preserved and the
281
- * revive kickoff prompts the node to continue. No grace clock of our own: the
282
- * session file's mtime IS the clock (any append — a re-steer, an inbox-watcher
283
- * wake, a new turn — resets it). */
284
- function handleErrorStall(row, pid, now) {
285
- const id = row.node_id;
286
- if (row.intent !== null || isBusy(id)) {
287
- errorTermAt.delete(id);
288
- return;
289
- }
290
- const file = getNode(id)?.pi_session_file;
291
- if (file == null)
292
- return;
221
+ * Rides the existing ~2s poll, so several rapid writes from one login collapse
222
+ * to a single observed mtime change a single fan. Each broker is reloaded
223
+ * under its own try/catch (Promise.allSettled): a broker mid-shutdown or whose
224
+ * socket refuses connect is logged and skipped, never aborting the others. */
225
+ async function handleAuthReload(now) {
293
226
  let mtimeMs;
294
227
  try {
295
- mtimeMs = statSync(file).mtimeMs;
228
+ mtimeMs = statSync(piAuthPath()).mtimeMs;
296
229
  }
297
230
  catch {
298
- return; // session file unreadable no evidence, never kill on "can't tell"
231
+ return; // no auth.json yet (never logged in) nothing to watch
299
232
  }
300
- const quietFor = now - mtimeMs;
301
- if (quietFor < ERROR_STALL_QUIET_MS) {
302
- errorTermAt.delete(id);
233
+ if (knownAuthMtimeMs === null) {
234
+ knownAuthMtimeMs = mtimeMs; // seed on first sight; never fan on daemon start
303
235
  return;
304
236
  }
305
- let memo = errorTailMemo.get(id);
306
- if (memo === undefined || memo.mtimeMs !== mtimeMs) {
307
- memo = { mtimeMs, error: trailingEngineError(readTail(file)) };
308
- errorTailMemo.set(id, memo);
237
+ if (mtimeMs === knownAuthMtimeMs)
238
+ return;
239
+ knownAuthMtimeMs = mtimeMs;
240
+ // Enumerate live brokers (pid alive + view.sock present) only when auth
241
+ // actually changed — the rare path, so the extra list/stat cost is paid only
242
+ // on a real login, never every tick.
243
+ let live;
244
+ try {
245
+ live = listNodes({ status: ['active', 'idle'] })
246
+ .filter((r) => r.kind !== 'human')
247
+ .map((r) => getNode(r.node_id))
248
+ .filter((m) => m !== null && isBrokerLive(m))
249
+ .map((m) => m.node_id);
309
250
  }
310
- if (errorStallVerdict(true, row.intent, false, quietFor, memo.error !== null) !== 'kill')
251
+ catch (err) {
252
+ process.stderr.write(`[crtrd] auth-reload enumerate error: ${err.message}\n`);
311
253
  return;
312
- const termed = errorTermAt.get(id);
313
- if (termed === undefined) {
314
- process.stderr.write(`[crtrd] kill ${id} (error-stall: engine parked on a trailing error turn, session quiet ${Math.round(quietFor / 1000)}s — SIGTERM; last error: ${String(memo.error).slice(0, 200)})\n`);
315
- try {
316
- process.kill(pid, 'SIGTERM');
317
- }
318
- catch {
319
- /* already gone */
320
- }
321
- errorTermAt.set(id, now);
322
254
  }
323
- else if (now - termed >= KILL_ESCALATE_MS) {
324
- process.stderr.write(`[crtrd] kill ${id} (error-stall: pi ${pid} survived SIGTERM — SIGKILL)\n`);
255
+ if (live.length === 0)
256
+ return;
257
+ const results = await Promise.allSettled(live.map(async (id) => {
325
258
  try {
326
- process.kill(pid, 'SIGKILL');
259
+ await reloadAuthLive(id);
327
260
  }
328
- catch {
329
- /* already gone */
261
+ catch (err) {
262
+ process.stderr.write(`[crtrd] auth-reload ${id} failed: ${err.message}\n`);
263
+ throw err;
330
264
  }
331
- }
265
+ }));
266
+ const ok = results.filter((r) => r.status === 'fulfilled').length;
267
+ process.stderr.write(`[crtrd] auth-reload: auth.json changed — fanned to ${live.length} live broker(s) (${ok} ok, ${live.length - ok} failed)\n`);
332
268
  }
333
269
  /** Decide what to do with a node whose engine pid is DEAD, from how long it's
334
270
  * been dead. Pure — the time/revive side effects live in handleNodeLiveness;
@@ -354,12 +290,14 @@ export function livenessVerdict(piPidAlive, deadFor) {
354
290
  * with the turn over) and, once the engine has sat alive past the grace,
355
291
  * kills it — so the daemon, not the (possibly stale) in-process stophook, is
356
292
  * the authority on refresh, and the dead-pid refresh branch below then
357
- * revives it fresh. AND enforce §I: handleErrorStall self-guards (no-op
358
- * unless intent is null with the turn over) and kills an engine parked on a
359
- * trailing-error turn whose session has been quiet past the grace, so the
360
- * crash branch below grace-revives it RESUME.
293
+ * revives it fresh. Fault recovery now runs in a separate pass: live,
294
+ * daemon-owned provider faults are retried by inbox nudge, while dead-pid
295
+ * crashes still fall through the ordinary resume path below.
361
296
  * • pid null + pi_session_id set (a relaunch in flight — reviveNode clears
362
- * pi_pid right after launch) → leave; the fresh engine re-records its pid.
297
+ * pi_pid right after launch) → leave through a boot grace; a healthy engine
298
+ * re-records its pid well within it. If the grace ELAPSES with still no pid,
299
+ * the relaunch died after the pid-clear → grace-revive RESUME on the saved
300
+ * session (else the node strands 'active' with no engine forever).
363
301
  * • pid null + pi_session_id null (NEVER booted) → normally the sub-second SDK
364
302
  * boot gap, but a broker that throws BEFORE session_start records no pid and
365
303
  * no session ever — so after a boot grace with STILL nothing, crash +
@@ -380,37 +318,50 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
380
318
  if (pid != null && isPidAlive(pid)) {
381
319
  unhealthySince.delete(id);
382
320
  handleYieldStall(row, pid, now);
383
- handleErrorStall(row, pid, now);
384
321
  return;
385
322
  }
386
323
  if (pid == null) {
387
- // No supervised pid recorded. Two very different cases turn on pi_session_id:
388
- // a relaunch in flight reviveNode clears pi_pid right after launch, but
389
- // the node ALREADY booted once (pi_session_id captured), so the fresh
390
- // engine re-records its pid within a tick or two; leave it.
391
- // a NEVER-BOOTED broker (pi_session_id null) normally the sub-second SDK
392
- // boot gap, BUT a broker that THROWS before session_start (malformed
393
- // broker-launch.json, SessionManager.open on a missing .jsonl, a loader/
394
- // registry/createAgentSession failure, the fork / bare-id guards, or
395
- // broker-cli's own fatal catch) records NO pid and NO session — EVER.
396
- // With pid==null read unconditionally as "still booting" that strands the
397
- // node 'active' with no engine forever and its parent waits on a dead
398
- // child. After a boot grace with STILL no pid AND no session, crash +
324
+ // No supervised pid recorded. BOTH sub-cases run the SAME boot grace clock and
325
+ // differ only in how it ENDS, keyed on pi_session_id:
326
+ // session SET either a relaunch in flight (reviveNode clears pi_pid right
327
+ // after launch; the fresh engine re-records within a tick or two, well
328
+ // inside the grace, and the pid-alive branch above clears the clock) OR a
329
+ // relaunch whose broker DIED after the pid-clear and before it re-recorded.
330
+ // The healthy case never reaches the grace; if the grace DOES elapse with
331
+ // still no pid, the relaunch is dead → grace-revive RESUME on the saved
332
+ // session. Returning unconditionally here (the old behavior) read null+
333
+ // session as "in flight" FOREVER and stranded such a node 'active' with no
334
+ // engine its parent waiting on a dead child indefinitely.
335
+ // session NULL normally the sub-second SDK boot gap, BUT a broker that
336
+ // THROWS before session_start (malformed broker-launch.json, SessionManager
337
+ // .open on a missing .jsonl, a loader/registry/createAgentSession failure,
338
+ // the fork / bare-id guards, or broker-cli's own fatal catch) records NO
339
+ // pid and NO session — EVER. After the grace with STILL nothing, crash +
399
340
  // surfaceBootFailure up the spine (M-1).
341
+ // The 20s grace doubles as the double-spawn guard: a healthy relaunch always
342
+ // re-records its pid well within it, so neither branch can fire on one.
400
343
  const meta = getNode(id);
401
- if (meta === null || meta.pi_session_id != null) {
402
- unhealthySince.delete(id); // relaunch in flight (or identity already bound)
344
+ if (meta === null) {
345
+ unhealthySince.delete(id); // row gone from meta nothing to supervise
403
346
  return;
404
347
  }
405
348
  const since = unhealthySince.get(id);
406
349
  if (since === undefined) {
407
- unhealthySince.set(id, now); // start the boot-grace clock
350
+ unhealthySince.set(id, now); // start the boot grace clock
408
351
  return;
409
352
  }
410
353
  if (now - since < REVIVE_GRACE_MS)
411
354
  return; // still inside the boot grace
412
- // Boot grace elapsed, still no pid and no session → the broker never booted.
413
355
  unhealthySince.delete(id);
356
+ if (meta.pi_session_id != null) {
357
+ // Grace elapsed with a session but still no pid → a relaunch that died after
358
+ // the pid-clear, before re-record. Resume it on the saved session.
359
+ process.stderr.write(`[crtrd] revive ${id} (stranded relaunch — pid never re-recorded)\n`);
360
+ reviveNode(id, { resume: true });
361
+ revivedThisTick.add(id); // third-pass bare double-spawn guard (Maj-4)
362
+ return;
363
+ }
364
+ // Grace elapsed with no pid AND no session → the broker never booted.
414
365
  process.stderr.write(`[crtrd] boot-failed ${id} (broker exited before session_start)\n`);
415
366
  transition(id, 'crash');
416
367
  try {
@@ -421,12 +372,10 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
421
372
  }
422
373
  return;
423
374
  }
424
- // The engine pid is DEAD — any pending refresh/error-stall clock is moot (the
425
- // dead-pid paths below own recovery now). Branch on intent.
375
+ // The engine pid is DEAD — the dead-pid paths below own recovery now. Branch
376
+ // on intent.
426
377
  yieldStallSince.delete(id);
427
378
  yieldTermAt.delete(id);
428
- errorTermAt.delete(id);
429
- errorTailMemo.delete(id);
430
379
  if (row.intent === 'refresh') {
431
380
  // Clean yield — or the §H force-kill above just landed — → respawn FRESH,
432
381
  // immediately (no grace wait).
@@ -578,11 +527,12 @@ function gcStaleFocuses() {
578
527
  // Pidfile
579
528
  // ---------------------------------------------------------------------------
580
529
  function pidfilePath() {
581
- return join(crtrHome(), 'crtrd.pid');
530
+ const override = process.env['CRTR_PIDFILE'];
531
+ return override !== undefined && override !== '' ? override : join(crtrHome(), 'crtrd.pid');
582
532
  }
583
533
  function writePidfile() {
584
- // Ensure the canvas home exists before writing.
585
- mkdirSync(crtrHome(), { recursive: true });
534
+ // Ensure the pidfile parent exists before writing.
535
+ mkdirSync(dirname(pidfilePath()), { recursive: true });
586
536
  writeFileSync(pidfilePath(), String(process.pid), 'utf8');
587
537
  }
588
538
  function removePidfile() {
@@ -615,7 +565,18 @@ export function isDaemonRunning() {
615
565
  // ---------------------------------------------------------------------------
616
566
  // Supervisor tick
617
567
  // ---------------------------------------------------------------------------
568
+ /** Supervision tick over the daemon's current live set. */
618
569
  export async function superviseTick(now = Date.now()) {
570
+ // Auth-reload fan FIRST, independent of node supervision: a change to the
571
+ // shared auth.json propagates a live credential reload to every broker. Kept
572
+ // ahead of the listNodes guard below so a transient list error in the
573
+ // supervision passes never skips an auth fan (it does its own enumeration).
574
+ try {
575
+ await handleAuthReload(now);
576
+ }
577
+ catch (err) {
578
+ process.stderr.write(`[crtrd] handleAuthReload error: ${err.message}\n`);
579
+ }
619
580
  let rows;
620
581
  try {
621
582
  rows = listNodes({ status: ['active', 'idle'] });
@@ -801,7 +762,7 @@ export async function superviseTick(now = Date.now()) {
801
762
  // stored recipe) so the born node's kickoff leads with a <crtr-wake>
802
763
  // block — it learns, by construction, that a timer birthed it (and, for
803
764
  // a spawn-cron, that it is one run of a standing job).
804
- spawnChild({ ...recipe, wakeOrigin: wakeOriginFrom(w) });
765
+ await spawnChild({ ...recipe, wakeOrigin: wakeOriginFrom(w) });
805
766
  notifiedWakeFailures.delete(w.wakeup_id); // success clears the dedup latch
806
767
  }
807
768
  catch (err) {
@@ -842,7 +803,7 @@ export function runDaemon(opts = {}) {
842
803
  }
843
804
  const interval = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
844
805
  writePidfile();
845
- process.stderr.write(`[crtrd] started (pid ${process.pid}, interval ${interval}ms)\n`);
806
+ log.info(`started (pid ${process.pid}, interval ${interval}ms)`);
846
807
  let running = true;
847
808
  // Cleanup — idempotent.
848
809
  const cleanup = () => {
@@ -6,11 +6,28 @@ export interface SpawnDaemonResult {
6
6
  /** PID of the already-running daemon, if it was already up. */
7
7
  existing_pid?: number;
8
8
  }
9
+ export interface DaemonStartupDeps {
10
+ readPidfile?: () => number | null;
11
+ isPidAlive?: (pid: number) => boolean;
12
+ childExited?: () => {
13
+ code: number | null;
14
+ signal: NodeJS.Signals | null;
15
+ } | null;
16
+ sleepMs?: (ms: number) => Promise<void> | void;
17
+ now?: () => number;
18
+ }
19
+ /** Tiny bounded post-spawn guard: wait for the daemon pidfile and live pid to
20
+ * appear before reporting success. This catches the "started:true but not yet
21
+ * plausibly alive" race without turning startup into a retry loop.
22
+ *
23
+ * Returns null when the spawned pid owns the pidfile; returns a different live
24
+ * daemon pid when startup lost the race to an already-running daemon. */
25
+ export declare function verifyDaemonStartup(pid: number, timeoutMs?: number, deps?: DaemonStartupDeps): Promise<number | null>;
9
26
  /** Spawn crtrd detached. Returns immediately; the child outlives this process.
10
27
  *
11
28
  * If the daemon is already running, returns {started:false, existing_pid}.
12
29
  * If spawning fails (e.g. missing dist — run `npm run build` first), throws. */
13
- export declare function spawnDaemon(): SpawnDaemonResult;
30
+ export declare function spawnDaemon(): Promise<SpawnDaemonResult>;
14
31
  /** Start the daemon if it is not already running. No-op if already up.
15
32
  * Silently swallows spawn errors (the canvas still works without the daemon;
16
33
  * nodes just won't be auto-revived). */
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { mkdirSync } from 'node:fs';
10
10
  import { crtrHome } from '../core/canvas/paths.js';
11
11
  import { hostExecPath } from '../core/runtime/branded-host.js';
12
- import { isDaemonRunning, readPidfile } from './crtrd.js';
12
+ import { isDaemonRunning, readPidfile, isPidAlive } from './crtrd.js';
13
13
  // ---------------------------------------------------------------------------
14
14
  // Entry point resolution
15
15
  // ---------------------------------------------------------------------------
@@ -21,11 +21,45 @@ function resolveCrtrdEntry() {
21
21
  const here = dirname(fileURLToPath(import.meta.url));
22
22
  return join(here, 'crtrd-cli.js');
23
23
  }
24
+ // Cold guests can take a few seconds to settle their pidfile + liveness probe.
25
+ const STARTUP_VERIFY_WINDOW_MS = 5_000;
26
+ const STARTUP_VERIFY_POLL_MS = 10;
27
+ function sleepMs(ms) {
28
+ return new Promise((resolve) => {
29
+ setTimeout(resolve, ms);
30
+ });
31
+ }
32
+ /** Tiny bounded post-spawn guard: wait for the daemon pidfile and live pid to
33
+ * appear before reporting success. This catches the "started:true but not yet
34
+ * plausibly alive" race without turning startup into a retry loop.
35
+ *
36
+ * Returns null when the spawned pid owns the pidfile; returns a different live
37
+ * daemon pid when startup lost the race to an already-running daemon. */
38
+ export async function verifyDaemonStartup(pid, timeoutMs = STARTUP_VERIFY_WINDOW_MS, deps = {}) {
39
+ const read = deps.readPidfile ?? readPidfile;
40
+ const alive = deps.isPidAlive ?? isPidAlive;
41
+ const exited = deps.childExited;
42
+ const sleep = deps.sleepMs ?? sleepMs;
43
+ const now = deps.now ?? Date.now;
44
+ const deadline = now() + timeoutMs;
45
+ while (now() <= deadline) {
46
+ const owner = read();
47
+ if (owner !== null && alive(owner))
48
+ return owner === pid ? null : owner;
49
+ const exitState = exited?.();
50
+ if (exitState !== null && exitState !== undefined) {
51
+ const exitDetail = exitState.signal !== null ? `by ${exitState.signal}` : `with exit code ${exitState.code ?? '?'}`;
52
+ throw new Error(`daemon ${pid} exited before becoming ready (${exitDetail})`);
53
+ }
54
+ await sleep(STARTUP_VERIFY_POLL_MS);
55
+ }
56
+ throw new Error(`daemon ${pid} did not become ready within ${timeoutMs}ms`);
57
+ }
24
58
  /** Spawn crtrd detached. Returns immediately; the child outlives this process.
25
59
  *
26
60
  * If the daemon is already running, returns {started:false, existing_pid}.
27
61
  * If spawning fails (e.g. missing dist — run `npm run build` first), throws. */
28
- export function spawnDaemon() {
62
+ export async function spawnDaemon() {
29
63
  if (isDaemonRunning()) {
30
64
  return { started: false, existing_pid: readPidfile() ?? undefined };
31
65
  }
@@ -41,7 +75,20 @@ export function spawnDaemon() {
41
75
  stdio: 'ignore',
42
76
  });
43
77
  const pid = child.pid;
78
+ if (pid === undefined) {
79
+ throw new Error('daemon spawn did not return a pid');
80
+ }
81
+ let exitState = null;
82
+ child.once('exit', (code, signal) => {
83
+ exitState = { code, signal };
84
+ });
44
85
  child.unref();
86
+ const existingPid = await verifyDaemonStartup(pid, STARTUP_VERIFY_WINDOW_MS, {
87
+ childExited: () => exitState,
88
+ });
89
+ if (existingPid !== null) {
90
+ return { started: false, existing_pid: existingPid };
91
+ }
45
92
  return { started: true, pid };
46
93
  }
47
94
  // ---------------------------------------------------------------------------
@@ -51,12 +98,10 @@ export function spawnDaemon() {
51
98
  * Silently swallows spawn errors (the canvas still works without the daemon;
52
99
  * nodes just won't be auto-revived). */
53
100
  export function ensureDaemon() {
54
- try {
55
- if (!isDaemonRunning())
56
- spawnDaemon();
57
- }
58
- catch {
59
- // Intentionally silent — a missing dist/daemon/crtrd-cli.js (dev mode,
60
- // pre-build) must not break the calling command.
101
+ if (!isDaemonRunning()) {
102
+ void spawnDaemon().catch(() => {
103
+ // Intentionally silent — a missing dist/daemon/crtrd-cli.js (dev mode,
104
+ // pre-build) must not break the calling command.
105
+ });
61
106
  }
62
107
  }