@north-light/crouter 0.3.233 → 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 (379) hide show
  1. package/dist/api/client.js +16 -15
  2. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +1 -1
  3. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +14 -17
  4. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -11
  5. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +8 -8
  6. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +10 -11
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +2 -2
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +3 -3
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -7
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +8 -8
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +6 -6
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +3 -3
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +1 -1
  14. package/dist/clients/attach/chrome/ticket-panel.js +15 -2
  15. package/dist/clients/attach/overlays/graph.js +0 -3
  16. package/dist/clients/attach/overlays/mcp.js +1 -2
  17. package/dist/clients/attach/render/chat-view.js +0 -12
  18. package/dist/clients/attach/render/crtr-output.js +0 -2
  19. package/dist/clients/attach/render/tool-calls.js +0 -6
  20. package/dist/clients/attach/slash/dispatch.js +8 -11
  21. package/dist/clients/attach/viewer.js +532 -534
  22. package/dist/clients/conversation/projection.js +0 -2
  23. package/dist/clients/inbox/controller.js +22 -3
  24. package/dist/clients/inbox/review/companion-pane.js +0 -4
  25. package/dist/clients/inbox/review-adapter.js +0 -7
  26. package/dist/clients/inbox/tui/slots.d.ts +3 -3
  27. package/dist/clients/inbox/tui/types.d.ts +2 -2
  28. package/dist/commands/api-client.js +0 -8
  29. package/dist/commands/attention.js +0 -8
  30. package/dist/commands/canvas-browse.js +2 -1
  31. package/dist/commands/canvas-config.js +0 -10
  32. package/dist/commands/canvas-history.js +2 -1
  33. package/dist/commands/cron.js +5 -18
  34. package/dist/commands/dashboard.js +0 -2
  35. package/dist/commands/human/feedback.js +2 -1
  36. package/dist/commands/human/prompts.js +2 -5
  37. package/dist/commands/human/queue.js +2 -7
  38. package/dist/commands/human/request.js +0 -14
  39. package/dist/commands/human/review.js +2 -1
  40. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +4 -4
  41. package/dist/commands/memory/find.js +2 -2
  42. package/dist/commands/memory/lint.js +2 -1
  43. package/dist/commands/memory/read.d.ts +2 -7
  44. package/dist/commands/memory/read.js +5 -16
  45. package/dist/commands/memory/shared.js +4 -7
  46. package/dist/commands/memory/write.js +4 -3
  47. package/dist/commands/node/bash.js +2 -1
  48. package/dist/commands/node/create.js +2 -1
  49. package/dist/commands/node/inspect.js +0 -6
  50. package/dist/commands/node/lifecycle.js +4 -13
  51. package/dist/commands/node/message.js +3 -2
  52. package/dist/commands/node/subscription.js +2 -1
  53. package/dist/commands/node/wait.js +4 -3
  54. package/dist/commands/node-context.js +2 -1
  55. package/dist/commands/node-lifecycle-revive.js +0 -2
  56. package/dist/commands/node-worktree.js +2 -1
  57. package/dist/commands/pkg/browse/actions.d.ts +1 -1
  58. package/dist/commands/pkg/browse/actions.js +4 -4
  59. package/dist/commands/pkg/browse/catalog.d.ts +1 -1
  60. package/dist/commands/pkg/browse/catalog.js +15 -15
  61. package/dist/commands/pkg/browse/command-view.d.ts +1 -1
  62. package/dist/commands/pkg/browse/command-view.js +3 -3
  63. package/dist/commands/pkg/browse/detail-panel.d.ts +1 -1
  64. package/dist/commands/pkg/browse/detail-panel.js +8 -8
  65. package/dist/commands/pkg/browse/doc-view.d.ts +1 -1
  66. package/dist/commands/pkg/browse/doc-view.js +3 -3
  67. package/dist/commands/pkg/browse/list-panel.d.ts +1 -1
  68. package/dist/commands/pkg/browse/list-panel.js +3 -3
  69. package/dist/commands/pkg/browse/model.d.ts +3 -3
  70. package/dist/commands/pkg/browse/shell.d.ts +2 -2
  71. package/dist/commands/pkg/browse/shell.js +13 -13
  72. package/dist/commands/pkg/plugin-manage.js +2 -1
  73. package/dist/commands/pkg/shared.js +0 -2
  74. package/dist/commands/profile/delete.js +2 -1
  75. package/dist/commands/push.js +2 -3
  76. package/dist/commands/revive.js +0 -2
  77. package/dist/commands/surface/node/placement.js +2 -1
  78. package/dist/commands/surface-edit.js +2 -1
  79. package/dist/commands/sys/config.js +0 -4
  80. package/dist/commands/sys/daemon.js +0 -10
  81. package/dist/commands/sys/doctor.js +3 -2
  82. package/dist/commands/sys/panels/broker-limits-panel.d.ts +2 -2
  83. package/dist/commands/sys/panels/broker-limits-panel.js +4 -4
  84. package/dist/commands/sys/panels/dependencies-panel.d.ts +2 -2
  85. package/dist/commands/sys/panels/dependencies-panel.js +2 -2
  86. package/dist/commands/sys/panels/keybindings-panel.d.ts +1 -1
  87. package/dist/commands/sys/panels/keybindings-panel.js +5 -5
  88. package/dist/commands/sys/panels/models-panel.d.ts +4 -4
  89. package/dist/commands/sys/panels/models-panel.js +11 -10
  90. package/dist/commands/sys/panels/plugins-panel.d.ts +2 -2
  91. package/dist/commands/sys/panels/plugins-panel.js +2 -2
  92. package/dist/commands/sys/panels/prefs-panel.d.ts +2 -2
  93. package/dist/commands/sys/panels/prefs-panel.js +4 -4
  94. package/dist/commands/sys/panels/profiles-panel.d.ts +1 -1
  95. package/dist/commands/sys/panels/profiles-panel.js +14 -13
  96. package/dist/commands/sys/panels/provider-panel.d.ts +3 -3
  97. package/dist/commands/sys/panels/provider-panel.js +8 -8
  98. package/dist/commands/sys/settings-shell.d.ts +3 -3
  99. package/dist/commands/sys/settings-shell.js +15 -15
  100. package/dist/commands/sys/setup-settings-runner.d.ts +3 -3
  101. package/dist/commands/sys/setup-settings-runner.js +5 -5
  102. package/dist/commands/sys/setup-wizard.d.ts +4 -4
  103. package/dist/commands/sys/setup-wizard.js +8 -8
  104. package/dist/commands/sys/support/prepare.js +4 -4
  105. package/dist/commands/sys/support/submit.js +2 -2
  106. package/dist/commands/sys/sync-deps.js +0 -2
  107. package/dist/commands/sys/sync-project-guidance.js +2 -1
  108. package/dist/commands/sys/sysprompt.js +2 -1
  109. package/dist/core/__tests__/boot.test.js +0 -8
  110. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +23 -1
  111. package/dist/core/__tests__/canvas-db-boundary.test.d.ts +1 -0
  112. package/dist/core/__tests__/canvas-db-boundary.test.js +46 -0
  113. package/dist/core/__tests__/canvas.test.js +1 -1
  114. package/dist/core/__tests__/child-death-wake.test.js +0 -2
  115. package/dist/core/__tests__/cron-armed-context-failure.test.d.ts +1 -0
  116. package/dist/core/__tests__/cron-armed-context-failure.test.js +145 -0
  117. package/dist/core/__tests__/daemon-wedge.test.js +0 -4
  118. package/dist/core/__tests__/dead-node-policy-table.test.js +0 -4
  119. package/dist/core/__tests__/fixtures/fake-engine.js +4 -14
  120. package/dist/core/__tests__/fixtures/fake-pi-host.js +5 -5
  121. package/dist/core/__tests__/focuses.test.js +0 -6
  122. package/dist/core/__tests__/fs-utils.test.d.ts +1 -0
  123. package/dist/core/__tests__/fs-utils.test.js +50 -0
  124. package/dist/core/__tests__/helpers/harness.js +1 -1
  125. package/dist/core/__tests__/inline-memory-refs.test.js +0 -2
  126. package/dist/core/__tests__/integration/broker-fork-seam.test.js +0 -2
  127. package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +3 -16
  128. package/dist/core/__tests__/integration/command-plugins.test.js +0 -24
  129. package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -6
  130. package/dist/core/__tests__/integration/flagship-lifecycle.test.js +0 -26
  131. package/dist/core/__tests__/integration/live-mutation-verbs.test.js +0 -4
  132. package/dist/core/__tests__/integration/live-mutation.test.js +1 -5
  133. package/dist/core/__tests__/integration/refresh-stall-recycle.test.js +1 -5
  134. package/dist/core/__tests__/integration/revive.test.js +0 -12
  135. package/dist/core/__tests__/integration/subscription-delivery.test.js +0 -6
  136. package/dist/core/__tests__/integration/tmux-surface.test.js +24 -8
  137. package/dist/core/__tests__/integration/worktree-reap.test.js +0 -6
  138. package/dist/core/__tests__/lifecycle.test.js +0 -18
  139. package/dist/core/__tests__/memory-resolver-precedence.test.js +1 -1
  140. package/dist/core/__tests__/migration.test.js +2 -2
  141. package/dist/core/__tests__/on-read-dedup-resume.test.js +3 -3
  142. package/dist/core/__tests__/pid-identity-match.test.js +1 -1
  143. package/dist/core/__tests__/plugin-kinds.test.js +1 -1
  144. package/dist/core/__tests__/plugin-page-components.test.js +1 -1
  145. package/dist/core/__tests__/preview-registry-sync.test.js +42 -1
  146. package/dist/core/__tests__/respawn-throttle.test.js +0 -4
  147. package/dist/core/__tests__/revive-all.test.js +5 -5
  148. package/dist/core/__tests__/revive-parked-fresh.test.js +62 -11
  149. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +0 -8
  150. package/dist/core/__tests__/seam/broker-attach-stream.test.js +0 -8
  151. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +0 -2
  152. package/dist/core/__tests__/tmux-driver-boundary.test.d.ts +1 -0
  153. package/dist/core/__tests__/tmux-driver-boundary.test.js +50 -0
  154. package/dist/core/activity-signal.d.ts +7 -0
  155. package/dist/core/activity-signal.js +38 -0
  156. package/dist/core/auto-update.js +2 -1
  157. package/dist/core/bootstrap.js +2 -1
  158. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +1 -1
  159. package/dist/core/canvas/__tests__/remote-transport.test.js +1 -1
  160. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  161. package/dist/core/canvas/browse/app.js +3 -2
  162. package/dist/core/canvas/browse/model.d.ts +1 -1
  163. package/dist/core/canvas/browse/model.js +0 -4
  164. package/dist/core/canvas/canvas.d.ts +22 -0
  165. package/dist/core/canvas/canvas.js +50 -18
  166. package/dist/core/canvas/crons.js +0 -6
  167. package/dist/core/canvas/db.d.ts +14 -20
  168. package/dist/core/canvas/db.js +30 -1332
  169. package/dist/core/canvas/focuses.js +0 -4
  170. package/dist/core/canvas/history.js +2 -1
  171. package/dist/core/canvas/migrations.d.ts +20 -0
  172. package/dist/core/canvas/migrations.js +1339 -0
  173. package/dist/core/canvas/nav-model.js +1 -11
  174. package/dist/core/canvas/nav-render.js +0 -8
  175. package/dist/core/canvas/remote-canvas-source.js +0 -2
  176. package/dist/core/canvas/render-source.js +31 -13
  177. package/dist/core/command-manifests/manifest.js +0 -10
  178. package/dist/core/command-manifests/schema.js +0 -4
  179. package/dist/core/command-plugins/transport/http-invoke.js +0 -14
  180. package/dist/core/command.js +0 -4
  181. package/dist/core/config.js +2 -1
  182. package/dist/core/feed/inbox.d.ts +10 -0
  183. package/dist/core/feed/inbox.js +18 -12
  184. package/dist/core/fs-utils.d.ts +9 -2
  185. package/dist/core/fs-utils.js +7 -15
  186. package/dist/core/help.js +0 -8
  187. package/dist/core/host-exports/export.d.ts +2 -2
  188. package/dist/core/host-exports/export.js +9 -12
  189. package/dist/core/human/answer.d.ts +2 -2
  190. package/dist/core/human/convention.d.ts +2 -5
  191. package/dist/core/human/convention.js +3 -13
  192. package/dist/core/human/page-schema.d.ts +2 -2
  193. package/dist/core/human/page-schema.js +2 -2
  194. package/dist/core/human/root.js +6 -23
  195. package/dist/core/human/scan.js +24 -7
  196. package/dist/core/human/tickets.d.ts +4 -1
  197. package/dist/core/human/tickets.js +20 -6
  198. package/dist/core/io.js +0 -12
  199. package/dist/core/memory/identity.js +0 -4
  200. package/dist/core/memory/inline-ref-inventory.js +3 -2
  201. package/dist/core/memory/lint.js +3 -6
  202. package/dist/core/memory-resolver.d.ts +5 -0
  203. package/dist/core/memory-resolver.js +14 -12
  204. package/dist/core/pagination.js +0 -2
  205. package/dist/core/preview-registry.d.ts +14 -0
  206. package/dist/core/preview-registry.js +2 -4
  207. package/dist/core/preview-result-path.js +2 -1
  208. package/dist/core/profiles/manifest.d.ts +37 -5
  209. package/dist/core/profiles/manifest.js +44 -24
  210. package/dist/core/profiles/select.js +0 -4
  211. package/dist/core/review/signal.js +4 -23
  212. package/dist/core/review/ticket-filter.d.ts +3 -3
  213. package/dist/core/review/ticket-filter.js +4 -9
  214. package/dist/core/runtime/__tests__/engine-route.test.js +1 -1
  215. package/dist/core/runtime/__tests__/spawn-env.test.js +0 -8
  216. package/dist/core/runtime/bearings-render.d.ts +56 -0
  217. package/dist/core/runtime/bearings-render.js +241 -0
  218. package/dist/core/runtime/bearings.d.ts +2 -17
  219. package/dist/core/runtime/bearings.js +12 -198
  220. package/dist/core/runtime/broker/auth-reload.d.ts +39 -0
  221. package/dist/core/runtime/broker/auth-reload.js +149 -0
  222. package/dist/core/runtime/broker/engine-drive.d.ts +26 -0
  223. package/dist/core/runtime/broker/engine-drive.js +257 -0
  224. package/dist/core/runtime/broker/engine-routing.d.ts +143 -0
  225. package/dist/core/runtime/broker/engine-routing.js +211 -0
  226. package/dist/core/runtime/broker/event-projection.js +3 -2
  227. package/dist/core/runtime/broker/fault-retry.js +5 -4
  228. package/dist/core/runtime/broker/frame-client.d.ts +38 -0
  229. package/dist/core/runtime/broker/frame-client.js +129 -0
  230. package/dist/core/runtime/broker/frame-dispatch.d.ts +12 -189
  231. package/dist/core/runtime/broker/frame-dispatch.js +89 -1476
  232. package/dist/core/runtime/broker/frame-engine-control.d.ts +20 -0
  233. package/dist/core/runtime/broker/frame-engine-control.js +231 -0
  234. package/dist/core/runtime/broker/frame-memory-refs.d.ts +19 -0
  235. package/dist/core/runtime/broker/frame-memory-refs.js +41 -0
  236. package/dist/core/runtime/broker/frame-model-query.d.ts +13 -0
  237. package/dist/core/runtime/broker/frame-model-query.js +69 -0
  238. package/dist/core/runtime/broker/frame-read.d.ts +19 -0
  239. package/dist/core/runtime/broker/frame-read.js +116 -0
  240. package/dist/core/runtime/broker/frame-replies.d.ts +24 -0
  241. package/dist/core/runtime/broker/frame-replies.js +64 -0
  242. package/dist/core/runtime/broker/frame-session.d.ts +34 -0
  243. package/dist/core/runtime/broker/frame-session.js +203 -0
  244. package/dist/core/runtime/broker/turn-ignition.d.ts +42 -0
  245. package/dist/core/runtime/broker/turn-ignition.js +74 -0
  246. package/dist/core/runtime/broker-extension-render.js +9 -85
  247. package/dist/core/runtime/broker-protocol.js +0 -4
  248. package/dist/core/runtime/broker-sdk.js +2 -1
  249. package/dist/core/runtime/broker.d.ts +1 -1
  250. package/dist/core/runtime/broker.js +5 -23
  251. package/dist/core/runtime/fault.js +3 -2
  252. package/dist/core/runtime/headless-pi.js +3 -2
  253. package/dist/core/runtime/host.js +2 -3
  254. package/dist/core/runtime/kickoff.js +0 -4
  255. package/dist/core/runtime/launch-target.js +2 -1
  256. package/dist/core/runtime/launch.js +3 -6
  257. package/dist/core/runtime/lifecycle.js +17 -26
  258. package/dist/core/runtime/managed-provider-cooling.d.ts +1 -1
  259. package/dist/core/runtime/naming.js +0 -2
  260. package/dist/core/runtime/nodes.js +2 -1
  261. package/dist/core/runtime/persona.js +0 -6
  262. package/dist/core/runtime/placement-tmux.js +2 -5
  263. package/dist/core/runtime/placement.js +4 -11
  264. package/dist/core/runtime/recap.js +4 -3
  265. package/dist/core/runtime/reopen.js +4 -7
  266. package/dist/core/runtime/reset.js +0 -4
  267. package/dist/core/runtime/resume-root.js +0 -2
  268. package/dist/core/runtime/revive.js +17 -13
  269. package/dist/core/runtime/session-list-cache.js +2 -2
  270. package/dist/core/runtime/shell-expansion.js +0 -2
  271. package/dist/core/runtime/spawn.js +0 -2
  272. package/dist/core/runtime/stream-watchdog.d.ts +0 -1
  273. package/dist/core/runtime/stream-watchdog.js +13 -18
  274. package/dist/core/runtime/tmux-bindings.js +0 -2
  275. package/dist/core/runtime/tmux-driver.d.ts +12 -0
  276. package/dist/core/runtime/tmux-driver.js +18 -13
  277. package/dist/core/runtime/warm-pool.js +0 -4
  278. package/dist/core/scope.js +4 -3
  279. package/dist/core/spawn.js +2 -3
  280. package/dist/core/subscription-state.js +10 -17
  281. package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +3 -3
  282. package/dist/core/substrate/render.js +0 -12
  283. package/dist/core/substrate/schema.js +0 -12
  284. package/dist/core/substrate/surface-match.js +0 -2
  285. package/dist/core/termrender/display.js +2 -1
  286. package/dist/core/tui/page-host.d.ts +3 -3
  287. package/dist/core/tui/page-host.js +4 -4
  288. package/dist/core/tui/panel.js +3 -3
  289. package/dist/core/wake.js +0 -12
  290. package/dist/core/worktree.js +0 -2
  291. package/dist/daemon/__tests__/reconciler-signature.test.d.ts +1 -0
  292. package/dist/daemon/__tests__/reconciler-signature.test.js +36 -0
  293. package/dist/daemon/api/__tests__/seam/api-server.test.js +2 -16
  294. package/dist/daemon/api/__tests__/seam/b10-attach-modelauth.test.js +3 -11
  295. package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +0 -6
  296. package/dist/daemon/api/handlers/attach.js +2 -1
  297. package/dist/daemon/api/handlers/broker-ops.js +3 -6
  298. package/dist/daemon/api/handlers/canvas.js +12 -18
  299. package/dist/daemon/api/handlers/feedback-comments.js +0 -8
  300. package/dist/daemon/api/handlers/health.js +3 -2
  301. package/dist/daemon/api/handlers/human-requests.js +0 -10
  302. package/dist/daemon/api/handlers/human.js +1 -5
  303. package/dist/daemon/api/handlers/inbox.js +0 -18
  304. package/dist/daemon/api/handlers/memory.js +1 -1
  305. package/dist/daemon/api/handlers/messages.js +4 -8
  306. package/dist/daemon/api/handlers/nodes.js +10 -21
  307. package/dist/daemon/api/handlers/reports.js +1 -1
  308. package/dist/daemon/api/handlers/reviews.js +1 -6
  309. package/dist/daemon/api/map.js +0 -6
  310. package/dist/daemon/api/router.js +0 -4
  311. package/dist/daemon/api/server.js +2 -5
  312. package/dist/daemon/boot-migrations.d.ts +14 -0
  313. package/dist/daemon/boot-migrations.js +31 -0
  314. package/dist/daemon/cron/armed-context.d.ts +21 -0
  315. package/dist/daemon/cron/armed-context.js +100 -0
  316. package/dist/daemon/cron/capture.d.ts +20 -0
  317. package/dist/daemon/cron/capture.js +45 -0
  318. package/dist/daemon/cron/disposition.d.ts +19 -0
  319. package/dist/daemon/cron/disposition.js +52 -0
  320. package/dist/daemon/cron/escalation.d.ts +12 -0
  321. package/dist/daemon/cron/escalation.js +124 -0
  322. package/dist/daemon/cron/held.d.ts +4 -0
  323. package/dist/daemon/cron/held.js +20 -0
  324. package/dist/daemon/cron/lease-recovery.d.ts +3 -0
  325. package/dist/daemon/cron/lease-recovery.js +119 -0
  326. package/dist/daemon/cron/live-runs.d.ts +11 -0
  327. package/dist/daemon/cron/live-runs.js +23 -0
  328. package/dist/daemon/cron/passes.d.ts +11 -0
  329. package/dist/daemon/cron/passes.js +78 -0
  330. package/dist/daemon/cron/settle.d.ts +28 -0
  331. package/dist/daemon/cron/settle.js +37 -0
  332. package/dist/daemon/cron/sinks.d.ts +2 -0
  333. package/dist/daemon/cron/sinks.js +100 -0
  334. package/dist/daemon/cron-run.d.ts +5 -41
  335. package/dist/daemon/cron-run.js +193 -882
  336. package/dist/daemon/crtrd-cli.js +2 -1
  337. package/dist/daemon/crtrd.js +22 -22
  338. package/dist/daemon/fleet.js +0 -4
  339. package/dist/daemon/human/deliver-action.js +2 -1
  340. package/dist/daemon/human/sweep.js +15 -4
  341. package/dist/daemon/manage.d.ts +2 -2
  342. package/dist/daemon/manage.js +4 -13
  343. package/dist/daemon/pidfile.js +2 -2
  344. package/dist/daemon/reconcilers/broker-supervision.js +26 -29
  345. package/dist/daemon/reconcilers/dormant-inbox.d.ts +1 -1
  346. package/dist/daemon/reconcilers/dormant-inbox.js +2 -1
  347. package/dist/daemon/reconcilers/live-obligation.js +2 -2
  348. package/dist/daemon/reconcilers/pending-review-submit.d.ts +1 -1
  349. package/dist/daemon/reconcilers/pending-review-submit.js +2 -1
  350. package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -1
  351. package/dist/daemon/reconcilers/storage-maintenance.js +2 -1
  352. package/dist/daemon/review/companion.js +2 -2
  353. package/dist/daemon/review/sweep.js +8 -3
  354. package/dist/migrations/001-surfaces-frontmatter.js +0 -6
  355. package/dist/migrations/corpus.js +2 -5
  356. package/dist/migrations/registry.js +15 -2
  357. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +0 -4
  358. package/dist/pi-extensions/broker-local.js +2 -1
  359. package/dist/pi-extensions/canvas-bash-valve.js +3 -17
  360. package/dist/pi-extensions/canvas-context-intro.js +2 -7
  361. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  362. package/dist/pi-extensions/canvas-goal-capture.js +2 -1
  363. package/dist/pi-extensions/canvas-inbox-watcher.js +5 -12
  364. package/dist/pi-extensions/canvas-passive-context.js +2 -5
  365. package/dist/pi-extensions/canvas-preview-result.js +2 -1
  366. package/dist/pi-extensions/canvas-prompt-scrub.js +2 -1
  367. package/dist/pi-extensions/canvas-recap.js +3 -16
  368. package/dist/pi-extensions/canvas-review-boundary.js +2 -5
  369. package/dist/pi-extensions/canvas-stophook.js +2 -26
  370. package/dist/pi-extensions/canvas-structured-output.js +2 -1
  371. package/dist/pi-extensions/canvas-tool-guide.js +2 -1
  372. package/dist/shared/__tests__/env-boundary.test.d.ts +1 -0
  373. package/dist/shared/__tests__/env-boundary.test.js +93 -0
  374. package/dist/shared/env.d.ts +68 -0
  375. package/dist/shared/env.js +171 -0
  376. package/package.json +3 -1
  377. package/runtime.lock.json +2 -2
  378. package/dist/core/canvas/render.d.ts +0 -47
  379. package/dist/core/canvas/render.js +0 -245
