@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
@@ -4,21 +4,20 @@
4
4
  import { existsSync } from 'node:fs';
5
5
  import { ApiError } from '../../../api/index.js';
6
6
  import { isSafeNodeId } from '../../../core/canvas/paths.js';
7
- import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
7
+ import { pageManifestPath, readTicketReplyRoute, reviewPath } from '../../../core/human/convention.js';
8
8
  import { ticketDir } from '../../../core/human/root.js';
9
9
  import { readTicketResult, validatePageTicketResponses } from '../../../core/human/tickets.js';
10
- import { readConfig } from '../../../core/config.js';
11
10
  import { getReviewByBridge } from '../../../core/review/store.js';
12
11
  import { ReviewOperationError } from '../../../core/review/types.js';
13
12
  import { spawnNode } from '../../../core/runtime/nodes.js';
14
13
  import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
15
14
  import { cancelReview } from '../../review/finish.js';
16
- function requireTicketNodeId(ctx) {
17
- const nodeId = ctx.params['node_id'];
18
- if (nodeId === undefined || !isSafeNodeId(nodeId)) {
19
- throw new ApiError(400, 'invalid_request', 'invalid ticket node id');
15
+ function requireInteractionTicketId(ctx) {
16
+ const ticketId = ctx.params['ticket_id'];
17
+ if (ticketId === undefined || !isSafeNodeId(ticketId)) {
18
+ throw new ApiError(400, 'invalid_request', 'invalid interaction ticket id');
20
19
  }
21
- return nodeId;
20
+ return ticketId;
22
21
  }
23
22
  // ===========================================================================
24
23
  // POST /v1/human/bridge — create a terminal bridge without a broker
@@ -48,7 +47,7 @@ function handleCreateBridge(ctx) {
48
47
  }
49
48
  }
50
49
  // ===========================================================================
51
- // POST /v1/human/tickets/:node_id/resolve and /cancel
50
+ // POST /v1/human/tickets/:ticket_id/resolve and /cancel
52
51
  // ===========================================================================
53
52
  function reviewSubmitGuidance() {
54
53
  return new ApiError(400, 'invalid_request', 'review approvals use POST /v1/human/reviews/:review_id/submit');
@@ -78,6 +77,8 @@ function requirePageTicket(dir) {
78
77
  }
79
78
  function invalidResolveRequest(error) {
80
79
  const detail = error instanceof Error ? error.message : String(error);
80
+ if (detail === 'page_has_no_response_lifecycle')
81
+ return new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
81
82
  return new ApiError(400, 'invalid_request', `invalid ticket resolve body: ${detail}`);
82
83
  }
83
84
  function preflightResolve(validate) {
@@ -89,26 +90,26 @@ function preflightResolve(validate) {
89
90
  }
90
91
  }
91
92
  async function handleResolve(ctx) {
92
- const nodeId = requireTicketNodeId(ctx);
93
+ const ticketId = requireInteractionTicketId(ctx);
93
94
  const request = requireResolveRequest(ctx);
94
- const dir = ticketDir(nodeId);
95
+ const dir = ticketDir(ticketId);
95
96
  requirePageTicket(dir);
96
- const productKinds = readConfig('user').page_components ?? [];
97
- const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
98
- await resolvePageTicket(nodeId, responses, productKinds);
99
- return { status: 200, body: { delivered: true, kind: 'page' } };
97
+ const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses));
98
+ const replyExpected = readTicketReplyRoute(dir) !== null;
99
+ await resolvePageTicket(ticketId, responses);
100
+ return { status: 200, body: { completed: true, reply_delivered: replyExpected, kind: 'page' } };
100
101
  }
