@north-light/crouter 0.3.197 → 0.3.199

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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -44,3 +44,27 @@ export function clearDefaultProfile(cwd) {
44
44
  else
45
45
  writeJson(bindingsPath(), value);
46
46
  }
47
+ /** Every directory currently pinned to `profileId`, in deterministic order. */
48
+ export function defaultProfileDirs(profileId) {
49
+ return Object.entries(bindings())
50
+ .filter(([, pinnedProfileId]) => pinnedProfileId === profileId)
51
+ .map(([dir]) => dir)
52
+ .sort();
53
+ }
54
+ /** Clear every per-directory default that names `profileId`. */
55
+ export function clearDefaultProfileId(profileId) {
56
+ const value = bindings();
57
+ const cleared = Object.entries(value)
58
+ .filter(([, pinnedProfileId]) => pinnedProfileId === profileId)
59
+ .map(([dir]) => dir)
60
+ .sort();
61
+ if (cleared.length === 0)
62
+ return [];
63
+ for (const dir of cleared)
64
+ delete value[dir];
65
+ if (Object.keys(value).length === 0)
66
+ removePath(bindingsPath());
67
+ else
68
+ writeJson(bindingsPath(), value);
69
+ return cleared;
70
+ }
@@ -0,0 +1,8 @@
1
+ /** Reserve one profile identity for its complete daemon-owned deletion lifecycle.
2
+ * The caller must release the returned lease after completion or failure. */
3
+ export declare function reserveProfileDeletion(profileId: string): () => void;
4
+ /** The synchronous birth gate. spawnNode calls this immediately before the
5
+ * canvas row is persisted. A reservation rejects a birth that reaches the gate
6
+ * during deletion; the exact manifest read rejects one whose async preflight
7
+ * started before a fast hard deletion and resumed after its lease was released. */
8
+ export declare function assertProfileAvailableForBirth(profileId: string | null): void;
@@ -0,0 +1,38 @@
1
+ import { usage } from '../errors.js';
2
+ import { loadExactProfileManifest } from './manifest.js';
3
+ const deletingProfileIds = new Set();
4
+ /** Reserve one profile identity for its complete daemon-owned deletion lifecycle.
5
+ * The caller must release the returned lease after completion or failure. */
6
+ export function reserveProfileDeletion(profileId) {
7
+ if (deletingProfileIds.has(profileId)) {
8
+ throw usage(`profile deletion is already in progress: ${profileId}`, {
9
+ error: 'profile_deletion_in_progress',
10
+ profile_id: profileId,
11
+ next: 'Wait for the accepted deletion to finish before using this profile again.',
12
+ });
13
+ }
14
+ deletingProfileIds.add(profileId);
15
+ let released = false;
16
+ return () => {
17
+ if (released)
18
+ return;
19
+ released = true;
20
+ deletingProfileIds.delete(profileId);
21
+ };
22
+ }
23
+ /** The synchronous birth gate. spawnNode calls this immediately before the
24
+ * canvas row is persisted. A reservation rejects a birth that reaches the gate
25
+ * during deletion; the exact manifest read rejects one whose async preflight
26
+ * started before a fast hard deletion and resumed after its lease was released. */
27
+ export function assertProfileAvailableForBirth(profileId) {
28
+ if (profileId === null)
29
+ return;
30
+ if (deletingProfileIds.has(profileId)) {
31
+ throw usage(`cannot create a node while its profile is being deleted: ${profileId}`, {
32
+ error: 'profile_deletion_in_progress',
33
+ profile_id: profileId,
34
+ next: 'Choose another profile or wait for this deletion to finish.',
35
+ });
36
+ }
37
+ loadExactProfileManifest(profileId);
38
+ }
@@ -30,6 +30,9 @@ export declare function listProfiles(): ProfileEntry[];
30
30
  * function every command leaf and runtime consumer should
31
31
  * call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
32
32
  * safe profile id. */
33
+ /** Read one exact profile id without falling back to manifest-name matching.
34
+ * Used at persistence gates that already hold a resolved durable identity. */
35
+ export declare function loadExactProfileManifest(profileId: string): ProfileEntry;
33
36
  export declare function loadProfileManifest(profileIdOrName: string): ProfileEntry;
34
37
  /** Hold the per-profile manifest lock for the duration of `fn`. ALL manifest
35
38
  * mutations (create/rename/add-project/remove-project/delete/last-used)
@@ -150,6 +150,18 @@ export function listProfiles() {
150
150
  * function every command leaf and runtime consumer should
151
151
  * call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
152
152
  * safe profile id. */