@@ -81,7 +81,7 @@ test('two plugins whose kinds derive one JSX tag are rejected, naming both sourc
81
81
  plantPlugin(root, 'beta', ['foo--bar']);
82
82
  assert.throws(() => resolvePageComponents(), /kind "foo--bar" \(plugin "beta"\) derives JSX tag "FooBar", which duplicates kind "foo-bar" from plugin "alpha"/);
83
83
  });
84
- // --- archive path -----------------------------------------------------------
84
+ // archive path
85
85
  function minimalCommandsJson() {
86
86
  return {
87
87
  schemaVersion: 1,
@@ -15,7 +15,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { join } from 'node:path';
17
17
  import { buildRoot } from '../../build-root.js';
18
- import { SUPPRESS_OUTPUT_LEAVES } from '../preview-registry.js';
18
+ import { SUPPRESS_OUTPUT_LEAVES, LEAF_ICON_CP, SUMMARIZERS } from '../preview-registry.js';
19
19
  import { resetScopeCache } from '../scope.js';
20
20
  // buildRoot() composes external command-plugin branches from the effective
21
21
  // enabled plugins (user scope + project ancestors). Pin HOME + profile to an
@@ -62,3 +62,44 @@ test('preview-registry.ts matches every leaf declaring help.preview.suppressOutp
62
62
  assert.deepEqual([...declared].sort(), [...SUPPRESS_OUTPUT_LEAVES].sort(), 'src/core/preview-registry.ts is out of sync with the leaves that declare ' +
63
63
  'help.preview.suppressOutput — update SUPPRESS_OUTPUT_LEAVES to match.');
64
64
  });
65
+ test('every LEAF_ICON_CP key and SUMMARIZERS path names a leaf or branch that still exists', async () => {
66
+ const root = await buildRoot();
67
+ // LEAF_ICON_CP is matched as a PREFIX against argv tokens (iconForPath), so
68
+ // a key may legitimately name either a leaf or a branch (e.g. "human
69
+ // review", a branch with no default action of its own). SUMMARIZERS keys
70
+ // ARE always a single invocable leaf. Both name sets are collected here.
71
+ const names = new Set();
72
+ const leafNames = new Set();
73
+ const walk = (branch) => {
74
+ names.add(branch.help.name);
75
+ for (const child of branch.children) {
76
+ if (child.kind === 'branch') {
77
+ walk(child);
78
+ }
79
+ else {
80
+ names.add(child.help.name);
81
+ leafNames.add(child.help.name);
82
+ }
83
+ }
84
+ };
85
+ for (const subtree of root.subtrees)
86
+ walk(subtree);
87
+ // The registry pre-declares icons for PLUGIN commands too (`search web` and
88
+ // friends), and a plugin is present only when installed — it is absent on a
89
+ // clean CI runner. So a key is only checkable when its ROOT command exists:
90
+ // root present means the rest of the path must resolve (this is what catches a
91
+ // genuinely stale entry, e.g. a renamed `human review`), while an absent root
92
+ // means an uninstalled plugin and is not evidence of staleness.
93
+ const rootNames = new Set(root.subtrees.map((s) => s.help.name));
94
+ const rootInstalled = (path) => rootNames.has(path.split(' ')[0]);
95
+ for (const path of Object.keys(LEAF_ICON_CP)) {
96
+ if (!rootInstalled(path))
97
+ continue;
98
+ assert.ok(names.has(path), `LEAF_ICON_CP names "${path}", which is not a leaf or branch in the live command tree — update or remove it.`);
99
+ }
100
+ for (const { path } of SUMMARIZERS) {
101
+ if (!rootInstalled(path))
102
+ continue;
103
+ assert.ok(leafNames.has(path), `SUMMARIZERS names "${path}", which is not a leaf in the live command tree — update or remove it.`);
104
+ }
105
+ });
@@ -73,9 +73,7 @@ after(() => {
73
73
  rmSync(home, { recursive: true, force: true });
74
74
  delete process.env['CRTR_HOME'];
75
75
  });
76
- // ===========================================================================
77
76
  // Short-lived exits 1-5 defer with doubling backoff; the 6th terminalizes.
78
- // ===========================================================================
79
77
  test('consecutive short-lived exits defer respawn on a doubling backoff, then terminalize at the cap', async () => {
80
78
  // A booted row (saved session, no crash markers) — table row 6, respawn-resume.
81
79
  createNode(node('T', { pi_session_id: 'sess-T' }));
@@ -95,9 +93,7 @@ test('consecutive short-lived exits defer respawn on a doubling backoff, then te
95
93
  assert.equal(getNode('T').status, 'dead', 'the respawn loop terminalizes at MAX_RESPAWN_ATTEMPTS');
96
94
  assert.equal(events().some((e) => e.event === 'broker.respawn.exhausted'), true, 'exhaustion is surfaced as its own event');
97
95
  });
98
- // ===========================================================================
99
96
  // A healthy-uptime exit resets the counter: the next respawn is immediate.
100
- // ===========================================================================
101
97
  test('a pre-launch respawn refusal terminalizes the stranded node and wakes its parent', async () => {
102
98
  const missingCwd = join(home, 'reaped-worktree');
103
99
  createNode(node('P', { lifecycle: 'resident' }));
@@ -51,7 +51,7 @@ function meta(over = {}) {
51
51
  ...over,
52
52
  };
53
53
  }
54
- // --- engine liveness branch -------------------------------------------------
54
+ // engine liveness branch
55
55
  test('a node whose engine is RUNNING (live pid) is NOT disconnected — never double-spawn', () => {
56
56
  assert.equal(isDisconnected(meta({ pi_pid: 1234 }), ALIVE), false);
57
57
  });
@@ -61,14 +61,14 @@ test('a node whose engine is DEAD but has a saved session IS disconnected', () =
61
61
  test('a node with a null pid (never/ no longer recorded) and a session IS disconnected', () => {
62
62
  assert.equal(isDisconnected(meta({ pi_pid: null }), DEAD), true);
63
63
  });
64
- // --- resumable-session requirement -----------------------------------------
64
+ // resumable-session requirement
65
65
  test('a dead node with NO resumable saved session is NOT disconnected', () => {
66
66
  assert.equal(isDisconnected(meta({ pi_session_id: null, pi_session_file: null }), DEAD), false);
67
67
  });
68
68
  test('a session FILE alone (older node, no id) is enough to be disconnected', () => {
69
69
  assert.equal(isDisconnected(meta({ pi_session_id: null }), DEAD), true);
70
70
  });
71
- // --- terminal-by-choice scope ----------------------------------------------
71
+ // terminal-by-choice scope
72
72
  for (const status of TERMINAL_BY_CHOICE) {
73
73
  test(`a dead-engine '${status}' node is EXCLUDED (terminal-by-choice, no opt-in)`, () => {
74
74
  assert.equal(isDisconnected(meta({ status: status }), DEAD), false);
@@ -89,11 +89,11 @@ test('a node with final_report set is EXCLUDED even when status is dead (finishe
89
89
  test("dormant 'idle' nodes with a saved session are swept by default", () => {
90
90
  assert.equal(isDisconnected(meta({ status: 'idle', intent: 'idle-release' }), DEAD), true);
91
91
  });
92
- // --- human-bridge carve-out -------------------------------------------------
92
+ // human-bridge carve-out
93
93
  test("kind:'human' rows (the human bridge, never a pi engine) are never disconnected", () => {
94
94
  assert.equal(isDisconnected(meta({ kind: 'human', pi_pid: null }), DEAD), false);
95
95
  });
96
- // --- listDisconnected over a live canvas (the sweep's selection) -------------
96
+ // listDisconnected over a live canvas (the sweep's selection)
97
97
  //
98
98
  // Locks that the canvas-wide select returns EXACTLY the disconnected set against
99
99
  // the REAL isPidAlive probe — a live-engine node (this process's pid) is kept
@@ -1,11 +1,17 @@
1
1
  // Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/revive-parked-fresh.test.ts
2
2
  //
3
3
  // Re-entering a parked node starts a FRESH cycle grounded in goal + roadmap
4
- // rather than replaying the transcript its parking turn already concluded. The
5
- // rule is a property of the node, evaluated once inside reviveNode, so every
6
- // doorway gets it from one strict-resume veto. Its one exception is a park with
7
- // nothing on disk to stand on (reachable only on the degraded path, which
8
- // writes no summary): resuming an old transcript beats waking amnesiac.
4
+ // rather than replaying the transcript its parking turn already concluded —
5
+ // but only when the revival CARRIES NEW INPUT. Wake-worthy mail that the node
6
+ // has not seen is that input: every doorway that wakes a parked node because
7
+ // something arrived appends its durable inbox entry before calling in. An
8
+ // input-less revival (a bare lifecycle revive, a viewer attach, a focus) has
9
+ // nothing to answer, so it quietly resumes the old transcript and injects no
10
+ // kickoff. The rule is a property of the node, evaluated once inside
11
+ // reviveNode, so every doorway gets it from one strict-resume veto. Its other
12
+ // exception is a park with nothing on disk to stand on (reachable only on the
13
+ // degraded path, which writes no summary): resuming an old transcript beats
14
+ // waking amnesiac.
9
15
  import { after, afterEach, before, beforeEach, test } from 'node:test';
10
16
  import assert from 'node:assert/strict';
11
17
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
@@ -14,6 +20,7 @@ import { join } from 'node:path';
14
20
  import { createNode, getNode } from '../canvas/canvas.js';
15
21
  import { contextDir } from '../canvas/paths.js';
16
22
  import { closeDb } from '../canvas/db.js';
23
+ import { appendInbox, readCursor, writeCursor } from '../feed/inbox.js';
17
24
  import { transition } from '../runtime/lifecycle.js';
18
25
  import { reviveNode } from '../runtime/revive.js';
19
26
  import { headlessBrokerHost } from '../runtime/host.js';
@@ -49,6 +56,21 @@ function parkResident(id, ground = {}) {
49
56
  writeFileSync(join(contextDir(id), 'initial-prompt.md'), ground.goal);
50
57
  transition(id, 'park');
51
58
  }
59
+ /** Durable wake-worthy mail, appended exactly as every message doorway appends
60
+ * it before calling reviveNode. */
61
+ function deliverMail(id, tier = 'normal') {
62
+ return appendInbox(id, { from: null, tier, kind: 'message', label: 'work', data: { body: 'work' } }).entry_id;
63
+ }
64
+ /** Did this launch carry the fresh-revive kickoff? `buildReviveKickoff` always
65
+ * opens its card body with this sentence (src/core/runtime/kickoff.ts:325). */
66
+ function kickedOff(entry) {
67
+ return entry.argv.some((arg) => arg.includes('You have been revived fresh'));
68
+ }
69
+ /** The two facts every row below asserts together: a fresh row is
70
+ * `resuming:false` + kickoff, a quiet row is `resuming:true` + none. */
71
+ function placements() {
72
+ return launches.map(({ nodeId, resuming }) => ({ nodeId, resuming }));
73
+ }
52
74
  before(() => {
53
75
  home = mkdtempSync(join(tmpdir(), 'crtr-revive-parked-'));
54
76
  priorHome = process.env['CRTR_HOME'];
@@ -62,8 +84,8 @@ beforeEach(() => {
62
84
  fleet = bindTestFleet();
63
85
  launches = [];
64
86
  originalLaunch = headlessBrokerHost.launch;
65
- headlessBrokerHost.launch = ((nodeId, _inv, opts) => {
66
- launches.push({ nodeId, resuming: opts.resuming });
87
+ headlessBrokerHost.launch = ((nodeId, inv, opts) => {
88
+ launches.push({ nodeId, resuming: opts.resuming, argv: inv.argv });
67
89
  const handle = { pid: 0x7ffffffe, exited: new Promise(() => { }) };
68
90
  fleet.fleet.register(nodeId, handle);
69
91
  return handle;
@@ -81,25 +103,54 @@ after(() => {
81
103
  else
82
104
  process.env['CRTR_HOME'] = priorHome;
83
105
  });
84
- test('a parked node with a roadmap re-enters fresh even when the caller asked to resume, and the marker clears', () => {
106
+ test('a parked node with mail waiting re-enters fresh even when the caller asked to resume, and the marker clears', () => {
85
107
  parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
108
+ deliverMail('parked');
86
109
  const result = reviveNode('parked', { resume: true });
87
110
  assert.equal(result.resumed, false, 'the concluded transcript is not replayed');
88
- assert.deepEqual(launches, [{ nodeId: 'parked', resuming: false }]);
111
+ assert.deepEqual(placements(), [{ nodeId: 'parked', resuming: false }]);
112
+ assert.equal(kickedOff(launches[0]), true, 'the fresh window is handed its bearings');
89
113
  const reopened = getNode('parked');
90
114
  assert.equal(reopened.status, 'active');
91
115
  assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
92
116
  assert.equal(reopened.cycle_pending, true, 'the fresh cycle is provisional until session_start confirms it');
93
117
  });
94
- test('a parked node grounded only by a goal still re-enters fresh', () => {
118
+ test('a parked node grounded only by a goal still re-enters fresh when mail rides the revive', () => {
95
119
  parkResident('goal-only', { goal: 'Watch the deploy lane.' });
120
+ deliverMail('goal-only');
96
121
  assert.equal(reviveNode('goal-only', { resume: true }).resumed, false);
122
+ assert.equal(kickedOff(launches[0]), true);
123
+ });
124
+ test('an input-less revival of a parked node quietly resumes and injects no kickoff', () => {
125
+ parkResident('quiet', { roadmap: '# Roadmap\n\n- one open item\n' });
126
+ const result = reviveNode('quiet', { resume: true });
127
+ assert.equal(result.resumed, true, 'nothing arrived, so there is nothing a fresh window would answer');
128
+ assert.deepEqual(placements(), [{ nodeId: 'quiet', resuming: true }]);
129
+ assert.equal(kickedOff(launches[0]), false, 'a quiet resume generates nothing');
130
+ assert.equal(readCursor('quiet'), undefined, 'drainBearings did not run, so nothing was consumed');
131
+ const reopened = getNode('quiet');
132
+ assert.equal(reopened.status, 'active');
133
+ assert.equal(reopened.intent ?? null, null, 'the park marker clears on a quiet resume too');
134
+ });
135
+ test('deferred mail alone is not input, so the revival still resumes quietly', () => {
136
+ parkResident('deferred', { roadmap: '# Roadmap\n\n- one open item\n' });
137
+ deliverMail('deferred', 'deferred');
138
+ assert.equal(reviveNode('deferred', { resume: true }).resumed, true);
139
+ assert.equal(kickedOff(launches[0]), false);
140
+ assert.equal(readCursor('deferred'), undefined, 'the deferred entry rides the next natural cycle, unconsumed');
141
+ });
142
+ test('mail already covered by the cursor is not input, so the revival still resumes quietly', () => {
143
+ parkResident('stale', { roadmap: '# Roadmap\n\n- one open item\n' });
144
+ writeCursor('stale', deliverMail('stale'));
145
+ assert.equal(reviveNode('stale', { resume: true }).resumed, true, 'only the UNSEEN suffix counts as input');
146
+ assert.equal(kickedOff(launches[0]), false);
97
147
  });
98
148
  test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', () => {
99
149
  parkResident('bare');
150
+ deliverMail('bare');
100
151
  const result = reviveNode('bare', { resume: true });
101
152
  assert.equal(result.resumed, true, 'the degraded park has nothing on disk to ground a fresh window');
102
- assert.deepEqual(launches, [{ nodeId: 'bare', resuming: true }]);
153
+ assert.deepEqual(placements(), [{ nodeId: 'bare', resuming: true }]);
103
154
  });
104
155
  test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', () => {
105
156
  parkResident('finished', { roadmap: '# Roadmap\n\n- one open item\n' });
@@ -79,7 +79,6 @@ const barrier = async (c, label) => {
79
79
  const isRequest = (f) => f.type === 'extension_ui_request';
80
80
  const dismissFor = (reqId) => (f) => f.type === 'extension_ui_dismiss' && f.id === reqId;
81
81
  const welcomes = (frames) => frames.filter((f) => f.type === 'welcome');
82
- // ---------------------------------------------------------------------------
83
82
  // G4 — multi-writer admission + read-only gating + broadcast fan-out. Guards: two
84
83
  // simultaneous `controller` hellos are BOTH welcomed writable, each one's own
85
84
  // submission reaches the engine, every connected client sees BOTH turns, an
@@ -90,7 +89,6 @@ const welcomes = (frames) => frames.filter((f) => f.type === 'welcome');
90
89
  // Failure mode: a singleton writer slot demoting the second client, a writer
91
90
  // whose prompt is silently dropped, fan-out that misses a viewer, a read-only
92
91
  // client that can drive the engine, or a model switch only its requester hears.
93
- // ---------------------------------------------------------------------------
94
92
  test('G4 — two simultaneous clients are both welcomed controller, each submits, all clients see both turns and the model switch; an observer drive → error{read_only}', async () => {
95
93
  const [w1, w2] = await Promise.all([
96
94
  attach(id, 'controller', 'g4-writer-1'),
@@ -132,7 +130,6 @@ test('G4 — two simultaneous clients are both welcomed controller, each submits
132
130
  const om = await obs.waitFrame((f) => f.type === 'model_changed', 'G4 observer receives model_changed');
133
131
  assert.ok(om.type === 'model_changed' && om.model?.id === 'fake-fable', 'G4: a read-only observer ALSO hears the switch');
134
132
  });
135
- // ---------------------------------------------------------------------------
136
133
  // G5 — dialog fan-out, mid-dialog handoff, and first-response settlement.
137
134
  // Guards: ONE blocking dialog reaches EVERY writable client (never a single
138
135
  // owner), a writable client attaching MID-dialog is handed it via
@@ -143,7 +140,6 @@ test('G4 — two simultaneous clients are both welcomed controller, each submits
143
140
  // Failure modes: a dialog only one viewer can see (the others sit blind), a
144
141
  // late-joining writable viewer blind to an in-flight dialog, peers left holding
145
142
  // an overlay nothing can answer, or a late peer answer re-settling a dead dialog.
146
- // ---------------------------------------------------------------------------
147
143
  test('G5 — one dialog fans out to every writable client and to a mid-dialog joiner, the first response settles it, all are dismissed, and a late peer response is a no-op', async () => {
148
144
  const base = h.dialogResults(id).length;
149
145
  const a = await writer('g5-A');
@@ -184,7 +180,6 @@ test('G5 — one dialog fans out to every writable client and to a mid-dialog jo
184
180
  assert.equal(settled[base].resolved, true, "G5: the late peer response did not overwrite the first answerer's value");
185
181
  assert.equal(isPidAlive(brokerPid()), true, 'G5: the late no-op response did not fault the broker');
186
182
  });
187
- // ---------------------------------------------------------------------------
188
183
  // G9 — re-welcome after an IN-PLACE session mutation (real bug, observed
189
184
  // 2026-06-11 in `crtr surface attach`: double-Esc/`/tree` opened the session-tree
190
185
  // picker, but SELECTING an entry visibly did nothing). session.navigateTree()
@@ -194,7 +189,6 @@ test('G5 — one dialog fans out to every writable client and to a mid-dialog jo
194
189
  // attached client rendering the abandoned branch and dropping the navigated-to
195
190
  // user message's editorText. Failure mode: no second welcome arrives at all, or
196
191
  // the re-welcome serves a stale snapshot.
197
- // ---------------------------------------------------------------------------
198
192
  test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript and acks the editor text', async () => {
199
193
  const c = await writer('nav-ctrl');
200
194
  // Accrue one observable turn so the rewind has something to drop.
@@ -222,7 +216,6 @@ test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript
222
216
  assert.ok(!frameHas(second, token), 'G9: re-welcome serves the rewound transcript (turn dropped)');
223
217
  assert.equal(second.snapshot.messages.length, first.snapshot.messages.length - 2, 'G9: re-welcome history is exactly one turn shorter (a turn = user + assistant = 2 tree messages)');
224
218
  });
225
- // ---------------------------------------------------------------------------
226
219
  // G10 — catch-up is ONE atomic frame. `ctx.ui.setStatus/setWidget/setTitle` are
227
220
  // LEVELS with no pi getter, so the broker is their only holder; it ships that
228
221
  // held state as `welcome.snapshot.display` and sends NO display edge after the
@@ -232,7 +225,6 @@ test('G9 — navigate_tree re-welcomes every viewer onto the rewound transcript
232
225
  // re-emitted N synthetic display frames after the snapshot: catch-up was
233
226
  // non-atomic and the viewer had to clear-and-rebuild, painting blank chrome in
234
227
  // the gap. Both halves of that cut fail this test.
235
- // ---------------------------------------------------------------------------
236
228
  test('G10 — retained extension display state rides IN welcome.snapshot.display, and no display edge follows the welcome', async () => {
237
229
  const DISPLAY_METHODS = new Set(['setStatus', 'setWidget', 'setTitle']);
238
230
  const isDisplayFrame = (f) => f.type === 'extension_ui_request' && f.method !== undefined && DISPLAY_METHODS.has(f.method);
@@ -61,11 +61,9 @@ afterEach(() => {
61
61
  kit.closeAll();
62
62
  });
63
63
  const brokerPid = () => h.node(id).pi_pid;
64
- // ---------------------------------------------------------------------------
65
64
  // G1 — controller drive + live relay. Guards: a controller's `prompt` runs the
66
65
  // engine AND the streaming AgentSessionEvents are fanned out to the client
67
66
  // VERBATIM. Failure mode: a broken relay/fan-out (viewer sees nothing).
68
- // ---------------------------------------------------------------------------
69
67
  test('G1 — controller prompt runs the engine and the streamed AgentSessionEvents relay to the client', async () => {
70
68
  const c = await ctrl('g1-ctrl');
71
69
  assert.equal(c.welcome.role, 'controller', 'first controller is admitted as controller');
@@ -80,7 +78,6 @@ test('G1 — controller prompt runs the engine and the streamed AgentSessionEven
80
78
  }
81
79
  assert.ok(c.frames.some((f) => f.type === 'message_start' && frameHas(f, token)), 'G1: a relayed message carries the prompt token');
82
80
  });
83
- // ---------------------------------------------------------------------------
84
81
  // G1b — message_update coalescing preserves ordering (regression for the F2
85
82
  // attach typing-lag fix, 2026-06-09). The broker holds the latest message_update
86
83
  // on a ~75ms timer; any OTHER event must flush it FIRST, so a viewer can never
@@ -88,7 +85,6 @@ test('G1 — controller prompt runs the engine and the streamed AgentSessionEven
88
85
  // streaming text over the final message), and the LAST coalesced update must not
89
86
  // be silently dropped at end-of-turn. Failure modes: a flush ordered after the
90
87
  // non-update event, or a pending update discarded when message_end wins the race.
91
- // ---------------------------------------------------------------------------
92
88
  test('G1b — coalesced message_update never arrives after message_end; updates still relayed', async () => {
93
89
  const c = await ctrl('g1b-ctrl');
94
90
  const token = tok('G1B');
@@ -114,10 +110,8 @@ test('G1b — coalesced message_update never arrives after message_end; updates
114
110
  assert.ok(i < 0 || lastUpdate < i, `G1b: message_update relayed after ${t}`);
115
111
  }
116
112
  });
117
- // ---------------------------------------------------------------------------
118
113
  // G2 — detach leaves the engine running. Guards: `bye`/close drops ONE listener,
119
114
  // never the engine. Failure mode: a detach that disposes the broker/engine.
120
- // ---------------------------------------------------------------------------
121
115
  test('G2 — detach (bye+close) leaves the broker alive + the engine still emitting; engine pid unchanged', async () => {
122
116
  const pid = brokerPid();
123
117
  const boots = h.bootCount(id);
@@ -139,7 +133,6 @@ test('G2 — detach (bye+close) leaves the broker alive + the engine still emitt
139
133
  assert.ok(o.welcome.snapshot.messages.length > 0, 'G2: the engine produced a message after the detach');
140
134
  assert.equal(isPidAlive(pid), true, 'G2: broker still alive after the post-detach turn');
141
135
  });
142
- // ---------------------------------------------------------------------------
143
136
  // G3 — the interactive `deliver` ack reports the ENGINE CALL's real outcome, not
144
137
  // the fact that a call was routed. A `session.prompt()` that REJECTS outright —
145
138
  // the shape a broker whose model resolved but whose provider has no installed
@@ -150,7 +143,6 @@ test('G2 — detach (bye+close) leaves the broker alive + the engine still emitt
150
143
  // engine's own agent_start) against rejection and records
151
144
  // `broker.prompt.deliver_failed` in the node's job/log.jsonl. Shares this file's
152
145
  // one broker: the contract is two frames on an attached controller.
153
- // ---------------------------------------------------------------------------
154
146
  test('G3 — a deliver whose prompt rejects acks ok:false and records deliver_failed; a succeeding deliver acks ok:true via prompt', async () => {
155
147
  // Both delivers must route as `prompt` (idle), never `steer` — the steer route
156
148
  // legitimately acks at routing time — so attach only once the engine is idle.
@@ -43,11 +43,9 @@ after(async () => {
43
43
  else
44
44
  process.env['PI_FAKE_ENGINE_HOLD_TURN'] = priorHoldTurn;
45
45
  });
46
- // ===========================================================================
47
46
  // Items 4 + 5 + 6 — two independent real-process journeys:
48
47
  // graceful busy teardown → strict resume + continuation;
49
48
  // dirty interrupted-turn crash → recovery cycle → close.
50
- // ===========================================================================
51
49
  test('SIGTERM of a busy broker aborts cleanly and resumes with a continuation', { timeout: 30_000 }, async () => {
52
50
  const id = await h.spawnHeadlessChild(root, 'headless worker — graceful crash recovery');
53
51
  const boot1 = await h.awaitBoot(id);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ // tmux-driver.ts owns the raw tmux process boundary — every direct
2
+ // `spawnSync('tmux', ...)` elsewhere is a duplicated probe that should call
3
+ // through tmux-driver.ts's exported primitives instead. This grep-enforces
4
+ // that boundary with an explicit allowlist: the owner itself and three
5
+ // pre-existing direct spawners with a genuinely distinct semantic (documented
6
+ // per-entry below).
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
10
+ import { join, resolve, relative } from 'node:path';
11
+ const SRC_ROOT = resolve(process.cwd(), 'src');
12
+ // Relative to src/, forward-slash form.
13
+ const ALLOWLIST = new Set([
14
+ 'core/runtime/tmux-driver.ts', // the owner
15
+ 'clients/attach/session/mouse.ts', // mouse-mode detection — a distinct probe, not reachability
16
+ 'core/clipboard-text.ts', // OSC-52 paste-buffer write — not a reachability probe
17
+ 'core/termrender/display.ts', // pane-count probe — not a reachability probe
18
+ ]);
19
+ function tsFilesUnder(dir) {
20
+ const out = [];
21
+ for (const entry of readdirSync(dir)) {
22
+ const path = join(dir, entry);
23
+ const stat = statSync(path);
24
+ if (stat.isDirectory()) {
25
+ if (entry === '__tests__')
26
+ continue;
27
+ out.push(...tsFilesUnder(path));
28
+ }
29
+ else if (entry.endsWith('.ts') && !entry.endsWith('.test.ts')) {
30
+ out.push(path);
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ test('every direct spawnSync("tmux", …) call site is inside the tmux-driver.ts allowlist', () => {
36
+ const offenders = [];
37
+ for (const file of tsFilesUnder(SRC_ROOT)) {
38
+ const relPath = relative(SRC_ROOT, file).split('\\').join('/');
39
+ if (ALLOWLIST.has(relPath))
40
+ continue;
41
+ const source = readFileSync(file, 'utf8');
42
+ const lines = source.split('\n');
43
+ lines.forEach((line, index) => {
44
+ if (/spawnSync\(\s*['"]tmux['"]/.test(line)) {
45
+ offenders.push(`${relPath}:${index + 1}`);
46
+ }
47
+ });
48
+ }
49
+ assert.deepEqual(offenders, [], `direct spawnSync('tmux', …) call(s) outside the tmux-driver.ts allowlist:\n${offenders.join('\n')}`);
50
+ });
@@ -0,0 +1,7 @@
1
+ /** One durable-mutation signal file under `crtrHome()/signals/<name>`, shared by
2
+ * every subsystem that needs to tell every open viewer "something changed"
3
+ * without watching each individual record's directory. */
4
+ export declare function activityPath(name: string): string;
5
+ /** Signal one durable mutation to every open watcher of `name`. */
6
+ export declare function signalActivity(name: string): void;
7
+ export declare function watchActivity(name: string, onChange: () => void): () => void;
@@ -0,0 +1,38 @@
1
+ import { basename, dirname, join } from 'node:path';
2
+ import { mkdirSync, watch, writeFileSync } from 'node:fs';
3
+ import { crtrHome } from './canvas/paths.js';
4
+ /** One durable-mutation signal file under `crtrHome()/signals/<name>`, shared by
5
+ * every subsystem that needs to tell every open viewer "something changed"
6
+ * without watching each individual record's directory. */
7
+ export function activityPath(name) {
8
+ return join(crtrHome(), 'signals', name);
9
+ }
10
+ /** Signal one durable mutation to every open watcher of `name`. */
11
+ export function signalActivity(name) {
12
+ const path = activityPath(name);
13
+ try {
14
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
15
+ writeFileSync(path, `${Date.now()}\n`, { mode: 0o600 });
16
+ }
17
+ catch {
18
+ // A later read of the underlying record is still authoritative.
19
+ }
20
+ }
21
+ export function watchActivity(name, onChange) {
22
+ const path = activityPath(name);
23
+ try {
24
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
25
+ const watcher = watch(dirname(path), (_event, filename) => {
26
+ if (String(filename) === basename(path))
27
+ onChange();
28
+ });
29
+ return () => watcher.close();
30
+ }
31
+ catch (error) {
32
+ // The caller keeps a working no-op unwatch — a watch failure must not crash
33
+ // whatever set it up to be best-effort — but it stays surfaced once, since a
34
+ // silently-never-refreshing viewer is a real symptom, not a background detail.
35
+ console.error(`activity-signal: failed to watch "${name}": ${error instanceof Error ? error.message : String(error)}`);
36
+ return () => { };
37
+ }
38
+ }
@@ -3,6 +3,7 @@ import { readConfig, readState, updateState } from './config.js';
3
3
  import { nowIso } from './fs-utils.js';
4
4
  import { info } from './output.js';
5
5
  import { SELF_UPDATE_SCRIPT, selfCheck, contentCheck } from './self-update.js';
6
+ import { envDebug } from '../shared/env.js';
6
7
  const HOUR_MS = 60 * 60 * 1000;
7
8
  const SKIP_SUBCOMMANDS = new Set([
8
9
  'update',
@@ -85,7 +86,7 @@ export function maybeAutoUpdate(argv) {
85
86
  }
86
87
  }
87
88
  catch (e) {
88
- if (process.env.CRTR_DEBUG === '1') {
89
+ if (envDebug()) {
89
90
  const msg = e instanceof Error ? e.message : String(e);
90
91
  process.stderr.write(`crtr: auto-update hook error: ${msg}\n`);
91
92
  }
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { findProjectScopeRoot, resetScopeCache, userScopeRoot } from './scope.js';
4
4
  import { ensureScopeInitialized } from './config.js';
5
5
  import { CRTR_DIR_NAME } from '../types.js';
6
+ import { envDebug } from '../shared/env.js';
6
7
  const SKIP_SUBCOMMANDS = new Set([
7
8
  'help',
8
9
  '--help',
@@ -42,7 +43,7 @@ export function ensureProjectScope(argv) {
42
43
  resetScopeCache();
43
44
  }
44
45
  catch (e) {
45
- if (process.env.CRTR_DEBUG === '1') {
46
+ if (envDebug()) {
46
47
  const msg = e instanceof Error ? e.message : String(e);
47
48
  process.stderr.write(`crtr: project-init error: ${msg}\n`);
48
49
  }
@@ -10,7 +10,7 @@ import { test } from 'node:test';
10
10
  import assert from 'node:assert/strict';
11
11
  import { createServer } from 'node:http';
12
12
  import { RemoteCanvasSource } from '../remote-canvas-source.js';
13
- import { dashboardRowsAllFromSource, renderForestFromSource } from '../render.js';
13
+ import { dashboardRowsAllFromSource, renderForestFromSource } from '../render-source.js';
14
14
  /** Stand up a bare `node:http` server on an ephemeral loopback port — the
15
15
  * `/__crtr/source` endpoint a `RemoteCanvasSource` POSTs to. `route` decides
16
16
  * the response per captured argv; every request is recorded first so a test
@@ -62,7 +62,7 @@ test('a normal fast response is unaffected by the timeout wiring', async (t) =>
62
62
  assert.equal(result.ok, true);
63
63
  assert.equal(result.stdout, JSON.stringify({ nodes: [] }));
64
64
  });
65
- // --- Phase 3 review Minor 1: no token bytes may ever reach returned stderr ---
65
+ // Phase 3 review Minor 1: no token bytes may ever reach returned stderr
66
66
  test('an unreachable endpoint returns a flat generic network-error message, never the raw exception text', async () => {
67
67
  const exec = createRemoteExec('http://127.0.0.1:1', 'valid-token-123');
68
68
  const result = await exec('crtr', ['--json', 'node', 'inspect', 'list']);
@@ -21,7 +21,7 @@ import { tmpdir } from 'node:os';
21
21
  import { join } from 'node:path';
22
22
  import { atomicWriteJson, pageManifestPath } from '#core/human/convention';
23
23
  import { RemoteCanvasSource } from '../remote-canvas-source.js';
24
- import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render.js';
24
+ import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
25
25
  import { createNode } from '../canvas.js';
26
26
  import { closeDb } from '../db.js';
27
27
  import { openFocusRow } from '../focuses.js';
@@ -7,6 +7,7 @@
7
7
  // Resume is one action: Enter routes the chosen node through `crtr surface node focus`,
8
8
  // which goes via reviveNode() — the ONLY sanctioned open. NEVER spawn
9
9
  // `pi --session` directly (see reviveNode() in revive.ts for the desync hazard).
10
+ import { envNodeId } from '../../../shared/env.js';
10
11
  import { execFileSync } from 'node:child_process';
11
12
  import { resolve } from 'node:path';
12
13
  import { dashboardRowsAllFromSource, enrichRowsFromSource, loadPreviewFromSource, renderForestFromSource } from '../render-source.js';
@@ -452,7 +453,7 @@ export async function runBrowse(opts) {
452
453
  // ancestors so it is actually visible. No-op outside a node or when it's filtered
453
454
  // out of the current tab/scope. Same move the boot performs, on demand (`.`).
454
455
  const recenterOnSelf = () => {
455
- const selfId = process.env['CRTR_NODE_ID'];
456
+ const selfId = envNodeId();
456
457
  if (selfId === undefined || selfId === '' || !tree.nodes.has(selfId))
457
458
  return;
458
459
  for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
@@ -804,7 +805,7 @@ export async function runBrowse(opts) {
804
805
  // not the top of the tree. Expand its ancestors so the row is actually visible,
805
806
  // then home the cursor onto it. No-op when launched outside a node, or when the
806
807
  // node is filtered out of the current tab/scope.
807
- const selfId = process.env['CRTR_NODE_ID'];
808
+ const selfId = envNodeId();
808
809
  if (selfId !== undefined && selfId !== '' && tree.nodes.has(selfId)) {
809
810
  for (let p = tree.nodes.get(selfId)?.parentId ?? null; p !== null; p = tree.nodes.get(p)?.parentId ?? null) {
810
811
  state.collapsed.delete(p);
@@ -1,4 +1,4 @@
1
- import type { DashboardRow } from '../render.js';
1
+ import type { DashboardRow } from '../render-source.js';
2
2
  export type Tab = 'All' | 'Live' | 'Dormant' | 'Attention' | 'Pinned';
3
3
  export declare const TABS: readonly Tab[];
4
4
  /** How the visible rows are ordered.
@@ -119,9 +119,7 @@ export function pruneNode(tree, id) {
119
119
  }
120
120
  tree.nodes.delete(id);
121
121
  }
122
- // ---------------------------------------------------------------------------
123
122
  // Fuzzy match
124
- // ---------------------------------------------------------------------------
125
123
  /** Case-insensitive subsequence match: every char of `query` appears in `text`
126
124
  * in order (gaps allowed). Empty query matches everything. Substrings are a
127
125
  * subsequence, so this subsumes substring matching too. */
@@ -322,9 +320,7 @@ export function scoreRow(query, row) {
322
320
  const strength = matches.reduce((sum, { match, weight }) => sum + (match.tier === tier ? match.quality * weight : 0), 0);
323
321
  return { tier, strength };
324
322
  }
325
- // ---------------------------------------------------------------------------
326
323
  // Attention tiering (default sort)
327
- // ---------------------------------------------------------------------------
328
324
  /** The "most recent message" signal for the attention sort: the pi session-file
329
325
  * mtime (ms) the snapshot stamps on each row, falling back to the `created`
330
326
  * birth timestamp when no session file exists. Larger = more recent.