@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
@@ -17,6 +17,7 @@
17
17
  // so node target metadata comes from each node's durable meta.json — the source
18
18
  // of truth the database index is rebuilt from — through its existing read-only
19
19
  // helper. Missing or corrupt metadata remains a global blocker.
20
+ import { envProfileId } from '../shared/env.js';
20
21
  import { readdirSync } from 'node:fs';
21
22
  import { dirname, join } from 'node:path';
22
23
  import { readMetaObject } from '../core/canvas/meta-file.js';
@@ -79,7 +80,7 @@ function nodeStoresWithDocs() {
79
80
  * so discovery must run after that lane, not against `listProfiles()`. */
80
81
  export async function discoverCorpus(opts) {
81
82
  const cwd = realpathOrSelf(opts.cwd ?? process.cwd());
82
- const selectedProfileId = process.env['CRTR_PROFILE_ID'] || null;
83
+ const selectedProfileId = envProfileId() || null;
83
84
  const explicitDirs = opts.explicitDirs ?? [];
84
85
  const roots = [];
85
86
  const rootSeen = new Set();
@@ -336,9 +337,7 @@ export async function discoverCorpus(opts) {
336
337
  nodeMetadata,
337
338
  };
338
339
  }
339
- // ---------------------------------------------------------------------------
340
340
  // Snapshots
341
- // ---------------------------------------------------------------------------
342
341
  /** Read every discovered store's documents. `preloaded` supplies the per-store
343
342
  * convergent lane's FOLDED snapshots so the corpus plans against the bytes a
344
343
  * write run leaves behind — identical under `--dry-run`, where the same fold
@@ -387,9 +386,7 @@ export function buildCorpusSnapshot(discovery, snapshots) {
387
386
  }));
388
387
  return makeCorpusSnapshot(stores, discovery.targets, discovery.roots);
389
388
  }
390
- // ---------------------------------------------------------------------------
391
389
  // The runner
392
- // ---------------------------------------------------------------------------
393
390
  function cloneCorpus(corpus) {
394
391
  const stores = corpus.stores.map((store) => ({
395
392
  ...store,
@@ -1,5 +1,18 @@
1
- // The ordered state-migration registry — ONE list for both lanes, so a cut
2
- // that spans canvas-db and document stores ships as adjacent entries.
1
+ // The ordered state-migration registry — the SECOND of this repo's three
2
+ // migration homes, and the one for on-disk memory documents, profile
3
+ // manifests, and the corpus lane, driven by `crtr sys migrate`. ONE list for
4
+ // both non-journaled lanes here, so a cut that spans canvas-db and document
5
+ // stores ships as adjacent entries (the journaled lane's entries splice into
6
+ // `../core/canvas/migrations.ts`'s `MIGRATIONS` chain at its tail).
7
+ //
8
+ // The other two homes:
9
+ // - `../core/canvas/migrations.ts` — canvas.db's own versioned schema chain,
10
+ // gated by `PRAGMA user_version`. Steps there may freely read node meta,
11
+ // query other tables mid-step, and build shell commands from canvas data
12
+ // — exactly what this registry's import contract (below) forbids, which
13
+ // is why that chain is not expressed as registrants here.
14
+ // - `../daemon/boot-migrations.ts` — filesystem repairs that run
15
+ // unconditionally at every daemon boot, outside any version gate.
3
16
  //
4
17
  // Authoring contract (each migration is one file, `NNN-<desc>.ts`, or one
5
18
  // directory, `NNN-<desc>/index.ts`, appended here in order):
@@ -221,7 +221,6 @@ test('queued continuation finishes before natural-stop routing commits', { timeo
221
221
  assert.equal(shutdown, true, 'settlement routes from the completed continuation');
222
222
  assert.equal(getNode('leaf')?.intent ?? null, null, 'the intermediate natural stop never idle-released the node');
223
223
  });
224
- // ---------------------------------------------------------------------------
225
224
  // §5.1 — the §1.7 settlement branch map on the focuses table. Every assertion is
226
225
  // on the canvas focuses/runtime rows after firing agent_settled (TMUX_PANE is
227
226
  // cleared in beforeEach). NOTE (broker-host cut): the placement read `focusOf`
@@ -232,7 +231,6 @@ test('queued continuation finishes before natural-stop routing commits', { timeo
232
231
  // but-not-yet-realized viewer, which liveOrPrune passes through). status='done'
233
232
  // is reached by setting the runtime row directly (the branch reads
234
233
  // getNode(nodeId).status).
235
- // ---------------------------------------------------------------------------
236
234
  test('§5.1.2 truly-done + focused → focus row CLOSED, own presence nulled (Q1)', async () => {
237
235
  // R carries a LOCATION so the close-path presence-null is observable.
238
236
  createNode(node('R', { parent: null, lifecycle: 'terminal', pane: '%r', tmux_session: 'Suser', window: '@wr' }));
@@ -323,10 +321,8 @@ test('refresh-yield (intent=refresh) writes NO push — silent to subscribers',
323
321
  assert.equal(readInboxSince('root').length, 0, 'a yield is silent: NO inbox pointer');
324
322
  assert.equal(pi.injected.length, 0, 'no reprompt on a refresh-yield');
325
323
  });
326
- // ---------------------------------------------------------------------------
327
324
  // turn_end — stale-⚠ fix: reaching turn_end proves the current provider
328
325
  // round-trip and any wedge episode are both over, so both fault links clear.
329
- // ---------------------------------------------------------------------------
330
326
  /** A turn_end event carrying one assistant text block. */
331
327
  function turnEndEvent(text) {
332
328
  return { message: { role: 'assistant', content: [{ type: 'text', text }] } };
@@ -0,0 +1,220 @@
1
+ // Regression: the `pre-command` gate holds a bash command BEFORE it runs and
2
+ // releases it once its memory is in the transcript. The release is not a stored
3
+ // decision — it is the exposure ledger, and that is exactly what makes it
4
+ // fragile:
5
+ //
6
+ // • Compaction drops the delivered guidance out of the transcript while the
7
+ // ledger still claims it was delivered. Without the demotion pass the doc is
8
+ // held once and then silently absent for the rest of the session. Worse, a
9
+ // demotion that left a rankless entry behind would crash the node's whole
10
+ // substrate on its next load — so the ledger is reloaded FROM DISK here.
11
+ // • pi preflights the siblings of one assistant message sequentially and then
12
+ // executes them concurrently, so without a per-turn flag the second sibling
13
+ // would find the doc already exposed and slip past the hold its sibling just
14
+ // raised.
15
+ // • A session whose corpus carries no `pre-command` doc at all is the common
16
+ // case and must never pay a daemon round-trip. Proven by closing the daemon:
17
+ // if the short-circuit stopped working the call would fail loud.
18
+ //
19
+ // The gate runs against the real canvas api server, so the subject the matcher
20
+ // gates on is the one crtrd actually serves.
21
+ //
22
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/pi-extensions/__tests__/pre-command-gate.test.ts
23
+ import { test, before, beforeEach, afterEach, after } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
26
+ import { tmpdir } from 'node:os';
27
+ import { join } from 'node:path';
28
+ import { registerCanvasDocSubstrate } from '../canvas-doc-substrate.js';
29
+ import { createNode } from '../../core/canvas/canvas.js';
30
+ import { closeDb } from '../../core/canvas/db.js';
31
+ import { apiSocketPath } from '../../core/canvas/paths.js';
32
+ import { resetScopeCache } from '../../core/scope.js';
33
+ import { loadContextExposureState } from '../../core/substrate/injected-store.js';
34
+ import { clearSessionCache } from '../../core/substrate/session-cache.js';
35
+ import { createApiServer } from '../../daemon/api/server.js';
36
+ const DOCTRINE_BODY = 'PRE-COMMAND-DOCTRINE-BODY';
37
+ let home;
38
+ let cwd;
39
+ let server = null;
40
+ let origCwd;
41
+ let origHome;
42
+ let origNode;
43
+ let origProfile;
44
+ async function waitForSocket(path) {
45
+ for (let i = 0; i < 200; i++) {
46
+ if (existsSync(path))
47
+ return;
48
+ await new Promise((resolve) => setTimeout(resolve, 10));
49
+ }
50
+ throw new Error(`api socket never bound at ${path}`);
51
+ }
52
+ async function closeServer() {
53
+ const running = server;
54
+ server = null;
55
+ if (running !== null)
56
+ await running.close();
57
+ }
58
+ /** A user-scope doc that holds every `deploy …` invocation until it is read. */
59
+ function writeDoctrineDoc(event) {
60
+ const store = join(home, 'user', '.crouter', 'memory');
61
+ mkdirSync(store, { recursive: true });
62
+ const path = join(store, 'deploy-doctrine.md');
63
+ writeFileSync(path, '---\nkind: knowledge\n' +
64
+ 'when-and-why-to-read: When deploying, this knowledge should be read because it carries the approval boundary.\n' +
65
+ `surfaces:\n - {on: ${event}, at: content, match: "deploy*"}\n---\n` +
66
+ `${DOCTRINE_BODY}\n`);
67
+ clearSessionCache();
68
+ return realpathSync(path);
69
+ }
70
+ /** A canvas node the api server can assemble a subject for, selected as the
71
+ * session's node. Each test uses its own id: the extension's exposure state is
72
+ * a per-node process singleton. */
73
+ function seedNode(id) {
74
+ createNode({
75
+ node_id: id,
76
+ name: id,
77
+ created: new Date().toISOString(),
78
+ cwd,
79
+ kind: 'general',
80
+ mode: 'base',
81
+ lifecycle: 'terminal',
82
+ status: 'active',
83
+ });
84
+ mkdirSync(join(home, 'nodes', id), { recursive: true });
85
+ process.env['CRTR_NODE_ID'] = id;
86
+ }
87
+ /** The registered extension, driven through the pi events it actually binds. */
88
+ function registerSubstrate() {
89
+ const handlers = new Map();
90
+ registerCanvasDocSubstrate({
91
+ on(event, handler) {
92
+ const bound = handlers.get(event) ?? [];
93
+ bound.push(handler);
94
+ handlers.set(event, bound);
95
+ },
96
+ });
97
+ async function fire(event, payload) {
98
+ const bound = handlers.get(event) ?? [];
99
+ assert.equal(bound.length, 1, `the substrate binds exactly one ${event} handler`);
100
+ return await bound[0](payload, {});
101
+ }
102
+ return {
103
+ toolCall: (toolName, input) => fire('tool_call', { toolName, input }),
104
+ bash: (command) => fire('tool_call', { toolName: 'bash', input: { command } }),
105
+ endTurn: async () => void (await fire('turn_end', {})),
106
+ compact: async () => void (await fire('session_compact', {})),
107
+ };
108
+ }
109
+ function assertHeld(result, what) {
110
+ assert.ok(result !== undefined, `${what} is held`);
111
+ assert.equal(result.block, true);
112
+ assert.ok(result.reason.startsWith('HELD — nothing ran.'), `the held result opens with the notice; got: ${result.reason}`);
113
+ return result;
114
+ }
115
+ before(() => {
116
+ origCwd = process.cwd();
117
+ origHome = process.env['HOME'];
118
+ origNode = process.env['CRTR_NODE_ID'];
119
+ origProfile = process.env['CRTR_PROFILE_ID'];
120
+ });
121
+ beforeEach(async () => {
122
+ closeDb();
123
+ if (home)
124
+ rmSync(home, { recursive: true, force: true });
125
+ home = mkdtempSync(join(tmpdir(), 'crtr-pre-command-'));
126
+ cwd = join(home, 'user', 'work');
127
+ mkdirSync(cwd, { recursive: true });
128
+ process.chdir(cwd);
129
+ process.env['HOME'] = join(home, 'user');
130
+ process.env['CRTR_HOME'] = home;
131
+ delete process.env['CRTR_NODE_ID'];
132
+ delete process.env['CRTR_PROFILE_ID'];
133
+ resetScopeCache();
134
+ clearSessionCache();
135
+ server = createApiServer();
136
+ await waitForSocket(apiSocketPath());
137
+ });
138
+ afterEach(async () => {
139
+ try {
140
+ await closeServer();
141
+ }
142
+ finally {
143
+ process.chdir(origCwd);
144
+ }
145
+ });
146
+ after(() => {
147
+ process.chdir(origCwd);
148
+ closeDb();
149
+ if (home)
150
+ rmSync(home, { recursive: true, force: true });
151
+ resetScopeCache();
152
+ clearSessionCache();
153
+ delete process.env['CRTR_HOME'];
154
+ if (origHome === undefined)
155
+ delete process.env['HOME'];
156
+ else
157
+ process.env['HOME'] = origHome;
158
+ if (origNode === undefined)
159
+ delete process.env['CRTR_NODE_ID'];
160
+ else
161
+ process.env['CRTR_NODE_ID'] = origNode;
162
+ if (origProfile === undefined)
163
+ delete process.env['CRTR_PROFILE_ID'];
164
+ else
165
+ process.env['CRTR_PROFILE_ID'] = origProfile;
166
+ });
167
+ test('a matching command is held with its doc, every sibling in the message with it, and the re-issue runs', async () => {
168
+ const docPath = writeDoctrineDoc('pre-command');
169
+ seedNode('gate-hold');
170
+ const pi = registerSubstrate();
171
+ const held = assertHeld(await pi.bash('deploy prod'), 'the first matching command');
172
+ assert.ok(held.reason.includes(DOCTRINE_BODY), `the doc rides back as the blocked call's result; got: ${held.reason}`);
173
+ assert.match(held.reason, /re-issue your WHOLE command/, 'the copy steers a whole-command re-issue, never one clause');
174
+ // Exposure is registered AT BLOCK TIME — physical identity and content alias
175
+ // both — and that single fact is what releases the re-issue below.
176
+ const afterBlock = loadContextExposureState('gate-hold');
177
+ assert.deepEqual(afterBlock.exposures.get(docPath), { transcript: 3 }, 'the held doc is recorded as delivered');
178
+ assert.equal(afterBlock.exposures.size, 2, 'the content alias is recorded alongside the path');
179
+ // Siblings of the same assistant message. The doc is already exposed, so
180
+ // without the per-turn flag the render would come back empty and let them
181
+ // through. The flag covers the whole message, matching or not: every command
182
+ // in it was authored under the worldview this doc just corrected.
183
+ const sibling = assertHeld(await pi.bash('deploy staging'), 'a matching sibling');
184
+ assert.match(sibling.reason, /Another command in this same message/);
185
+ assertHeld(await pi.bash('ls -la'), 'a non-matching sibling of a held command');
186
+ await pi.endTurn();
187
+ // The release: the doc is in the transcript, so there is nothing left to
188
+ // deliver and nothing left to hold.
189
+ assert.equal(await pi.bash('deploy prod'), undefined, 'the identical command re-issued in a later turn runs');
190
+ assert.equal(await pi.bash('ls -la'), undefined, 'a command matching nothing is untouched');
191
+ });
192
+ test('compaction re-arms the hold and leaves an exposure ledger that reloads', async () => {
193
+ const docPath = writeDoctrineDoc('pre-command');
194
+ seedNode('gate-compact');
195
+ const pi = registerSubstrate();
196
+ assertHeld(await pi.bash('deploy prod'), 'the first matching command');
197
+ await pi.endTurn();
198
+ assert.equal(await pi.bash('deploy prod'), undefined, 'the doc is in the transcript, so the command runs');
199
+ await pi.endTurn();
200
+ await pi.compact();
201
+ // Reloaded from disk: the demotion must DELETE the rankless entry rather than
202
+ // leave `{}` behind, which the v3 loader rejects outright — this call is the
203
+ // proof it did.
204
+ const afterCompact = loadContextExposureState('gate-compact');
205
+ assert.equal(afterCompact.exposures.has(docPath), false, 'compaction forgets the delivery that compaction dropped');
206
+ assert.equal(afterCompact.exposures.size, 0, 'the content alias is forgotten too, and no rankless entry survives');
207
+ const reheld = assertHeld(await pi.bash('deploy prod'), 'the same command after compaction');
208
+ assert.ok(reheld.reason.includes(DOCTRINE_BODY), 'guidance the transcript no longer carries is delivered again');
209
+ });
210
+ test('a corpus with no pre-command doc never reaches the daemon', async () => {
211
+ writeDoctrineDoc('command');
212
+ seedNode('gate-quiet');
213
+ const pi = registerSubstrate();
214
+ // No daemon. Every path below must return before the subject lookup; one that
215
+ // does not fails loud with `daemon_unavailable` instead of passing quietly.
216
+ await closeServer();
217
+ assert.equal(await pi.toolCall('read', { file: '/tmp/x' }), undefined, 'a non-bash call is not the gate’s business');
218
+ assert.equal(await pi.toolCall('bash', { command: ' ' }), undefined, 'a bash call carrying no command is not held');
219
+ assert.equal(await pi.bash('deploy prod'), undefined, 'a post-execution `command` doc never holds anything');
220
+ });
@@ -3,6 +3,7 @@
3
3
  // These helpers are intentionally independent of the canvas state store. A
4
4
  // broker may read its own context files directly, but all node metadata reads
5
5
  // and generated-name writes go through crtrd's broker operation projection.
6
+ import { envHomeOverride } from '../shared/env.js';
6
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
8
  import { homedir } from 'node:os';
8
9
  import { join } from 'node:path';
@@ -14,7 +15,7 @@ function contextDir(nodeId) {
14
15
  if (nodeId === '' || nodeId === '.' || nodeId === '..' || /[\\/\0]/u.test(nodeId)) {
15
16
  throw new TypeError('node_id must be a non-empty path segment');
16
17
  }
17
- const canvasHome = process.env['CRTR_HOME']?.trim() || join(homedir(), CRTR_DIR_NAME, 'canvas');
18
+ const canvasHome = envHomeOverride()?.trim() || join(homedir(), CRTR_DIR_NAME, 'canvas');
18
19
  return join(canvasHome, 'nodes', nodeId, 'context');
19
20
  }
20
21
  /** Persist a bare root's first real interactive message without clobbering an
@@ -24,6 +24,7 @@
24
24
  // Deviation from upstream: this backend pins `bash` as the shell (upstream
25
25
  // consults user shell config via non-exported helpers). Acceptable — crtr's
26
26
  // agents are bash-contract anyway.
27
+ import { envBashValveMs, envNodeId } from '../shared/env.js';
27
28
  import { spawn } from 'node:child_process';
28
29
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
29
30
  import { homedir } from 'node:os';
@@ -34,20 +35,12 @@ import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-resu
34
35
  import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
35
36
  import { Type } from 'typebox';
36
37
  import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
37
- // ---------------------------------------------------------------------------
38
38
  // Deadline — 5 minutes by default, overridable via CRTR_BASH_VALVE_MS
39
39
  // (undocumented; test-only seam so the E2E valve check doesn't sleep 5m).
40
- // ---------------------------------------------------------------------------
41
- const DEFAULT_VALVE_MS = 5 * 60 * 1000;
42
40
  const POLL_MS = 200;
43
41
  function resolveValveMs() {
44
- const raw = process.env['CRTR_BASH_VALVE_MS'];
45
- if (raw === undefined)
46
- return DEFAULT_VALVE_MS;
47
- const n = Number(raw);
48
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_VALVE_MS;
42
+ return envBashValveMs();
49
43
  }
50
- // ---------------------------------------------------------------------------
51
44
  // The detached supervisor. Spawned with `detached: true` so it becomes its
52
45
  // own process-group leader (pgid === its pid) — killing `-pgid` reaches the
53
46
  // real command and anything it forked, even after this pi process exits.
@@ -59,7 +52,6 @@ function resolveValveMs() {
59
52
  //
60
53
  // `--` before the paths is a dummy $0 (a standard `bash -c script -- args`
61
54
  // idiom) so the real payload lands at $1 onward.
62
- // ---------------------------------------------------------------------------
63
55
  // The elapsed rendering mirrors formatBashElapsed() in core/bash-jobs.ts
64
56
  // (`12s` / `3m 40s` / `1h 04m`) so the completion message reads the same as the
65
57
  // handoff notice; it measures TOTAL runtime, foreground portion included.
@@ -166,12 +158,10 @@ function readExitCode(path) {
166
158
  return null;
167
159
  }
168
160
  }
169
- // ---------------------------------------------------------------------------
170
161
  // Long-sleep guard. A command that opens with `sleep <long>` is a node burning
171
162
  // a live window to wait on a clock — the runtime already has waiting primitives
172
163
  // that cost nothing (`crtr cron` for a scheduled wake, `crtr node wait deadline`
173
164
  // for an inbox-vs-deadline race). Anything over 10s is refused before it runs.
174
- // ---------------------------------------------------------------------------
175
165
  const MAX_INLINE_SLEEP_SECONDS = 10;
176
166
  const SLEEP_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
177
167
  /** Seconds a leading `sleep ...` will block for, or null when the command does
@@ -371,10 +361,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
371
361
  },
372
362
  };
373
363
  }
374
- // ---------------------------------------------------------------------------
375
364
  // Extension
376
- // ---------------------------------------------------------------------------
377
- // ---------------------------------------------------------------------------
378
365
  // `purpose` — the one line a person reads while a command runs
379
366
  //
380
367
  // An optional deviation from taking pi's definition verbatim, and the only one
@@ -396,7 +383,6 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
396
383
  // Upstream ask: pi's own bash tool should carry this (Claude Code's bash tool
397
384
  // has shipped an equivalent `description` field for years). Until it does, the
398
385
  // field is re-declared here on top of pi's schema rather than forked into it.
399
- // ---------------------------------------------------------------------------
400
386
  const PURPOSE_DESCRIPTION = [
401
387
  'One short clause naming what this step is FOR, in the words of the person you',
402
388
  'are working for — "Reading the Acme thread", not "Fetching emails" and never',
@@ -437,7 +423,7 @@ export function createValveToolDefinition(nodeId, contextDir, cwd, exposePurpose
437
423
  return { ...schemaDefinition, execute };
438
424
  }
439
425
  export default function (pi) {
440
- const nodeId = process.env['CRTR_NODE_ID'];
426
+ const nodeId = envNodeId();
441
427
  if (nodeId === undefined || nodeId.trim() === '')
442
428
  return; // inert — builtin bash stays
443
429
  const contextDir = process.env['CRTR_CONTEXT_DIR'];
@@ -22,6 +22,7 @@
22
22
  //
23
23
  // Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
24
24
  // crouter's own tsc build without a dep on the pi packages.
25
+ import { envNodeId } from '../shared/env.js';
25
26
  import { CONTEXT_INTRO_CUSTOM_TYPE, formatCard, parseCard } from '../shared/generated-context.js';
26
27
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
27
28
  import { buildContextBearingsFromState, buildForkBearingsFromState, buildProjectContextBlockForBroker, renderPreferencesFromState, } from '../core/runtime/broker-extension-render.js';
@@ -35,9 +36,7 @@ import { truncateToWidth } from './truncate.js';
35
36
  * the block builder in `core/runtime/bearings.ts` because the warm-pool claim's
36
37
  * live delivery needs it too; re-exported here as this module's own surface. */
37
38
  export { CONTEXT_INTRO_CUSTOM_TYPE };
38
- // ---------------------------------------------------------------------------
39
39
  // Block builder
40
- // ---------------------------------------------------------------------------
41
40
  /** Build the full bearings baseline for a conversation without inherited
42
41
  * crouter context. Exported for testing. */
43
42
  export async function buildContextIntro(nodeId, target) {
@@ -50,9 +49,7 @@ export async function buildForkContextIntro(nodeId) {
50
49
  const state = await brokerExtensionState(nodeId);
51
50
  return buildForkBearingsFromState(state, situationalContextEnvelope(nodeId) ?? '');
52
51
  }
53
- // ---------------------------------------------------------------------------
54
52
  // Collapsed-by-default rendering
55
- // ---------------------------------------------------------------------------
56
53
  /** Pull the plain text out of a custom message's content (string or blocks). */
57
54
  function messageText(message) {
58
55
  if (typeof message.content === 'string')
@@ -127,16 +124,14 @@ export function renderContextMessage(message, options, theme) {
127
124
  },
128
125
  };
129
126
  }
130
- // ---------------------------------------------------------------------------
131
127
  // Extension
132
- // ---------------------------------------------------------------------------
133
128
  /** Register new-context baseline assembly; a strict resume changes nothing. */
134
129
  export function registerCanvasContextIntro(pi) {
135
130
  // Collapse the block to a one-liner until the user expands tool output (Ctrl+O).
136
131
  // Harmless to register outside TUI mode (it's only consulted while rendering).
137
132
  pi.registerMessageRenderer(CONTEXT_INTRO_CUSTOM_TYPE, renderContextMessage);
138
133
  pi.on('session_start', async (event, ctx) => {
139
- const nodeId = process.env['CRTR_NODE_ID'];
134
+ const nodeId = envNodeId();
140
135
  if (nodeId === undefined || nodeId.trim() === '')
141
136
  return;
142
137
  const reason = event?.reason;
@@ -29,9 +29,23 @@ interface ToolResultCtxLike {
29
29
  type ToolResultResultLike = {
30
30
  content: ContentBlockLike[];
31
31
  } | void;
32
+ /** tool_call: fired BEFORE the tool runs. Returning `{block: true, reason}`
33
+ * stops it — nothing executes, no `tool_result` fires, and `reason` becomes the
34
+ * call's synthesized error result, carrying arbitrary-length text. */
35
+ interface ToolCallEventLike {
36
+ toolName: string;
37
+ input?: unknown;
38
+ }
39
+ type ToolCallResultLike = {
40
+ block: true;
41
+ reason: string;
42
+ } | void;
32
43
  interface PiLike {
33
44
  on(event: 'before_agent_start', handler: (event: BeforeAgentStartEventLike) => BeforeAgentStartResultLike | void | Promise<BeforeAgentStartResultLike | void>): void;
34
45
  on(event: 'session_start', handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
46
+ on(event: 'session_compact', handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
47
+ on(event: 'turn_end', handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
48
+ on(event: 'tool_call', handler: (event: ToolCallEventLike, ctx: unknown) => ToolCallResultLike | Promise<ToolCallResultLike>): void;
35
49
  on(event: 'tool_result', handler: (event: ToolResultEventLike, ctx: ToolResultCtxLike) => ToolResultResultLike | Promise<ToolResultResultLike>): void;
36
50
  }
37
51
  export declare function registerCanvasDocSubstrate(pi: PiLike): void;
@@ -7,29 +7,62 @@
7
7
  //
8
8
  // before_agent_start splices the context's frozen preference snapshot after the
9
9
  // native tool list. Successful read/bash results append matching memory through
10
- // the shared context exposure state. Pure rendering lives in core/substrate/.
10
+ // the shared context exposure state. A bash call matching a `pre-command` doc is
11
+ // BLOCKED before it runs and the doc comes back as its synthesized error result.
12
+ // Pure rendering lives in core/substrate/.
11
13
  //
12
14
  // Plain TS-with-types — NO imports from @earendil-works/* (a local structural
13
15
  // PiLike interface stands in), so it compiles inside crouter's own tsc build
14
16
  // with no dep on the pi packages. Mirrors canvas-context-intro.ts.
17
+ import { envNodeId } from '../shared/env.js';
15
18
  import { homedir } from 'node:os';
16
19
  import { join, resolve } from 'node:path';
17
20
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
18
21
  import { renderPreferencesFromState } from '../core/runtime/broker-extension-render.js';
19
- import { renderOnCommandDocsForSubject, renderOnReadDocsForSubject } from '../core/substrate/on-read.js';
22
+ import { corpusHasPreCommandSurfaces, demotePreCommandExposures, renderOnCommandDocsForSubject, renderOnReadDocsForSubject, renderPreCommandDocsForSubject, } from '../core/substrate/on-read.js';
20
23
  import { clearSessionCache } from '../core/substrate/session-cache.js';
21
24
  import { cloneContextExposureState, exposureTarget, freezePreferenceSnapshot, loadContextExposureState, mergeContextExposureState, replaceContextExposureState, saveContextExposureState, sharedContextExposureState, } from '../core/substrate/injected-store.js';
22
25
  import { autoLoadedContextInner, mergeAutoLoadedContext } from './envelope-merge.js';
23
- // ---------------------------------------------------------------------------
24
26
  // Extension
25
- // ---------------------------------------------------------------------------
26
27
  /** The stable seam that closes pi's native "Available tools" list. We splice the
27
28
  * substrate's boot sections right before it — the agent decides which capability
28
29
  * to reach for while reading the tools, so memory docs/preferences must sit
29
30
  * there, not far below. Falls back to appending if absent. */
30
31
  const TOOLS_ANCHOR = '\n\nGuidelines:';
32
+ /** The bash command string a tool call carries, or `null` when there is none. */
33
+ function bashCommandOf(input) {
34
+ const command = input?.command;
35
+ return typeof command === 'string' && command.trim() !== '' ? command : null;
36
+ }
37
+ /** The blocked call's synthesized error result. A blocked command is discarded
38
+ * IN FULL — pi executes none of it — so the copy must steer a re-issue of the
39
+ * WHOLE command, never of one clause: when the same command also built its own
40
+ * input (a heredoc, a `>` redirect, a `$(…)`), that setup did not run either,
41
+ * and re-issuing only the tail would act on stale or missing data. */
42
+ function heldReason(rendered) {
43
+ return [
44
+ 'HELD — nothing ran.',
45
+ '',
46
+ 'Your bash command was NOT executed. It matched memory you had not yet read, and that memory is below.',
47
+ '',
48
+ 'Read it, then re-issue your WHOLE command unchanged — every clause of it, including any heredoc, redirect, or `$(…)` that built its input, because none of that ran either. The re-issue executes: this memory is now in your transcript, so there is nothing left to deliver and nothing left to hold.',
49
+ '',
50
+ rendered,
51
+ ].join('\n');
52
+ }
53
+ /** The follow-on notice for a SIBLING command in the same assistant message.
54
+ * pi preflights siblings sequentially and then executes them concurrently, so
55
+ * without one flag per turn the second sibling would find the doc already
56
+ * exposed and slip past the hold its sibling just raised. */
57
+ const HELD_SIBLING_REASON = [
58
+ 'HELD — nothing ran.',
59
+ '',
60
+ 'Your bash command was NOT executed. Another command in this same message was held against memory you had not read, and that memory was returned with it.',
61
+ '',
62
+ 'Read it, then re-issue your WHOLE command unchanged. The re-issue executes.',
63
+ ].join('\n');
31
64
  export function registerCanvasDocSubstrate(pi) {
32
- const nodeId = process.env['CRTR_NODE_ID'];
65
+ const nodeId = envNodeId();
33
66
  if (nodeId === undefined || nodeId.trim() === '')
34
67
  return; // not a canvas node — inert
35
68
  // One in-place state object is shared with context-intro and rehydrated on a
@@ -57,6 +90,45 @@ export function registerCanvasDocSubstrate(pi) {
57
90
  systemPrompt: `${event.systemPrompt.slice(0, idx)}\n\n${block}${event.systemPrompt.slice(idx)}`,
58
91
  };
59
92
  });
93
+ // One flag per assistant turn: the first hold covers every matching command
94
+ // in the same message.
95
+ let blockedThisTurn = false;
96
+ pi.on('turn_end', () => {
97
+ blockedThisTurn = false;
98
+ });
99
+ // Compaction drops delivered guidance out of the transcript while the ledger
100
+ // still records it as delivered. Forget those deliveries so the holds re-arm.
101
+ pi.on('session_compact', () => {
102
+ mergeContextExposureState(contextExposure, loadContextExposureState(nodeId));
103
+ demotePreCommandExposures(contextExposure);
104
+ saveContextExposureState(nodeId, contextExposure);
105
+ blockedThisTurn = false;
106
+ });
107
+ pi.on('tool_call', async (event) => {
108
+ if (event.toolName !== 'bash')
109
+ return;
110
+ const command = bashCommandOf(event.input);
111
+ if (command === null)
112
+ return;
113
+ // The common case is a corpus with no pre-command doc at all, and it must
114
+ // cost nothing — short-circuit before the daemon round-trip below.
115
+ if (!corpusHasPreCommandSurfaces())
116
+ return;
117
+ if (blockedThisTurn)
118
+ return { block: true, reason: HELD_SIBLING_REASON };
119
+ const state = await brokerExtensionState(nodeId);
120
+ mergeContextExposureState(contextExposure, loadContextExposureState(nodeId));
121
+ const next = cloneContextExposureState(contextExposure);
122
+ const rendered = renderPreCommandDocsForSubject(state.subject, command, exposureTarget(next, 'transcript'));
123
+ // Everything this command matches is already in the transcript: the hold is
124
+ // released and the command runs.
125
+ if (rendered === '')
126
+ return;
127
+ blockedThisTurn = true;
128
+ saveContextExposureState(nodeId, next);
129
+ replaceContextExposureState(contextExposure, next);
130
+ return { block: true, reason: heldReason(rendered) };
131
+ });
60
132
  pi.on('tool_result', async (event, ctx) => {
61
133
  if (event.isError === true)
62
134
  return;
@@ -29,6 +29,7 @@
29
29
  //
30
30
  // Plain TS-with-types — no imports from @earendil-works/* so this compiles inside
31
31
  // crouter's own tsc build without a dep on the pi packages.
32
+ import { envNodeId } from '../shared/env.js';
32
33
  import { captureGoalIfAbsent, generateAndCommitInitialName, } from './broker-local.js';
33
34
  import { parseCard } from '../shared/generated-context.js';
34
35
  /**
@@ -41,7 +42,7 @@ import { parseCard } from '../shared/generated-context.js';
41
42
  export function registerCanvasGoalCapture(pi) {
42
43
  pi.on('input', (event) => {
43
44
  try {
44
- const nodeId = process.env['CRTR_NODE_ID'];
45
+ const nodeId = envNodeId();
45
46
  if (nodeId === undefined || nodeId.trim() === '')
46
47
  return; // not a canvas node
47
48
  const text = (event.text ?? '').trim();