153
+ /** Read one exact profile id without falling back to manifest-name matching.
154
+ * Used at persistence gates that already hold a resolved durable identity. */
155
+ export function loadExactProfileManifest(profileId) {
156
+ assertSafeProfileId(profileId);
157
+ const manifest = readManifestFile(profileManifestPath(profileId), profileId);
158
+ if (manifest !== null)
159
+ return { profileId, manifest };
160
+ throw notFound(`profile not found: ${profileId}`, {
161
+ received: profileId,
162
+ next: 'Run `crtr profile list` to see available profiles.',
163
+ });
164
+ }
153
165
  export function loadProfileManifest(profileIdOrName) {
154
166
  const all = listProfiles();
155
167
  if (ID_SHAPE.test(profileIdOrName)) {
@@ -296,10 +296,10 @@ function rootProfileMessage(cwd) {
296
296
  /** The one synchronous (pre-boot) decision point: no profile covers cwd, and
297
297
  * the session IS interactive. Ask on the controlling terminal — create a
298
298
  * profile pointing at cwd, or proceed as root — and block until answered.
299
- * Deliberately a minimal `readline` prompt, not `crtr human ask`: that bridge
299
+ * Deliberately a minimal `readline` prompt, not `crtr human send`: that bridge
300
300
  * is asynchronous (registers an interaction in the ticket store, then waits for
301
301
  * the human to answer it from a separate inbox) and cannot gate pi's boot; those
302
- * entry points (`human ask`/`review`) are built for a queued,
302
+ * entry points (`human send`/`review`) are built for a queued,
303
303
  * asynchronously-answered interaction, not a two-choice gate on the same TTY pi
304
304
  * is about to take over — a raw `readline` question is the actual minimal
305
305
  * fit here. */
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
2
+ import { basename } from 'node:path';
3
3
  import { getNode, subscribersOf, subscribe } from '../canvas/index.js';
4
4
  import { withCanvasWrite } from '../canvas/db.js';
5
- import { atomicWriteJson } from '../human/convention.js';
5
+ import { bindTicketReplyRoute } from '../human/convention.js';
6
6
  import { ticketDir } from '../human/root.js';
7
7
  import { submitReview } from '../human/tickets.js';
8
8
  import { NodeIdConflictError, spawnNode } from '../runtime/nodes.js';
9
+ import { loadProfileManifest } from '../profiles/manifest.js';
9
10
  import { transition } from '../runtime/lifecycle.js';
10
11
  import { reviveNode } from '../runtime/revive.js';
11
12
  import { awaitCompanionBind, spawnReviewCompanion } from './companion.js';
@@ -107,7 +108,7 @@ function realizeBridge(review) {
107
108
  profile_id: origin.profile_id ?? null,
108
109
  name: 'human-review',
109
110
  });
110
- atomicWriteJson(join(ticketDir(bridge.node_id), 'run.json'), { mode: 'review' });
111
+ bindTicketReplyRoute(ticketDir(bridge.node_id), bridge.node_id);
111
112
  if (!bindBridgeNode(current.review_id, bridge.node_id)) {
112
113
  cancelInvisibleBridge(bridge.node_id);
113
114
  current = requireReview(current.review_id);
@@ -126,6 +127,10 @@ function publishTicket(review) {
126
127
  throw bridgeUnsubscribed(review, '');
127
128
  if (review.subtitle === null)
128
129
  throw new Error(`ticket review has no subtitle: ${review.review_id}`);
130
+ const origin = requireOrigin(review);
131
+ const profileName = origin.profile_id == null
132
+ ? undefined
133
+ : loadProfileManifest(origin.profile_id).manifest.name;
129
134
  submitReview({
130
135
  dir: ticketDir(review.bridge_node_id),
131
136
  review: {
@@ -133,7 +138,10 @@ function publishTicket(review) {
133
138
  ...(review.output_path !== null ? { output: review.output_path } : {}),
134
139
  title: basename(review.file),
135
140
  subtitle: review.subtitle,
136
- source: { nodeId: review.origin_node_id },
141
+ source: {
142
+ nodeId: review.origin_node_id,
143
+ ...(profileName === undefined ? {} : { profileName }),
144
+ },
137
145
  blockedSince: review.created,
138
146
  },
139
147
  });
@@ -28,6 +28,12 @@ export declare function listReviews(filter: {
28
28
  origin_node_id?: string;
29
29
  file?: string;
30
30
  }): ReviewRecord[];
31
+ /** Review ids whose immutable origin is one of `nodeIds`, including binding
32
+ * rows that normal user-facing review listings intentionally hide. */
33
+ export declare function reviewIdsByOriginNodes(nodeIds: readonly string[]): string[];
34
+ /** Delete review audit state for the selected origins in dependency order.
35
+ * Filesystem cleanup is deliberately left to the daemon lifecycle owner. */
36
+ export declare function deleteReviewsByOriginNodes(nodeIds: readonly string[]): string[];
31
37
  export declare function listBindingReviews(): ReviewRecord[];
32
38
  /** Open reviews the human has already submitted, still waiting on their companion. */
33
39
  export declare function listSubmitRequestedReviews(): ReviewRecord[];
@@ -116,6 +116,37 @@ export function listReviews(filter) {
116
116
  }
117
117
  return readRecords(openDb(), clauses.join(' AND '), ...params);
118
118
  }
119
+ /** Review ids whose immutable origin is one of `nodeIds`, including binding
120
+ * rows that normal user-facing review listings intentionally hide. */
121
+ export function reviewIdsByOriginNodes(nodeIds) {
122
+ const ids = [...new Set(nodeIds)];
123
+ if (ids.length === 0)
124
+ return [];
125
+ const placeholders = ids.map(() => '?').join(',');
126
+ return openDb()
127
+ .prepare(`SELECT review_id FROM reviews WHERE origin_node_id IN (${placeholders}) ORDER BY created, review_id`)
128
+ .all(...ids).map(({ review_id }) => review_id);
129
+ }
130
+ /** Delete review audit state for the selected origins in dependency order.
131
+ * Filesystem cleanup is deliberately left to the daemon lifecycle owner. */
132
+ export function deleteReviewsByOriginNodes(nodeIds) {
133
+ const ids = [...new Set(nodeIds)];
134
+ if (ids.length === 0)
135
+ return [];
136
+ const originPlaceholders = ids.map(() => '?').join(',');
137
+ return withCanvasWrite((db) => {
138
+ const reviewIds = db
139
+ .prepare(`SELECT review_id FROM reviews WHERE origin_node_id IN (${originPlaceholders}) ORDER BY created, review_id`)
140
+ .all(...ids).map(({ review_id }) => review_id);
141
+ if (reviewIds.length === 0)
142
+ return [];
143
+ const reviewPlaceholders = reviewIds.map(() => '?').join(',');
144
+ db.prepare(`DELETE FROM review_comment_events WHERE review_id IN (${reviewPlaceholders})`).run(...reviewIds);
145
+ db.prepare(`DELETE FROM review_comments WHERE review_id IN (${reviewPlaceholders})`).run(...reviewIds);
146
+ db.prepare(`DELETE FROM reviews WHERE review_id IN (${reviewPlaceholders})`).run(...reviewIds);
147
+ return reviewIds;
148
+ });
149
+ }
119
150
  export function listBindingReviews() {
120
151
  return readRecords(openDb(), "state = 'binding'");
121
152
  }
@@ -61,10 +61,6 @@ export declare function cronWakeOrigin(c: Cron, kind: WakeOrigin['kind']): WakeO
61
61
  * carry the signal, and a raw ISO instant is noise an agent cannot cheaply turn
62
62
  * into an elapsed delta. */
63
63
  export declare function buildWakeBearings(origin: WakeOrigin): string;
64
- /** The node-scoped update for a fork whose copied branch already contains a
65
- * full crouter baseline. Shared project and operating context stays in the
66
- * inherited branch; this block replaces only source-owned facts. */
67
- export declare function buildForkBearings(nodeId: string): string;
68
64
  /** The full boot intro, four top-level sections:
69
65
  *
70
66
  * 1. `<crtr-bearings>` — the orienting facts about THIS node: the identity
@@ -3,9 +3,10 @@
3
3
  // A conversation with no inherited crouter bearings receives the full baseline:
4
4
  // node identity and graph position, context-directory guidance, routed knowledge,
5
5
  // auto-loaded project docs, situational context, and the current environment.
6
- // A fork whose copied branch already carries that baseline receives only
7
- // buildForkBearings(): the new node-scoped facts that supersede its source's
8
- // identity. The copied baseline remains useful reference without being repeated.
6
+ // A fork whose copied branch already carries that baseline receives only the
7
+ // new node-scoped facts that supersede its source's identity — rendered by
8
+ // buildForkBearingsFromState() in broker-extension-render.ts, the broker-side
9
+ // mirror. The copied baseline remains useful reference without being repeated.
9
10
  //
10
11
  // promote.ts separately delivers orchestratorContextNote() when a base node
11
12
  // becomes an orchestrator after birth.
@@ -329,65 +330,6 @@ function managedWorktreeNote(node) {
329
330
  return '';
330
331
  return `Branch: \`${wt.branch}\`\nWorktree: \`${wt.path}\`\nBase: \`${wt.base_ref}\` @ \`${wt.base_sha}\`\nCommit work here. Land it with \`crtr node worktree close\` before finishing. Auto-drop applies only when the worktree is clean (no uncommitted changes) and nothing remains to land (zero commits ahead of the base, or its commits are already contained in the base); unlanded commits (commits not already contained in the base) or uncommitted changes require an explicit close/landing decision.`;
331
332
  }
332
- /** The review-companion block — rendered ONLY when a node carries its immutable
333
- * review binding. Names the target file (the document under review), clarifies
334
- * that inherited context belongs to the origin node and is not visible to the
335
- * person, and notes that the visible transcript begins at the boundary marker
336
- * above. */
337
- function reviewCompanionBlock(meta) {
338
- if (meta?.review_binding === undefined || meta?.review_binding === null)
339
- return '';
340
- const targetFile = meta.review_binding.target_file;
341
- return (`You exist for one human review of \`${targetFile}\`. The inherited conversation belongs to ` +
342
- `the origin node, not you — carry only the context needed for this review. The person's visible ` +
343
- `transcript begins at the boundary marker above; everything earlier is context they cannot see.`);
344
- }
345
- /** The node-scoped update for a fork whose copied branch already contains a
346
- * full crouter baseline. Shared project and operating context stays in the
347
- * inherited branch; this block replaces only source-owned facts. */
348
- export function buildForkBearings(nodeId) {
349
- const node = getNode(nodeId);
350
- const bearings = [
351
- '<crtr-bearings>',
352
- buildIdentityAssertion(nodeId),
353
- '',
354
- 'The source node’s earlier task, graph position, lifecycle, children, schedules, worktree, situational context, and context-directory path do not carry over. Shared conversation, project, and operating context remain reference material.',
355
- '',
356
- '## Your node now',
357
- `Lifecycle: ${node?.lifecycle ?? 'terminal'}.`,
358
- ];
359
- const map = buildGraphMap(nodeId);
360
- if (map === '') {
361
- bearings.push('Canvas: independent root with no parent or inherited graph edges.');
362
- }
363
- else {
364
- bearings.push('', map);
365
- }
366
- bearings.push(`Context directory: \`${contextDir(nodeId)}\` (\`$CRTR_CONTEXT_DIR\`). Use this path in place of every source-node context path above.`);
367
- const forkFrom = node?.fork_from;
368
- const source = forkFrom !== undefined && forkFrom !== null && isSafeNodeId(forkFrom)
369
- ? getNode(forkFrom)
370
- : null;
371
- if (node !== null && (source === null || source.cwd !== node.cwd)) {
372
- bearings.push(`Working directory: \`${node.cwd}\`.`);
373
- }
374
- if (node !== null && (source === null || source.profile_id !== node.profile_id)) {
375
- const profileSection = profileNote(nodeId);
376
- if (profileSection !== '')
377
- bearings.push(profileSection);
378
- }
379
- const worktree = managedWorktreeNote(node);
380
- if (worktree !== '')
381
- bearings.push('', '## Your managed worktree', worktree);
382
- if (node?.mode === 'orchestrator')
383
- bearings.push('', orchestratorContextNote(nodeId));
384
- const review = reviewCompanionBlock(node);
385
- if (review !== '')
386
- bearings.push('', review);
387
- bearings.push('', 'The first real user message after these bearings is your mandate.', '</crtr-bearings>');
388
- const situational = situationalContextBlock(nodeId);
389
- return [bearings.join('\n'), situational].filter((s) => s !== '').join('\n');
390
- }
391
333
  /** The full boot intro, four top-level sections:
392
334
  *
393
335
  * 1. `<crtr-bearings>` — the orienting facts about THIS node: the identity
@@ -0,0 +1,44 @@
1
+ import type { Scope } from '../../types.js';
2
+ /** One resolved bare command: the name a node's bash sees, and the absolute
3
+ * real path the shim links to. */
4
+ export interface ResolvedBin {
5
+ name: string;
6
+ /** Absolute target validated inside the contributing root when this set resolved. */
7
+ target: string;
8
+ /** Human-readable contributor, e.g. `plugin "deploy-tools"` or
9
+ * `config.json (/repo/.crouter)`. */
10
+ origin: string;
11
+ scope: Scope;
12
+ }
13
+ /** A declaration that did NOT become a bare command, with why. Surfaced by
14
+ * `crtr sys doctor`; never thrown, so one bad entry can never stop a node
15
+ * from launching. */
16
+ export interface BinIssue {
17
+ name: string;
18
+ origin: string;
19
+ scope: Scope;
20
+ message: string;
21
+ }
22
+ export interface BinResolution {
23
+ /** Effective name → resolved bin, after layering. */
24
+ bins: Map<string, ResolvedBin>;
25
+ issues: BinIssue[];
26
+ }
27
+ /** The effective bare-command set for one node's cwd/profile, plus every
28
+ * declaration that failed to become one.
29
+ *
30
+ * Layers ascend in precedence (see `contributionLayers`). A name claimed by
31
+ * two contributors in the SAME layer selects no winner: it is reported and
32
+ * removed, so an ambiguous name resolves to nothing rather than to whichever
33
+ * plugin sorted first. A higher layer may still claim that name explicitly —
34
+ * that is an unambiguous choice, not a collision. */
35
+ export declare function resolveBinContributions(targetCwd: string, targetProfileId: string | null): BinResolution;
36
+ /** Materialize (idempotently) the shim directory for one effective set and
37
+ * return its absolute path, or null when the set is empty. Built in a staging
38
+ * dir and renamed into place, so a concurrent launch either sees no directory
39
+ * or sees a complete one. */
40
+ export declare function ensureBinShimDir(bins: Map<string, ResolvedBin>): string | null;
41
+ /** The shim directory to prepend to a target node's PATH, or null when that
42
+ * node's scopes declare no bare command. The one entry point the spawn-env
43
+ * boundary calls. */
44
+ export declare function binShimDirFor(targetCwd: string, targetProfileId: string | null): string | null;
@@ -0,0 +1,186 @@
1
+ // Bare-binary contributions — the plain executables a node's bash can call by
2
+ // name, with no `crtr` command leaf and no exec-command JSON protocol between
3
+ // them and the agent.
4
+ //
5
+ // A plugin declares them in its manifest (`bin`), a user or project scope in
6
+ // its own `config.json` (`bin`); both are a small name → root-relative-path
7
+ // map. This module resolves those declarations across the same layering the
8
+ // rest of the config uses (project stack > user, and within one root: that
9
+ // root's own config > its enabled plugins), materializes one crouter-owned
10
+ // shim directory of symlinks, and hands its path to the spawn-env boundary
11
+ // (`spawn-env.ts`), which PREPENDS it to every broker child's PATH while
12
+ // leaving the host PATH intact behind it.
13
+ //
14
+ // The shim directory exists so the declaration stays a per-name grant: crouter
15
+ // links exactly the names that were declared, rather than putting a
16
+ // contributor's whole `bin/` directory on the agent's PATH and admitting every
17
+ // sibling file in it.
18
+ //
19
+ // These binaries are trusted local code running with the caller's authority —
20
+ // the same authority the agent's bash tool already has. There is no sandbox,
21
+ // no install hook, and no shell evaluation anywhere in this path. Containment
22
+ // is checked while resolving the effective set; a contributor can change later
23
+ // because it is trusted local code, not a sandboxed artifact.
24
+ import { createHash } from 'node:crypto';
25
+ import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync } from 'node:fs';
26
+ import { dirname, join, resolve, sep } from 'node:path';
27
+ import { binDeclarationIssues, normalizeBin, readRawConfigAtRoot } from '../config.js';
28
+ import { listInstalledPluginsInRoot } from '../installed-plugins.js';
29
+ import { crtrHome } from '../canvas/paths.js';
30
+ import { findProjectScopeRoots, scopeRoot } from '../scope.js';
31
+ function contributorIssues(origin, scope, raw) {
32
+ return binDeclarationIssues(raw).map((issue) => ({ name: issue.name, origin, scope, message: issue.message }));
33
+ }
34
+ function pluginContributors(scope, root) {
35
+ return listInstalledPluginsInRoot(scope, root)
36
+ .filter((plugin) => plugin.enabled && plugin.manifest.bin !== undefined)
37
+ .sort((a, b) => a.name.localeCompare(b.name))
38
+ .map((plugin) => {
39
+ const origin = `plugin "${plugin.name}"`;
40
+ return { origin, scope, root: plugin.root, bin: normalizeBin(plugin.manifest.bin), issues: contributorIssues(origin, scope, plugin.manifest.bin) };
41
+ });
42
+ }
43
+ /** A scope's own `config.json` contributions. Paths resolve against the scope's
44
+ * AUTHORING root: the project directory for a project scope (so a repo writes
45
+ * `tools/repoctl`, where its tools actually live, not `../tools/repoctl` out
46
+ * of `.crouter/`), and `~/.crouter` itself for the user scope, which has no
47
+ * enclosing project. */
48
+ function configContributor(scope, scopeRootPath) {
49
+ const raw = readRawConfigAtRoot(scopeRootPath)?.bin;
50
+ if (raw === undefined)
51
+ return [];
52
+ const origin = `config.json (${scopeRootPath})`;
53
+ const bin = normalizeBin(raw);
54
+ const root = scope === 'project' ? dirname(scopeRootPath) : scopeRootPath;
55
+ return [{ origin, scope, root, bin, issues: contributorIssues(origin, scope, raw) }];
56
+ }
57
+ /** The contribution LAYERS in ascending precedence: a later layer's entry
58
+ * replaces an earlier layer's same name. Within one root the root's own
59
+ * `config.json` outranks its plugins (the rule `layerPluginKinds` follows for
60
+ * `kinds`), and the project stack — nearest root last — outranks the user
61
+ * scope entirely. Contributors WITHIN one layer never outrank each other:
62
+ * two of them claiming one name is a collision, not a precedence question. */
63
+ function contributionLayers(targetCwd, targetProfileId) {
64
+ const layers = [];
65
+ const userRoot = scopeRoot('user');
66
+ if (userRoot !== null) {
67
+ layers.push(pluginContributors('user', userRoot), configContributor('user', userRoot));
68
+ }
69
+ const projectRootsNearestFirst = findProjectScopeRoots(targetCwd, targetProfileId);
70
+ for (const root of [...projectRootsNearestFirst].reverse()) {
71
+ layers.push(pluginContributors('project', root), configContributor('project', root));
72
+ }
73
+ return layers.filter((layer) => layer.length > 0);
74
+ }
75
+ /** Resolve one declared path against its contributing root: it must stay
76
+ * inside that root, be a regular file, and carry a POSIX exec bit when the
77
+ * effective set is resolved. Returns the absolute real path, or the reason it
78
+ * is not usable. */
79
+ function resolveTarget(root, relative) {
80
+ let rootReal;
81
+ let target;
82
+ try {
83
+ rootReal = realpathSync(root);
84
+ target = realpathSync(resolve(root, relative));
85
+ }
86
+ catch {
87
+ return { reason: `target does not exist: ${join(root, relative)}` };
88
+ }
89
+ if (target !== rootReal && !target.startsWith(rootReal + sep)) {
90
+ return { reason: `target escapes its contributing root: ${relative}` };
91
+ }
92
+ const stat = statSync(target);
93
+ if (!stat.isFile())
94
+ return { reason: `target is not a regular file: ${target}` };
95
+ if ((stat.mode & 0o111) === 0)
96
+ return { reason: `target lacks the POSIX exec bit: ${target}` };
97
+ return { target };
98
+ }
99
+ /** The effective bare-command set for one node's cwd/profile, plus every
100
+ * declaration that failed to become one.
101
+ *
102
+ * Layers ascend in precedence (see `contributionLayers`). A name claimed by
103
+ * two contributors in the SAME layer selects no winner: it is reported and
104
+ * removed, so an ambiguous name resolves to nothing rather than to whichever
105
+ * plugin sorted first. A higher layer may still claim that name explicitly —
106
+ * that is an unambiguous choice, not a collision. */
107
+ export function resolveBinContributions(targetCwd, targetProfileId) {
108
+ const bins = new Map();
109
+ const issues = [];
110
+ for (const layer of contributionLayers(targetCwd, targetProfileId)) {
111
+ const claims = new Map();
112
+ for (const contributor of layer) {
113
+ issues.push(...contributor.issues);
114
+ for (const name of Object.keys(contributor.bin)) {
115
+ const existing = claims.get(name);
116
+ if (existing === undefined)
117
+ claims.set(name, [contributor]);
118
+ else
119
+ existing.push(contributor);
120
+ }
121
+ }
122
+ for (const [name, claimants] of claims) {
123
+ if (claimants.length > 1) {
124
+ const origins = claimants.map((claimant) => claimant.origin).join(', ');
125
+ bins.delete(name);
126
+ for (const claimant of claimants) {
127
+ issues.push({ name, origin: claimant.origin, scope: claimant.scope, message: `bare command "${name}" is claimed by ${origins} at the same precedence — no winner is selected and the name is not installed` });
128
+ }
129
+ continue;
130
+ }
131
+ const contributor = claimants[0];
132
+ const resolved = resolveTarget(contributor.root, contributor.bin[name]);
133
+ if ('reason' in resolved) {
134
+ issues.push({ name, origin: contributor.origin, scope: contributor.scope, message: resolved.reason });
135
+ continue;
136
+ }
137
+ bins.set(name, { name, target: resolved.target, origin: contributor.origin, scope: contributor.scope });
138
+ }
139
+ }
140
+ return { bins, issues };
141
+ }
142
+ /** Content address of one effective bare-command set — the shim directory's
143
+ * name. Because the address covers every name and target, an existing
144
+ * directory is already correct: nothing is ever rewritten in place, a changed
145
+ * set simply materializes beside the old one, and two processes racing the
146
+ * same set produce byte-identical trees. */
147
+ function shimAddress(bins) {
148
+ const lines = [...bins.values()]
149
+ .sort((a, b) => a.name.localeCompare(b.name))
150
+ .map((bin) => `${bin.name}\u0000${bin.target}`)
151
+ .join('\n');
152
+ return createHash('sha256').update(lines).digest('hex').slice(0, 32);
153
+ }
154
+ /** Materialize (idempotently) the shim directory for one effective set and
155
+ * return its absolute path, or null when the set is empty. Built in a staging
156
+ * dir and renamed into place, so a concurrent launch either sees no directory
157
+ * or sees a complete one. */
158
+ export function ensureBinShimDir(bins) {
159
+ if (bins.size === 0)
160
+ return null;
161
+ const dir = join(crtrHome(), 'bin-shims', shimAddress(bins));
162
+ if (existsSync(dir))
163
+ return dir;
164
+ const staging = `${dir}.staging-${process.pid}-${Date.now().toString(36)}`;
165
+ mkdirSync(staging, { recursive: true });
166
+ try {
167
+ for (const bin of bins.values())
168
+ symlinkSync(bin.target, join(staging, bin.name));
169
+ renameSync(staging, dir);
170
+ }
171
+ catch (error) {
172
+ rmSync(staging, { recursive: true, force: true });
173
+ // Another launch materializing the same address won the rename — its tree
174
+ // is identical by construction, so the address is still correct.
175
+ if (existsSync(dir))
176
+ return dir;
177
+ throw error;
178
+ }
179
+ return dir;
180
+ }
181
+ /** The shim directory to prepend to a target node's PATH, or null when that
182
+ * node's scopes declare no bare command. The one entry point the spawn-env
183
+ * boundary calls. */
184
+ export function binShimDirFor(targetCwd, targetProfileId) {
185
+ return ensureBinShimDir(resolveBinContributions(targetCwd, targetProfileId).bins);
186
+ }
@@ -89,7 +89,12 @@ export async function bootRoot(opts) {
89
89
  // user's OWN pre-existing shell pane (not one crtr split open): on a
90
90
  // detach-reap of an empty root it must delete the node WITHOUT kill-pane, else
91
91
  // a plain ctrl+c closes the user's whole tmux pane.
92
- const attachEnv = { ...process.env, ...viewerSplitEnv(), CRTR_ATTACH_WAIT_MS: '30000' };
92
+ const attachEnv = {
93
+ ...process.env,
94
+ ...viewerSplitEnv(),
95
+ CRTR_ATTACH_WAIT_MS: '30000',
96
+ ...(opts.prompt !== undefined && opts.prompt !== '' ? { CRTR_ATTACH_INITIAL_WHIP: '1' } : {}),
97
+ };
93
98
  if (here !== null)
94
99
  attachEnv['CRTR_ATTACH_BORROWED_PANE'] = here.pane;
95
100
  mark('boot_root.viewer_spawn');
@@ -1,5 +1,9 @@
1
1
  import type { Socket } from 'node:net';
2
2
  import { FrameDecoder, type BrokerSnapshot, type BrokerToClient, type ClientRole } from '../broker-protocol.js';
3
+ /** True when the live broker has at least one helloed terminal or web viewer.
4
+ * Callers establish broker liveness separately because a crash can leave the
5
+ * last persisted count behind. */
6
+ export declare function hasAttachedBrokerViewers(nodeId: string): boolean;
3
7
  export interface BrokerClient {
4
8
  id: string;
5
9
  role: ClientRole;
@@ -1,10 +1,22 @@
1
- import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { jobDir } from '../../canvas/paths.js';
3
3
  import { emitEvent } from '../../events/emit.js';
4
4
  import { encodeFrame, FrameDecoder, BROKER_READ_CAPS, } from '../broker-protocol.js';
5
5
  /** Per-viewer outbound high-water mark. Slow viewers are shed rather than allowed to backpressure the shared engine indefinitely. */
6
6
  const MAX_QUEUED_FRAMES = 1000;
7
7
  const MAX_PENDING_BYTES = 32 * 1024 * 1024;
8
+ /** True when the live broker has at least one helloed terminal or web viewer.
9
+ * Callers establish broker liveness separately because a crash can leave the
10
+ * last persisted count behind. */
11
+ export function hasAttachedBrokerViewers(nodeId) {
12
+ try {
13
+ const record = JSON.parse(readFileSync(`${jobDir(nodeId)}/attach.json`, 'utf8'));
14
+ return typeof record.viewers === 'number' && record.viewers > 0;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
8
20
  /** Owns connected viewers, their outbound backpressure, and durable attach presence. */
9
21
  export class BrokerClientRegistry {
10
22
  nodeId;
@@ -5,5 +5,5 @@ interface AbortableSession {
5
5
  /** Mark one synchronous ExtensionContext.abort() call as a refresh boundary. */
6
6
  export declare function runRefreshAbort(abort: () => void): void;
7
7
  /** Bound into Pi's extension context by the broker for every session generation. */
8
- export declare function abortSessionFromExtension(session: AbortableSession): void;
8
+ export declare function abortSessionFromExtension(session: AbortableSession, stageRefreshAbort?: () => void): void;
9
9
  export {};
@@ -16,8 +16,10 @@ export function runRefreshAbort(abort) {
16
16
  }
17
17
  }
18
18
  /** Bound into Pi's extension context by the broker for every session generation. */
19
- export function abortSessionFromExtension(session) {
20
- if (refreshAbortScope)
19
+ export function abortSessionFromExtension(session, stageRefreshAbort) {
20
+ if (refreshAbortScope) {
21
21
  session.clearQueue();
22
+ stageRefreshAbort?.();
23
+ }
22
24
  void session.abort();
23
25
  }
@@ -48,9 +48,11 @@ export class FaultRetry {
48
48
  publishSettledProviderFault(generation) {
49
49
  const agentEnd = generation.stagedProviderAgentEnd;
50
50
  const watchdogAbort = generation.stagedWatchdogAbort;
51
+ const refreshAbort = generation.stagedRefreshAbort;
51
52
  const overflowFailure = generation.stagedOverflowFailure;
52
53
  generation.stagedProviderAgentEnd = null;
53
54
  generation.stagedWatchdogAbort = false;
55
+ generation.stagedRefreshAbort = false;
54
56
  generation.stagedOverflowFailure = null;
55
57
  const settledSession = generation.session;
56
58
  // A failed daemon retry settles here with the episode's fault marker still
@@ -84,6 +86,8 @@ export class FaultRetry {
84
86
  }
85
87
  if (last?.stopReason !== 'error')
86
88
  return;
89
+ if (refreshAbort)
90
+ return;
87
91
  const raw = typeof last.errorMessage === 'string' ? last.errorMessage : 'engine error (no errorMessage recorded)';
88
92
  const uncompactedOverflow = isContextOverflow(last, settledSession.model?.contextWindow);
89
93
  const classified = classify('pi→provider', last);
@@ -181,7 +181,9 @@ export type FrameDispatchDeps = {
181
181
  toolGroups: ToolGroupTracker;
182
182
  projection: EventProjection;
183
183
  rebind: RebindDriver;
184
- reviewCompanion: boolean;
184
+ /** The bound review id whose boundary marker opens this companion's visible
185
+ * transcript; undefined for ordinary nodes. */
186
+ boundaryReviewId: string | undefined;
185
187
  pendingDialogs: Map<string, PendingDialog>;
186
188
  sendWelcome: (client: BrokerClient) => void;
187
189
  replayExtraPendingDialogsTo: (client: BrokerClient) => void;