@north-light/crouter 0.3.198 → 0.3.200

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 (316) 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__/helpers/harness.js +17 -55
  99. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  100. package/dist/core/__tests__/human-deliver.test.js +31 -26
  101. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  102. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  103. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +4 -5
  104. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  105. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  106. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  107. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  108. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  109. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  110. package/dist/core/__tests__/session-cycles.test.js +22 -0
  111. package/dist/core/__tests__/stop-guard.test.js +4 -4
  112. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  113. package/dist/core/bash-jobs.d.ts +6 -0
  114. package/dist/core/bash-jobs.js +10 -0
  115. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  116. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  117. package/dist/core/canvas/attention.js +8 -6
  118. package/dist/core/canvas/browse/model.d.ts +8 -2
  119. package/dist/core/canvas/browse/model.js +11 -4
  120. package/dist/core/canvas/canvas.d.ts +7 -0
  121. package/dist/core/canvas/canvas.js +37 -0
  122. package/dist/core/canvas/crons.d.ts +9 -0
  123. package/dist/core/canvas/crons.js +51 -0
  124. package/dist/core/canvas/node-order.d.ts +22 -1
  125. package/dist/core/canvas/node-order.js +26 -1
  126. package/dist/core/canvas/render-source.d.ts +5 -0
  127. package/dist/core/canvas/render-source.js +1 -0
  128. package/dist/core/canvas/types.d.ts +9 -3
  129. package/dist/core/command-plugins/bundle.d.ts +6 -1
  130. package/dist/core/command-plugins/bundle.js +26 -13
  131. package/dist/core/command.js +2 -1
  132. package/dist/core/config.d.ts +47 -1
  133. package/dist/core/config.js +137 -4
  134. package/dist/core/help.js +1 -1
  135. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  136. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  137. package/dist/core/human/__tests__/page.test.js +56 -98
  138. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  139. package/dist/core/human/answer-text.js +3 -3
  140. package/dist/core/human/answer.d.ts +6 -4
  141. package/dist/core/human/answer.js +4 -10
  142. package/dist/core/human/component-docs.d.ts +11 -7
  143. package/dist/core/human/component-docs.js +361 -88
  144. package/dist/core/human/convention.d.ts +17 -3
  145. package/dist/core/human/convention.js +36 -5
  146. package/dist/core/human/feedback-companion.d.ts +20 -0
  147. package/dist/core/human/feedback-companion.js +98 -0
  148. package/dist/core/human/feedback.d.ts +65 -0
  149. package/dist/core/human/feedback.js +116 -0
  150. package/dist/core/human/page-catalog.d.ts +40 -5
  151. package/dist/core/human/page-catalog.js +102 -30
  152. package/dist/core/human/page-errors.d.ts +4 -0
  153. package/dist/core/human/page-errors.js +7 -0
  154. package/dist/core/human/page-eval.d.ts +20 -0
  155. package/dist/core/human/page-eval.js +123 -0
  156. package/dist/core/human/page-schema.d.ts +23 -191
  157. package/dist/core/human/page-schema.js +60 -89
  158. package/dist/core/human/page-synth.d.ts +1 -1
  159. package/dist/core/human/page-synth.js +15 -18
  160. package/dist/core/human/page.d.ts +12 -27
  161. package/dist/core/human/page.js +142 -474
  162. package/dist/core/human/root.d.ts +6 -2
  163. package/dist/core/human/root.js +12 -4
  164. package/dist/core/human/scan.d.ts +3 -4
  165. package/dist/core/human/scan.js +16 -15
  166. package/dist/core/human/summary.d.ts +1 -2
  167. package/dist/core/human/summary.js +2 -2
  168. package/dist/core/human/tickets.d.ts +20 -14
  169. package/dist/core/human/tickets.js +91 -31
  170. package/dist/core/human/types.d.ts +10 -2
  171. package/dist/core/preview-registry.js +8 -10
  172. package/dist/core/profiles/default-binding.d.ts +4 -0
  173. package/dist/core/profiles/default-binding.js +24 -0
  174. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  175. package/dist/core/profiles/deletion-reservation.js +38 -0
  176. package/dist/core/profiles/manifest.d.ts +3 -0
  177. package/dist/core/profiles/manifest.js +12 -0
  178. package/dist/core/profiles/select.js +2 -2
  179. package/dist/core/review/realize.js +3 -3
  180. package/dist/core/review/store.d.ts +6 -0
  181. package/dist/core/review/store.js +31 -0
  182. package/dist/core/runtime/bearings.d.ts +0 -4
  183. package/dist/core/runtime/bearings.js +4 -62
  184. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  185. package/dist/core/runtime/bin-contributions.js +186 -0
  186. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  187. package/dist/core/runtime/broker/client-registry.js +13 -1
  188. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  189. package/dist/core/runtime/broker/extension-abort.js +4 -2
  190. package/dist/core/runtime/broker/fault-retry.js +4 -0
  191. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  192. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  193. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  194. package/dist/core/runtime/broker/read-ops.js +2 -2
  195. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  196. package/dist/core/runtime/broker/rebind.js +6 -1
  197. package/dist/core/runtime/broker-extension-render.js +19 -0
  198. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  199. package/dist/core/runtime/broker.d.ts +1 -1
  200. package/dist/core/runtime/broker.js +17 -6
  201. package/dist/core/runtime/front-door-env.d.ts +5 -0
  202. package/dist/core/runtime/front-door-env.js +16 -0
  203. package/dist/core/runtime/front-door.d.ts +1 -5
  204. package/dist/core/runtime/front-door.js +2 -5
  205. package/dist/core/runtime/host.d.ts +1 -1
  206. package/dist/core/runtime/invocation.d.ts +6 -0
  207. package/dist/core/runtime/invocation.js +10 -0
  208. package/dist/core/runtime/kickoff.js +34 -0
  209. package/dist/core/runtime/launch.d.ts +2 -6
  210. package/dist/core/runtime/node-read.js +4 -1
  211. package/dist/core/runtime/nodes.js +2 -0
  212. package/dist/core/runtime/promote.d.ts +1 -0
  213. package/dist/core/runtime/promote.js +3 -1
  214. package/dist/core/runtime/session-cycles.d.ts +6 -0
  215. package/dist/core/runtime/session-cycles.js +15 -1
  216. package/dist/core/runtime/session-visibility.d.ts +17 -8
  217. package/dist/core/runtime/session-visibility.js +19 -10
  218. package/dist/core/runtime/spawn-env.d.ts +9 -1
  219. package/dist/core/runtime/spawn-env.js +62 -2
  220. package/dist/core/runtime/stop-guard.d.ts +2 -2
  221. package/dist/core/runtime/stop-guard.js +2 -2
  222. package/dist/core/runtime/stop-signals.d.ts +1 -1
  223. package/dist/core/runtime/stop-signals.js +2 -2
  224. package/dist/core/runtime/tmux-bindings.js +1 -1
  225. package/dist/core/session-model/session-state.d.ts +4 -3
  226. package/dist/core/session-model/session-state.js +14 -1
  227. package/dist/core/user-settings.d.ts +21 -2
  228. package/dist/core/user-settings.js +32 -11
  229. package/dist/core/worktree.d.ts +15 -45
  230. package/dist/core/worktree.js +59 -261
  231. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  232. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  233. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  234. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  235. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  236. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  237. package/dist/daemon/api/handlers/human.js +24 -20
  238. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  239. package/dist/daemon/api/handlers/inbox.js +39 -115
  240. package/dist/daemon/api/handlers/profiles.js +11 -29
  241. package/dist/daemon/api/map.d.ts +6 -1
  242. package/dist/daemon/api/map.js +23 -0
  243. package/dist/daemon/api/server.js +3 -1
  244. package/dist/daemon/companion-retire.d.ts +2 -0
  245. package/dist/daemon/companion-retire.js +33 -0
  246. package/dist/daemon/cron-run.js +14 -16
  247. package/dist/daemon/human/finish.d.ts +12 -7
  248. package/dist/daemon/human/finish.js +101 -38
  249. package/dist/daemon/human/sweep.js +5 -1
  250. package/dist/daemon/profile-delete.d.ts +7 -0
  251. package/dist/daemon/profile-delete.js +306 -0
  252. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  253. package/dist/daemon/review/deliver.js +3 -3
  254. package/dist/daemon/review/finish.d.ts +0 -2
  255. package/dist/daemon/review/finish.js +1 -29
  256. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  257. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  258. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  259. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  260. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  261. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  262. package/dist/pi-extensions/canvas-stophook.js +4 -4
  263. package/dist/shared/generated-context.js +2 -2
  264. package/dist/types.d.ts +44 -2
  265. package/dist/types.js +7 -2
  266. package/package.json +6 -5
  267. package/runtime.lock.json +5 -33
  268. package/dist/builtin-memory/init.md +0 -38
  269. package/dist/builtin-memory/plan.md +0 -19
  270. package/dist/builtin-memory/spec.md +0 -19
  271. package/dist/commands/human/doc.d.ts +0 -2
  272. package/dist/commands/human/doc.js +0 -91
  273. package/dist/commands/human/html.js +0 -81
  274. package/dist/commands/human/inbox.d.ts +0 -2
  275. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  276. package/dist/core/human/__tests__/page-render.test.js +0 -66
  277. package/dist/core/human/html-markdown.d.ts +0 -3
  278. package/dist/core/human/html-markdown.js +0 -288
  279. package/dist/core/human/page-render.d.ts +0 -4
  280. package/dist/core/human/page-render.js +0 -105
  281. package/dist/pages/bundle.css +0 -1
  282. package/dist/pages/bundle.js +0 -1778
  283. package/dist/pages/comments.d.ts +0 -72
  284. package/dist/pages/comments.js +0 -176
  285. package/dist/pages/controls.d.ts +0 -98
  286. package/dist/pages/controls.js +0 -261
  287. package/dist/pages/elements/cards.d.ts +0 -28
  288. package/dist/pages/elements/cards.js +0 -915
  289. package/dist/pages/elements/chart.d.ts +0 -23
  290. package/dist/pages/elements/chart.js +0 -664
  291. package/dist/pages/elements/options.d.ts +0 -30
  292. package/dist/pages/elements/options.js +0 -742
  293. package/dist/pages/elements/pages.d.ts +0 -30
  294. package/dist/pages/elements/pages.js +0 -377
  295. package/dist/pages/elements/slot.d.ts +0 -26
  296. package/dist/pages/elements/slot.js +0 -112
  297. package/dist/pages/elements/table.d.ts +0 -36
  298. package/dist/pages/elements/table.js +0 -1043
  299. package/dist/pages/elements/text.d.ts +0 -31
  300. package/dist/pages/elements/text.js +0 -1175
  301. package/dist/pages/entry.d.ts +0 -8
  302. package/dist/pages/entry.js +0 -8
  303. package/dist/pages/host.d.ts +0 -134
  304. package/dist/pages/host.js +0 -165
  305. package/dist/pages/readonly.d.ts +0 -24
  306. package/dist/pages/readonly.js +0 -31
  307. package/dist/pages/register.d.ts +0 -2
  308. package/dist/pages/register.js +0 -3
  309. package/dist/pages/responses.d.ts +0 -37
  310. package/dist/pages/responses.js +0 -56
  311. package/dist/pages/slot-config.d.ts +0 -36
  312. package/dist/pages/slot-config.js +0 -50
  313. package/dist/pages/types.d.ts +0 -98
  314. package/dist/pages/types.js +0 -23
  315. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  316. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -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,8 +1,8 @@
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';
@@ -108,7 +108,7 @@ function realizeBridge(review) {
108
108
  profile_id: origin.profile_id ?? null,
109
109
  name: 'human-review',
110
110
  });
