@north-light/crouter 0.3.232 → 0.3.234

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 (392) hide show
  1. package/dist/api/client.js +16 -15
  2. package/dist/builtin-memory/internal/memory-loading.md +4 -3
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +1 -1
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +14 -17
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -11
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +8 -8
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +10 -11
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +2 -2
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +3 -3
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -7
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +8 -8
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +6 -6
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +3 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +1 -1
  15. package/dist/clients/attach/chrome/ticket-panel.js +15 -2
  16. package/dist/clients/attach/overlays/graph.js +0 -3
  17. package/dist/clients/attach/overlays/mcp.js +1 -2
  18. package/dist/clients/attach/render/chat-view.js +0 -12
  19. package/dist/clients/attach/render/crtr-output.js +0 -2
  20. package/dist/clients/attach/render/tool-calls.js +0 -6
  21. package/dist/clients/attach/slash/dispatch.js +8 -11
  22. package/dist/clients/attach/viewer.js +532 -534
  23. package/dist/clients/conversation/projection.js +0 -2
  24. package/dist/clients/inbox/controller.js +22 -3
  25. package/dist/clients/inbox/review/companion-pane.js +0 -4
  26. package/dist/clients/inbox/review-adapter.js +0 -7
  27. package/dist/clients/inbox/tui/slots.d.ts +3 -3
  28. package/dist/clients/inbox/tui/types.d.ts +2 -2
  29. package/dist/commands/api-client.js +0 -8
  30. package/dist/commands/attention.js +0 -8
  31. package/dist/commands/canvas-browse.js +2 -1
  32. package/dist/commands/canvas-config.js +0 -10
  33. package/dist/commands/canvas-history.js +2 -1
  34. package/dist/commands/cron.js +5 -18
  35. package/dist/commands/dashboard.js +0 -2
  36. package/dist/commands/human/feedback.js +2 -1
  37. package/dist/commands/human/prompts.js +2 -5
  38. package/dist/commands/human/queue.js +2 -7
  39. package/dist/commands/human/request.js +0 -14
  40. package/dist/commands/human/review.js +2 -1
  41. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
  42. package/dist/commands/memory/find.js +2 -2
  43. package/dist/commands/memory/lint.js +2 -1
  44. package/dist/commands/memory/read.d.ts +2 -7
  45. package/dist/commands/memory/read.js +5 -16
  46. package/dist/commands/memory/shared.d.ts +1 -1
  47. package/dist/commands/memory/shared.js +7 -10
  48. package/dist/commands/memory/write.js +4 -3
  49. package/dist/commands/node/bash.js +2 -1
  50. package/dist/commands/node/create.js +2 -1
  51. package/dist/commands/node/inspect.js +0 -6
  52. package/dist/commands/node/lifecycle.js +4 -13
  53. package/dist/commands/node/message.js +3 -2
  54. package/dist/commands/node/subscription.js +2 -1
  55. package/dist/commands/node/wait.js +4 -3
  56. package/dist/commands/node-context.js +2 -1
  57. package/dist/commands/node-lifecycle-revive.js +0 -2
  58. package/dist/commands/node-worktree.js +2 -1
  59. package/dist/commands/pkg/browse/actions.d.ts +1 -1
  60. package/dist/commands/pkg/browse/actions.js +4 -4
  61. package/dist/commands/pkg/browse/catalog.d.ts +1 -1
  62. package/dist/commands/pkg/browse/catalog.js +15 -15
  63. package/dist/commands/pkg/browse/command-view.d.ts +1 -1
  64. package/dist/commands/pkg/browse/command-view.js +3 -3
  65. package/dist/commands/pkg/browse/detail-panel.d.ts +1 -1
  66. package/dist/commands/pkg/browse/detail-panel.js +8 -8
  67. package/dist/commands/pkg/browse/doc-view.d.ts +1 -1
  68. package/dist/commands/pkg/browse/doc-view.js +3 -3
  69. package/dist/commands/pkg/browse/list-panel.d.ts +1 -1
  70. package/dist/commands/pkg/browse/list-panel.js +3 -3
  71. package/dist/commands/pkg/browse/model.d.ts +3 -3
  72. package/dist/commands/pkg/browse/shell.d.ts +2 -2
  73. package/dist/commands/pkg/browse/shell.js +13 -13
  74. package/dist/commands/pkg/plugin-manage.js +2 -1
  75. package/dist/commands/pkg/shared.js +0 -2
  76. package/dist/commands/profile/delete.js +2 -1
  77. package/dist/commands/push.js +2 -3
  78. package/dist/commands/revive.js +0 -2
  79. package/dist/commands/surface/node/placement.js +2 -1
  80. package/dist/commands/surface-edit.js +2 -1
  81. package/dist/commands/sys/config.js +0 -4
  82. package/dist/commands/sys/daemon.js +0 -10
  83. package/dist/commands/sys/doctor.js +3 -2
  84. package/dist/commands/sys/panels/broker-limits-panel.d.ts +2 -2
  85. package/dist/commands/sys/panels/broker-limits-panel.js +4 -4
  86. package/dist/commands/sys/panels/dependencies-panel.d.ts +2 -2
  87. package/dist/commands/sys/panels/dependencies-panel.js +2 -2
  88. package/dist/commands/sys/panels/keybindings-panel.d.ts +1 -1
  89. package/dist/commands/sys/panels/keybindings-panel.js +5 -5
  90. package/dist/commands/sys/panels/models-panel.d.ts +4 -4
  91. package/dist/commands/sys/panels/models-panel.js +11 -10
  92. package/dist/commands/sys/panels/plugins-panel.d.ts +2 -2
  93. package/dist/commands/sys/panels/plugins-panel.js +2 -2
  94. package/dist/commands/sys/panels/prefs-panel.d.ts +2 -2
  95. package/dist/commands/sys/panels/prefs-panel.js +4 -4
  96. package/dist/commands/sys/panels/profiles-panel.d.ts +1 -1
  97. package/dist/commands/sys/panels/profiles-panel.js +14 -13
  98. package/dist/commands/sys/panels/provider-panel.d.ts +3 -3
  99. package/dist/commands/sys/panels/provider-panel.js +8 -8
  100. package/dist/commands/sys/settings-shell.d.ts +3 -3
  101. package/dist/commands/sys/settings-shell.js +15 -15
  102. package/dist/commands/sys/setup-settings-runner.d.ts +3 -3
  103. package/dist/commands/sys/setup-settings-runner.js +5 -5
  104. package/dist/commands/sys/setup-wizard.d.ts +4 -4
  105. package/dist/commands/sys/setup-wizard.js +8 -8
  106. package/dist/commands/sys/support/prepare.js +4 -4
  107. package/dist/commands/sys/support/submit.js +2 -2
  108. package/dist/commands/sys/sync-deps.js +0 -2
  109. package/dist/commands/sys/sync-project-guidance.js +2 -1
  110. package/dist/commands/sys/sysprompt.js +2 -1
  111. package/dist/core/__tests__/boot.test.js +0 -8
  112. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +23 -1
  113. package/dist/core/__tests__/canvas-db-boundary.test.d.ts +1 -0
  114. package/dist/core/__tests__/canvas-db-boundary.test.js +46 -0
  115. package/dist/core/__tests__/canvas.test.js +1 -1
  116. package/dist/core/__tests__/child-death-wake.test.js +0 -2
  117. package/dist/core/__tests__/cron-armed-context-failure.test.d.ts +1 -0
  118. package/dist/core/__tests__/cron-armed-context-failure.test.js +145 -0
  119. package/dist/core/__tests__/daemon-wedge.test.js +0 -4
  120. package/dist/core/__tests__/dead-node-policy-table.test.js +0 -4
  121. package/dist/core/__tests__/fixtures/fake-engine.js +4 -14
  122. package/dist/core/__tests__/fixtures/fake-pi-host.js +5 -5
  123. package/dist/core/__tests__/focuses.test.js +0 -6
  124. package/dist/core/__tests__/fs-utils.test.d.ts +1 -0
  125. package/dist/core/__tests__/fs-utils.test.js +50 -0
  126. package/dist/core/__tests__/helpers/harness.js +1 -1
  127. package/dist/core/__tests__/inline-memory-refs.test.js +0 -2
  128. package/dist/core/__tests__/integration/broker-fork-seam.test.js +0 -2
  129. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +3 -16
  130. package/dist/core/__tests__/integration/command-plugins.test.js +0 -24
  131. package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -6
  132. package/dist/core/__tests__/integration/flagship-lifecycle.test.js +0 -26
  133. package/dist/core/__tests__/integration/live-mutation-verbs.test.js +0 -4
  134. package/dist/core/__tests__/integration/live-mutation.test.js +1 -5
  135. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +1 -5
  136. package/dist/core/__tests__/integration/revive.test.js +0 -12
  137. package/dist/core/__tests__/integration/subscription-delivery.test.js +0 -6
  138. package/dist/core/__tests__/integration/tmux-surface.test.js +24 -8
  139. package/dist/core/__tests__/integration/worktree-reap.test.js +0 -6
  140. package/dist/core/__tests__/lifecycle.test.js +0 -18
  141. package/dist/core/__tests__/memory-resolver-precedence.test.js +1 -1
  142. package/dist/core/__tests__/migration.test.js +2 -2
  143. package/dist/core/__tests__/on-read-dedup-resume.test.js +3 -3
  144. package/dist/core/__tests__/pid-identity-match.test.js +1 -1
  145. package/dist/core/__tests__/plugin-kinds.test.js +1 -1
  146. package/dist/core/__tests__/plugin-page-components.test.js +1 -1
  147. package/dist/core/__tests__/preview-registry-sync.test.js +42 -1
  148. package/dist/core/__tests__/respawn-throttle.test.js +0 -4
  149. package/dist/core/__tests__/revive-all.test.js +5 -5
  150. package/dist/core/__tests__/revive-parked-fresh.test.js +62 -11
  151. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +0 -8
  152. package/dist/core/__tests__/seam/broker-attach-stream.test.js +0 -8
  153. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +0 -2
  154. package/dist/core/__tests__/tmux-driver-boundary.test.d.ts +1 -0
  155. package/dist/core/__tests__/tmux-driver-boundary.test.js +50 -0
  156. package/dist/core/activity-signal.d.ts +7 -0
  157. package/dist/core/activity-signal.js +38 -0
  158. package/dist/core/auto-update.js +2 -1
  159. package/dist/core/bootstrap.js +2 -1
  160. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
  161. package/dist/core/canvas/__tests__/remote-transport.test.js +1 -1
  162. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  163. package/dist/core/canvas/browse/app.js +3 -2
  164. package/dist/core/canvas/browse/model.d.ts +1 -1
  165. package/dist/core/canvas/browse/model.js +0 -4
  166. package/dist/core/canvas/canvas.d.ts +22 -0
  167. package/dist/core/canvas/canvas.js +50 -18
  168. package/dist/core/canvas/crons.js +0 -6
  169. package/dist/core/canvas/db.d.ts +14 -20
  170. package/dist/core/canvas/db.js +30 -1332
  171. package/dist/core/canvas/focuses.js +0 -4
  172. package/dist/core/canvas/history.js +2 -1
  173. package/dist/core/canvas/migrations.d.ts +20 -0
  174. package/dist/core/canvas/migrations.js +1339 -0
  175. package/dist/core/canvas/nav-model.js +1 -11
  176. package/dist/core/canvas/nav-render.js +0 -8
  177. package/dist/core/canvas/remote-canvas-source.js +0 -2
  178. package/dist/core/canvas/render-source.js +31 -13
  179. package/dist/core/command-manifests/manifest.js +0 -10
  180. package/dist/core/command-manifests/schema.js +0 -4
  181. package/dist/core/command-plugins/transport/http-invoke.js +0 -14
  182. package/dist/core/command.js +0 -4
  183. package/dist/core/config.js +2 -1
  184. package/dist/core/feed/inbox.d.ts +10 -0
  185. package/dist/core/feed/inbox.js +18 -12
  186. package/dist/core/fs-utils.d.ts +9 -2
  187. package/dist/core/fs-utils.js +7 -15
  188. package/dist/core/help.js +0 -8
  189. package/dist/core/host-exports/export.d.ts +2 -2
  190. package/dist/core/host-exports/export.js +9 -12
  191. package/dist/core/human/answer.d.ts +2 -2
  192. package/dist/core/human/convention.d.ts +2 -5
  193. package/dist/core/human/convention.js +3 -13
  194. package/dist/core/human/page-schema.d.ts +2 -2
  195. package/dist/core/human/page-schema.js +2 -2
  196. package/dist/core/human/root.js +6 -23
  197. package/dist/core/human/scan.js +24 -7
  198. package/dist/core/human/tickets.d.ts +4 -1
  199. package/dist/core/human/tickets.js +20 -6
  200. package/dist/core/io.js +0 -12
  201. package/dist/core/memory/identity.js +0 -4
  202. package/dist/core/memory/inline-ref-inventory.js +3 -2
  203. package/dist/core/memory/lint.js +3 -6
  204. package/dist/core/memory-resolver.d.ts +5 -0
  205. package/dist/core/memory-resolver.js +14 -12
  206. package/dist/core/pagination.js +0 -2
  207. package/dist/core/preview-registry.d.ts +14 -0
  208. package/dist/core/preview-registry.js +2 -4
  209. package/dist/core/preview-result-path.js +2 -1
  210. package/dist/core/profiles/manifest.d.ts +37 -5
  211. package/dist/core/profiles/manifest.js +44 -24
  212. package/dist/core/profiles/select.js +0 -4
  213. package/dist/core/review/signal.js +4 -23
  214. package/dist/core/review/ticket-filter.d.ts +3 -3
  215. package/dist/core/review/ticket-filter.js +4 -9
  216. package/dist/core/runtime/__tests__/engine-route.test.js +1 -1
  217. package/dist/core/runtime/__tests__/spawn-env.test.js +0 -8
  218. package/dist/core/runtime/bearings-render.d.ts +56 -0
  219. package/dist/core/runtime/bearings-render.js +241 -0
  220. package/dist/core/runtime/bearings.d.ts +2 -17
  221. package/dist/core/runtime/bearings.js +12 -198
  222. package/dist/core/runtime/broker/auth-reload.d.ts +39 -0
  223. package/dist/core/runtime/broker/auth-reload.js +149 -0
  224. package/dist/core/runtime/broker/engine-drive.d.ts +26 -0
  225. package/dist/core/runtime/broker/engine-drive.js +257 -0
  226. package/dist/core/runtime/broker/engine-routing.d.ts +143 -0
  227. package/dist/core/runtime/broker/engine-routing.js +211 -0
  228. package/dist/core/runtime/broker/event-projection.js +3 -2
  229. package/dist/core/runtime/broker/fault-retry.js +5 -4
  230. package/dist/core/runtime/broker/frame-client.d.ts +38 -0
  231. package/dist/core/runtime/broker/frame-client.js +129 -0
  232. package/dist/core/runtime/broker/frame-dispatch.d.ts +12 -189
  233. package/dist/core/runtime/broker/frame-dispatch.js +89 -1476
  234. package/dist/core/runtime/broker/frame-engine-control.d.ts +20 -0
  235. package/dist/core/runtime/broker/frame-engine-control.js +231 -0
  236. package/dist/core/runtime/broker/frame-memory-refs.d.ts +19 -0
  237. package/dist/core/runtime/broker/frame-memory-refs.js +41 -0
  238. package/dist/core/runtime/broker/frame-model-query.d.ts +13 -0
  239. package/dist/core/runtime/broker/frame-model-query.js +69 -0
  240. package/dist/core/runtime/broker/frame-read.d.ts +19 -0
  241. package/dist/core/runtime/broker/frame-read.js +116 -0
  242. package/dist/core/runtime/broker/frame-replies.d.ts +24 -0
  243. package/dist/core/runtime/broker/frame-replies.js +64 -0
  244. package/dist/core/runtime/broker/frame-session.d.ts +34 -0
  245. package/dist/core/runtime/broker/frame-session.js +203 -0
  246. package/dist/core/runtime/broker/turn-ignition.d.ts +42 -0
  247. package/dist/core/runtime/broker/turn-ignition.js +74 -0
  248. package/dist/core/runtime/broker-extension-render.js +9 -85
  249. package/dist/core/runtime/broker-protocol.js +0 -4
  250. package/dist/core/runtime/broker-sdk.js +2 -1
  251. package/dist/core/runtime/broker.d.ts +1 -1
  252. package/dist/core/runtime/broker.js +5 -23
  253. package/dist/core/runtime/fault.js +3 -2
  254. package/dist/core/runtime/headless-pi.js +3 -2
  255. package/dist/core/runtime/host.js +2 -3
  256. package/dist/core/runtime/kickoff.js +0 -4
  257. package/dist/core/runtime/launch-target.js +2 -1
  258. package/dist/core/runtime/launch.js +3 -6
  259. package/dist/core/runtime/lifecycle.js +17 -26
  260. package/dist/core/runtime/managed-provider-cooling.d.ts +1 -1
  261. package/dist/core/runtime/naming.js +0 -2
  262. package/dist/core/runtime/nodes.js +2 -1
  263. package/dist/core/runtime/persona.js +0 -6
  264. package/dist/core/runtime/placement-tmux.js +2 -5
  265. package/dist/core/runtime/placement.js +4 -11
  266. package/dist/core/runtime/recap.js +4 -3
  267. package/dist/core/runtime/reopen.js +4 -7
  268. package/dist/core/runtime/reset.js +0 -4
  269. package/dist/core/runtime/resume-root.js +0 -2
  270. package/dist/core/runtime/revive.js +17 -13
  271. package/dist/core/runtime/session-list-cache.js +2 -2
  272. package/dist/core/runtime/shell-expansion.js +0 -2
  273. package/dist/core/runtime/spawn.js +0 -2
  274. package/dist/core/runtime/stream-watchdog.d.ts +0 -1
  275. package/dist/core/runtime/stream-watchdog.js +13 -18
  276. package/dist/core/runtime/tmux-bindings.js +0 -2
  277. package/dist/core/runtime/tmux-driver.d.ts +12 -0
  278. package/dist/core/runtime/tmux-driver.js +18 -13
  279. package/dist/core/runtime/warm-pool.js +0 -4
  280. package/dist/core/scope.js +4 -3
  281. package/dist/core/spawn.js +2 -3
  282. package/dist/core/subscription-state.js +10 -17
  283. package/dist/core/substrate/__tests__/surface-match-pre-command.test.d.ts +1 -0
  284. package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
  285. package/dist/core/substrate/frontmatter-validation.js +1 -1
  286. package/dist/core/substrate/injected-store.d.ts +6 -0
  287. package/dist/core/substrate/injected-store.js +24 -0
  288. package/dist/core/substrate/on-read.d.ts +17 -1
  289. package/dist/core/substrate/on-read.js +36 -2
  290. package/dist/core/substrate/render.js +0 -12
  291. package/dist/core/substrate/schema.d.ts +3 -3
  292. package/dist/core/substrate/schema.js +4 -16
  293. package/dist/core/substrate/surface-match.d.ts +19 -0
  294. package/dist/core/substrate/surface-match.js +60 -2
  295. package/dist/core/termrender/display.js +2 -1
  296. package/dist/core/tui/page-host.d.ts +3 -3
  297. package/dist/core/tui/page-host.js +4 -4
  298. package/dist/core/tui/panel.js +3 -3
  299. package/dist/core/wake.js +0 -12
  300. package/dist/core/worktree.js +0 -2
  301. package/dist/daemon/__tests__/reconciler-signature.test.d.ts +1 -0
  302. package/dist/daemon/__tests__/reconciler-signature.test.js +36 -0
  303. package/dist/daemon/api/__tests__/seam/api-server.test.js +2 -16
  304. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +3 -11
  305. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +0 -6
  306. package/dist/daemon/api/handlers/attach.js +2 -1
  307. package/dist/daemon/api/handlers/broker-ops.js +3 -6
  308. package/dist/daemon/api/handlers/canvas.js +12 -18
  309. package/dist/daemon/api/handlers/feedback-comments.js +0 -8
  310. package/dist/daemon/api/handlers/health.js +3 -2
  311. package/dist/daemon/api/handlers/human-requests.js +0 -10
  312. package/dist/daemon/api/handlers/human.js +1 -5
  313. package/dist/daemon/api/handlers/inbox.js +0 -18
  314. package/dist/daemon/api/handlers/memory.js +1 -1
  315. package/dist/daemon/api/handlers/messages.js +4 -8
  316. package/dist/daemon/api/handlers/nodes.js +10 -21
  317. package/dist/daemon/api/handlers/reports.js +1 -1
  318. package/dist/daemon/api/handlers/reviews.js +1 -6
  319. package/dist/daemon/api/map.js +0 -6
  320. package/dist/daemon/api/router.js +0 -4
  321. package/dist/daemon/api/server.js +2 -5
  322. package/dist/daemon/boot-migrations.d.ts +14 -0
  323. package/dist/daemon/boot-migrations.js +31 -0
  324. package/dist/daemon/cron/armed-context.d.ts +21 -0
  325. package/dist/daemon/cron/armed-context.js +100 -0
  326. package/dist/daemon/cron/capture.d.ts +20 -0
  327. package/dist/daemon/cron/capture.js +45 -0
  328. package/dist/daemon/cron/disposition.d.ts +19 -0
  329. package/dist/daemon/cron/disposition.js +52 -0
  330. package/dist/daemon/cron/escalation.d.ts +12 -0
  331. package/dist/daemon/cron/escalation.js +124 -0
  332. package/dist/daemon/cron/held.d.ts +4 -0
  333. package/dist/daemon/cron/held.js +20 -0
  334. package/dist/daemon/cron/lease-recovery.d.ts +3 -0
  335. package/dist/daemon/cron/lease-recovery.js +119 -0
  336. package/dist/daemon/cron/live-runs.d.ts +11 -0
  337. package/dist/daemon/cron/live-runs.js +23 -0
  338. package/dist/daemon/cron/passes.d.ts +11 -0
  339. package/dist/daemon/cron/passes.js +78 -0
  340. package/dist/daemon/cron/settle.d.ts +28 -0
  341. package/dist/daemon/cron/settle.js +37 -0
  342. package/dist/daemon/cron/sinks.d.ts +2 -0
  343. package/dist/daemon/cron/sinks.js +100 -0
  344. package/dist/daemon/cron-run.d.ts +5 -41
  345. package/dist/daemon/cron-run.js +193 -882
  346. package/dist/daemon/crtrd-cli.js +2 -1
  347. package/dist/daemon/crtrd.js +22 -22
  348. package/dist/daemon/fleet.js +0 -4
  349. package/dist/daemon/human/deliver-action.js +2 -1
  350. package/dist/daemon/human/sweep.js +15 -4
  351. package/dist/daemon/manage.d.ts +2 -2
  352. package/dist/daemon/manage.js +4 -13
  353. package/dist/daemon/pidfile.js +2 -2
  354. package/dist/daemon/reconcilers/broker-supervision.js +26 -29
  355. package/dist/daemon/reconcilers/dormant-inbox.d.ts +1 -1
  356. package/dist/daemon/reconcilers/dormant-inbox.js +2 -1
  357. package/dist/daemon/reconcilers/live-obligation.js +2 -2
  358. package/dist/daemon/reconcilers/pending-review-submit.d.ts +1 -1
  359. package/dist/daemon/reconcilers/pending-review-submit.js +2 -1
  360. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -1
  361. package/dist/daemon/reconcilers/storage-maintenance.js +2 -1
  362. package/dist/daemon/review/companion.js +2 -2
  363. package/dist/daemon/review/sweep.js +8 -3
  364. package/dist/migrations/001-surfaces-frontmatter.js +0 -6
  365. package/dist/migrations/corpus.js +2 -5
  366. package/dist/migrations/registry.js +15 -2
  367. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +0 -4
  368. package/dist/pi-extensions/__tests__/pre-command-gate.test.d.ts +1 -0
  369. package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
  370. package/dist/pi-extensions/broker-local.js +2 -1
  371. package/dist/pi-extensions/canvas-bash-valve.js +3 -17
  372. package/dist/pi-extensions/canvas-context-intro.js +2 -7
  373. package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
  374. package/dist/pi-extensions/canvas-doc-substrate.js +77 -5
  375. package/dist/pi-extensions/canvas-goal-capture.js +2 -1
  376. package/dist/pi-extensions/canvas-inbox-watcher.js +5 -12
  377. package/dist/pi-extensions/canvas-passive-context.js +2 -5
  378. package/dist/pi-extensions/canvas-preview-result.js +2 -1
  379. package/dist/pi-extensions/canvas-prompt-scrub.js +2 -1
  380. package/dist/pi-extensions/canvas-recap.js +3 -16
  381. package/dist/pi-extensions/canvas-review-boundary.js +2 -5
  382. package/dist/pi-extensions/canvas-stophook.js +2 -26
  383. package/dist/pi-extensions/canvas-structured-output.js +2 -1
  384. package/dist/pi-extensions/canvas-tool-guide.js +2 -1
  385. package/dist/shared/__tests__/env-boundary.test.d.ts +1 -0
  386. package/dist/shared/__tests__/env-boundary.test.js +93 -0
  387. package/dist/shared/env.d.ts +68 -0
  388. package/dist/shared/env.js +171 -0
  389. package/package.json +3 -1
  390. package/runtime.lock.json +2 -2
  391. package/dist/core/canvas/render.d.ts +0 -47
  392. package/dist/core/canvas/render.js +0 -245