101
102
  function hasPublishedTicket(dir) {
102
103
  return existsSync(pageManifestPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
103
104
  }
104
105
  async function handleCancel(ctx) {
105
- const nodeId = requireTicketNodeId(ctx);
106
+ const ticketId = requireInteractionTicketId(ctx);
106
107
  const request = ctx.body;
107
108
  if (request !== undefined && (typeof request !== 'object' || request === null)) {
108
109
  throw new ApiError(400, 'invalid_request', 'ticket cancel body must be an object');
109
110
  }
110
- const dir = ticketDir(nodeId);
111
- const review = getReviewByBridge(nodeId);
111
+ const dir = ticketDir(ticketId);
112
+ const review = getReviewByBridge(ticketId);
112
113
  try {
113
114
  if (review !== null) {
114
115
  if (review.state === 'approved') {
@@ -125,12 +126,15 @@ async function handleCancel(ctx) {
125
126
  if (!hasPublishedTicket(dir)) {
126
127
  throw new ApiError(404, 'ticket_not_found', 'no matching ticket');
127
128
  }
128
- await cancelHumanTicket(nodeId, request ?? {});
129
+ await cancelHumanTicket(ticketId, request ?? {});
129
130
  return { status: 200, body: { canceled: true } };
130
131
  }
131
132
  catch (error) {
132
133
  if (error instanceof ApiError)
133
134
  throw error;
135
+ if (error instanceof Error && error.message === 'page_has_no_response_lifecycle') {
136
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
137
+ }
134
138
  // Review errors are not part of the retained ticket route's vocabulary.
135
139
  if (error instanceof ReviewOperationError) {
136
140
  throw new ApiError(409, 'ticket_already_resolved', 'ticket already resolved');
@@ -140,6 +144,6 @@ async function handleCancel(ctx) {
140
144
  }
141
145
  export const humanRoutes = [
142
146
  { method: 'POST', pattern: '/v1/human/bridge', handler: handleCreateBridge },
143
- { method: 'POST', pattern: '/v1/human/tickets/:node_id/resolve', handler: handleResolve },
144
- { method: 'POST', pattern: '/v1/human/tickets/:node_id/cancel', handler: handleCancel },
147
+ { method: 'POST', pattern: '/v1/human/tickets/:ticket_id/resolve', handler: handleResolve },
148
+ { method: 'POST', pattern: '/v1/human/tickets/:ticket_id/cancel', handler: handleCancel },
145
149
  ];
@@ -1,2 +1,12 @@
1
+ import type { InboxTicketIdDTO } from '../../../api/index.js';
1
2
  import type { RouteTable } from '../router.js';
3
+ export interface ResolvedTicket {
4
+ ticketId: string;
5
+ dir: string;
6
+ kind: 'page' | 'review';
7
+ }
8
+ /** Resolve one opaque inbox ticket id to its canonical directory. Shared with
9
+ * the feedback-comment routes, which address tickets by the same ids. */
10
+ export declare function resolveTicket(ticketId: InboxTicketIdDTO): ResolvedTicket | null;
11
+ export declare function requireTicketId(params: Record<string, string>): InboxTicketIdDTO;
2
12
  export declare const inboxRoutes: RouteTable;
@@ -2,16 +2,17 @@
2
2
  // v1, inbox-contract.md §A). This public ticket-serving resource enumerates,
3
3
  // reads, responds to, and cancels tickets while preserving its product DTOs.
4
4
  import { createHash } from 'node:crypto';
5
- import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
6
- import { basename, resolve, join } from 'node:path';
5
+ import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
6
+ import { basename, resolve } from 'node:path';
7
7
  import { ApiError } from '../../../api/index.js';
8
- import { assetRoot } from '../../../core/asset-root.js';
8
+ import { readPageFeedback } from '../../../core/human/feedback.js';
9
+ import { toPageFeedbackDTO } from '../map.js';
9
10
  import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
10
- import { renderPageHtml } from '../../../core/human/page-render.js';
11
+ import { parsePage } from '../../../core/human/page.js';
11
12
  import { ticketsRoot } from '../../../core/human/root.js';
12
13
  import { scanInbox, scanPageHistory } from '../../../core/human/scan.js';
13
14
  import { readPageTicket, readTicketResult, validatePageTicketResponses, writePageProgress, } from '../../../core/human/tickets.js';
14
- import { readConfig } from '../../../core/config.js';
15
+ import { describePageAnswer } from '../../../core/human/answer.js';
15
16
  import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
16
17
  import { filterTerminalReviewTickets } from '../../../core/review/ticket-filter.js';
17
18
  const DEFAULT_CANCEL_REASON = 'Canceled from the inbox.';
@@ -23,7 +24,9 @@ const TICKET_ID_PATTERN = /^[a-f0-9]{64}$/;
23
24
  function ticketHash(canonicalRoot, ticketBasename) {
24
25
  return createHash('sha256').update(`${canonicalRoot}\0${ticketBasename}`, 'utf8').digest('hex');
25
26
  }
26
- function resolveTicket(ticketId) {
27
+ /** Resolve one opaque inbox ticket id to its canonical directory. Shared with
28
+ * the feedback-comment routes, which address tickets by the same ids. */
29
+ export function resolveTicket(ticketId) {
27
30
  let canonicalRoot;
28
31
  try {
29
32
  canonicalRoot = realpathSync(ticketsRoot());
@@ -54,13 +57,13 @@ function resolveTicket(ticketId) {
54
57
  if (resolve(canonicalDir, '..') !== canonicalRoot || basename(canonicalDir) !== entry)
55
58
  continue;
56
59
  if (existsSync(pageManifestPath(canonicalDir)))
57
- return { nodeId: entry, dir: canonicalDir, kind: 'page' };
60
+ return { ticketId: entry, dir: canonicalDir, kind: 'page' };
58
61
  if (existsSync(reviewPath(canonicalDir)))
59
- return { nodeId: entry, dir: canonicalDir, kind: 'review' };
62
+ return { ticketId: entry, dir: canonicalDir, kind: 'review' };
60
63
  }
61
64
  return null;
62
65
  }
63
- function requireTicketId(params) {
66
+ export function requireTicketId(params) {
64
67
  const ticketId = params['ticket_id'];
65
68
  if (ticketId === undefined || !TICKET_ID_PATTERN.test(ticketId)) {
66
69
  throw new ApiError(400, 'invalid_request', 'invalid ticket id');
@@ -80,14 +83,16 @@ function pageSlotToDTO(slot) {
80
83
  dto.id = slot.id;
81
84
  if (slot.unvalidated === true)
82
85
  dto.unvalidated = true;
86
+ if (slot.display === true)
87
+ dto.display = true;
83
88
  return dto;
84
89
  }
85
90
  function pageManifestToDTO(manifest) {
86
91
  const dto = {
87
- schema: 'crtr.page/v1',
92
+ schema: 'crtr.page/v2',
88
93
  dialect: manifest.dialect,
89
94
  title: manifest.title,
90
- placement: manifest.placement,
95
+ delivery: manifest.delivery,
91
96
  document: manifest.document,
92
97
  steps: manifest.steps,
93
98
  slots: manifest.slots.map(pageSlotToDTO),
@@ -100,8 +105,10 @@ function pageManifestToDTO(manifest) {
100
105
  source.sessionName = manifest.source.sessionName;
101
106
  if (manifest.source.askedBy !== undefined)
102
107
  source.askedBy = manifest.source.askedBy;
103
- if (manifest.source.blockedSince !== undefined)
104
- source.blockedSince = manifest.source.blockedSince;
108
+ if (manifest.source.emittedAt !== undefined)
109
+ source.emittedAt = manifest.source.emittedAt;
110
+ if (manifest.source.profileName !== undefined)
111
+ source.profileName = manifest.source.profileName;
105
112
  if (manifest.source.nodeId !== undefined)
106
113
  source.nodeId = manifest.source.nodeId;
107
114
  if (Object.keys(source).length > 0)
@@ -163,8 +170,7 @@ function isSlotValidationError(err) {
163
170
  // GET /v1/human/inbox — enumerate
164
171
  // ===========================================================================
165
172
  function handleList() {
166
- const productKinds = readConfig('user').page_components ?? [];
167
- const allItems = scanInbox(productKinds);
173
+ const allItems = scanInbox();
168
174
  const filtered = filterTerminalReviewTickets(allItems);
169
175
  const tickets = filtered.map((item) => {
170
176
  if (item.kind === 'page') {
@@ -176,9 +182,10 @@ function handleList() {
176
182
  placement: item.placement,
177
183
  dialect: item.dialect,
178
184
  steps: item.steps,
179
- blocked_since: item.blockedSince,
185
+ emitted_at: item.emittedAt,
180
186
  source: item.source,
181
187
  slot_kinds: item.slotKinds,
188
+ inbox: item.inbox,
182
189
  awaits_response: item.awaitsResponse,
183
190
  };
184
191
  if (item.subtitle !== '')
@@ -197,7 +204,8 @@ function handleList() {
197
204
  }
198
205
  throw new Error(`unknown ticket kind: ${item.kind}`);
199
206
  }).sort((a, b) => {
200
- const byTime = Date.parse(b.blocked_since) - Date.parse(a.blocked_since);
207
+ const pageTime = (item) => item.kind === 'page' ? item.emitted_at : item.blocked_since;
208
+ const byTime = Date.parse(pageTime(b)) - Date.parse(pageTime(a));
201
209
  return byTime !== 0 ? byTime : a.ticket_id.localeCompare(b.ticket_id);
202
210
  });
203
211
  return { status: 200, body: { tickets } };
@@ -210,8 +218,7 @@ function handleHistory(ctx) {
210
218
  if (!nodeId || typeof nodeId !== 'string') {
211
219
  throw new ApiError(400, 'invalid_request', 'history requires a node_id query param');
212
220
  }
213
- const productKinds = readConfig('user').page_components ?? [];
214
- const history = scanPageHistory(nodeId, productKinds);
221
+ const history = scanPageHistory(nodeId);
215
222
  const canonicalRoot = realpathSync(ticketsRoot());
216
223
  const tickets = history.map((item) => {
217
224
  const ticketId = createHash('sha256').update(`${canonicalRoot}\0${item.id}`, 'utf8').digest('hex');
@@ -222,14 +229,17 @@ function handleHistory(ctx) {
222
229
  placement: item.placement,
223
230
  dialect: item.dialect,
224
231
  steps: item.steps,
225
- blocked_since: item.blockedSince,
232
+ emitted_at: item.emittedAt,
226
233
  source: item.source,
227
234
  slot_kinds: item.slotKinds,
235
+ inbox: item.inbox,
228
236
  awaits_response: item.awaitsResponse,
229
237
  state: item.state,
230
238
  };
231
239
  if (item.subtitle !== '')
232
240
  dto.subtitle = item.subtitle;
241
+ if (item.answerDigest !== undefined)
242
+ dto.answer_digest = item.answerDigest;
233
243
  return dto;
234
244
  });
235
245
  return { status: 200, body: { tickets } };
@@ -245,10 +255,9 @@ function handleGetPage(ctx) {
245
255
  if (resolved.kind === 'review') {
246
256
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page read operation');
247
257
  }
248
- const productKinds = readConfig('user').page_components ?? [];
249
258
  let ticket;
250
259
  try {
251
- ticket = readPageTicket(resolved.dir, productKinds);
260
+ ticket = readPageTicket(resolved.dir);
252
261
  }
253
262
  catch (err) {
254
263
  throw new ApiError(500, 'internal', 'the ticket could not be read');
@@ -262,9 +271,10 @@ function handleGetPage(ctx) {
262
271
  kind: 'page',
263
272
  state: ticket.state,
264
273
  page: pageManifestToDTO(ticket.manifest),
265
- document: ticket.document,
266
- document_media_type: ticket.manifest.dialect === 'md' ? 'text/markdown' : 'text/html',
274
+ document: ticket.manifest.dialect === 'jsx' ? ticket.module : ticket.document,
275
+ document_media_type: ticket.manifest.dialect === 'jsx' ? 'text/jsx' : 'text/html',
267
276
  progress: progressDTO,
277
+ feedback: toPageFeedbackDTO(readPageFeedback(resolved.dir)),
268
278
  },
269
279
  };
270
280
  }
@@ -280,6 +290,8 @@ async function handleRespond(ctx) {
280
290
  if (resolved.kind === 'review') {
281
291
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets submit through their own path');
282
292
  }
293
+ if (readPageTicket(resolved.dir).state === 'passive')
294
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
283
295
  const result = readTicketResult(resolved.dir);
284
296
  if (result !== null && result.kind !== 'canceled') {
285
297
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
@@ -287,9 +299,8 @@ async function handleRespond(ctx) {
287
299
  if (result?.kind === 'canceled') {
288
300
  throw new ApiError(409, 'ticket_canceled', 'canceled tickets cannot be answered');
289
301
  }
290
- const productKinds = readConfig('user').page_components ?? [];
291
302
  try {
292
- validatePageTicketResponses(resolved.dir, request.responses, productKinds);
303
+ validatePageTicketResponses(resolved.dir, request.responses);
293
304
  }
294
305
  catch (error) {
295
306
  if (isSlotValidationError(error)) {
@@ -298,8 +309,8 @@ async function handleRespond(ctx) {
298
309
  throw new ApiError(500, 'internal', 'validation failed');
299
310
  }
300
311
  try {
301
- // The shared completion path: it publishes the result and wakes the asking node.
302
- const completed = await resolvePageTicket(resolved.nodeId, request.responses, productKinds);
312
+ // The shared completion path publishes the result and follows the ticket's optional reply route.
313
+ const completed = await resolvePageTicket(resolved.ticketId, request.responses);
303
314
  if (completed.kind !== 'page') {
304
315
  throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
305
316
  }
@@ -326,13 +337,14 @@ function handleProgress(ctx) {
326
337
  if (resolved.kind === 'review') {
327
338
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets do not support progress autosave');
328
339
  }
340
+ if (readPageTicket(resolved.dir).state === 'passive')
341
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
329
342
  const result = readTicketResult(resolved.dir);
330
343
  if (result !== null && result.kind !== 'canceled') {
331
344
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
332
345
  }
333
- const productKinds = readConfig('user').page_components ?? [];
334
346
  try {
335
- writePageProgress(resolved.dir, request.responses, productKinds);
347
+ writePageProgress(resolved.dir, request.responses);
336
348
  return { status: 200, body: {} };
337
349
  }
338
350
  catch (error) {
@@ -354,8 +366,10 @@ async function handleCancel(ctx) {
354
366
  if (resolved.kind === 'review') {
355
367
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have their own cancel path');
356
368
  }
369
+ if (readPageTicket(resolved.dir).state === 'passive')
370
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
357
371
  try {
358
- const result = await cancelHumanTicket(resolved.nodeId, {
372
+ const result = await cancelHumanTicket(resolved.ticketId, {
359
373
  reason: request.reason ?? DEFAULT_CANCEL_REASON,
360
374
  actor: 'human',
361
375
  });
@@ -390,93 +404,18 @@ function handleGetResponse(ctx) {
390
404
  if (result.kind !== 'page') {
391
405
  throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
392
406
  }
393
- return { status: 200, body: result };
394
- }
395
- // ===========================================================================
396
- // GET /v1/human/inbox/:ticket_id/render — the page's HTML payload
397
- // ===========================================================================
398
- function handleRenderPage(ctx) {
399
- const ticketId = requireTicketId(ctx.params);
400
- const resolved = resolveTicket(ticketId);
401
- if (resolved === null)
402
- throw new ApiError(404, 'ticket_not_found', 'no matching ticket');
403
- if (resolved.kind === 'review') {
404
- throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page render operation');
405
- }
406
- const productKinds = readConfig('user').page_components ?? [];
407
- // Addressable for pending, resolved, and canceled tickets alike — the same
408
- // read rule as the detail route: a page stays readable after it resolves.
409
- let ticket;
410
407
  try {
411
- ticket = readPageTicket(resolved.dir, productKinds);
408
+ const manifest = parsePage(resolved.dir);
409
+ return { status: 200, body: { ...result, answer: describePageAnswer(manifest, result.responses, { completedAt: result.completedAt }) } };
412
410
  }
413
411
  catch {
414
- throw new ApiError(500, 'internal', 'the ticket could not be read');
415
- }
416
- try {
417
- return { status: 200, body: { html: renderPageHtml(ticket.manifest, ticket.document, productKinds) } };
412
+ throw new ApiError(500, 'internal', 'the ticket answer could not be read');
418
413
  }
419
- catch {
420
- throw new ApiError(500, 'internal', 'the page could not be rendered');
421
- }
422
- }
423
- // ===========================================================================
424
- // GET /v1/human/pages/bundle.js|.css — the page runtime assets
425
- // ===========================================================================
426
- const BUNDLE_MEDIA_TYPES = {
427
- 'bundle.js': 'text/javascript; charset=utf-8',
428
- 'bundle.css': 'text/css; charset=utf-8',
429
- };
430
- const bundleCache = new Map();
431
- /** Read one built bundle asset, reusing the cached bytes while the file on disk
432
- * is unchanged. A runtime generation swap changes mtime, so the next request
433
- * re-reads and re-hashes. */
434
- function readBundleAsset(name) {
435
- let path;
436
- let stat;
437
- try {
438
- path = join(assetRoot(), 'pages', name);
439
- stat = statSync(path);
440
- }
441
- catch {
442
- throw new ApiError(500, 'page_bundle_unavailable', `${name} is not present in this runtime generation; the page runtime is built by \`npm run build\``);
443
- }
444
- const cached = bundleCache.get(name);
445
- if (cached !== undefined && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
446
- return cached;
447
- const bytes = readFileSync(path);
448
- const asset = {
449
- mtimeMs: stat.mtimeMs,
450
- size: stat.size,
451
- bytes,
452
- etag: `"${createHash('sha256').update(bytes).digest('hex').slice(0, 32)}"`,
453
- };
454
- bundleCache.set(name, asset);
455
- return asset;
456
- }
457
- function etagMatches(header, etag) {
458
- if (header === undefined)
459
- return false;
460
- return header.split(',').map((candidate) => candidate.trim()).some((candidate) => candidate === '*' || candidate === etag || candidate === `W/${etag}`);
461
- }
462
- function handleBundleAsset(name) {
463
- return (ctx) => {
464
- const asset = readBundleAsset(name);
465
- const headers = { etag: asset.etag, 'cache-control': 'no-cache' };
466
- const ifNoneMatch = ctx.req.headers['if-none-match'];
467
- if (etagMatches(Array.isArray(ifNoneMatch) ? ifNoneMatch.join(',') : ifNoneMatch, asset.etag)) {
468
- return { status: 304, headers };
469
- }
470
- return { status: 200, rawBody: asset.bytes, headers: { ...headers, 'content-type': BUNDLE_MEDIA_TYPES[name] } };
471
- };
472
414
  }
473
415
  export const inboxRoutes = [
474
- { method: 'GET', pattern: '/v1/human/pages/bundle.js', handler: handleBundleAsset('bundle.js') },
475
- { method: 'GET', pattern: '/v1/human/pages/bundle.css', handler: handleBundleAsset('bundle.css') },
476
416
  { method: 'GET', pattern: '/v1/human/inbox', handler: handleList },
477
417
  { method: 'GET', pattern: '/v1/human/inbox/history', handler: handleHistory },
478
418
  { method: 'GET', pattern: '/v1/human/inbox/:ticket_id', handler: handleGetPage },
479
- { method: 'GET', pattern: '/v1/human/inbox/:ticket_id/render', handler: handleRenderPage },
480
419
  { method: 'GET', pattern: '/v1/human/inbox/:ticket_id/response', handler: handleGetResponse },
481
420
  { method: 'POST', pattern: '/v1/human/inbox/:ticket_id/respond', handler: handleRespond },
482
421
  { method: 'POST', pattern: '/v1/human/inbox/:ticket_id/progress', handler: handleProgress },
@@ -1,17 +1,13 @@
1
- // Profile handlers (spec §6.6). Server-side for remote/Core (P2) consumers; the
2
- // local `crtr profile *` verbs stay fs-local (they never open canvas.db, so the
3
- // exit criterion is untouched by their staying local). The profile store is a
4
- // deliberately multi-writer fs store serialized by `withProfileManifestLock`
5
- // (spec §12), so crtrd installing a profile here coexists with the CLI verbs.
1
+ // Profile handlers (spec §6.6). Manifest-only operations share the locked
2
+ // profile filesystem store. Deletion crosses canvas, cron, inbox, review, and
3
+ // broker state, so it is one daemon-owned lifecycle operation.
6
4
  //
7
5
  // PUT /v1/profiles/:name idempotent ensure (create-or-return)
8
6
  // GET /v1/profiles list all profiles
9
7
  // GET /v1/profiles/:name resolve one profile by name or id
10
- // DELETE /v1/profiles/:name idempotent delete (missing → 204)
11
- //
12
- // The primitive is unchanged — the caller is relocated into crtrd (spec §4.3).
13
- import { CrtrError } from '../../../core/errors.js';
14
- import { createProfile, deleteProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
8
+ // DELETE /v1/profiles/:name required-force cascade or detach
9
+ import { createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
10
+ import { requestProfileDeletion } from '../../profile-delete.js';
15
11
  import { toProfileDTO } from '../map.js';
16
12
  const ROOT_PROFILE_NAME = 'root';
17
13
  /** `PUT /v1/profiles/:name` — idempotent ensure. `root` maps to the singleton
@@ -41,25 +37,11 @@ function handleGet(ctx) {
41
37
  const name = ctx.params['name'];
42
38
  return { status: 200, body: toProfileDTO(loadProfileManifest(name)) };
43
39
  }
44
- /** `DELETE /v1/profiles/:name` — idempotent delete by exact id or unique name.
45
- * A miss is success (204): the caller's goal — the profile is gone — already
46
- * holds, so reconcile/retry loops (e.g. Core's applet-delete leg) are safe to
47
- * re-run. An ambiguous name still throws `ambiguous` (→ 400): with several
48
- * same-named profiles there is no safe pick, and the state is NOT "already
49
- * deleted". */
50
- function handleDelete(ctx) {
51
- const name = ctx.params['name'];
52
- let profileId;
53
- try {
54
- ({ profileId } = loadProfileManifest(name));
55
- }
56
- catch (err) {
57
- if (err instanceof CrtrError && err.code === 'not_found')
58
- return { status: 204 };
59
- throw err;
60
- }
61
- deleteProfile(profileId);
62
- return { status: 204 };
40
+ /** `DELETE /v1/profiles/:name` — forced cascade by default, or forced
41
+ * identity detach when requested. Exact resolution, blockers, counts, broker
42
+ * teardown, and final manifest removal all live in the daemon lifecycle. */
43
+ async function handleDelete(ctx) {
44
+ return requestProfileDeletion(ctx.params['name'], ctx.body);
63
45
  }
64
46
  export const profileRoutes = [
65
47
  { method: 'PUT', pattern: '/v1/profiles/:name', handler: handleEnsure },
@@ -5,7 +5,8 @@ import type { ProfileEntry } from '../../core/profiles/manifest.js';
5
5
  import type { ReviveResult } from '../../core/runtime/revive.js';
6
6
  import type { CloseNodeResult } from '../../core/runtime/close.js';
7
7
  import { type PushResult } from '../../core/feed/feed.js';
8
- import type { CloseResultDTO, CronDTO, CronRunDTO, ErrorBody, NodeEdgesDTO, NodePathsDTO, NodeStatusDTO, NodeSummaryDTO, NodeDetailDTO, NodeWorktreeDTO, ProfileDTO, ReviewCommentDTO, ReviewCommentEventDTO, ReviewDTO, FeedbackResultDTO, PushReportResultDTO, ReviveResultDTO, SubscriptionDTO } from '../../api/index.js';
8
+ import type { PageFeedback, PageFeedbackComment } from '../../core/human/feedback.js';
9
+ import type { CloseResultDTO, CronDTO, CronRunDTO, ErrorBody, NodeEdgesDTO, NodePathsDTO, NodeStatusDTO, NodeSummaryDTO, NodeDetailDTO, NodeWorktreeDTO, ProfileDTO, ReviewCommentDTO, ReviewCommentEventDTO, ReviewDTO, FeedbackResultDTO, PageFeedbackCommentDTO, PageFeedbackDTO, PushReportResultDTO, ReviveResultDTO, SubscriptionDTO } from '../../api/index.js';
9
10
  /** Project the queryable row (what `listNodes` yields) into a summary DTO. */
10
11
  export declare function toNodeSummaryDTO(row: NodeRow): NodeSummaryDTO;
11
12
  /** The subscription/child edges a detail projection needs. The handler gathers
@@ -70,6 +71,10 @@ export interface ReviewDTOExtras {
70
71
  export declare function toReviewDTO(row: ReviewRecord, extras?: ReviewDTOExtras): ReviewDTO;
71
72
  /** Project one daemon-owned comment, omitting absent node identities on human actions. */
72
73
  export declare function toReviewCommentDTO(comment: ReviewComment): ReviewCommentDTO;
74
+ /** Project one ticket-attached page feedback comment. */
75
+ export declare function toPageFeedbackCommentDTO(comment: PageFeedbackComment): PageFeedbackCommentDTO;
76
+ /** Project one ticket's page feedback state. */
77
+ export declare function toPageFeedbackDTO(feedback: PageFeedback): PageFeedbackDTO;
73
78
  /** Project one immutable review-comment audit event. */
74
79
  export declare function toReviewCommentEventDTO(event: ReviewCommentEvent): ReviewCommentEventDTO;
75
80
  /** Project the immutable snake_case review result to the existing camelCase feedback wire shape. */
@@ -81,6 +81,8 @@ export function toNodeWorktreeDTO(w) {
81
81
  base_sha: w.base_sha,
82
82
  created: w.created,
83
83
  };
84
+ if (w.cleanup !== undefined)
85
+ dto.cleanup = w.cleanup;
84
86
  if (w.closed !== undefined)
85
87
  dto.closed = w.closed;
86
88
  return dto;
@@ -130,6 +132,7 @@ export function toNodeDetailDTO(meta, edges) {
130
132
  dto.review_binding = meta.review_binding === null
131
133
  ? null
132
134
  : {
135
+ ...(meta.review_binding.kind === undefined ? {} : { kind: meta.review_binding.kind }),
133
136
  review_id: meta.review_binding.review_id,
134
137
  origin_node_id: meta.review_binding.origin_node_id,
135
138
  branch_file: meta.review_binding.branch_file,
@@ -331,6 +334,26 @@ export function toReviewCommentDTO(comment) {
331
334
  created_seq: comment.created_seq,
332
335
  };
333
336
  }
337
+ /** Project one ticket-attached page feedback comment. */
338
+ export function toPageFeedbackCommentDTO(comment) {
339
+ return {
340
+ id: comment.id,
341
+ quote: comment.quote,
342
+ ...(comment.context === undefined ? {} : { context: comment.context }),
343
+ note: comment.note,
344
+ status: comment.status,
345
+ created_at: comment.createdAt,
346
+ ...(comment.resolvedAt === undefined ? {} : { resolved_at: comment.resolvedAt }),
347
+ };
348
+ }
349
+ /** Project one ticket's page feedback state. */
350
+ export function toPageFeedbackDTO(feedback) {
351
+ return {
352
+ companion_node_id: feedback.companionNodeId ?? null,
353
+ open_count: feedback.comments.filter((comment) => comment.status === 'open').length,
354
+ comments: feedback.comments.map(toPageFeedbackCommentDTO),
355
+ };
356
+ }
334
357
  /** Project one immutable review-comment audit event. */
335
358
  export function toReviewCommentEventDTO(event) {
336
359
  return {
@@ -28,6 +28,7 @@ import { focusRoutes } from './handlers/focus.js';
28
28
  import { healthRoutes } from './handlers/health.js';
29
29
  import { humanRoutes } from './handlers/human.js';
30
30
  import { inboxRoutes } from './handlers/inbox.js';
31
+ import { feedbackCommentRoutes } from './handlers/feedback-comments.js';
31
32
  import { memoryRoutes } from './handlers/memory.js';
32
33
  import { messageRoutes } from './handlers/messages.js';
33
34
  import { modelAuthRoutes } from './handlers/modelauth.js';
@@ -64,7 +65,8 @@ function buildRouter() {
64
65
  .registerAll(humanRoutes)
65
66
  .registerAll(reviewRoutes)
66
67
  .registerAll(reviewCommentRoutes)
67
- .registerAll(inboxRoutes);
68
+ .registerAll(inboxRoutes)
69
+ .registerAll(feedbackCommentRoutes);
68
70
  }
69
71
  /** Is this request an attach upgrade — `GET /v1/nodes/{id}/attach` — and if so,
70
72
  * what is `{id}`? Returns null for any non-attach path (the caller destroys the
@@ -0,0 +1,2 @@
1
+ /** Retire a companion only through a lifecycle transition legal for its current status. */
2
+ export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
@@ -0,0 +1,33 @@
1
+ // Companion retirement — the one lifecycle exit shared by human-review and
2
+ // page-feedback companions. Lives outside review/finish.ts so human/finish.ts
3
+ // can retire a feedback companion without importing the review finisher (which
4
+ // itself imports human/finish.ts).
5
+ import { getNode } from '../core/canvas/canvas.js';
6
+ import { transition } from '../core/runtime/lifecycle.js';
7
+ import { headlessBrokerHost } from '../core/runtime/host.js';
8
+ /** Retire a companion only through a lifecycle transition legal for its current status. */
9
+ export function retireCompanion(nodeId, outcome) {
10
+ const node = getNode(nodeId);
11
+ if (node === null)
12
+ return 'missing';
13
+ if (node.status === 'done' || node.status === 'canceled')
14
+ return 'already';
15
+ if (outcome === 'approved' && (node.status === 'active' || node.status === 'idle')) {
16
+ transition(nodeId, 'finish');
17
+ try {
18
+ headlessBrokerHost.teardown(nodeId);
19
+ }
20
+ catch { /* the terminal transition is authoritative */ }
21
+ return 'retired';
22
+ }
23
+ // Cancellation is legal from every status. An already-dead companion cannot
24
+ // be finished, so it is retired as canceled even when approval won.
25
+ transition(nodeId, 'cancel');
26
+ if (node.status === 'active' || node.status === 'idle') {
27
+ try {
28
+ headlessBrokerHost.teardown(nodeId);
29
+ }
30
+ catch { /* the terminal transition is authoritative */ }
31
+ }
32
+ return 'retired';
33
+ }