111
- atomicWriteJson(join(ticketDir(bridge.node_id), 'run.json'), { mode: 'review' });
111
+ bindTicketReplyRoute(ticketDir(bridge.node_id), bridge.node_id);
112
112
  if (!bindBridgeNode(current.review_id, bridge.node_id)) {
113
113
  cancelInvisibleBridge(bridge.node_id);
114
114
  current = requireReview(current.review_id);
@@ -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
+ }
@@ -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;
@@ -14,7 +14,7 @@ import { clearFault, readFault } from '../fault.js';
14
14
  import { isManagedProvider, readSubscriptionPool, } from '../../subscription-state.js';
15
15
  import { SessionListCache } from '../session-list-cache.js';
16
16
  import { sessionListCachePath } from '../../canvas/paths.js';
17
- import { REVIEW_BOUNDARY_CUSTOM_TYPE } from '../session-visibility.js';
17
+ import { isReviewBoundary } from '../session-visibility.js';
18
18
  import { isUnknownModel, parseModelSpec } from '../model-selection.js';
19
19
  import { buildCommandList, buildGetTreeData, buildListMemoryRefsData, buildListModelsData, buildScopedModelsData, buildSessionsData, buildSettingsData, modelKey, } from './read-ops.js';
20
20
  export const REVIEW_BOUNDARY_NAVIGATION_CODE = 'review_boundary_navigation';