@@ -64,13 +64,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
64
64
  { timeout: 60_000 }, async () => {
65
65
  const h = await createHeadlessHarness({ sessionPrefix: 'crtr-flagship' });
66
66
  try {
67
- // ===================================================================
68
67
  // S1 — A's ROW SHAPE: base × RESIDENT (the user's front door). A spawnRoot
69
68
  // fixture pins the PERSISTED row shape the scenario builds on; the real
70
69
  // resident-birth boot path (bootRoot execs pi inline) is unreachable
71
70
  // headlessly, exactly as in the tmux flagship. The resident BEHAVIOR (no
72
71
  // idle-release) is exercised in live-mutation.test.ts.
73
- // ===================================================================
74
72
  const A = h.spawnRoot('top-level orchestrator');
75
73
  {
76
74
  const a = h.node(A);
@@ -79,11 +77,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
79
77
  assert.equal(a.status, 'active', 'A active');
80
78
  assert.equal(a.intent ?? null, null, 'A no intent');
81
79
  }
82
- // ===================================================================
83
80
  // S2 — A promotes: orchestrator × RESIDENT (top-level orchestrator). The
84
81
  // real `node promote` verb; mode flips, lifecycle/status/intent
85
82
  // untouched (no transition()).
86
- // ===================================================================
87
83
  {
88
84
  const res = h.cli(A, ['node', 'promote']);
89
85
  assert.equal(res.code, 0, `promote A exit 0\n${res.stderr}`);
@@ -93,13 +89,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
93
89
  assert.equal(a.status, 'active', 'A still active (promote does not transition)');
94
90
  assert.equal(a.intent ?? null, null, 'A intent untouched by promote');
95
91
  }
96
- // ===================================================================
97
92
  // S3 — A spawns terminal child B: base × TERMINAL. Fabricated as a broker-
98
93
  // hosted node (pi_pid dead + pi_session_id set, so its later dormant
99
94
  // wake RESUMES that session); the spine seed is wired explicitly
100
95
  // (fabrication does not auto-subscribe). B is born acked to its own
101
96
  // persona (commitPersonaAck), mirroring a real birth.
102
- // ===================================================================
103
97
  const B = h.fabricateBrokerNode({
104
98
  parent: A, kind: 'developer', mode: 'base', lifecycle: 'terminal',
105
99
  status: 'active', pi_pid: deadPid(), pi_session_id: 'sess-B',
@@ -117,10 +111,8 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
117
111
  assert.deepEqual(h.subscribers(B), [{ node_id: A, active: true }], "A is B's sole ACTIVE subscriber (spine seed)");
118
112
  assert.equal(h.inbox(A).length, 0, 'A inbox empty (spawn pushes nothing)');
119
113
  }
120
- // ===================================================================
121
114
  // S4 — B spawns child C (base × terminal). B now holds an active live
122
115
  // subscription to C → B is legitimately "awaiting" if it stops.
123
- // ===================================================================
124
116
  const C = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
125
117
  subscribe(B, C, true);
126
118
  {
@@ -132,7 +124,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
132
124
  assert.deepEqual(h.subscriptions(B), [{ node_id: C, active: true }], 'B subscribes_to C (active)');
133
125
  assert.equal(h.status(B), 'active', 'B still active');
134
126
  }
135
- // ===================================================================
136
127
  // S5 — B goes DORMANT. PURE precondition: B is TERMINAL holding an active
137
128
  // live sub to C, so the stop-guard classifies its stop 'awaiting'
138
129
  // (legitimate idle-release). We then enact the post-stop MODEL state
@@ -140,7 +131,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
140
131
  // broker is a dead pid; pi_pid was fabricated dead at S3). The actual
141
132
  // agent_end idle-release COMMIT is locked by canvas-stophook-agentend
142
133
  // ('natural stop while awaiting a live worker → idle-release …').
143
- // ===================================================================
144
134
  {
145
135
  const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
146
136
  assert.equal(stop.action, 'allow', 'awaiting terminal B → allow (legit idle-release)');
@@ -155,11 +145,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
155
145
  assert.equal(h.status(C), 'active', 'C still active while B sleeps');
156
146
  assert.equal(h.status(A), 'active', 'A still active (a dormant worker does not disturb its manager)');
157
147
  assert.equal(h.inbox(A).length, 0, 'A inbox still empty');
158
- // ===================================================================
159
148
  // S6 — C FINISHES (pushFinal). The pointer fans to subscribersOf(C) = {B}
160
149
  // ONLY. C → done/done. A is NOT a subscriber of C → A inbox stays
161
150
  // empty — the one-hop wake contract at the push site.
162
- // ===================================================================
163
151
  await pushFinal(C, 'C result body');
164
152
  {
165
153
  const c = h.node(C);
@@ -169,7 +157,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
169
157
  assert.ok(cFinal, "B inbox received C's final pointer (one hop)");
170
158
  assert.equal(h.inbox(A).length, 0, 'A inbox empty — A NOT woken by C (one-hop)');
171
159
  }
172
- // ===================================================================
173
160
  // S7 — WAKE B. The in-process daemon second pass sees B idle/idle-release,
174
161
  // pi dead, an unseen inbox entry → reviveNode(resume). reviveNode
175
162
  // bumps cycles + transition('revive') BEFORE the (detached, unawaited)
@@ -177,7 +164,6 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
177
164
  // awaitBoot. (The real inbox-watcher's redelivery of C's report on the
178
165
  // fresh boot is an engine artifact, out of the model tier; the wake
179
166
  // DECISION is locked here by cycles + the S6 inbox entry.)
180
- // ===================================================================
181
167
  assert.equal(h.node(B).cycles ?? 0, 0, 'B not yet revived');
182
168
  await h.tick();
183
169
  {
@@ -188,10 +174,8 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
188
174
  }
189
175
  assert.equal(h.inbox(A).length, 0, 'A inbox STILL empty after B woke (one-hop confirmed)');
190
176
  assert.equal(h.status(A), 'active', 'A unchanged');
191
- // ===================================================================
192
177
  // S8 — B PROMOTES: base → orchestrator, lifecycle TERMINAL unchanged
193
178
  // (no --resident). orchestrator × terminal (the sub-orchestrator).
194
- // ===================================================================
195
179
  {
196
180
  const res = h.cli(B, ['node', 'promote', '--kind', 'developer']);
197
181
  assert.equal(res.code, 0, `promote B exit 0\n${res.stderr}`);
@@ -201,11 +185,9 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
201
185
  assert.equal(b.status, 'active', 'B still active');
202
186
  assert.equal(b.intent ?? null, null, 'B intent untouched by promote');
203
187
  }
204
- // ===================================================================
205
188
  // S9 — The next provider request must first deliver the pending persona
206
189
  // transition. Independently, agent_end would stall this orchestrator
207
190
  // because it has no live subscription and has not pushed a final.
208
- // ===================================================================
209
191
  {
210
192
  const drift = personaDrift(B);
211
193
  assert.ok(drift !== null, 'the provider gate has a pending persona transition');
@@ -217,13 +199,11 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
217
199
  assert.equal(stop.reason, 'stalled', 'classified stalled');
218
200
  assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', {}, STALL_REPROMPT), 'the stall reprompt nudge, delivered as a whole-message stop-guard envelope');
219
201
  }
220
- // ===================================================================
221
202
  // S10 — B YIELDS. The real plain `node yield` verb sets intent=refresh
222
203
  // while preserving mode (already orchestrator); persona survives the request. We
223
204
  // then model the daemon's completed refresh-revive with transition
224
205
  // ('revive') (the in-place fresh boot is engine; its model effect —
225
206
  // active, intent cleared, persona preserved — is asserted here).
226
- // ===================================================================
227
207
  {
228
208
  const res = h.cli(B, ['node', 'yield', 'refresh against the roadmap']);
229
209
  assert.equal(res.code, 0, `node yield exit 0\n${res.stderr}`);
@@ -240,9 +220,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
240
220
  assert.equal(b.mode, 'orchestrator', 'B mode survives the refresh');
241
221
  assert.equal(b.lifecycle, 'terminal', 'B lifecycle survives the refresh');
242
222
  }
243
- // ===================================================================
244
223
  // S11 — B spawns child D (base × terminal). New spine seed B→D active.
245
- // ===================================================================
246
224
  const D = h.fabricateBrokerNode({ parent: B, mode: 'base', lifecycle: 'terminal', status: 'active' });
247
225
  subscribe(B, D, true);
248
226
  {
@@ -252,20 +230,16 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
252
230
  assert.equal(d.status, 'active', 'D active');
253
231
  assert.ok(h.subscriptions(B).some((s) => s.node_id === D && s.active), 'B subscribes_to D (active)');
254
232
  }
255
- // ===================================================================
256
233
  // S12 — D FINISHES → pointer to B (its only subscriber).
257
- // ===================================================================
258
234
  await pushFinal(D, 'D result body');
259
235
  {
260
236
  assert.equal(h.node(D).status, 'done', 'D done');
261
237
  const dFinal = h.inbox(B).find((e) => e.from === D && e.kind === 'final');
262
238
  assert.ok(dFinal, "B inbox received D's final pointer");
263
239
  }
264
- // ===================================================================
265
240
  // S13 — B pushes its FINAL up the spine → done/done; the pointer fans to
266
241
  // subscribersOf(B) = {A}. NOW A finally hears B (its own explicit
267
242
  // push — the only way the chain propagates a hop).
268
- // ===================================================================
269
243
  await pushFinal(B, 'all work complete');
270
244
  {
271
245
  const b = h.node(B);
@@ -47,7 +47,6 @@ function persona(m) {
47
47
  return { mode: m.mode, lifecycle: m.lifecycle };
48
48
  }
49
49
  const FULL_CLI_TEST_TIMEOUT_MS = 180_000;
50
- // ===========================================================================
51
50
  // (b) THE demote / recycle SPLIT — two DISTINCT verbs:
52
51
  // • `node lifecycle demote` flips a LIVE node's lifecycle→TERMINAL IN PLACE — it keeps
53
52
  // its MODE and parentage, keeps running (active), is NOT finalized; it now
@@ -57,7 +56,6 @@ const FULL_CLI_TEST_TIMEOUT_MS = 180_000;
57
56
  // node keeps mode=orchestrator (it is merely `done`).
58
57
  // This test drives demote via the real CLI verb and recycle via the real
59
58
  // recycleNode, pinning each behavior at the model layer.
60
- // ===========================================================================
61
59
  test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle recycle is FINISH+RECYCLE',
62
60
  // Drives several full-CLI subprocess spawns (each a cold tsx compile of the
63
61
  // whole CLI) + recycleNode; this is a subprocess-heavy regression and gets a
@@ -118,13 +116,11 @@ test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle
118
116
  await h.dispose();
119
117
  }
120
118
  });
121
- // ===========================================================================
122
119
  // (b) A4 BOUNDARY — plain yield is mode-preserving; `yield --promote` is the
123
120
  // explicit one-step promotion + refresh. A plain yield sets intent=refresh
124
121
  // only, leaves the base persona acknowledged, and does NOT seed a roadmap.
125
122
  // With `--promote`, the mode flips and a roadmap is seeded; persona drift
126
123
  // remains pending until the replacement context persists its baseline.
127
- // ===========================================================================
128
124
  test('A4: a plain yield preserves base mode and does not seed a roadmap',
129
125
  // Full-CLI subprocess spawn under load — see the sibling test's budget note.
130
126
  { timeout: FULL_CLI_TEST_TIMEOUT_MS }, async () => {
@@ -42,13 +42,11 @@ function persona(m) {
42
42
  return { mode: m.mode, lifecycle: m.lifecycle };
43
43
  }
44
44
  const SIGNALS = { pushedFinal: false, requestedHumanReply: false };
45
- // ===========================================================================
46
45
  // (a) LIFECYCLE FLIP — `crtr node config --lifecycle` on a LIVE node, both directions,
47
46
  // observing the idle-release DECISION change. A round-trip on ONE node
48
47
  // (terminal→resident→terminal) proves both directions: a flipped-resident
49
48
  // node no longer idle-releases (evaluateStop → dormant); flipped-terminal it
50
49
  // idle-releases again (evaluateStop → awaiting, on its active live sub).
51
- // ===========================================================================
52
50
  test('live lifecycle flip: terminal→resident suppresses idle-release; resident→terminal restores it',
53
51
  // Full-CLI subprocess spawns under load — 20s blew on the slower CI runner
54
52
  // (passes fast locally); 60s = safe margin. Same class as live-mutation-verbs.
@@ -102,7 +100,7 @@ test('live lifecycle flip: terminal→resident suppresses idle-release; resident
102
100
  assert.equal(stop.action, 'allow', 'resident B → allow');
103
101
  assert.equal(stop.reason, 'dormant', 'resident B → dormant, NOT awaiting — idle-release SUPPRESSED by the live flip');
104
102
  }
105
- // --- FLIP 2: resident → TERMINAL (live), on the now-resident node. ---
103
+ // FLIP 2: resident → TERMINAL (live), on the now-resident node.
106
104
  {
107
105
  const res = h.cli(B, ['node', 'config', '--lifecycle', 'terminal', '--node', B]);
108
106
  assert.equal(res.code, 0, `config --lifecycle terminal exit 0\n${res.stderr}`);
@@ -124,11 +122,9 @@ test('live lifecycle flip: terminal→resident suppresses idle-release; resident
124
122
  await h.dispose();
125
123
  }
126
124
  });
127
- // ===========================================================================
128
125
  // (b) MODE FLIP — promote: base → orchestrator on a LIVE node. Promotion leaves
129
126
  // persona_ack pending; the next provider request appends and persists the
130
127
  // transition before a guarded ack. This test pins that model precondition.
131
- // ===========================================================================
132
128
  test('live mode flip: promote base→orchestrator leaves persona_ack pending for provider-gated delivery',
133
129
  // Full-CLI subprocess spawn under load — see the sibling test's budget note.
134
130
  { timeout: 60_000 }, async () => {
@@ -25,9 +25,7 @@ import { createHeadlessHarness } from '../helpers/harness.js';
25
25
  import { recordPid } from '../../canvas/canvas.js';
26
26
  import { isPidAlive } from '../../canvas/pid.js';
27
27
  import { yieldStallVerdict, YIELD_STALL_GRACE_MS } from '../../../daemon/reconcilers/broker-supervision.js';
28
- // ---------------------------------------------------------------------------
29
28
  // D4a — the pure refresh-stall truth table.
30
- // ---------------------------------------------------------------------------
31
29
  test('D4a yieldStallVerdict: kill ONLY a live, not-busy, refresh engine past the grace', () => {
32
30
  const G = YIELD_STALL_GRACE_MS;
33
31
  // The one 'kill' case: engine alive, intent=refresh, turn over (not busy), past grace.
@@ -35,9 +33,7 @@ test('D4a yieldStallVerdict: kill ONLY a live, not-busy, refresh engine past the
35
33
  // A working engine is NEVER killed, even past the grace.
36
34
  assert.equal(yieldStallVerdict(true, 'refresh', true, G + 60_000), 'leave', 'busy (mid-turn) → leave');
37
35
  });
38
- // ---------------------------------------------------------------------------
39
36
  // D4b — daemon integration: a stuck refresh on a LIVE engine is force-recycled.
40
- // ---------------------------------------------------------------------------
41
37
  const T0 = 7_000_000;
42
38
  const T_KILL = T0 + YIELD_STALL_GRACE_MS + 1; // one ms past the grace from t0
43
39
  test('D4b daemon force-recycles a stuck refresh: a live, not-busy refresh engine past the grace is SIGTERM\'d', { timeout: 30_000 }, async () => {
@@ -68,7 +64,7 @@ test('D4b daemon force-recycles a stuck refresh: a live, not-busy refresh engine
68
64
  h.fleet.register(id, { pid: enginePid, exited: new Promise(() => { }) });
69
65
  assert.equal(h.node(id).intent, 'refresh', 'precondition: intent=refresh pending');
70
66
  assert.notEqual(h.node(id).pi_pid_identity, null, 'precondition: live engine has a recorded identity');
71
- // --- TICK @ t0: arms the stall clock (yieldStallSince). No kill yet. ---
67
+ // TICK @ t0: arms the stall clock (yieldStallSince). No kill yet.
72
68
  await h.tick(T0);
73
69
  assert.equal(isPidAlive(enginePid), true, 'within grace: the engine is NOT killed (clock just armed)');
74
70
  // --- TICK past the grace: the daemon concludes the refresh stalled and
@@ -104,9 +104,7 @@ after(() => {
104
104
  }
105
105
  }
106
106
  });
107
- // ---------------------------------------------------------------------------
108
107
  // resumeArgs — the pure resume-source selection
109
- // ---------------------------------------------------------------------------
110
108
  test('resumeArgs resumes by the absolute file path when it exists on disk', () => {
111
109
  mkdirSync(home, { recursive: true });
112
110
  const sessionFile = join(home, 'resume-args-exists.jsonl');
@@ -144,9 +142,7 @@ test('resumeArgs selects neither source on a refresh revive with no `.jsonl` on
144
142
  });
145
143
  assert.deepEqual(resumeArgs(m, false), {});
146
144
  });
147
- // ---------------------------------------------------------------------------
148
145
  // buildPiArgv — the `--session` argument the revive builds
149
- // ---------------------------------------------------------------------------
150
146
  test('buildPiArgv resumes by ABSOLUTE path when pi_session_file exists on disk', () => {
151
147
  mkdirSync(home, { recursive: true });
152
148
  const sessionFile = join(home, 'build-argv-exists.jsonl');
@@ -169,10 +165,8 @@ test('buildPiArgv passes no --session on a fresh (no-resume) launch', () => {
169
165
  assert.ok(!argv.includes('--session'), 'a fresh launch never resumes');
170
166
  assert.ok(argv.includes('go'), 'the kickoff prompt is the last positional');
171
167
  });
172
- // ---------------------------------------------------------------------------
173
168
  // buildPiArgv — the resumed argv replays the persisted launch spec faithfully
174
169
  // (kind/mode/lifecycle come back as the node's *current* self).
175
- // ---------------------------------------------------------------------------
176
170
  test('a resumed revive replays the persisted LaunchSpec (model, tools, extensions, prompt, env)', () => {
177
171
  mkdirSync(home, { recursive: true });
178
172
  const sessionFile = join(home, 'launchspec-replay.jsonl');
@@ -211,13 +205,11 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
211
205
  assert.equal(env['CRTR_KIND'], 'developer');
212
206
  assert.equal(env['CRTR_MODE'], 'orchestrator');
213
207
  });
214
- // ---------------------------------------------------------------------------
215
208
  // Double-revive guard — reviveNode no-ops when the node has a live FLEET ENTRY
216
209
  // (design D-1: the entry IS liveness). Fabricated as a BROKER node with a
217
210
  // fabricated entry registered in the harness's in-process fleet, so the guard
218
211
  // is exercised with NO tmux and NO real boot. An entry → guard short-circuits
219
212
  // (cycles flat); no entry → revive proceeds (cycles bumped before the launch).
220
- // ---------------------------------------------------------------------------
221
213
  test('reviveNode no-ops when the node has a live fleet entry — double-revive guard', async () => {
222
214
  const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-guard' });
223
215
  try {
@@ -272,14 +264,12 @@ test('reviveNode PROCEEDS when the node has NO fleet entry — cycle counter bum
272
264
  await h.dispose();
273
265
  }
274
266
  });
275
- // ---------------------------------------------------------------------------
276
267
  // BUG LOCKED — a recovery relaunch must not eat the node's armed deadline.
277
268
  // A node armed `crtr node wait deadline` and dormed; its broker then exited and
278
269
  // the daemon respawned it (fleet.#respawn → reviveNode). reviveNode used to
279
270
  // cancel every cancel-on-wake cron unconditionally, so the deadline vanished
280
271
  // with nothing having woken the node and the wait could never settle. A wake
281
272
  // (inbox delivery, attach, explicit revive) still consumes it.
282
- // ---------------------------------------------------------------------------
283
273
  function armDeadline(anchor, cronId) {
284
274
  armCron({
285
275
  cron_id: cronId,
@@ -449,11 +439,9 @@ test('reviveNode crashes and clears pid when launch/preflight throws', async ()
449
439
  await h.dispose();
450
440
  }
451
441
  });
452
- // ---------------------------------------------------------------------------
453
442
  // Crash-to-dead doctrine wake — a launch-refusal marks the node dead; a
454
443
  // manager waiting dormant on it must be told, or it hangs forever (the
455
444
  // daemon no longer supervises a dead node). Mirrors closeNode's step-4 fan-out.
456
- // ---------------------------------------------------------------------------
457
445
  test('reviveNode fans a doctrine wake to subscribers when launch/preflight throws (crash-to-dead)', async () => {
458
446
  const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-crashwake' });
459
447
  const originalLaunch = headlessBrokerHost.launch;
@@ -59,11 +59,9 @@ function edgeSet(arr) {
59
59
  test('one daemon tick revives the active subscriber of a finished target and leaves the passive one dormant', { timeout: 30_000 }, async () => {
60
60
  const h = await createHarness({ headless: true, sessionPrefix: 'crtr-subdeliv' });
61
61
  try {
62
- // ===================================================================
63
62
  // Build the graph by FABRICATION (no boots). A is a resident root
64
63
  // (inline, daemon-untouched); B/T/K/P are broker nodes whose liveness
65
64
  // fields are set directly. Each parent→child spine edge is wired ACTIVE.
66
- // ===================================================================
67
65
  const A = h.spawnRoot('front door'); // resident root, active, no placement
68
66
  // T (the target) and K (keepalive) are LIVE-active workers; B and P start
69
67
  // already DORMANT (idle-released, broker exited = dead pid) — exactly the
@@ -88,20 +86,16 @@ test('one daemon tick revives the active subscriber of a finished target and lea
88
86
  assert.equal(passive(P).length, 0, 'P passive accumulator empty before the push');
89
87
  assert.equal(h.node(B).cycles ?? 0, 0, 'B not yet revived');
90
88
  assert.equal(h.node(P).cycles ?? 0, 0, 'P not yet revived');
91
- // ===================================================================
92
89
  // T FINISHES via the model layer — the precondition the daemon decides on:
93
90
  // B's pointer lands in inbox.jsonl (the wake channel), P's in passive.jsonl.
94
- // ===================================================================
95
91
  await pushFinal(T, 'TARGET-FINAL-BODY: the worker completed');
96
92
  closeDb();
97
93
  assert.ok(h.inbox(B).find((e) => e.from === T && e.kind === 'final'), 'ACTIVE subscriber B: T-final pointer delivered to inbox.jsonl');
98
94
  assert.equal(h.inbox(P).length, 0, 'PASSIVE subscriber P: inbox.jsonl stays empty (no wake channel)');
99
95
  assert.equal(passive(P).length, 1, 'PASSIVE subscriber P: exactly one passive entry');
100
- // ===================================================================
101
96
  // The DAEMON decision pass. Its 2nd pass revives an idle-released node
102
97
  // ONLY on an unseen INBOX entry. B has one (active) → REVIVED. P has none
103
98
  // (its entry went to passive.jsonl) → stays idle. Decided in ONE tick.
104
- // ===================================================================
105
99
  await h.tick();
106
100
  // ACTIVE subscriber: WOKEN — reviveNode bumped cycles + transition(revive).
107
101
  {
@@ -84,12 +84,13 @@ test('§2.2 driver invariant: every placement verb in tmux-driver.ts passes an e
84
84
  // three (new-window / split-window / respawn-pane).
85
85
  assert.ok(found >= 3, `expected to scan ≥3 placement verbs, saw ${found}`);
86
86
  });
87
- // ---------------------------------------------------------------------------
88
- // Lint guard — §5.1, ENFORCED. The driver (tmux-driver.ts) is imported only by
89
- // placement.ts, placement-tmux.ts, tmux-chrome.ts, and tmux-bindings.ts. Every
90
- // other module — runtime, daemon, commands, stophook, and tests — must route
91
- // through those sanctioned seams.
92
- // ---------------------------------------------------------------------------
87
+ // Lint guard — §5.1, ENFORCED. The driver's PLACEMENT verbs (tmux-driver.ts)
88
+ // are imported only by placement.ts, placement-tmux.ts, tmux-chrome.ts, and
89
+ // tmux-bindings.ts. Every other module — runtime, daemon, commands, stophook,
90
+ // and tests — must route through those sanctioned seams, and may import only
91
+ // the two read-only reachability probes: those answer a question, drive no
92
+ // pane, and are the very calls tmux-driver-boundary.test.ts requires every
93
+ // would-be direct `spawnSync('tmux', …)` prober to route through instead.
93
94
  /** Every `.ts` file under `src` (recursively), excluding nothing. */
94
95
  function allTsFiles(dir) {
95
96
  const out = [];
@@ -102,13 +103,28 @@ function allTsFiles(dir) {
102
103
  }
103
104
  return out;
104
105
  }
105
- /** Files (basename) sanctioned to import the driver, per §2.1. */
106
+ /** Files (basename) sanctioned to import the driver's full surface, per §2.1. */
106
107
  const ALLOWED_IMPORTERS = new Set(['placement.ts', 'placement-tmux.ts', 'tmux-chrome.ts', 'tmux-bindings.ts']);
108
+ /** The read-only probes any module may import — they drive no pane. */
109
+ const PROBE_IMPORTS = new Set(['tmuxSessionsReachable', 'tmuxClientsReachable']);
107
110
  function importsDriver(file) {
108
111
  const src = readFileSync(file, 'utf8');
109
112
  // A specifier whose basename is exactly `tmux-driver.js` (so
110
113
  // `tmux-bindings.js` is not matched). Covers `from '...'` and `import('...')`.
111
- return [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].some((m) => basename(m[1]) === 'tmux-driver.js');
114
+ const specifiers = [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].filter((m) => basename(m[1]) === 'tmux-driver.js');
115
+ if (specifiers.length === 0)
116
+ return false;
117
+ // Every reference must be a named import clause naming only the probes; any
118
+ // other shape (a placement verb, a namespace, a re-export, a dynamic import)
119
+ // is a direct use of the driver.
120
+ const clauses = [...src.matchAll(/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*'([^']+)'/g)].filter((m) => basename(m[2]) === 'tmux-driver.js');
121
+ if (clauses.length !== specifiers.length)
122
+ return true;
123
+ return !clauses.every((m) => m[1]
124
+ .split(',')
125
+ .map((name) => name.trim().split(/\s+as\s+/)[0].trim())
126
+ .filter((name) => name !== '')
127
+ .every((name) => PROBE_IMPORTS.has(name)));
112
128
  }
113
129
  test('§5.1 lint: only sanctioned seams import the tmux driver', () => {
114
130
  const offenders = allTsFiles(SRC_ROOT)
@@ -252,7 +252,6 @@ test('autoDropCleanManagedWorktree refuses when the recorded base branch no long
252
252
  assert.ok(existsSync(wt.path));
253
253
  rmSync(repo, { recursive: true, force: true });
254
254
  });
255
- // ---------------------------------------------------------------------------
256
255
  // The Critical fix: the TOCTOU race between proving clean-and-contained and
257
256
  // the destructive branch delete. `proveCleanEmptyManagedWorktree` and
258
257
  // `autoDropCleanManagedWorktreeLocked` are exported specifically so these
@@ -260,7 +259,6 @@ test('autoDropCleanManagedWorktree refuses when the recorded base branch no long
260
259
  // exactly the window the repository lock does NOT close (it only serializes
261
260
  // crtr's own worktree mutations, not ordinary `git commit`/`git reset`/an
262
261
  // external base rewrite run by any other process against the checkout).
263
- // ---------------------------------------------------------------------------
264
262
  // A concurrent commit lands on the managed branch after the predicate proved
265
263
  // it clean-and-contained, but before cleanup deletes it. The atomic
266
264
  // compare-and-delete must refuse, leaving the branch (and the new commit)
@@ -326,7 +324,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
326
324
  assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], repo).status, 0, 'the branch ref must still exist — nothing was lost');
327
325
  rmSync(repo, { recursive: true, force: true });
328
326
  });
329
- // ---------------------------------------------------------------------------
330
327
  // The base-ref TOCTOU: a branch-only compare-and-delete refuses when the
331
328
  // BRANCH moves, but a base force-reset between the containment proof and the
332
329
  // delete could still leave the delete's own condition (the branch equals the
@@ -338,7 +335,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
338
335
  // re-check and its ref-deletion transaction — the same technique used above
339
336
  // for the branch-only race, and the same one the independent re-review used
340
337
  // to reproduce this exact Critical against `6de1c8d`.
341
- // ---------------------------------------------------------------------------
342
338
  test('close-landed atomic delete refuses when the base is force-reset between landing and the cleanup delete', () => {
343
339
  const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-close-base-window-'));
344
340
  run(['init', '-b', 'main'], repo);
@@ -387,13 +383,11 @@ PATH="$CRTR_TEST_REAL_PATH" exec git "$@"
387
383
  assert.equal(refCheck.stdout.trim(), result.landed_sha, 'the preserved branch must still name the landed commit — no orphaned landed commit');
388
384
  rmSync(repo, { recursive: true, force: true });
389
385
  });
390
- // ---------------------------------------------------------------------------
391
386
  // Checkout-removal refusal: `bestEffortRemoveWorktreeCheckout` can report
392
387
  // `worktreeRemoved: false` (real Git refused `worktree remove` because a
393
388
  // file reappeared after the clean predicate ran). That refusal must itself
394
389
  // refuse the whole cleanup — never reach the low-level ref-deletion
395
390
  // transaction, which bypasses Git's own checked-out-branch protection.
396
- // ---------------------------------------------------------------------------
397
391
  test('auto-drop refuses and preserves the branch and checkout when checkout removal is refused after the clean predicate', async () => {
398
392
  const { proveCleanEmptyManagedWorktree, autoDropCleanManagedWorktreeLocked } = await import('../../worktree.js');
399
393
  const nodeId = 'wt-auto-drop-removal-refused';
@@ -48,9 +48,7 @@ after(() => {
48
48
  const ALL = ['active', 'idle', 'done', 'dead', 'canceled'];
49
49
  const LIVE = ['active', 'idle'];
50
50
  const TERMINAL = ['done', 'dead', 'canceled'];
51
- // ---------------------------------------------------------------------------
52
51
  // finalize → done + intent='done', legal only from active|idle
53
- // ---------------------------------------------------------------------------
54
52
  test('finalize: active|idle → done + intent=done', () => {
55
53
  for (const from of LIVE) {
56
54
  mk(`n_${from}`, from, 'refresh');
@@ -66,11 +64,9 @@ test('finalize: illegal from done|dead|canceled → throws, status untouched', (
66
64
  assert.equal(getNode(`n_${from}`)?.status, from, `${from} unchanged on illegal finalize`);
67
65
  }
68
66
  });
69
- // ---------------------------------------------------------------------------
70
67
  // park → done + intent='parked', legal only from active|idle. The intent IS the
71
68
  // durable park marker: it must land atomically with the `done` it qualifies,
72
69
  // read only through isParked, and be cleared by revival.
73
- // ---------------------------------------------------------------------------
74
70
  test('park: active|idle → done + intent=parked, and isParked reads exactly that pair', () => {
75
71
  for (const from of LIVE) {
76
72
  mk(`n_${from}`, from);
@@ -94,13 +90,11 @@ test('park: revival clears the marker, and no other done is parked', () => {
94
90
  mk('finished', 'active');
95
91
  assert.equal(isParked(transition('finished', 'finish')), false, 'a finished done is not a park');
96
92
  });
97
- // ---------------------------------------------------------------------------
98
93
  // cancel → canceled + intent cleared, legal from ANY status (forced teardown).
99
94
  // A5 (human-confirmed 2026-06-06): the `reap` event was COLLAPSED into `cancel`
100
95
  // — they were identical (status differed only; both intent=null, from=ANY, no
101
96
  // side effects in transition()). Every external reap (close cascade AND
102
97
  // reset/relaunch) now routes through `cancel`; `done` is reserved for finalize.
103
- // ---------------------------------------------------------------------------
104
98
  test('cancel: any status → canceled + intent cleared', () => {
105
99
  for (const from of ALL) {
106
100
  mk(`n_${from}`, from, 'idle-release');
@@ -109,9 +103,7 @@ test('cancel: any status → canceled + intent cleared', () => {
109
103
  assert.equal(m.intent, null, `cancel from ${from} clears intent`);
110
104
  }
111
105
  });
112
- // ---------------------------------------------------------------------------
113
106
  // crash → dead, intent UNCHANGED, legal only from active|idle
114
- // ---------------------------------------------------------------------------
115
107
  test('crash: active|idle → dead, intent preserved', () => {
116
108
  for (const from of LIVE) {
117
109
  mk(`n_${from}`, from, 'refresh');
@@ -126,9 +118,7 @@ test('crash: illegal from done|dead|canceled → throws', () => {
126
118
  assert.throws(() => transition(`n_${from}`, 'crash'), /illegal lifecycle transition/);
127
119
  }
128
120
  });
129
- // ---------------------------------------------------------------------------
130
121
  // yield → intent='refresh', status UNCHANGED, legal only from active|idle
131
- // ---------------------------------------------------------------------------
132
122
  test('yield: active|idle → intent=refresh, status preserved', () => {
133
123
  for (const from of LIVE) {
134
124
  mk(`n_${from}`, from);
@@ -143,9 +133,7 @@ test('yield: illegal from done|dead|canceled → throws', () => {
143
133
  assert.throws(() => transition(`n_${from}`, 'yield'), /illegal lifecycle transition/);
144
134
  }
145
135
  });
146
- // ---------------------------------------------------------------------------
147
136
  // release → idle + intent='idle-release', legal only from active|idle
148
- // ---------------------------------------------------------------------------
149
137
  test('release: active|idle → idle + intent=idle-release', () => {
150
138
  for (const from of LIVE) {
151
139
  mk(`n_${from}`, from);
@@ -160,10 +148,8 @@ test('release: illegal from done|dead|canceled → throws', () => {
160
148
  assert.throws(() => transition(`n_${from}`, 'release'), /illegal lifecycle transition/);
161
149
  }
162
150
  });
163
- // ---------------------------------------------------------------------------
164
151
  // refresh-launch → active + intent=refresh, legal from ANY status. This is the
165
152
  // provisional launch transition; session_start commits through revive below.
166
- // ---------------------------------------------------------------------------
167
153
  test('refresh-launch: any status → active + intent=refresh', () => {
168
154
  for (const from of ALL) {
169
155
  mk(`n_${from}`, from, 'idle-release');
@@ -172,9 +158,7 @@ test('refresh-launch: any status → active + intent=refresh', () => {
172
158
  assert.equal(m.intent, 'refresh', `refresh-launch from ${from} preserves refresh authority`);
173
159
  }
174
160
  });
175
- // ---------------------------------------------------------------------------
176
161
  // revive → active + intent cleared, legal from ANY status (the universal "back")
177
- // ---------------------------------------------------------------------------
178
162
  test('revive: any status → active + intent cleared', () => {
179
163
  for (const from of ALL) {
180
164
  mk(`n_${from}`, from, 'idle-release');
@@ -183,9 +167,7 @@ test('revive: any status → active + intent cleared', () => {
183
167
  assert.equal(m.intent, null, `revive from ${from} clears intent`);
184
168
  }
185
169
  });
186
- // ---------------------------------------------------------------------------
187
170
  // unknown node → throws for every event
188
- // ---------------------------------------------------------------------------
189
171
  test('transition on an unknown node throws', () => {
190
172
  for (const ev of ['finish', 'park', 'cancel', 'crash', 'yield', 'release', 'refresh-launch', 'revive']) {
191
173
  assert.throws(() => transition('ghost', ev), /unknown node/);
@@ -66,7 +66,7 @@ beforeEach(() => {
66
66
  grandchild = join(child, 'grandchild');
67
67
  initWorktreeRepository(root, child);
68
68
  mkdirSync(grandchild, { recursive: true });
69
- // --- main (FARTHER store, namespace root) ---
69
+ // main (FARTHER store, namespace root)
70
70
  writeDoc(join(root, '.crouter', 'memory', 'INDEX.md'), 'ROOT STORE INDEX', 'namespace: root');
71
71
  writeDoc(join(root, '.crouter', 'memory', 'shared.md'), 'ROOT SHARED BODY'); // root/shared
72
72
  writeDoc(join(root, '.crouter', 'memory', 'taste', 'INDEX.md'), 'ROOT TASTE INDEX'); // root/taste
@@ -215,7 +215,7 @@ test('a fresh db has the four additive runtime columns', () => {
215
215
  assert.equal(rt['window'], null);
216
216
  assert.equal(rt['tmux_session'], null);
217
217
  });
218
- // --- v4: edges FK ON DELETE CASCADE (the one irreversible rebuild) ------------
218
+ // v4: edges FK ON DELETE CASCADE (the one irreversible rebuild)
219
219
  /** Build a hand-rolled db at exactly v3 (baseline tables + the four runtime
220
220
  * columns, edges with NO foreign key, `user_version = 3`) — the shape the live
221
221
  * fleet reaches just before v4. Caller seeds it, we close it; the next openDb()
@@ -352,7 +352,7 @@ test('v16 repairs a v15 database missing canonical finalization columns', () =>
352
352
  db.close();
353
353
  }
354
354
  });
355
- // --- v22: legacy schedules → crons ------------------------------------------
355
+ // v22: legacy schedules → crons
356
356
  function buildV21Db() {
357
357
  ensureHome();
358
358
  const raw = new DatabaseSync(canvasDbPath());
@@ -94,7 +94,7 @@ test('on-read exposure survives a strict resume and resets for a fresh context',
94
94
  const readFile = join(work, 'src', 'file.ts');
95
95
  mkdirSync(join(work, 'src'), { recursive: true });
96
96
  writeFileSync(readFile, 'export const x = 1;\n');
97
- // --- pi process #1: cold load; first read surfaces the doc ---
97
+ // pi process #1: cold load; first read surfaces the doc
98
98
  const state = loadContextExposureState(node);
99
99
  const transcript = exposureTarget(state, 'transcript');
100
100
  const first = renderOnReadDocs(node, readFile, transcript);
@@ -102,11 +102,11 @@ test('on-read exposure survives a strict resume and resets for a fresh context',
102
102
  saveContextExposureState(node, state);
103
103
  // The active ledger suppresses an equal-rung repeat.
104
104
  assert.ok(!renderOnReadDocs(node, readFile, transcript).includes(FIXTURE_BODY), 'same-context repeat read is already exposed');
105
- // --- dormancy → revive(resume): NEW process rehydrates the state from disk ---
105
+ // dormancy → revive(resume): NEW process rehydrates the state from disk
106
106
  const stateAfterRevive = loadContextExposureState(node);
107
107
  assert.deepEqual(stateAfterRevive, state, 'revive(resume) rehydrates the persisted context state');
108
108
  assert.ok(!renderOnReadDocs(node, readFile, exposureTarget(stateAfterRevive, 'transcript')).includes(FIXTURE_BODY), 'strict resume does not re-deliver the document');
109
- // --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the state ---
109
+ // fresh launch (revive resume=false, e.g. a refresh-yield) clears the state
110
110
  clearContextExposureState(node);
111
111
  const stateFresh = loadContextExposureState(node);
112
112
  assert.ok(renderOnReadDocs(node, readFile, exposureTarget(stateFresh, 'transcript')).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
@@ -47,7 +47,7 @@ test('one side lacking ticks fails open to a base compare (transient /proc miss)
47
47
  assert.equal(identitiesMatch('Sat Jul 11 17:50:24 2026', 'Sat Jul 11 17:50:24 2026#53459'), true, 'coarse-vs-fine with matching base → match');
48
48
  assert.equal(identitiesMatch('Sat Jul 11 17:50:24 2026', 'Sat Jul 11 17:51:45 2026#53459'), false, 'coarse-vs-fine with different base → reuse');
49
49
  });
50
- // --- boot-scoped (new-format) identities ---
50
+ // boot-scoped (new-format) identities
51
51
  const BOOT_A = '4b0c1d2e-3f40-4152-8637-a1b2c3d4e5f6';
52
52
  const BOOT_B = '9e8d7c6b-5a40-4938-9271-fedcba098765';
53
53
  test('new-format: equal boot_id + equal ticks → match', () => {
@@ -110,7 +110,7 @@ test('a project-root plugin layers above user config, and the project config abo
110
110
  assert.equal(kinds['proj-kind']?.model, 'openai/medium'); // project config > project plugin
111
111
  assert.equal(kinds['proj-kind']?.whenToUse, 'project plugin kind');
112
112
  });
113
- // --- archive path -----------------------------------------------------------
113
+ // archive path
114
114
  function minimalCommandsJson() {
115
115
  return {
116
116
  schemaVersion: 1,