@north-light/crouter 0.3.198 → 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 (314) hide show
  1. package/dist/api/client.d.ts +18 -18
  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 +81 -34
  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 +6 -6
  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 +2 -2
  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/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  99. package/dist/core/__tests__/human-deliver.test.js +31 -26
  100. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  101. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  102. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  103. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  104. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  105. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  106. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  107. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  108. package/dist/core/__tests__/session-cycles.test.js +22 -0
  109. package/dist/core/__tests__/stop-guard.test.js +4 -4
  110. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  111. package/dist/core/bash-jobs.d.ts +6 -0
  112. package/dist/core/bash-jobs.js +10 -0
  113. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  114. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  115. package/dist/core/canvas/attention.js +8 -6
  116. package/dist/core/canvas/browse/model.d.ts +8 -2
  117. package/dist/core/canvas/browse/model.js +11 -4
  118. package/dist/core/canvas/canvas.d.ts +7 -0
  119. package/dist/core/canvas/canvas.js +37 -0
  120. package/dist/core/canvas/crons.d.ts +9 -0
  121. package/dist/core/canvas/crons.js +51 -0
  122. package/dist/core/canvas/node-order.d.ts +22 -1
  123. package/dist/core/canvas/node-order.js +26 -1
  124. package/dist/core/canvas/render-source.d.ts +5 -0
  125. package/dist/core/canvas/render-source.js +1 -0
  126. package/dist/core/canvas/types.d.ts +9 -3
  127. package/dist/core/command-plugins/bundle.d.ts +6 -1
  128. package/dist/core/command-plugins/bundle.js +26 -13
  129. package/dist/core/command.js +2 -1
  130. package/dist/core/config.d.ts +47 -1
  131. package/dist/core/config.js +137 -4
  132. package/dist/core/help.js +1 -1
  133. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  134. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  135. package/dist/core/human/__tests__/page.test.js +56 -98
  136. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  137. package/dist/core/human/answer-text.js +3 -3
  138. package/dist/core/human/answer.d.ts +6 -4
  139. package/dist/core/human/answer.js +4 -10
  140. package/dist/core/human/component-docs.d.ts +11 -7
  141. package/dist/core/human/component-docs.js +361 -88
  142. package/dist/core/human/convention.d.ts +17 -3
  143. package/dist/core/human/convention.js +36 -5
  144. package/dist/core/human/feedback-companion.d.ts +20 -0
  145. package/dist/core/human/feedback-companion.js +98 -0
  146. package/dist/core/human/feedback.d.ts +65 -0
  147. package/dist/core/human/feedback.js +116 -0
  148. package/dist/core/human/page-catalog.d.ts +40 -5
  149. package/dist/core/human/page-catalog.js +102 -30
  150. package/dist/core/human/page-errors.d.ts +4 -0
  151. package/dist/core/human/page-errors.js +7 -0
  152. package/dist/core/human/page-eval.d.ts +20 -0
  153. package/dist/core/human/page-eval.js +123 -0
  154. package/dist/core/human/page-schema.d.ts +23 -191
  155. package/dist/core/human/page-schema.js +60 -89
  156. package/dist/core/human/page-synth.d.ts +1 -1
  157. package/dist/core/human/page-synth.js +15 -18
  158. package/dist/core/human/page.d.ts +12 -27
  159. package/dist/core/human/page.js +142 -474
  160. package/dist/core/human/root.d.ts +6 -2
  161. package/dist/core/human/root.js +12 -4
  162. package/dist/core/human/scan.d.ts +3 -4
  163. package/dist/core/human/scan.js +16 -15
  164. package/dist/core/human/summary.d.ts +1 -2
  165. package/dist/core/human/summary.js +2 -2
  166. package/dist/core/human/tickets.d.ts +20 -14
  167. package/dist/core/human/tickets.js +91 -31
  168. package/dist/core/human/types.d.ts +10 -2
  169. package/dist/core/preview-registry.js +8 -10
  170. package/dist/core/profiles/default-binding.d.ts +4 -0
  171. package/dist/core/profiles/default-binding.js +24 -0
  172. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  173. package/dist/core/profiles/deletion-reservation.js +38 -0
  174. package/dist/core/profiles/manifest.d.ts +3 -0
  175. package/dist/core/profiles/manifest.js +12 -0
  176. package/dist/core/profiles/select.js +2 -2
  177. package/dist/core/review/realize.js +3 -3
  178. package/dist/core/review/store.d.ts +6 -0
  179. package/dist/core/review/store.js +31 -0
  180. package/dist/core/runtime/bearings.d.ts +0 -4
  181. package/dist/core/runtime/bearings.js +4 -62
  182. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  183. package/dist/core/runtime/bin-contributions.js +186 -0
  184. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  185. package/dist/core/runtime/broker/client-registry.js +13 -1
  186. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  187. package/dist/core/runtime/broker/extension-abort.js +4 -2
  188. package/dist/core/runtime/broker/fault-retry.js +4 -0
  189. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  190. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  191. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  192. package/dist/core/runtime/broker/read-ops.js +2 -2
  193. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  194. package/dist/core/runtime/broker/rebind.js +6 -1
  195. package/dist/core/runtime/broker-extension-render.js +19 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  197. package/dist/core/runtime/broker.d.ts +1 -1
  198. package/dist/core/runtime/broker.js +17 -6
  199. package/dist/core/runtime/front-door-env.d.ts +5 -0
  200. package/dist/core/runtime/front-door-env.js +16 -0
  201. package/dist/core/runtime/front-door.d.ts +1 -5
  202. package/dist/core/runtime/front-door.js +2 -5
  203. package/dist/core/runtime/host.d.ts +1 -1
  204. package/dist/core/runtime/invocation.d.ts +6 -0
  205. package/dist/core/runtime/invocation.js +10 -0
  206. package/dist/core/runtime/kickoff.js +34 -0
  207. package/dist/core/runtime/launch.d.ts +2 -6
  208. package/dist/core/runtime/node-read.js +4 -1
  209. package/dist/core/runtime/nodes.js +2 -0
  210. package/dist/core/runtime/promote.d.ts +1 -0
  211. package/dist/core/runtime/promote.js +3 -1
  212. package/dist/core/runtime/session-cycles.d.ts +6 -0
  213. package/dist/core/runtime/session-cycles.js +15 -1
  214. package/dist/core/runtime/session-visibility.d.ts +17 -8
  215. package/dist/core/runtime/session-visibility.js +19 -10
  216. package/dist/core/runtime/spawn-env.d.ts +9 -1
  217. package/dist/core/runtime/spawn-env.js +62 -2
  218. package/dist/core/runtime/stop-guard.d.ts +2 -2
  219. package/dist/core/runtime/stop-guard.js +2 -2
  220. package/dist/core/runtime/stop-signals.d.ts +1 -1
  221. package/dist/core/runtime/stop-signals.js +2 -2
  222. package/dist/core/runtime/tmux-bindings.js +1 -1
  223. package/dist/core/session-model/session-state.d.ts +4 -3
  224. package/dist/core/session-model/session-state.js +14 -1
  225. package/dist/core/user-settings.d.ts +21 -2
  226. package/dist/core/user-settings.js +32 -11
  227. package/dist/core/worktree.d.ts +15 -45
  228. package/dist/core/worktree.js +59 -261
  229. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  230. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  231. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  232. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  233. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  234. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  235. package/dist/daemon/api/handlers/human.js +24 -20
  236. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  237. package/dist/daemon/api/handlers/inbox.js +39 -115
  238. package/dist/daemon/api/handlers/profiles.js +11 -29
  239. package/dist/daemon/api/map.d.ts +6 -1
  240. package/dist/daemon/api/map.js +23 -0
  241. package/dist/daemon/api/server.js +3 -1
  242. package/dist/daemon/companion-retire.d.ts +2 -0
  243. package/dist/daemon/companion-retire.js +33 -0
  244. package/dist/daemon/cron-run.js +14 -16
  245. package/dist/daemon/human/finish.d.ts +12 -7
  246. package/dist/daemon/human/finish.js +101 -38
  247. package/dist/daemon/human/sweep.js +5 -1
  248. package/dist/daemon/profile-delete.d.ts +7 -0
  249. package/dist/daemon/profile-delete.js +306 -0
  250. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  251. package/dist/daemon/review/deliver.js +3 -3
  252. package/dist/daemon/review/finish.d.ts +0 -2
  253. package/dist/daemon/review/finish.js +1 -29
  254. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  255. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  256. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  257. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  258. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  259. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  260. package/dist/pi-extensions/canvas-stophook.js +4 -4
  261. package/dist/shared/generated-context.js +2 -2
  262. package/dist/types.d.ts +44 -2
  263. package/dist/types.js +7 -2
  264. package/package.json +4 -4
  265. package/runtime.lock.json +5 -33
  266. package/dist/builtin-memory/init.md +0 -38
  267. package/dist/builtin-memory/plan.md +0 -19
  268. package/dist/builtin-memory/spec.md +0 -19
  269. package/dist/commands/human/doc.d.ts +0 -2
  270. package/dist/commands/human/doc.js +0 -91
  271. package/dist/commands/human/html.js +0 -81
  272. package/dist/commands/human/inbox.d.ts +0 -2
  273. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  274. package/dist/core/human/__tests__/page-render.test.js +0 -66
  275. package/dist/core/human/html-markdown.d.ts +0 -3
  276. package/dist/core/human/html-markdown.js +0 -288
  277. package/dist/core/human/page-render.d.ts +0 -4
  278. package/dist/core/human/page-render.js +0 -105
  279. package/dist/pages/bundle.css +0 -1
  280. package/dist/pages/bundle.js +0 -1778
  281. package/dist/pages/comments.d.ts +0 -72
  282. package/dist/pages/comments.js +0 -176
  283. package/dist/pages/controls.d.ts +0 -98
  284. package/dist/pages/controls.js +0 -261
  285. package/dist/pages/elements/cards.d.ts +0 -28
  286. package/dist/pages/elements/cards.js +0 -915
  287. package/dist/pages/elements/chart.d.ts +0 -23
  288. package/dist/pages/elements/chart.js +0 -664
  289. package/dist/pages/elements/options.d.ts +0 -30
  290. package/dist/pages/elements/options.js +0 -742
  291. package/dist/pages/elements/pages.d.ts +0 -30
  292. package/dist/pages/elements/pages.js +0 -377
  293. package/dist/pages/elements/slot.d.ts +0 -26
  294. package/dist/pages/elements/slot.js +0 -112
  295. package/dist/pages/elements/table.d.ts +0 -36
  296. package/dist/pages/elements/table.js +0 -1043
  297. package/dist/pages/elements/text.d.ts +0 -31
  298. package/dist/pages/elements/text.js +0 -1175
  299. package/dist/pages/entry.d.ts +0 -8
  300. package/dist/pages/entry.js +0 -8
  301. package/dist/pages/host.d.ts +0 -134
  302. package/dist/pages/host.js +0 -165
  303. package/dist/pages/readonly.d.ts +0 -24
  304. package/dist/pages/readonly.js +0 -31
  305. package/dist/pages/register.d.ts +0 -2
  306. package/dist/pages/register.js +0 -3
  307. package/dist/pages/responses.d.ts +0 -37
  308. package/dist/pages/responses.js +0 -56
  309. package/dist/pages/slot-config.d.ts +0 -36
  310. package/dist/pages/slot-config.js +0 -50
  311. package/dist/pages/types.d.ts +0 -98
  312. package/dist/pages/types.js +0 -23
  313. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  314. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -46,6 +46,20 @@ export interface BrokerSnapshot {
46
46
  }>;