@@ -279,7 +279,7 @@ export function stripHiddenEntriesFromJsonlExport(filePath) {
279
279
  }
280
280
  /** Owns frame-local input state while dispatching through explicit broker collaborators. */
281
281
  export function createFrameDispatchContext(deps) {
282
- const { nodeId, cfg, registry, toolGroups, projection, rebind, reviewCompanion, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
282
+ const { nodeId, cfg, registry, toolGroups, projection, rebind, boundaryReviewId, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, reWelcomeAll, disposeAndExit, persistModelChoice, broadcastModelChanged, registryOf, formatModelSpec, formatExactModelSpec, resolveLaunchModel, } = deps;
283
283
  const currentSession = () => rebind.session();
284
284
  const currentServices = () => rebind.services();
285
285
  let kickoffPending;
@@ -657,11 +657,10 @@ export function createFrameDispatchContext(deps) {
657
657
  // pass no id (unchanged behavior).
658
658
  ...(id !== undefined ? { id } : {}),
659
659
  });
660
- const isPreBoundaryReviewEntry = (entryId) => reviewCompanion &&
660
+ const isPreBoundaryReviewEntry = (entryId) => boundaryReviewId !== undefined &&
661
661
  !currentSession()
662
662
  .sessionManager.getBranch(entryId)
663
- .some((entry) => entry.type === 'custom_message' &&
664
- entry.customType === REVIEW_BOUNDARY_CUSTOM_TYPE);
663
+ .some((entry) => isReviewBoundary(entry, boundaryReviewId));
665
664
  const reviewBoundaryRefusalTo = (client, targetId) => {
666
665
  const refusal = new ReviewBoundaryNavigationError(targetId);
667
666
  registry.sendFrame(client, {
@@ -1112,7 +1111,7 @@ export function createFrameDispatchContext(deps) {
1112
1111
  }
1113
1112
  case 'get_tree': {
1114
1113
  try {
1115
- registry.sendFrame(client, buildGetTreeData(frame.id, currentSession(), reviewCompanion));
1114
+ registry.sendFrame(client, buildGetTreeData(frame.id, currentSession(), boundaryReviewId));
1116
1115
  }
1117
1116
  catch (err) {
1118
1117
  engineErrorTo(client, frame.id)(err);
@@ -18,6 +18,6 @@ export declare function buildCommandList(session: BrokerSession): CommandDisclos
18
18
  export declare function buildListMemoryRefsData(id: string, refs: RefMeta[], engineLeadingCommandNames: string[]): ListMemoryRefsData;
19
19
  export declare function buildListModelsData(id: string, session: BrokerSession): ListModelsData;
20
20
  export declare function buildScopedModelsData(id: string, session: BrokerSession): ListScopedModelsData;
21
- export declare function buildGetTreeData(id: string, session: BrokerSession, reviewCompanion: boolean): GetTreeData;
21
+ export declare function buildGetTreeData(id: string, session: BrokerSession, boundaryReviewId: string | undefined): GetTreeData;
22
22
  export declare function buildSettingsData(id: string, session: BrokerSession): GetSettingsData;
23
23
  export declare function buildSessionsData(id: string, scope: 'cwd' | 'all', session: BrokerSession, cache: SessionListCache): Promise<ListSessionsData>;
@@ -67,9 +67,9 @@ export function buildScopedModelsData(id, session) {
67
67
  enabledModelIds: session.settingsManager.getEnabledModels() ?? null,
68
68
  };
69
69
  }
70
- export function buildGetTreeData(id, session, reviewCompanion) {
70
+ export function buildGetTreeData(id, session, boundaryReviewId) {
71
71
  const manager = session.sessionManager;
72
- const tree = reviewCompanion ? boundaryRootedTree(manager.getTree()) : manager.getTree();
72
+ const tree = boundaryReviewId !== undefined ? boundaryRootedTree(manager.getTree(), boundaryReviewId) : manager.getTree();
73
73
  const visibleEntryIds = new Set();
74
74
  const collectEntryIds = (nodes) => {
75
75
  for (const node of nodes) {
@@ -17,6 +17,7 @@ export interface SubscriptionGeneration {
17
17
  messages?: unknown[];
18
18
  } | null;
19
19
  stagedWatchdogAbort: boolean;
20
+ stagedRefreshAbort: boolean;
20
21
  stagedOverflowFailure: {
21
22
  errorMessage: string;
22
23
  } | null;
@@ -56,7 +56,11 @@ export class RebindDriver {
56
56
  await candidateSession.bindExtensions({
57
57
  uiContext: this.deps.uiContext(),
58
58
  mode: 'rpc',
59
- abortHandler: () => abortSessionFromExtension(candidateSession),
59
+ abortHandler: () => abortSessionFromExtension(candidateSession, () => {
60
+ const generation = this.installed;
61
+ if (generation?.session === candidateSession)
62
+ generation.stagedRefreshAbort = true;
63
+ }),
60
64
  shutdownHandler: () => this.requestShutdown(),
61
65
  });
62
66
  if (ticket !== this.latestRebindRequest)
@@ -92,6 +96,7 @@ export class RebindDriver {
92
96
  fallback: { blocked: false },
93
97
  stagedProviderAgentEnd: null,
94
98
  stagedWatchdogAbort: false,
99
+ stagedRefreshAbort: false,
95
100
  stagedOverflowFailure: null,
96
101
  executingTools: new Set(),
97
102
  };
@@ -114,10 +114,29 @@ function worktreeNote(node) {
114
114
  return '';
115
115
  return `Branch: \`${worktree.branch}\`\nWorktree: \`${worktree.path}\`\nBase: \`${worktree.base_ref}\` @ \`${worktree.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.`;
116
116
  }
117
+ /** The companion note — rendered ONLY when a node carries its immutable
118
+ * companion binding. Names the target (the document under review, or the page
119
+ * the feedback is about), and for a page-feedback companion carries the whole
120
+ * behavioral contract — the companion is an ordinary fork of the sending node,
121
+ * so this note is the only place it learns the role. */
117
122
  function reviewNote(node) {
118
123
  const review = node.review_binding;
119
124
  if (review === null || review === undefined)
120
125
  return '';
126
+ if (review.kind === 'page_feedback') {
127
+ const ticketId = review.review_id;
128
+ const targetFile = review.target_file;
129
+ return (`You exist for one page's feedback conversation — ticket \`${ticketId}\`, page source \`${targetFile}\`. ` +
130
+ `Think with the person about this one page and answer in a few plain sentences from the context you already carry. ` +
131
+ `Revise the page in place with \`crtr human send --replace ${ticketId} --page ${targetFile}\` — it swaps in on their screen immediately — and briefly say what changed; a silent swap leaves them guessing. ` +
132
+ `After dealing with an open feedback item — by revising the page or answering it here — resolve it with \`crtr human feedback resolve <comment_id>\`; ` +
133
+ `\`crtr human feedback list ${ticketId}\` shows each item's id and status. ` +
134
+ `Never author feedback or resolve an item before dealing with it — a sent comment is immutable, and resolution is your report that the work is done. ` +
135
+ `Asked to act beyond this page, give your advice and point the person at submitting it: the submitted answer is what reaches the task's owner, while work done here is invisible to it and ends when the ticket settles. ` +
136
+ `When a question needs real exploration, delegate it to a child node and keep talking while it runs. ` +
137
+ `The inherited conversation belongs to the origin node, not you — its task stays with it; carry only the context needed for this page. ` +
138
+ `The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see — never treat it as shared ground; when they ask about it, answer plainly in your own words rather than replaying it.`);
139
+ }
121
140
  return `You exist for one human review of \`${review.target_file}\`. The inherited conversation belongs to the origin node, not you — carry only the context needed for this review. The person's visible transcript begins at the boundary marker above; everything earlier is context they cannot see.`;
122
141
  }
123
142
  export function buildContextBearingsFromState(state, projectContext, situational = '', seen) {