@north-light/crouter 0.3.197 → 0.3.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +126 -33
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +8 -8
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -3
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -0,0 +1,306 @@
1
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { cronIdsForProfileDeletion, cronIdsForProfileDetach, deleteCronsForProfile, deleteNodes, getNode, getRow, globalizeCronsForProfile, listNodesByProfile, nodesRoot, reviewDir, } from '../core/canvas/index.js';
4
+ import { usage } from '../core/errors.js';
5
+ import { emitEvent } from '../core/events/emit.js';
6
+ import { clearDefaultProfileId, defaultProfileDirs, } from '../core/profiles/default-binding.js';
7
+ import { reserveProfileDeletion } from '../core/profiles/deletion-reservation.js';
8
+ import { deleteProfile, loadProfileManifest, } from '../core/profiles/manifest.js';
9
+ import { deleteReviewsByOriginNodes, reviewIdsByOriginNodes, } from '../core/review/store.js';
10
+ import { boundFleet } from '../core/runtime/fleet.js';
11
+ import { headlessBrokerHost } from '../core/runtime/host.js';
12
+ import { transition } from '../core/runtime/lifecycle.js';
13
+ import { tearDownNode } from '../core/runtime/placement.js';
14
+ import { reshapeNode } from '../core/runtime/promote.js';
15
+ import { reviveNode } from '../core/runtime/revive.js';
16
+ const SELF_DELETE_GRACE_MS = 1_000;
17
+ const TICKET_FILES = new Set([
18
+ 'page.json',
19
+ 'page.tsx',
20
+ 'page.html',
21
+ 'page.js',
22
+ 'review.json',
23
+ 'response.json',
24
+ 'progress.json',
25
+ 'claim.json',
26
+ 'reply-route.json',
27
+ 'feedback-comments.json',
28
+ 'feedback.json',
29
+ '.ticket-lock',
30
+ ]);
31
+ const pending = new Map();
32
+ function requestObject(body) {
33
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
34
+ throw usage('profile deletion requires a JSON request body', {
35
+ error: 'force_required',
36
+ message: 'profile deletion requires --force',
37
+ next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
38
+ });
39
+ }
40
+ return body;
41
+ }
42
+ function validateRequest(body) {
43
+ const value = requestObject(body);
44
+ if (value['force'] !== true) {
45
+ throw usage('profile deletion requires --force', {
46
+ error: 'force_required',
47
+ message: 'profile deletion requires --force',
48
+ next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
49
+ });
50
+ }
51
+ if (value['detach'] !== undefined && typeof value['detach'] !== 'boolean') {
52
+ throw usage('detach must be a boolean', {
53
+ error: 'invalid_detach',
54
+ message: 'detach must be a boolean',
55
+ next: 'Use --detach as a presence-only flag, or omit it.',
56
+ });
57
+ }
58
+ const caller = value['caller_node_id'];
59
+ if (caller !== undefined && caller !== null && (typeof caller !== 'string' || caller === '')) {
60
+ throw usage('caller_node_id must be a nonempty string or null');
61
+ }
62
+ return {
63
+ force: true,
64
+ ...(value['detach'] === true ? { detach: true } : {}),
65
+ ...(caller === undefined ? {} : { caller_node_id: caller }),
66
+ };
67
+ }
68
+ function manifestSourceNodeId(path) {
69
+ if (!existsSync(path))
70
+ return null;
71
+ try {
72
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
73
+ return typeof parsed.source?.nodeId === 'string' && parsed.source.nodeId !== ''
74
+ ? parsed.source.nodeId
75
+ : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ function ticketCleanupForNodes(nodeIds) {
82
+ const wanted = new Set(nodeIds);
83
+ const root = nodesRoot();
84
+ if (wanted.size === 0 || !existsSync(root))
85
+ return [];
86
+ const cleanup = [];
87
+ for (const dirent of readdirSync(root, { withFileTypes: true })) {
88
+ if (!dirent.isDirectory())
89
+ continue;
90
+ const dir = join(root, dirent.name);
91
+ const sourceIds = [
92
+ manifestSourceNodeId(join(dir, 'page.json')),
93
+ manifestSourceNodeId(join(dir, 'review.json')),
94
+ ];
95
+ if (!sourceIds.some((sourceId) => sourceId !== null && wanted.has(sourceId)))
96
+ continue;
97
+ cleanup.push({
98
+ dir,
99
+ wholeDirectory: wanted.has(dirent.name) || getRow(dirent.name) === null,
100
+ });
101
+ }
102
+ return cleanup.sort((a, b) => a.dir.localeCompare(b.dir));
103
+ }
104
+ function isTicketOwnedResidue(name) {
105
+ if (TICKET_FILES.has(name))
106
+ return true;
107
+ for (const base of TICKET_FILES) {
108
+ if (name.startsWith(`${base}.`) && (name.endsWith('.tmp') || name.endsWith('.lock'))) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+ function cleanTickets(tickets) {
115
+ let cleaned = 0;
116
+ for (const ticket of tickets) {
117
+ if (!existsSync(ticket.dir))
118
+ continue;
119
+ if (ticket.wholeDirectory) {
120
+ rmSync(ticket.dir, { recursive: true, force: true });
121
+ cleaned += 1;
122
+ continue;
123
+ }
124
+ for (const dirent of readdirSync(ticket.dir, { withFileTypes: true })) {
125
+ if (!isTicketOwnedResidue(dirent.name))
126
+ continue;
127
+ rmSync(join(ticket.dir, dirent.name), { recursive: true, force: true });
128
+ }
129
+ cleaned += 1;
130
+ }
131
+ return cleaned;
132
+ }
133
+ function openWorktreeError(nodes) {
134
+ const blocked = nodes
135
+ .filter((node) => node.managed_worktree?.state === 'open')
136
+ .map((node) => ({ node_id: node.node_id, path: node.managed_worktree.path }));
137
+ if (blocked.length === 0)
138
+ return null;
139
+ const message = `profile deletion is blocked by ${blocked.length} open managed worktree${blocked.length === 1 ? '' : 's'}`;
140
+ return usage(message, {
141
+ error: 'open_managed_worktree',
142
+ message,
143
+ nodes: blocked,
144
+ next: `Close ${blocked.map(({ node_id }) => node_id).join(', ')} with \`crtr node worktree close <node>\`, then retry.`,
145
+ });
146
+ }
147
+ function prepareDeletion(entry, request) {
148
+ const nodes = listNodesByProfile(entry.profileId);
149
+ const blocked = openWorktreeError(nodes);
150
+ if (blocked !== null)
151
+ throw blocked;
152
+ const nodeIds = nodes.map(({ node_id }) => node_id);
153
+ const mode = request.detach === true ? 'detached' : 'deleted';
154
+ return {
155
+ entry,
156
+ mode,
157
+ nodes,
158
+ nodeIds,
159
+ tickets: mode === 'deleted' ? ticketCleanupForNodes(nodeIds) : [],
160
+ cronIds: mode === 'deleted'
161
+ ? cronIdsForProfileDeletion(entry.profileId, nodeIds)
162
+ : cronIdsForProfileDetach(entry.profileId),
163
+ reviewIds: mode === 'deleted' ? reviewIdsByOriginNodes(nodeIds) : [],
164
+ defaultDirs: defaultProfileDirs(entry.profileId),
165
+ };
166
+ }
167
+ function assertDeletionPlanPreconditions(plan) {
168
+ const currentNodes = plan.nodeIds.flatMap((nodeId) => {
169
+ const node = getNode(nodeId);
170
+ return node === null ? [] : [node];
171
+ });
172
+ const blocked = openWorktreeError(currentNodes);
173
+ if (blocked !== null)
174
+ throw blocked;
175
+ }
176
+ function resultForPlan(plan) {
177
+ return {
178
+ profile_id: plan.entry.profileId,
179
+ name: plan.entry.manifest.name,
180
+ mode: plan.mode,
181
+ nodes: plan.nodeIds.length,
182
+ crons: plan.cronIds.length,
183
+ tickets: plan.tickets.length,
184
+ reviews: plan.reviewIds.length,
185
+ default_pins: plan.defaultDirs.length,
186
+ };
187
+ }
188
+ async function detachProfile(plan) {
189
+ assertDeletionPlanPreconditions(plan);
190
+ const existingNodeIds = plan.nodeIds.filter((nodeId) => getNode(nodeId) !== null);
191
+ const fleet = boundFleet();
192
+ const live = existingNodeIds.flatMap((nodeId) => {
193
+ const entry = fleet.get(nodeId);
194
+ return entry === undefined ? [] : [{ nodeId, exited: entry.handle.exited }];
195
+ });
196
+ const restartNodeIds = [];
197
+ for (const { nodeId } of live) {
198
+ const current = getNode(nodeId);
199
+ if (current?.status === 'active' || current?.status === 'idle') {
200
+ transition(nodeId, 'release');
201
+ restartNodeIds.push(nodeId);
202
+ }
203
+ headlessBrokerHost.teardown(nodeId);
204
+ }
205
+ await Promise.all(live.map(({ exited }) => exited));
206
+ const detachedNodeIds = [];
207
+ for (const nodeId of existingNodeIds) {
208
+ if (getNode(nodeId) === null)
209
+ continue;
210
+ reshapeNode(nodeId, { profileId: null });
211
+ detachedNodeIds.push(nodeId);
212
+ }
213
+ const cronIds = globalizeCronsForProfile(plan.entry.profileId);
214
+ const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
215
+ deleteProfile(plan.entry.profileId);
216
+ const detached = new Set(detachedNodeIds);
217
+ for (const nodeId of restartNodeIds) {
218
+ if (detached.has(nodeId))
219
+ reviveNode(nodeId, { resume: true, recovery: true });
220
+ }
221
+ return {
222
+ ...resultForPlan(plan),
223
+ nodes: detachedNodeIds.length,
224
+ crons: cronIds.length,
225
+ default_pins: defaultDirs.length,
226
+ };
227
+ }
228
+ function hardDeleteProfile(plan) {
229
+ assertDeletionPlanPreconditions(plan);
230
+ const cronIds = deleteCronsForProfile(plan.entry.profileId, plan.nodeIds);
231
+ for (const nodeId of plan.nodeIds) {
232
+ headlessBrokerHost.teardown(nodeId);
233
+ tearDownNode(nodeId);
234
+ }
235
+ const reviewIds = deleteReviewsByOriginNodes(plan.nodeIds);
236
+ for (const reviewId of reviewIds)
237
+ rmSync(reviewDir(reviewId), { recursive: true, force: true });
238
+ const tickets = cleanTickets(plan.tickets);
239
+ const deletedNodeIds = deleteNodes(plan.nodeIds);
240
+ const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
241
+ deleteProfile(plan.entry.profileId);
242
+ return {
243
+ ...resultForPlan(plan),
244
+ nodes: deletedNodeIds.length,
245
+ crons: cronIds.length,
246
+ tickets,
247
+ reviews: reviewIds.length,
248
+ default_pins: defaultDirs.length,
249
+ };
250
+ }
251
+ async function executePlan(plan) {
252
+ return plan.mode === 'detached' ? detachProfile(plan) : hardDeleteProfile(plan);
253
+ }
254
+ function scheduleSelfDeletion(plan, releaseReservation) {
255
+ const result = {
256
+ ...resultForPlan(plan),
257
+ accepted: true,
258
+ grace_ms: SELF_DELETE_GRACE_MS,
259
+ };
260
+ pending.set(plan.entry.profileId, { result });
261
+ setTimeout(() => {
262
+ void (async () => {
263
+ try {
264
+ await executePlan(plan);
265
+ }
266
+ catch (error) {
267
+ emitEvent({
268
+ level: 'error',
269
+ event: 'profile.delete.deferred_failed',
270
+ error: error instanceof Error ? error : new Error(String(error)),
271
+ fields: { profile_id: plan.entry.profileId, mode: plan.mode },
272
+ });
273
+ }
274
+ finally {
275
+ pending.delete(plan.entry.profileId);
276
+ releaseReservation();
277
+ }
278
+ })();
279
+ }, SELF_DELETE_GRACE_MS);
280
+ return result;
281
+ }
282
+ /** Validate and enact one forced profile deletion. A request from a node that
283
+ * belongs to the target profile is acknowledged before its broker is stopped. */
284
+ export async function requestProfileDeletion(name, body) {
285
+ const request = validateRequest(body);
286
+ const entry = loadProfileManifest(name);
287
+ const existing = pending.get(entry.profileId);
288
+ if (existing !== undefined)
289
+ return { status: 202, body: existing.result };
290
+ const releaseReservation = reserveProfileDeletion(entry.profileId);
291
+ let deferred = false;
292
+ try {
293
+ const plan = prepareDeletion(entry, request);
294
+ const selfDeleting = request.caller_node_id != null && plan.nodeIds.includes(request.caller_node_id);
295
+ if (selfDeleting) {
296
+ const result = scheduleSelfDeletion(plan, releaseReservation);
297
+ deferred = true;
298
+ return { status: 202, body: result };
299
+ }
300
+ return { status: 200, body: await executePlan(plan) };
301
+ }
302
+ finally {
303
+ if (!deferred)
304
+ releaseReservation();
305
+ }
306
+ }
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
- import { getRow, getNode, subscribersOf } from '../../core/canvas/index.js';
6
+ import { getRow, getNode, isWarmSpare, subscribersOf } from '../../core/canvas/index.js';
7
7
  import { contextDir, isSafeNodeId } from '../../core/canvas/paths.js';
8
8
  import { fullName } from '../../core/canvas/labels.js';
9
9
  import { recordedPidLiveness } from '../../core/canvas/pid.js';
@@ -20,7 +20,8 @@ import { operationIdContext } from '../../core/events/operation-id.js';
20
20
  import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
21
21
  import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
22
22
  import { focusOf } from '../../core/runtime/placement.js';
23
- import { hasLiveObligation } from './live-obligation.js';
23
+ import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
24
+ import { parkEvent } from './live-obligation.js';
24
25
  const execFileAsync = promisify(execFile);
25
26
  function configuredPositiveInteger(name, fallback) {
26
27
  const raw = process.env[name];
@@ -174,6 +175,14 @@ export class BrokerSupervisionReconciler {
174
175
  return thresholds;
175
176
  }
176
177
  handleUnattendedParking(id, row, livePid, now) {
178
+ // An unclaimed warm-pool spare is not a conversation: the booted broker it
179
+ // holds IS its whole value, and nobody is meant to be attending it. Its
180
+ // lifetime belongs to `reapStaleSpares` (warm-pool.ts) and its own TTL, so
181
+ // parking one here would empty the pool every 15 minutes.
182
+ if (isWarmSpare(id)) {
183
+ this.unattendedSince.delete(id);
184
+ return;
185
+ }
177
186
  if (row === null
178
187
  || row.pi_session_id == null
179
188
  || row.pi_pid !== livePid
@@ -183,7 +192,8 @@ export class BrokerSupervisionReconciler {
183
192
  || row.intent !== null
184
193
  || isBusy(id)
185
194
  || activeBackgroundBashJobs(contextDir(id)).length > 0
186
- || focusOf(id) !== null) {
195
+ || focusOf(id) !== null
196
+ || hasAttachedBrokerViewers(id)) {
187
197
  this.unattendedSince.delete(id);
188
198
  return;
189
199
  }
@@ -206,12 +216,7 @@ export class BrokerSupervisionReconciler {
206
216
  if (now - since < unattendedParkMsForDaemon())
207
217
  return;
208
218
  this.unattendedSince.delete(id);
209
- const liveObligation = hasLiveObligation(id);
210
- // A resident with nothing left to wake it has completed its conversation:
211
- // terminalize it so history retention can eventually prune it. A resident
212
- // still awaiting a child/controller/human/deadline/output stays wakeable, as does
213
- // every terminal node (whose own stop guard owns semantic completion).
214
- transition(id, row.lifecycle === 'resident' && !liveObligation ? 'finish' : 'release');
219
+ transition(id, parkEvent(id, row.lifecycle));
215
220
  headlessBrokerHost.teardown(id);
216
221
  }
217
222
  async warnOverCapInTmux(count, warning, max) {
@@ -7,7 +7,7 @@ import { appendControllerDeathNotice, readCursor, readInboxSince, } from '../../
7
7
  import { fanDoctrineWake } from '../../core/runtime/close.js';
8
8
  import { transition } from '../../core/runtime/lifecycle.js';
9
9
  import { reviveNode } from '../../core/runtime/revive.js';
10
- import { hasLiveObligation } from './live-obligation.js';
10
+ import { parkEvent } from './live-obligation.js';
11
11
  // Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
12
12
  // livelock shape as the stranded-relaunch case, but on the separate pass that
13
13
  // resumes a dormant idle-released node when its inbox gains an unseen entry.
@@ -80,11 +80,16 @@ export class DormantInboxReconciler {
80
80
  if (!entries.some((entry) => entry.tier !== 'deferred')) {
81
81
  this.relaunchState.delete(id);
82
82
  const meta = getNode(id);
83
- // An idle resident with no wake-worthy inbox entry still needs
84
- // lifecycle settlement. Once nothing live remains, this is completed
85
- // history rather than a permanently protected idle row.
86
- if (meta?.lifecycle === 'resident' && !hasLiveObligation(id)) {
87
- transition(id, 'finish');
83
+ // An idle row with no wake-worthy inbox entry still needs the same
84
+ // lifecycle settlement the live park clock applies: once nothing
85
+ // live remains, this is history rather than a permanently protected
86
+ // idle row. The row is already parked, so only a terminalizing
87
+ // verdict is worth writing. Unclaimed warm-pool spares never reach
88
+ // here — `listNodes` hides them from `ctx.rows`.
89
+ if (meta !== null) {
90
+ const event = parkEvent(id, meta.lifecycle);
91
+ if (event !== 'release')
92
+ transition(id, event);
88
93
  }
89
94
  return;
90
95
  }
@@ -1,2 +1,22 @@
1
- /** Durable work that requires a resident to remain automatically wakeable. */
1
+ import { type Lifecycle } from '../../core/canvas/index.js';
2
+ /** Durable work that requires a node to remain automatically wakeable. */
2
3
  export declare function hasLiveObligation(nodeId: string): boolean;
4
+ /** What parking an unattended node actually amounts to — the one rule both the
5
+ * live-broker park clock (broker-supervision) and the already-parked
6
+ * reconciliation (dormant-inbox) apply.
7
+ *
8
+ * `release` — something can still wake it, so parking is a pause.
9
+ * `finish` — a resident with nothing left to wake it has completed its
10
+ * conversation, and the row becomes prunable history.
11
+ * `cancel` — a terminal node that never pushed a final was torn down before
12
+ * finishing, which is what `canceled` records; it stays revivable
13
+ * and becomes prunable. A live subscriber still awaiting its
14
+ * report is an outstanding deliverable, so such a node keeps its
15
+ * wakeable idle row instead of being written off.
16
+ *
17
+ * Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
18
+ * never wakes a node on its own — it rides the next natural cycle — but a
19
+ * terminalized row no longer takes that cycle, so writing one off would drop
20
+ * content the sender already handed over.
21
+ */
22
+ export declare function parkEvent(nodeId: string, lifecycle: Lifecycle): 'release' | 'finish' | 'cancel';
@@ -1,7 +1,8 @@
1
- import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
1
+ import { hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCancelOnWakeCron, } from '../../core/canvas/index.js';
2
2
  import { countTickets } from '../../core/canvas/attention.js';
3
+ import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
3
4
  import { readOutputRequest } from '../../core/runtime/structured-output.js';
4
- /** Durable work that requires a resident to remain automatically wakeable. */
5
+ /** Durable work that requires a node to remain automatically wakeable. */
5
6
  export function hasLiveObligation(nodeId) {
6
7
  return hasActiveLiveSubscription(nodeId)
7
8
  || hasLiveMessageWait(nodeId)
@@ -9,3 +10,30 @@ export function hasLiveObligation(nodeId) {
9
10
  || countTickets(nodeId) > 0
10
11
  || readOutputRequest(nodeId).state !== 'absent';
11
12
  }
13
+ /** What parking an unattended node actually amounts to — the one rule both the
14
+ * live-broker park clock (broker-supervision) and the already-parked
15
+ * reconciliation (dormant-inbox) apply.
16
+ *
17
+ * `release` — something can still wake it, so parking is a pause.
18
+ * `finish` — a resident with nothing left to wake it has completed its
19
+ * conversation, and the row becomes prunable history.
20
+ * `cancel` — a terminal node that never pushed a final was torn down before
21
+ * finishing, which is what `canceled` records; it stays revivable
22
+ * and becomes prunable. A live subscriber still awaiting its
23
+ * report is an outstanding deliverable, so such a node keeps its
24
+ * wakeable idle row instead of being written off.
25
+ *
26
+ * Undelivered mail of ANY tier also holds the row. A deferred-only unseen set
27
+ * never wakes a node on its own — it rides the next natural cycle — but a
28
+ * terminalized row no longer takes that cycle, so writing one off would drop
29
+ * content the sender already handed over.
30
+ */
31
+ export function parkEvent(nodeId, lifecycle) {
32
+ if (hasLiveObligation(nodeId))
33
+ return 'release';
34
+ if (readInboxSince(nodeId, readCursor(nodeId)).length > 0)
35
+ return 'release';
36
+ if (lifecycle === 'resident')
37
+ return 'finish';
38
+ return hasLiveSubscriber(nodeId) ? 'release' : 'cancel';
39
+ }
@@ -4,7 +4,12 @@ export declare const DEAD_REAP_GRACE_MS: number;
4
4
  export declare class StorageMaintenanceReconciler {
5
5
  private lastPruneAt;
6
6
  private lastSpareSweepAt;
7
+ private lastGhostSweepAt;
7
8
  run(now: number): void;
9
+ /** Drop rows whose on-disk node dir is gone. Such a row can never be revived,
10
+ * focused, or closed — every one of those resolves the node through meta.json —
11
+ * so it sits in the roster jamming the surfaces that try. One readdir per sweep. */
12
+ private reapGhostRows;
8
13
  /** Milliseconds since meta/telemetry was last written. Inbox activity is
9
14
  * deliberately excluded: children can keep writing a dead manager's inbox. */
10
15
  private msSinceDiskActivity;
@@ -1,6 +1,6 @@
1
1
  import { statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { closeFocusRow, listFocuses, listNodes, pruneToLimit, } from '../../core/canvas/index.js';
3
+ import { closeFocusRow, listFocuses, listNodes, pruneToLimit, reapGhostRows, } from '../../core/canvas/index.js';
4
4
  import { isSafeNodeId, jobDir, nodeDir } from '../../core/canvas/paths.js';
5
5
  import { emitEvent } from '../../core/events/emit.js';
6
6
  import { operationIdContext } from '../../core/events/operation-id.js';
@@ -12,20 +12,47 @@ export const DEAD_REAP_GRACE_MS = 10 * 60_000;
12
12
  const AUTO_PRUNE_MAX = 2000;
13
13
  const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
14
14
  const SPARE_SWEEP_INTERVAL_MS = 60 * 1000;
15
+ const GHOST_SWEEP_INTERVAL_MS = 60 * 1000;
15
16
  /** Recurring storage cleanup. The in-memory throttle clocks intentionally reset
16
17
  * on daemon boot, so the first tick runs both full sweeps. */
17
18
  export class StorageMaintenanceReconciler {
18
19
  lastPruneAt = Number.NEGATIVE_INFINITY;
19
20
  lastSpareSweepAt = Number.NEGATIVE_INFINITY;
21
+ lastGhostSweepAt = Number.NEGATIVE_INFINITY;
20
22
  run(now) {
21
23
  // Placement reap MUST precede focus GC, so GC catches a focus row the reap
22
24
  // strands. Both run after dormant-inbox reconciliation, so a same-tick
23
25
  // revive has already had the chance to re-anchor its focus.
24
26
  this.reapDeadResidue(now);
25
27
  this.gcStaleFocuses();
28
+ this.reapGhostRows(now);
26
29
  this.autoPrune(now);
27
30
  this.gcWarmPool(now);
28
31
  }
32
+ /** Drop rows whose on-disk node dir is gone. Such a row can never be revived,
33
+ * focused, or closed — every one of those resolves the node through meta.json —
34
+ * so it sits in the roster jamming the surfaces that try. One readdir per sweep. */
35
+ reapGhostRows(now) {
36
+ if (now - this.lastGhostSweepAt < GHOST_SWEEP_INTERVAL_MS)
37
+ return;
38
+ this.lastGhostSweepAt = now;
39
+ operationIdContext.fresh(() => {
40
+ try {
41
+ const reaped = reapGhostRows();
42
+ if (reaped.length > 0) {
43
+ emitEvent({
44
+ level: 'info',
45
+ event: 'canvas.ghost_rows.reaped',
46
+ outcome: 'succeeded',
47
+ fields: { reaped_count: reaped.length, reaped_ids: reaped },
48
+ });
49
+ }
50
+ }
51
+ catch (err) {
52
+ emitEvent({ level: 'error', event: 'canvas.ghost_rows.reap_failed', error: err });
53
+ }
54
+ });
55
+ }
29
56
  /** Milliseconds since meta/telemetry was last written. Inbox activity is
30
57
  * deliberately excluded: children can keep writing a dead manager's inbox. */
31
58
  msSinceDiskActivity(nodeId, now) {
@@ -3,8 +3,8 @@ import { formatReviewApproval } from '../../shared/generated-context.js';
3
3
  import { deliverNodeMessage } from '../messaging/node-message.js';
4
4
  function approvalBody(review, resultPath) {
5
5
  const summary = review.changed
6
- ? `The human reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
7
- : `The human reviewed and approved \`${review.file}\`. The document was not modified.`;
6
+ ? `The user reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
7
+ : `The user reviewed and approved \`${review.file}\`. The document was not modified.`;
8
8
  return `${summary}\n\nDurable review result: ${resultPath}`;
9
9
  }
10
10
  /** Render the immutable approval fact without replaying comment text. */
@@ -25,7 +25,7 @@ export async function deliverApproval(review, resultPath) {
25
25
  const delivery = await deliverNodeMessage({
26
26
  node_id: review.origin_node_id,
27
27
  body,
28
- label: 'human review approved',
28
+ label: 'review approved by the user',
29
29
  from: 'crtrd',
30
30
  mode: 'interactive',
31
31
  data: { review_id: review.review_id, result_path: resultPath },
@@ -18,8 +18,6 @@ export interface ReviewCancellationFinishOutcome {
18
18
  ticket_result?: TicketResult;
19
19
  }
20
20
  export type ReviewFinishOutcome = ReviewApprovalFinishOutcome | ReviewCancellationFinishOutcome;
21
- /** Retire a companion only through a lifecycle transition legal for its current status. */
22
- export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
23
21
  /** Approve on the human's behalf, waiting first when the companion is still
24
22
  * working. Waiting is the only new outcome: an idle companion approves in the
25
23
  * same pass it always did, and the wait never freezes the review — comments
@@ -1,5 +1,4 @@
1
1
  import { withCanvasWrite } from '../../core/canvas/db.js';
2
- import { getNode } from '../../core/canvas/canvas.js';
3
2
  import { responsePath } from '../../core/human/convention.js';
4
3
  import { ticketDir } from '../../core/human/root.js';
5
4
  import { readTicketResult } from '../../core/human/tickets.js';
@@ -10,9 +9,8 @@ import { readReviewSource } from '../../core/review/document.js';
10
9
  import { signalReviewActivity } from '../../core/review/signal.js';
11
10
  import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
12
11
  import { ReviewOperationError } from '../../core/review/types.js';
13
- import { transition } from '../../core/runtime/lifecycle.js';
14
- import { headlessBrokerHost } from '../../core/runtime/host.js';
15
12
  import { emitEvent } from '../../core/events/emit.js';
13
+ import { retireCompanion } from '../companion-retire.js';
16
14
  import { deliverNodeMessage } from '../messaging/node-message.js';
17
15
  import { cancelHumanTicket, deliverTerminalResult } from '../human/finish.js';
18
16
  import { companionHasQueuedWork, companionMidTurn, companionStillWorking } from './companion.js';
@@ -69,32 +67,6 @@ async function serializedFinish(reviewId, work) {
69
67
  finishChains.delete(reviewId);
70
68
  }
71
69
  }
72
- /** Retire a companion only through a lifecycle transition legal for its current status. */
73
- export function retireCompanion(nodeId, outcome) {
74
- const node = getNode(nodeId);
75
- if (node === null)
76
- return 'missing';
77
- if (node.status === 'done' || node.status === 'canceled')
78
- return 'already';
79
- if (outcome === 'approved' && (node.status === 'active' || node.status === 'idle')) {
80
- transition(nodeId, 'finish');
81
- try {
82
- headlessBrokerHost.teardown(nodeId);
83
- }
84
- catch { /* the terminal transition is authoritative */ }
85
- return 'retired';
86
- }
87
- // Cancellation is legal from every status. An already-dead companion cannot
88
- // be finished, so it is retired as canceled even when approval won.
89
- transition(nodeId, 'cancel');
90
- if (node.status === 'active' || node.status === 'idle') {
91
- try {
92
- headlessBrokerHost.teardown(nodeId);
93
- }
94
- catch { /* the terminal transition is authoritative */ }
95
- }
96
- return 'retired';
97
- }
98
70
  async function finishApproved(review, result) {
99
71
  let current = requireVisibleReview(review.review_id);
100
72
  if (current.projected_at === null) {
@@ -4,6 +4,8 @@ import { reviewPath } from '../../core/human/convention.js';
4
4
  import { ticketDir } from '../../core/human/root.js';
5
5
  import { submitReview } from '../../core/human/tickets.js';
6
6
  import { reviewsRoot } from '../../core/canvas/paths.js';
7
+ import { getNode } from '../../core/canvas/index.js';
8
+ import { loadProfileManifest } from '../../core/profiles/manifest.js';
7
9
  import { projectReviewResult } from '../../core/review/project.js';
8
10
  import { realizeReview } from '../../core/review/realize.js';
9
11
  import { listBindingReviews, listReviews, markProjected, markProjectionError, } from '../../core/review/store.js';
@@ -23,6 +25,12 @@ function republishTicketReview(review) {
23
25
  const dir = ticketDir(review.bridge_node_id);
24
26
  if (existsSync(reviewPath(dir)))
25
27
  return;
28
+ const origin = getNode(review.origin_node_id);
29
+ if (origin === null)
30
+ throw new Error(`ticket review origin is missing: ${review.origin_node_id}`);
31
+ const profileName = origin.profile_id == null
32
+ ? undefined
33
+ : loadProfileManifest(origin.profile_id).manifest.name;
26
34
  submitReview({
27
35
  dir,
28
36
  review: {
@@ -30,7 +38,10 @@ function republishTicketReview(review) {
30
38
  ...(review.output_path === null ? {} : { output: review.output_path }),
31
39
  title: basename(review.file),
32
40
  subtitle: review.subtitle,
33
- source: { nodeId: review.origin_node_id },
41
+ source: {
42
+ nodeId: review.origin_node_id,
43
+ ...(profileName === undefined ? {} : { profileName }),
44
+ },
34
45
  blockedSince: review.created,
35
46
  },
36
47
  });