47
47
  title: string | undefined;
48
48
  };
49
+ /** The engine's UNRUN queue — the steer/follow-up texts a writable client
50
+ * sent mid-turn that have not started yet (pi's `_steeringMessages` +
51
+ * `_followUpMessages`, read non-destructively). `state.pendingMessageCount`
52
+ * is the same queue's SIZE; without the texts a (re)attaching viewer can say
53
+ * how many messages are waiting but not which, so it cannot render the
54
+ * queued rows it renders from `queue_update` while attached — the welcome
55
+ * would no longer fully determine viewer state.
56
+ *
57
+ * Ingest defensively: a broker pinned to a pre-`queued` runtime generation is
58
+ * still live and sends `welcome` without it. */
59
+ queued?: {
60
+ steering: string[];
61
+ followUp: string[];
62
+ };
49
63
  /** Broker-owned summaries keyed by the first tool-call id in each group.
50
64
  * Structured objects with bullets and filesDeleted.
51
65
  * Optional so snapshots from an older live runtime remain readable. */
@@ -29,7 +29,7 @@ interface PendingDialog {
29
29
  * bash children. No-op before the session is built or after a clean dispose. */
30
30
  export declare function disposeActiveSession(): void;
31
31
  export declare function runBroker(nodeId: string, startupAt?: bigint, startupTs?: string): Promise<void>;
32
- export declare function snapshotMessages(session: CreateAgentSessionResult['session'], reviewCompanion?: boolean): BrokerSnapshot['messages'];
32
+ export declare function snapshotMessages(session: CreateAgentSessionResult['session'], boundaryReviewId?: string): BrokerSnapshot['messages'];
33
33
  export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{
34
34
  session: CreateAgentSessionResult['session'];
35
35
  services: AgentSessionServices;
@@ -216,8 +216,11 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
216
216
  let { session, services, resuming, runtime } = await buildBrokerSession(engine, cfg);
217
217
  // Review identity is immutable for the broker's lifetime. Snapshot producers
218
218
  // close over this one construction-time classification rather than re-reading
219
- // canvas metadata for every welcome or session replacement.
220
- const reviewCompanion = (await brokerExtensionState(nodeId)).node.review_binding != null;
219
+ // canvas metadata for every welcome or session replacement. The BOUND review
220
+ // id (not a mere boolean) is what selects this companion's own boundary
221
+ // marker — a fork of another companion inherits that companion's earlier
222
+ // marker, and slicing at any marker would reveal its transcript.
223
+ const boundaryReviewId = (await brokerExtensionState(nodeId)).node.review_binding?.review_id;
221
224
  // M3: name the boot session as the process-fatal disposal target immediately —
222
225
  // enqueueRebind reassigns this on every later replacement, but without this
223
226
  // initial assignment `activeSession` stays null (and disposeActiveSession() a
@@ -355,7 +358,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
355
358
  const buildSnapshot = () => {
356
359
  const liveSession = rebind.session();
357
360
  return ({
358
- messages: snapshotMessages(liveSession, reviewCompanion),
361
+ messages: snapshotMessages(liveSession, boundaryReviewId),
359
362
  stats: liveSession.getSessionStats(),
360
363
  // pi's `get_state` (rpc-mode.js), field for field, from the same getters.
361
364
  // The ONE divergence: a model-less session (undefined OR the SDK
@@ -382,6 +385,14 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
382
385
  },
383
386
  toolGroupSummaries: toolGroups.summaries(),
384
387
  workingActivity: projection.currentWorkingActivity(),
388
+ // The UNRUN queue's texts, read non-destructively (`clearQueue()` would
389
+ // consume them). Same two arrays a live `queue_update` carries, so a client
390
+ // that attaches mid-queue lands on the state its next `queue_update` would
391
+ // have produced.
392
+ queued: {
393
+ steering: [...liveSession.getSteeringMessages()],
394
+ followUp: [...liveSession.getFollowUpMessages()],
395
+ },
385
396
  });
386
397
  };
387
398
  // Send a client its catch-up snapshot. welcome.pending_dialog carries a single
@@ -509,7 +520,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
509
520
  rebind.setRebindSession();
510
521
  await rebind.enqueueRebind(session, services);
511
522
  const frameDispatch = createFrameDispatchContext({
512
- nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion,
523
+ nodeId, cfg, registry, toolGroups, projection, rebind, boundaryReviewId,
513
524
  pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit,
514
525
  persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec,
515
526
  formatExactModelSpec, resolveLaunchModel,
@@ -673,13 +684,13 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
673
684
  // conversation instead of just the post-yield window; single-cycle sessions and
674
685
  // the fake-engine fixture (no tree accessors) fall through to plain
675
686
  // `buildSessionContext().messages` unchanged.
676
- export function snapshotMessages(session, reviewCompanion = false) {
687
+ export function snapshotMessages(session, boundaryReviewId) {
677
688
  // Cast: the real SDK's `SessionEntry.data` is `unknown` (untyped custom-entry
678
689
  // payload), narrower than `CycleSessionManagerLike`'s structural read of the
679
690
  // `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
680
691
  // are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
681
692
  const messages = cycleAwareMessages(session.sessionManager);
682
- return visibleMessages(messages, { reviewCompanion });
693
+ return visibleMessages(messages, { boundaryReviewId });
683
694
  }
684
695
  // ---------------------------------------------------------------------------
685
696
  // resolveLaunchModel — the SOLE launch model-selection policy, module-scoped
@@ -0,0 +1,5 @@
1
+ /** Env marker set on every pi the front door boots. Its presence means we are
2
+ * already inside a front-door-booted root, so a nested front-door launch must
3
+ * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
4
+ * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
5
+ export declare const FRONT_DOOR_ENV = "CRTR_FRONT_DOOR";
@@ -0,0 +1,16 @@
1
+ // The front-door recursion-guard env name, alone in a dependency-free module.
2
+ //
3
+ // `front-door.ts` owns the boot path and reaches the canvas through the
4
+ // dynamic imports inside `maybeBootRoot`; every module that only needs the
5
+ // MARKER NAME must not inherit that reach. The shipped broker extensions are
6
+ // gated on their static import graph, which cannot tell a lazily-imported boot
7
+ // dependency from an eagerly-loaded one, so importing the constant from
8
+ // `front-door.js` reads as an extension reaching canvas.db.
9
+ //
10
+ // `front-door.ts` re-exports this, so `FRONT_DOOR_ENV` stays importable from
11
+ // either module and every pi-booting path keeps setting the same marker.
12
+ /** Env marker set on every pi the front door boots. Its presence means we are
13
+ * already inside a front-door-booted root, so a nested front-door launch must
14
+ * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
15
+ * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
16
+ export const FRONT_DOOR_ENV = 'CRTR_FRONT_DOOR';
@@ -1,8 +1,4 @@
1
- /** Env marker set on every pi the front door boots. Its presence means we are
2
- * already inside a front-door-booted root, so a nested front-door launch must
3
- * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
4
- * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
5
- export declare const FRONT_DOOR_ENV = "CRTR_FRONT_DOOR";
1
+ export { FRONT_DOOR_ENV } from './front-door-env.js';
6
2
  /** If this invocation is a front-door (root) launch, boot it and never return.
7
3
  * Returns false when it's a recognized subcommand / help / unknown token (let
8
4
  * the dispatcher handle it — for unknown tokens it errors cleanly).
@@ -21,6 +21,7 @@
21
21
  import { existsSync, statSync } from 'node:fs';
22
22
  import { resolve as resolvePath } from 'node:path';
23
23
  import { SUBTREE_NAMES } from '../../build-root.js';
24
+ import { FRONT_DOOR_ENV } from './front-door-env.js';
24
25
  // NOTE: the boot-path deps — ./boot-root.js (bootRoot), ./resume-root.js (resumeRoot),
25
26
  // ./package-health.js (missingPackageWarning + piAgentDir) — are DYNAMICALLY
26
27
  // imported inside maybeBootRoot, NOT statically here. `cli.ts` calls
@@ -89,11 +90,7 @@ function parseRootArgs(tokens) {
89
90
  const prompt = positionals.length > 0 ? positionals.join(' ') : undefined;
90
91
  return { cwd, prompt, name, kind, profile, pickProfile, resume };
91
92
  }
92
- /** Env marker set on every pi the front door boots. Its presence means we are
93
- * already inside a front-door-booted root, so a nested front-door launch must
94
- * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
95
- * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
96
- export const FRONT_DOOR_ENV = 'CRTR_FRONT_DOOR';
93
+ export { FRONT_DOOR_ENV } from './front-door-env.js';
97
94
  /** If this invocation is a front-door (root) launch, boot it and never return.
98
95
  * Returns false when it's a recognized subcommand / help / unknown token (let
99
96
  * the dispatcher handle it — for unknown tokens it errors cleanly).
@@ -1,5 +1,5 @@
1
1
  import { type NodeRow } from '../canvas/index.js';
2
- import type { PiInvocation } from './launch.js';
2
+ import type { PiInvocation } from './invocation.js';
3
3
  /** View-side launch hints the broker host needs beyond the PiInvocation. The
4
4
  * broker uses `cwd` + `resuming`; `name` is carried for the node's display. */
5
5
  export interface LaunchPlacement {
@@ -0,0 +1,6 @@
1
+ export interface PiInvocation {
2
+ /** argv after the `pi` binary. */
3
+ argv: string[];
4
+ /** env to merge into the process. */
5
+ env: Record<string, string>;
6
+ }
@@ -0,0 +1,10 @@
1
+ // The broker-launch recipe shape, alone in a dependency-free module.
2
+ //
3
+ // `launch.ts` builds a PiInvocation and therefore reaches canvas state; the
4
+ // modules that only need to READ the recipe shape (spawn-env, host) must not.
5
+ // A type-only import is erased at compile time, but the shipped broker
6
+ // extensions are gated on their STATIC import graph — the gate cannot tell an
7
+ // erased edge from a live one, so an extension that reaches spawn-env would
8
+ // read as reaching canvas.db through launch.ts. Keeping the interface here
9
+ // makes the graph say what the runtime already does.
10
+ export {};
@@ -20,6 +20,7 @@
20
20
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
21
21
  import { join } from 'node:path';
22
22
  import { contextDir, reportsDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
23
+ import { activeBackgroundBashJobs, bashJobCommandLine, formatBashElapsed } from '../bash-jobs.js';
23
24
  import { readRoadmap, roadmapPath } from './roadmap.js';
24
25
  import { buildWakeBearings } from './bearings.js';
25
26
  import { personaDrift, commitPersonaAck } from './persona.js';
@@ -205,6 +206,33 @@ function feedBlock(nodeId, unreadDigest) {
205
206
  }
206
207
  return `<feed>\n${lines.join('\n')}\n</feed>`;
207
208
  }
209
+ /** One-line label for a job: its purpose when the caller gave one, else its real
210
+ * command line, clipped so a long pipeline stays scannable. */
211
+ function jobLabel(purpose, command) {
212
+ const raw = (purpose ?? bashJobCommandLine(command)).trim() || '(no command)';
213
+ return raw.length > 120 ? `${raw.slice(0, 117)}…` : raw;
214
+ }
215
+ /** Live file-backed background bash jobs this node owns. A fresh revive has no
216
+ * memory of handing anything off, so without this block the node cannot tell a
217
+ * command it started is still running: it can neither read the log nor decide
218
+ * whether to wait, and may relaunch the same work. Returns null — and the
219
+ * kickoff omits the block entirely — when nothing is live. */
220
+ function backgroundJobsBlock(nodeId) {
221
+ const jobs = activeBackgroundBashJobs(contextDir(nodeId));
222
+ if (jobs.length === 0)
223
+ return null;
224
+ const now = Date.now();
225
+ const one = jobs.length === 1;
226
+ const lines = [
227
+ `${jobs.length} background bash job${one ? '' : 's'} you started ${one ? 'is' : 'are'} STILL RUNNING, detached from any turn and unaffected by your refresh. ` +
228
+ `Each one sends you a message when it exits — that wake is automatic, so do not poll it and do not relaunch the same command.`,
229
+ ];
230
+ for (const job of jobs) {
231
+ lines.push(` - ${job.jobId} — ${jobLabel(job.purpose, job.command)} · running ${formatBashElapsed(now - job.startedAtMs)} · log ${job.logPath}`);
232
+ }
233
+ lines.push('', 'Read output so far: `tail -n 50 <log>` (or `crtr node bash list --tail 20`). Abandon one: `crtr node bash kill <job-id>`.');
234
+ return `<background-jobs>\n${lines.join('\n')}\n</background-jobs>`;
235
+ }
208
236
  // ---------------------------------------------------------------------------
209
237
  // buildReviveKickoff — assemble the full fresh-revive first message.
210
238
  // ---------------------------------------------------------------------------
@@ -257,6 +285,12 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
257
285
  // revive (a new pi process boot) and re-injects buildContextIntro(), which
258
286
  // already carries the sidecar as a hidden sibling of <crtr-bearings>; that seam
259
287
  // covers this path, so duplicating it into the visible prompt would leak it.
288
+ // Live background bash jobs sit beside the context-dir listing: both describe
289
+ // this node's OWN in-flight state, and both precede <feed>, which is about the
290
+ // other nodes it awaits. Absent when nothing is running.
291
+ const jobsBlock = backgroundJobsBlock(nodeId);
292
+ if (jobsBlock !== null)
293
+ parts.push(jobsBlock);
260
294
  parts.push(feedBlock(nodeId, bearings.unreadDigest));
261
295
  parts.push(bearings.yieldMsg !== null
262
296
  ? `<yield-message>\n${bearings.yieldMsg.trim()}\n</yield-message>`
@@ -1,3 +1,4 @@
1
+ import type { PiInvocation } from './invocation.js';
1
2
  export { CANVAS_EXTENSIONS, CANVAS_STOPHOOK_PATH, CANVAS_INBOX_WATCHER_PATH, CANVAS_RECAP_PATH, CANVAS_GOAL_CAPTURE_PATH, CANVAS_PASSIVE_CONTEXT_PATH, CANVAS_CONTEXT_INTRO_PATH, CANVAS_TOOL_GUIDE_PATH, CANVAS_DOC_SUBSTRATE_PATH, CANVAS_STRUCTURED_OUTPUT_PATH, CANVAS_BASH_VALVE_PATH, } from './canvas-extensions.js';
2
3
  import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
3
4
  import type { ModelProvider, ModelStrength, ScopeConfig } from '../../types.js';
@@ -104,12 +105,7 @@ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
104
105
  launch: LaunchSpec;
105
106
  lifecycle: 'terminal' | 'resident';
106
107
  };
107
- export interface PiInvocation {
108
- /** argv after the `pi` binary. */
109
- argv: string[];
110
- /** env to merge into the process. */
111
- env: Record<string, string>;
112
- }
108
+ export type { PiInvocation } from './invocation.js';
113
109
  /** Construct the pi invocation for a node.
114
110
  * - fresh start: pass `prompt` (the node's first user message), no resume.
115
111
  * - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
@@ -152,7 +152,7 @@ export async function readNodeSnapshot(nodeId) {
152
152
  copyFileSync(sessionFile, copy);
153
153
  const manager = SessionManager.open(copy);
154
154
  const context = manager.buildSessionContext();
155
- const messages = visibleMessages(cycleAwareMessages(manager), { reviewCompanion: node.review_binding != null });
155
+ const messages = visibleMessages(cycleAwareMessages(manager), { boundaryReviewId: node.review_binding?.review_id });
156
156
  const sessionId = manager.getSessionId();
157
157
  const sessionName = manager.getSessionName();
158
158
  const snapshot = {
@@ -179,6 +179,9 @@ export async function readNodeSnapshot(nodeId) {
179
179
  // process state (last setStatus/setWidget/setTitle) and is not persisted,
180
180
  // so an offline read has none.
181
181
  display: { statuses: {}, widgets: {}, title: undefined },
182
+ // The unrun queue lives in the engine's memory; a dormant node has no
183
+ // engine, so nothing is waiting to run.
184
+ queued: { steering: [], followUp: [] },
182
185
  // Sidecar summaries survive broker exit so a dormant transcript receives
183
186
  // the same completed-group data as a live welcome snapshot. Malformed or
184
187
  // absent sidecars are deliberately ignored: tool calls remain visible.
@@ -19,6 +19,7 @@ import { memoryDir } from './memory.js';
19
19
  import { resolveInstallId } from '../canvas/install-id.js';
20
20
  import { canvasDbPath } from '../canvas/paths.js';
21
21
  import { usage } from '../errors.js';
22
+ import { assertProfileAvailableForBirth } from '../profiles/deletion-reservation.js';
22
23
  // crtrd sets this once at boot (setInstallId) and it wins unconditionally —
23
24
  // a daemon process only ever serves one canvas home for its whole lifetime,
24
25
  // so no keying is needed there. Every OTHER in-process caller (tests,
@@ -259,6 +260,7 @@ export function spawnNode(opts) {
259
260
  if (parent !== null && getNode(parent) === null) {
260
261
  throw new Error(`cannot spawn under unknown parent node: ${parent}`);
261
262
  }
263
+ assertProfileAvailableForBirth(meta.profile_id ?? null);
262
264
  createNode(meta);
263
265
  // Create the node-local memory directory so substrate docs can be written
264
266
  // directly into it without a separate mkdir. Skip for ephemeral human-bridge
@@ -19,6 +19,7 @@ export declare function reshapeNode(nodeId: string, opts?: {
19
19
  model?: string;
20
20
  mode?: Mode;
21
21
  lifecycle?: Lifecycle;
22
+ profileId?: string | null;
22
23
  }, beforePersist?: () => void): NodeMeta;
23
24
  /** Promote a node to an orchestrator (mode→orchestrator), optionally
24
25
  * specializing its kind (e.g. a `general` worker becoming a
@@ -42,6 +42,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
42
42
  const targetModel = opts.model ?? node.model_override ?? undefined;
43
43
  const targetMode = opts.mode ?? node.mode;
44
44
  const targetLifecycle = opts.lifecycle ?? node.lifecycle;
45
+ const targetProfileId = opts.profileId === undefined ? node.profile_id : opts.profileId;
45
46
  // Rewrite the launch spec to the target kind/mode's persona so the *next*
46
47
  // revive comes back as this exact shape (polymorph stage 2). nodeEnv reads
47
48
  // meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for the live
@@ -66,7 +67,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
66
67
  // Resolve kind/ladder config against the NODE's own cwd + profile, not
67
68
  // this process's ambient scope.
68
69
  cwd: node.cwd,
69
- profileId: node.profile_id,
70
+ profileId: targetProfileId,
70
71
  });
71
72
  // Promotion seeds after its target launch has been built but before the
72
73
  // mutation is persisted, preserving the original promote() failure order.
@@ -81,6 +82,7 @@ export function reshapeNode(nodeId, opts = {}, beforePersist) {
81
82
  kind: targetKind,
82
83
  mode: targetMode,
83
84
  lifecycle: targetLifecycle,
85
+ profile_id: targetProfileId,
84
86
  launch,
85
87
  ...(carriesDefaultHandle ? { name: targetKind } : {}),
86
88
  // Persist a newly-chosen tier so it is durable across future revives; omit
@@ -20,6 +20,12 @@ export interface CycleSessionManagerLike {
20
20
  * path root-first. */
21
21
  getBranch?(fromId?: string): SessionEntry[];
22
22
  }
23
+ /** True when an assistant turn ended because its own abort fired — an ESC, a
24
+ * critical-tier interrupt, or the self-abort a refresh-yield performs. */
25
+ export declare function endedByAbort(message: {
26
+ stopReason?: string;
27
+ errorMessage?: string;
28
+ }): boolean;
23
29
  /** A refresh-yield deliberately aborts its own turn before the next cycle is
24
30
  * rooted. Preserve that assistant message (including tool calls and prose), but
25
31
  * omit the failure annotation: the cycle divider is the truthful visible seam. */
@@ -32,11 +32,25 @@ function cycleDivider(cycle, timestamp) {
32
32
  timestamp: new Date(timestamp).getTime(),
33
33
  };
34
34
  }
35
+ /** Text a provider attaches when the request's own AbortSignal fired. Pi's
36
+ * providers stamp `stopReason: 'aborted'` when they observe the signal
37
+ * themselves, but an abort that lands while the next request is starting
38
+ * reaches their catch as a bare AbortError instead, and is recorded as
39
+ * `stopReason: 'error'` carrying this text. Both spellings are the same healthy
40
+ * event — a turn that stopped because something asked it to stop. */
41
+ const ABORT_ERROR_TEXT = /\b(?:request|operation) was aborted\b|\bthe user aborted\b/i;
42
+ /** True when an assistant turn ended because its own abort fired — an ESC, a
43
+ * critical-tier interrupt, or the self-abort a refresh-yield performs. */
44
+ export function endedByAbort(message) {
45
+ if (message.stopReason === 'aborted')
46
+ return true;
47
+ return message.stopReason === 'error' && ABORT_ERROR_TEXT.test(message.errorMessage ?? '');
48
+ }
35
49
  /** A refresh-yield deliberately aborts its own turn before the next cycle is
36
50
  * rooted. Preserve that assistant message (including tool calls and prose), but
37
51
  * omit the failure annotation: the cycle divider is the truthful visible seam. */
38
52
  export function withoutYieldAbort(message) {
39
- if (message.role !== 'assistant' || message.stopReason !== 'aborted')
53
+ if (message.role !== 'assistant' || !endedByAbort(message))
40
54
  return message;
41
55
  const { stopReason: _stopReason, errorMessage: _errorMessage, ...visible } = message;
42
56
  return visible;
@@ -12,18 +12,23 @@ export declare function isReviewBoundary(entry: unknown, reviewId?: string): boo
12
12
  /**
13
13
  * Project snapshot messages to the visible window for a review companion.
14
14
  *
15
+ * A companion passes its binding's review id so the slice starts at ITS OWN
16
+ * boundary marker: a node forked from another companion inherits that
17
+ * companion's earlier marker, and slicing at the first marker of any review
18
+ * would reveal the origin companion's transcript.
19
+ *
15
20
  * Contract (in evaluation order):
16
- * 1. Marker present → slice from the **first** occurrence (marker included) to
17
- * the end. Everything before the marker is inherited context, hidden from
18
- * the person.
19
- * 2. Not a review companion → return input unchanged (same reference). Ordinary
20
- * nodes are not filtered.
21
- * 3. No marker and review companion → a leading compaction summary is replaced
21
+ * 1. This binding's marker present (any marker when the node has no binding) →
22
+ * slice from the **first** occurrence (marker included) to the end.
23
+ * Everything before the marker is inherited context, hidden from the person.
24
+ * 2. Not a companion (`boundaryReviewId` undefined) → return input unchanged
25
+ * (same reference). Ordinary nodes are not filtered.
26
+ * 3. Bound but no matching marker → a leading compaction summary is replaced
22
27
  * with a synthetic boundary while its retained tail remains visible. Every
23
28
  * other markerless shape fails closed with the synthetic boundary alone.
24
29
  */
25
30
  export declare function visibleMessages(messages: BrokerSnapshot['messages'], opts: {
26
- reviewCompanion: boolean;
31
+ boundaryReviewId: string | undefined;
27
32
  }): BrokerSnapshot['messages'];
28
33
  /**
29
34
  * Project a session tree to the review companion's visible transcript boundary.
@@ -33,5 +38,9 @@ export declare function visibleMessages(messages: BrokerSnapshot['messages'], op
33
38
  * boundary is crossed, its complete descendant subtree remains navigable. An
34
39
  * absent boundary produces an empty tree, so picker previews never disclose an
35
40
  * inherited entry before a mutation guard can reject its selection.
41
+ *
42
+ * `boundaryReviewId` selects only this binding's marker as a root, so a
43
+ * companion forked from another companion never navigates into the inherited
44
+ * subtree rooted at the origin's earlier marker.
36
45
  */
37
- export declare function boundaryRootedTree(tree: readonly WireSessionTreeNode[]): WireSessionTreeNode[];
46
+ export declare function boundaryRootedTree(tree: readonly WireSessionTreeNode[], boundaryReviewId?: string): WireSessionTreeNode[];
@@ -38,21 +38,26 @@ function syntheticBoundary(timestamp) {
38
38
  /**
39
39
  * Project snapshot messages to the visible window for a review companion.
40
40
  *
41
+ * A companion passes its binding's review id so the slice starts at ITS OWN
42
+ * boundary marker: a node forked from another companion inherits that
43
+ * companion's earlier marker, and slicing at the first marker of any review
44
+ * would reveal the origin companion's transcript.
45
+ *
41
46
  * Contract (in evaluation order):
42
- * 1. Marker present → slice from the **first** occurrence (marker included) to
43
- * the end. Everything before the marker is inherited context, hidden from
44
- * the person.
45
- * 2. Not a review companion → return input unchanged (same reference). Ordinary
46
- * nodes are not filtered.
47
- * 3. No marker and review companion → a leading compaction summary is replaced
47
+ * 1. This binding's marker present (any marker when the node has no binding) →
48
+ * slice from the **first** occurrence (marker included) to the end.
49
+ * Everything before the marker is inherited context, hidden from the person.
50
+ * 2. Not a companion (`boundaryReviewId` undefined) → return input unchanged
51
+ * (same reference). Ordinary nodes are not filtered.
52
+ * 3. Bound but no matching marker → a leading compaction summary is replaced
48
53
  * with a synthetic boundary while its retained tail remains visible. Every
49
54
  * other markerless shape fails closed with the synthetic boundary alone.
50
55
  */
51
56
  export function visibleMessages(messages, opts) {
52
- const markerIndex = messages.findIndex((message) => isReviewBoundary(message));
57
+ const markerIndex = messages.findIndex((message) => isReviewBoundary(message, opts.boundaryReviewId));
53
58
  if (markerIndex !== -1)
54
59
  return messages.slice(markerIndex);
55
- if (!opts.reviewCompanion)
60
+ if (opts.boundaryReviewId === undefined)
56
61
  return messages;
57
62
  const [first, ...retainedTail] = messages;
58
63
  if (first?.role === 'compactionSummary') {
@@ -68,14 +73,18 @@ export function visibleMessages(messages, opts) {
68
73
  * boundary is crossed, its complete descendant subtree remains navigable. An
69
74
  * absent boundary produces an empty tree, so picker previews never disclose an
70
75
  * inherited entry before a mutation guard can reject its selection.
76
+ *
77
+ * `boundaryReviewId` selects only this binding's marker as a root, so a
78
+ * companion forked from another companion never navigates into the inherited
79
+ * subtree rooted at the origin's earlier marker.
71
80
  */
72
- export function boundaryRootedTree(tree) {
81
+ export function boundaryRootedTree(tree, boundaryReviewId) {
73
82
  const retainSubtree = (node) => ({
74
83
  ...node,
75
84
  children: node.children.map(retainSubtree),
76
85
  });
77
86
  const project = (node) => {
78
- if (isReviewBoundary(node.entry))
87
+ if (isReviewBoundary(node.entry, boundaryReviewId))
79
88
  return [retainSubtree(node)];
80
89
  return node.children.flatMap(project);
81
90
  };
@@ -1,4 +1,12 @@
1
- import type { PiInvocation } from './launch.js';
1
+ import type { PiInvocation } from './invocation.js';
2
+ /** The unshimmed PATH admitted from the host. Broker and cron shells resolve
3
+ * crouter-owned executables from this value, while commands those shells run
4
+ * still receive the shim-prepended PATH. */
5
+ export declare const ADMITTED_HOST_PATH_ENV = "CRTR_ADMITTED_HOST_PATH";
6
+ /** Resolve a regular executable against a shell PATH without executing it.
7
+ * The result is absolute, or null when no PATH entry supplies the command. */
8
+ export declare function resolvePathExecutable(name: string, env: NodeJS.ProcessEnv, cwd: string): string | null;
9
+ export declare function resolveAdmittedHostCommand(name: string, env: NodeJS.ProcessEnv, cwd: string): string;
2
10
  /** Sources A–C: the operational base shared by the broker boundary (this
3
11
  * module's `buildBrokerEnv`) and cron executor — the single shared definition
4
12
  * of "non-secret host state the runtime exposes to any spawned shell." Does not include
@@ -15,6 +15,13 @@
15
15
  // `spawnEnv.allow` entry. `inv.env` (D) is crtr-constructed and trusted, so
16
16
  // it overlays unconditionally, layered after E.
17
17
  //
18
+ // One name IS rewritten rather than merely admitted: `PATH`. Bare-binary
19
+ // contributions (`bin` in a plugin manifest or a scope `config.json`) are
20
+ // materialized into a crouter-owned shim directory by
21
+ // `core/runtime/bin-contributions.ts`, and that directory is PREPENDED here so
22
+ // a declared bare command wins its name in every node's bash. The admitted
23
+ // host PATH stays intact behind it.
24
+ //
18
25
  // `GIT_*` is deliberately absent from the allowlist: the whole namespace is an
19
26
  // auth/config-injection surface (`GIT_CONFIG_KEY_*`/`GIT_CONFIG_VALUE_*`,
20
27
  // `GIT_ASKPASS`, `GIT_SSH_COMMAND`), not a non-secret operational family, and
@@ -22,9 +29,12 @@
22
29
  // `~/.gitconfig`, reached through the allowlisted `HOME`; SSH remotes via the
23
30
  // allowlisted `SSH_AUTH_SOCK`). A deliberate `GIT_*` need goes through
24
31
  // `spawnEnv.allow`.
32
+ import { statSync } from 'node:fs';
33
+ import { delimiter, resolve } from 'node:path';
25
34
  import { readMergedLaunchConfig } from '../config.js';
26
35
  import { readProfileEnvVars } from '../profiles/env-store.js';
27
- import { FRONT_DOOR_ENV } from './front-door.js';
36
+ import { binShimDirFor } from './bin-contributions.js';
37
+ import { FRONT_DOOR_ENV } from './front-door-env.js';
28
38
  /** A1 — literal env-var names copied verbatim (no value inspection): a login
29
39
  * shell plus dev toolchain needs exactly these, and none of them can carry a
30
40
  * credential. */
@@ -54,6 +64,39 @@ const OPERATIONAL_LITERAL_NAMES = new Set([
54
64
  * file-backed in `auth.json`, never an env value). `GIT_` is NOT here — see
55
65
  * module comment. */
56
66
  const OPERATIONAL_PREFIXES = ['LC_', 'XDG_', 'PI_'];
67
+ /** The unshimmed PATH admitted from the host. Broker and cron shells resolve
68
+ * crouter-owned executables from this value, while commands those shells run
69
+ * still receive the shim-prepended PATH. */
70
+ export const ADMITTED_HOST_PATH_ENV = 'CRTR_ADMITTED_HOST_PATH';
71
+ /** Resolve a regular executable against a shell PATH without executing it.
72
+ * The result is absolute, or null when no PATH entry supplies the command. */
73
+ export function resolvePathExecutable(name, env, cwd) {
74
+ const pathValue = env['PATH'];
75
+ if (pathValue === undefined || pathValue === '')
76
+ return null;
77
+ for (const directory of pathValue.split(delimiter)) {
78
+ const candidate = resolve(cwd, directory === '' ? '.' : directory, name);
79
+ try {
80
+ const stat = statSync(candidate);
81
+ if (stat.isFile() && (stat.mode & 0o111) !== 0)
82
+ return candidate;
83
+ }
84
+ catch {
85
+ // Keep searching exactly as PATH resolution does.
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ export function resolveAdmittedHostCommand(name, env, cwd) {
91
+ const pathValue = env[ADMITTED_HOST_PATH_ENV] ?? env['PATH'];
92
+ if (pathValue === undefined || pathValue === '') {
93
+ throw new Error(`cannot resolve crouter-owned ${name}: the admitted host PATH is empty`);
94
+ }
95
+ const command = resolvePathExecutable(name, { PATH: pathValue }, cwd);
96
+ if (command !== null)
97
+ return command;
98
+ throw new Error(`cannot resolve crouter-owned ${name} from the admitted host PATH`);
99
+ }
57
100
  /** A2 — proxy var names. Admitted only when the value's URL authority carries
58
101
  * no userinfo component (no `user@` / `user:pass@`) — the one value-inspected
59
102
  * entry in the whole builder, because a proxy var is the single operational
@@ -151,6 +194,21 @@ export function buildOperationalEnvBase(opts) {
151
194
  out[key] = value;
152
195
  }
153
196
  }
197
+ // Preserve the unshimmed admitted host PATH across broker and cron children.
198
+ // A nested `crtr` launched from an agent inherits this constructed value,
199
+ // rather than mistaking its shim-prepended PATH for the host path.
200
+ const admittedHostPath = host[ADMITTED_HOST_PATH_ENV] ?? out['PATH'];
201
+ if (admittedHostPath !== undefined && admittedHostPath !== '')
202
+ out[ADMITTED_HOST_PATH_ENV] = admittedHostPath;
203
+ // Bare-binary contributions (`core/runtime/bin-contributions.ts`): the shim
204
+ // dir holding one symlink per declared bare command, PREPENDED so a
205
+ // contribution wins its name, with the host PATH admitted above left intact
206
+ // behind it. Resolved from the TARGET's own cwd/profile like source C.
207
+ const shimDir = binShimDirFor(opts.targetCwd, opts.targetProfileId);
208
+ if (shimDir !== null) {
209
+ const hostPath = out['PATH'];
210
+ out['PATH'] = hostPath === undefined || hostPath === '' ? shimDir : `${shimDir}:${hostPath}`;
211
+ }
154
212
  return out;
155
213
  }
156
214
  /** The one authoritative broker child env: sources A–C (the operational
@@ -170,10 +228,12 @@ export function buildOperationalEnvBase(opts) {
170
228
  export function buildBrokerEnv(inv) {
171
229
  const targetCwd = inv.env['CRTR_NODE_CWD'] ?? process.cwd();
172
230
  const targetProfileId = inv.env['CRTR_PROFILE_ID'] || null;
231
+ const operational = buildOperationalEnvBase({ targetCwd, targetProfileId });
173
232
  return {
174
- ...buildOperationalEnvBase({ targetCwd, targetProfileId }),
233
+ ...operational,
175
234
  ...readProfileEnvVars(targetProfileId),
176
235
  ...inv.env,
236
+ [ADMITTED_HOST_PATH_ENV]: operational[ADMITTED_HOST_PATH_ENV],
177
237
  [FRONT_DOOR_ENV]: '1',
178
238
  };
179
239
  }
@@ -2,8 +2,8 @@ export { STALL_REPROMPT } from '../../shared/generated-context.js';
2
2
  export interface StopSignals {
3
3
  /** Did the node call `push --final` (finish) this turn? */
4
4
  pushedFinal: boolean;
5
- /** Did the node call `crtr ask` (escalate to the human) this turn? */
6
- askedHuman: boolean;
5
+ /** Did the node call `crtr ask` (escalate to the user) this turn? */
6
+ requestedHumanReply: boolean;
7
7
  }
8
8
  export type StopAction = {
9
9
  action: 'allow';
@@ -37,7 +37,7 @@ function formatInvalidStructuredOutputReprompt(error) {
37
37
  `1. Remove the invalid file (the output-schema.json in your node directory has been corrupted)\n` +
38
38
  `2. Fix the corruption if you know the original request schema\n\n` +
39
39
  `After resolving the file, retry the submission. ` +
40
- `If this is unexpected, escalate to the human using \`crtr human ask\`.`);
40
+ `If this is unexpected, escalate to the user using \`crtr human send\`.`);
41
41
  }
42
42
  /** Decide what to do when a node stops. crtrd supplies every current-canvas
43
43
  * fact; the broker contributes only whether its Pi-owned background job registry
@@ -45,7 +45,7 @@ function formatInvalidStructuredOutputReprompt(error) {
45
45
  export function evaluateStop(nodeId, signals, backgroundJobsRunning = activeBackgroundBashJobs(contextDir(nodeId)).length > 0) {
46
46
  if (signals.pushedFinal)
47
47
  return { action: 'allow', reason: 'finished' };
48
- if (signals.askedHuman)
48
+ if (signals.requestedHumanReply)
49
49
  return { action: 'allow', reason: 'escalated' };
50
50
  // A pending structured-output request overrides every stored-state dormancy
51
51
  // path: a post-final one-off follow-up still owes `submit` before it may stop.
@@ -1,6 +1,6 @@
1
1
  export interface StopSignals {
2
2
  pushedFinal: boolean;
3
- askedHuman: boolean;
3
+ requestedHumanReply: boolean;
4
4
  }
5
5
  export type StopSignal = keyof StopSignals;
6
6
  /** Record a successful command outcome for this broker process. The stophook