@north-light/crouter 0.3.198 → 0.3.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/dist/api/client.d.ts +18 -18
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +81 -34
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +6 -6
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  99. package/dist/core/__tests__/human-deliver.test.js +31 -26
  100. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  101. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  102. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  103. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  104. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  105. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  106. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  107. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  108. package/dist/core/__tests__/session-cycles.test.js +22 -0
  109. package/dist/core/__tests__/stop-guard.test.js +4 -4
  110. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  111. package/dist/core/bash-jobs.d.ts +6 -0
  112. package/dist/core/bash-jobs.js +10 -0
  113. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  114. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  115. package/dist/core/canvas/attention.js +8 -6
  116. package/dist/core/canvas/browse/model.d.ts +8 -2
  117. package/dist/core/canvas/browse/model.js +11 -4
  118. package/dist/core/canvas/canvas.d.ts +7 -0
  119. package/dist/core/canvas/canvas.js +37 -0
  120. package/dist/core/canvas/crons.d.ts +9 -0
  121. package/dist/core/canvas/crons.js +51 -0
  122. package/dist/core/canvas/node-order.d.ts +22 -1
  123. package/dist/core/canvas/node-order.js +26 -1
  124. package/dist/core/canvas/render-source.d.ts +5 -0
  125. package/dist/core/canvas/render-source.js +1 -0
  126. package/dist/core/canvas/types.d.ts +9 -3
  127. package/dist/core/command-plugins/bundle.d.ts +6 -1
  128. package/dist/core/command-plugins/bundle.js +26 -13
  129. package/dist/core/command.js +2 -1
  130. package/dist/core/config.d.ts +47 -1
  131. package/dist/core/config.js +137 -4
  132. package/dist/core/help.js +1 -1
  133. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  134. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  135. package/dist/core/human/__tests__/page.test.js +56 -98
  136. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  137. package/dist/core/human/answer-text.js +3 -3
  138. package/dist/core/human/answer.d.ts +6 -4
  139. package/dist/core/human/answer.js +4 -10
  140. package/dist/core/human/component-docs.d.ts +11 -7
  141. package/dist/core/human/component-docs.js +361 -88
  142. package/dist/core/human/convention.d.ts +17 -3
  143. package/dist/core/human/convention.js +36 -5
  144. package/dist/core/human/feedback-companion.d.ts +20 -0
  145. package/dist/core/human/feedback-companion.js +98 -0
  146. package/dist/core/human/feedback.d.ts +65 -0
  147. package/dist/core/human/feedback.js +116 -0
  148. package/dist/core/human/page-catalog.d.ts +40 -5
  149. package/dist/core/human/page-catalog.js +102 -30
  150. package/dist/core/human/page-errors.d.ts +4 -0
  151. package/dist/core/human/page-errors.js +7 -0
  152. package/dist/core/human/page-eval.d.ts +20 -0
  153. package/dist/core/human/page-eval.js +123 -0
  154. package/dist/core/human/page-schema.d.ts +23 -191
  155. package/dist/core/human/page-schema.js +60 -89
  156. package/dist/core/human/page-synth.d.ts +1 -1
  157. package/dist/core/human/page-synth.js +15 -18
  158. package/dist/core/human/page.d.ts +12 -27
  159. package/dist/core/human/page.js +142 -474
  160. package/dist/core/human/root.d.ts +6 -2
  161. package/dist/core/human/root.js +12 -4
  162. package/dist/core/human/scan.d.ts +3 -4
  163. package/dist/core/human/scan.js +16 -15
  164. package/dist/core/human/summary.d.ts +1 -2
  165. package/dist/core/human/summary.js +2 -2
  166. package/dist/core/human/tickets.d.ts +20 -14
  167. package/dist/core/human/tickets.js +91 -31
  168. package/dist/core/human/types.d.ts +10 -2
  169. package/dist/core/preview-registry.js +8 -10
  170. package/dist/core/profiles/default-binding.d.ts +4 -0
  171. package/dist/core/profiles/default-binding.js +24 -0
  172. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  173. package/dist/core/profiles/deletion-reservation.js +38 -0
  174. package/dist/core/profiles/manifest.d.ts +3 -0
  175. package/dist/core/profiles/manifest.js +12 -0
  176. package/dist/core/profiles/select.js +2 -2
  177. package/dist/core/review/realize.js +3 -3
  178. package/dist/core/review/store.d.ts +6 -0
  179. package/dist/core/review/store.js +31 -0
  180. package/dist/core/runtime/bearings.d.ts +0 -4
  181. package/dist/core/runtime/bearings.js +4 -62
  182. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  183. package/dist/core/runtime/bin-contributions.js +186 -0
  184. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  185. package/dist/core/runtime/broker/client-registry.js +13 -1
  186. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  187. package/dist/core/runtime/broker/extension-abort.js +4 -2
  188. package/dist/core/runtime/broker/fault-retry.js +4 -0
  189. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  190. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  191. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  192. package/dist/core/runtime/broker/read-ops.js +2 -2
  193. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  194. package/dist/core/runtime/broker/rebind.js +6 -1
  195. package/dist/core/runtime/broker-extension-render.js +19 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  197. package/dist/core/runtime/broker.d.ts +1 -1
  198. package/dist/core/runtime/broker.js +17 -6
  199. package/dist/core/runtime/front-door-env.d.ts +5 -0
  200. package/dist/core/runtime/front-door-env.js +16 -0
  201. package/dist/core/runtime/front-door.d.ts +1 -5
  202. package/dist/core/runtime/front-door.js +2 -5
  203. package/dist/core/runtime/host.d.ts +1 -1
  204. package/dist/core/runtime/invocation.d.ts +6 -0
  205. package/dist/core/runtime/invocation.js +10 -0
  206. package/dist/core/runtime/kickoff.js +34 -0
  207. package/dist/core/runtime/launch.d.ts +2 -6
  208. package/dist/core/runtime/node-read.js +4 -1
  209. package/dist/core/runtime/nodes.js +2 -0
  210. package/dist/core/runtime/promote.d.ts +1 -0
  211. package/dist/core/runtime/promote.js +3 -1
  212. package/dist/core/runtime/session-cycles.d.ts +6 -0
  213. package/dist/core/runtime/session-cycles.js +15 -1
  214. package/dist/core/runtime/session-visibility.d.ts +17 -8
  215. package/dist/core/runtime/session-visibility.js +19 -10
  216. package/dist/core/runtime/spawn-env.d.ts +9 -1
  217. package/dist/core/runtime/spawn-env.js +62 -2
  218. package/dist/core/runtime/stop-guard.d.ts +2 -2
  219. package/dist/core/runtime/stop-guard.js +2 -2
  220. package/dist/core/runtime/stop-signals.d.ts +1 -1
  221. package/dist/core/runtime/stop-signals.js +2 -2
  222. package/dist/core/runtime/tmux-bindings.js +1 -1
  223. package/dist/core/session-model/session-state.d.ts +4 -3
  224. package/dist/core/session-model/session-state.js +14 -1
  225. package/dist/core/user-settings.d.ts +21 -2
  226. package/dist/core/user-settings.js +32 -11
  227. package/dist/core/worktree.d.ts +15 -45
  228. package/dist/core/worktree.js +59 -261
  229. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  230. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  231. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  232. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  233. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  234. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  235. package/dist/daemon/api/handlers/human.js +24 -20
  236. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  237. package/dist/daemon/api/handlers/inbox.js +39 -115
  238. package/dist/daemon/api/handlers/profiles.js +11 -29
  239. package/dist/daemon/api/map.d.ts +6 -1
  240. package/dist/daemon/api/map.js +23 -0
  241. package/dist/daemon/api/server.js +3 -1
  242. package/dist/daemon/companion-retire.d.ts +2 -0
  243. package/dist/daemon/companion-retire.js +33 -0
  244. package/dist/daemon/cron-run.js +14 -16
  245. package/dist/daemon/human/finish.d.ts +12 -7
  246. package/dist/daemon/human/finish.js +101 -38
  247. package/dist/daemon/human/sweep.js +5 -1
  248. package/dist/daemon/profile-delete.d.ts +7 -0
  249. package/dist/daemon/profile-delete.js +306 -0
  250. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  251. package/dist/daemon/review/deliver.js +3 -3
  252. package/dist/daemon/review/finish.d.ts +0 -2
  253. package/dist/daemon/review/finish.js +1 -29
  254. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  255. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  256. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  257. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  258. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  259. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  260. package/dist/pi-extensions/canvas-stophook.js +4 -4
  261. package/dist/shared/generated-context.js +2 -2
  262. package/dist/types.d.ts +44 -2
  263. package/dist/types.js +7 -2
  264. package/package.json +4 -4
  265. package/runtime.lock.json +5 -33
  266. package/dist/builtin-memory/init.md +0 -38
  267. package/dist/builtin-memory/plan.md +0 -19
  268. package/dist/builtin-memory/spec.md +0 -19
  269. package/dist/commands/human/doc.d.ts +0 -2
  270. package/dist/commands/human/doc.js +0 -91
  271. package/dist/commands/human/html.js +0 -81
  272. package/dist/commands/human/inbox.d.ts +0 -2
  273. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  274. package/dist/core/human/__tests__/page-render.test.js +0 -66
  275. package/dist/core/human/html-markdown.d.ts +0 -3
  276. package/dist/core/human/html-markdown.js +0 -288
  277. package/dist/core/human/page-render.d.ts +0 -4
  278. package/dist/core/human/page-render.js +0 -105
  279. package/dist/pages/bundle.css +0 -1
  280. package/dist/pages/bundle.js +0 -1778
  281. package/dist/pages/comments.d.ts +0 -72
  282. package/dist/pages/comments.js +0 -176
  283. package/dist/pages/controls.d.ts +0 -98
  284. package/dist/pages/controls.js +0 -261
  285. package/dist/pages/elements/cards.d.ts +0 -28
  286. package/dist/pages/elements/cards.js +0 -915
  287. package/dist/pages/elements/chart.d.ts +0 -23
  288. package/dist/pages/elements/chart.js +0 -664
  289. package/dist/pages/elements/options.d.ts +0 -30
  290. package/dist/pages/elements/options.js +0 -742
  291. package/dist/pages/elements/pages.d.ts +0 -30
  292. package/dist/pages/elements/pages.js +0 -377
  293. package/dist/pages/elements/slot.d.ts +0 -26
  294. package/dist/pages/elements/slot.js +0 -112
  295. package/dist/pages/elements/table.d.ts +0 -36
  296. package/dist/pages/elements/table.js +0 -1043
  297. package/dist/pages/elements/text.d.ts +0 -31
  298. package/dist/pages/elements/text.js +0 -1175
  299. package/dist/pages/entry.d.ts +0 -8
  300. package/dist/pages/entry.js +0 -8
  301. package/dist/pages/host.d.ts +0 -134
  302. package/dist/pages/host.js +0 -165
  303. package/dist/pages/readonly.d.ts +0 -24
  304. package/dist/pages/readonly.js +0 -31
  305. package/dist/pages/register.d.ts +0 -2
  306. package/dist/pages/register.js +0 -3
  307. package/dist/pages/responses.d.ts +0 -37
  308. package/dist/pages/responses.js +0 -56
  309. package/dist/pages/slot-config.d.ts +0 -36
  310. package/dist/pages/slot-config.js +0 -50
  311. package/dist/pages/types.d.ts +0 -98
  312. package/dist/pages/types.js +0 -23
  313. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  314. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -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,18 +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
11
  import { parsePage } from '../../../core/human/page.js';
11
- import { renderPageHtml } from '../../../core/human/page-render.js';
12
12
  import { ticketsRoot } from '../../../core/human/root.js';
13
13
  import { scanInbox, scanPageHistory } from '../../../core/human/scan.js';
14
14
  import { readPageTicket, readTicketResult, validatePageTicketResponses, writePageProgress, } from '../../../core/human/tickets.js';
15
15
  import { describePageAnswer } from '../../../core/human/answer.js';
16
- import { readConfig } from '../../../core/config.js';
17
16
  import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
18
17
  import { filterTerminalReviewTickets } from '../../../core/review/ticket-filter.js';
19
18
  const DEFAULT_CANCEL_REASON = 'Canceled from the inbox.';
@@ -25,7 +24,9 @@ const TICKET_ID_PATTERN = /^[a-f0-9]{64}$/;
25
24
  function ticketHash(canonicalRoot, ticketBasename) {
26
25
  return createHash('sha256').update(`${canonicalRoot}\0${ticketBasename}`, 'utf8').digest('hex');
27
26
  }
28
- 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) {
29
30
  let canonicalRoot;
30
31
  try {
31
32
  canonicalRoot = realpathSync(ticketsRoot());
@@ -56,13 +57,13 @@ function resolveTicket(ticketId) {
56
57
  if (resolve(canonicalDir, '..') !== canonicalRoot || basename(canonicalDir) !== entry)
57
58
  continue;
58
59
  if (existsSync(pageManifestPath(canonicalDir)))
59
- return { nodeId: entry, dir: canonicalDir, kind: 'page' };
60
+ return { ticketId: entry, dir: canonicalDir, kind: 'page' };
60
61
  if (existsSync(reviewPath(canonicalDir)))
61
- return { nodeId: entry, dir: canonicalDir, kind: 'review' };
62
+ return { ticketId: entry, dir: canonicalDir, kind: 'review' };
62
63
  }
63
64
  return null;
64
65
  }
65
- function requireTicketId(params) {
66
+ export function requireTicketId(params) {
66
67
  const ticketId = params['ticket_id'];
67
68
  if (ticketId === undefined || !TICKET_ID_PATTERN.test(ticketId)) {
68
69
  throw new ApiError(400, 'invalid_request', 'invalid ticket id');
@@ -88,10 +89,10 @@ function pageSlotToDTO(slot) {
88
89
  }
89
90
  function pageManifestToDTO(manifest) {
90
91
  const dto = {
91
- schema: 'crtr.page/v1',
92
+ schema: 'crtr.page/v2',
92
93
  dialect: manifest.dialect,
93
94
  title: manifest.title,
94
- placement: manifest.placement,
95
+ delivery: manifest.delivery,
95
96
  document: manifest.document,
96
97
  steps: manifest.steps,
97
98
  slots: manifest.slots.map(pageSlotToDTO),
@@ -104,8 +105,8 @@ function pageManifestToDTO(manifest) {
104
105
  source.sessionName = manifest.source.sessionName;
105
106
  if (manifest.source.askedBy !== undefined)
106
107
  source.askedBy = manifest.source.askedBy;
107
- if (manifest.source.blockedSince !== undefined)
108
- source.blockedSince = manifest.source.blockedSince;
108
+ if (manifest.source.emittedAt !== undefined)
109
+ source.emittedAt = manifest.source.emittedAt;
109
110
  if (manifest.source.profileName !== undefined)
110
111
  source.profileName = manifest.source.profileName;
111
112
  if (manifest.source.nodeId !== undefined)
@@ -169,8 +170,7 @@ function isSlotValidationError(err) {
169
170
  // GET /v1/human/inbox — enumerate
170
171
  // ===========================================================================
171
172
  function handleList() {
172
- const productKinds = readConfig('user').page_components;
173
- const allItems = scanInbox(productKinds);
173
+ const allItems = scanInbox();
174
174
  const filtered = filterTerminalReviewTickets(allItems);
175
175
  const tickets = filtered.map((item) => {
176
176
  if (item.kind === 'page') {
@@ -182,9 +182,10 @@ function handleList() {
182
182
  placement: item.placement,
183
183
  dialect: item.dialect,
184
184
  steps: item.steps,
185
- blocked_since: item.blockedSince,
185
+ emitted_at: item.emittedAt,
186
186
  source: item.source,
187
187
  slot_kinds: item.slotKinds,
188
+ inbox: item.inbox,
188
189
  awaits_response: item.awaitsResponse,
189
190
  };
190
191
  if (item.subtitle !== '')
@@ -203,7 +204,8 @@ function handleList() {
203
204
  }
204
205
  throw new Error(`unknown ticket kind: ${item.kind}`);
205
206
  }).sort((a, b) => {
206
- 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));
207
209
  return byTime !== 0 ? byTime : a.ticket_id.localeCompare(b.ticket_id);
208
210
  });
209
211
  return { status: 200, body: { tickets } };
@@ -216,8 +218,7 @@ function handleHistory(ctx) {
216
218
  if (!nodeId || typeof nodeId !== 'string') {
217
219
  throw new ApiError(400, 'invalid_request', 'history requires a node_id query param');
218
220
  }
219
- const productKinds = readConfig('user').page_components;
220
- const history = scanPageHistory(nodeId, productKinds);
221
+ const history = scanPageHistory(nodeId);
221
222
  const canonicalRoot = realpathSync(ticketsRoot());
222
223
  const tickets = history.map((item) => {
223
224
  const ticketId = createHash('sha256').update(`${canonicalRoot}\0${item.id}`, 'utf8').digest('hex');
@@ -228,9 +229,10 @@ function handleHistory(ctx) {
228
229
  placement: item.placement,
229
230
  dialect: item.dialect,
230
231
  steps: item.steps,
231
- blocked_since: item.blockedSince,
232
+ emitted_at: item.emittedAt,
232
233
  source: item.source,
233
234
  slot_kinds: item.slotKinds,
235
+ inbox: item.inbox,
234
236
  awaits_response: item.awaitsResponse,
235
237
  state: item.state,
236
238
  };
@@ -253,10 +255,9 @@ function handleGetPage(ctx) {
253
255
  if (resolved.kind === 'review') {
254
256
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page read operation');
255
257
  }
256
- const productKinds = readConfig('user').page_components;
257
258
  let ticket;
258
259
  try {
259
- ticket = readPageTicket(resolved.dir, productKinds);
260
+ ticket = readPageTicket(resolved.dir);
260
261
  }
261
262
  catch (err) {
262
263
  throw new ApiError(500, 'internal', 'the ticket could not be read');
@@ -270,9 +271,10 @@ function handleGetPage(ctx) {
270
271
  kind: 'page',
271
272
  state: ticket.state,
272
273
  page: pageManifestToDTO(ticket.manifest),
273
- document: ticket.document,
274
- 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',
275
276
  progress: progressDTO,
277
+ feedback: toPageFeedbackDTO(readPageFeedback(resolved.dir)),
276
278
  },
277
279
  };
278
280
  }
@@ -288,6 +290,8 @@ async function handleRespond(ctx) {
288
290
  if (resolved.kind === 'review') {
289
291
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets submit through their own path');
290
292
  }
293
+ if (readPageTicket(resolved.dir).state === 'passive')
294
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
291
295
  const result = readTicketResult(resolved.dir);
292
296
  if (result !== null && result.kind !== 'canceled') {
293
297
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
@@ -295,9 +299,8 @@ async function handleRespond(ctx) {
295
299
  if (result?.kind === 'canceled') {
296
300
  throw new ApiError(409, 'ticket_canceled', 'canceled tickets cannot be answered');
297
301
  }
298
- const productKinds = readConfig('user').page_components;
299
302
  try {
300
- validatePageTicketResponses(resolved.dir, request.responses, productKinds);
303
+ validatePageTicketResponses(resolved.dir, request.responses);
301
304
  }
302
305
  catch (error) {
303
306
  if (isSlotValidationError(error)) {
@@ -306,8 +309,8 @@ async function handleRespond(ctx) {
306
309
  throw new ApiError(500, 'internal', 'validation failed');
307
310
  }
308
311
  try {
309
- // The shared completion path: it publishes the result and wakes the asking node.
310
- 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);
311
314
  if (completed.kind !== 'page') {
312
315
  throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
313
316
  }
@@ -334,13 +337,14 @@ function handleProgress(ctx) {
334
337
  if (resolved.kind === 'review') {
335
338
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets do not support progress autosave');
336
339
  }
340
+ if (readPageTicket(resolved.dir).state === 'passive')
341
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
337
342
  const result = readTicketResult(resolved.dir);
338
343
  if (result !== null && result.kind !== 'canceled') {
339
344
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
340
345
  }
341
- const productKinds = readConfig('user').page_components;
342
346
  try {
343
- writePageProgress(resolved.dir, request.responses, productKinds);
347
+ writePageProgress(resolved.dir, request.responses);
344
348
  return { status: 200, body: {} };
345
349
  }
346
350
  catch (error) {
@@ -362,8 +366,10 @@ async function handleCancel(ctx) {
362
366
  if (resolved.kind === 'review') {
363
367
  throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have their own cancel path');
364
368
  }
369
+ if (readPageTicket(resolved.dir).state === 'passive')
370
+ throw new ApiError(409, 'page_has_no_response_lifecycle', 'this page has no response lifecycle');
365
371
  try {
366
- const result = await cancelHumanTicket(resolved.nodeId, {
372
+ const result = await cancelHumanTicket(resolved.ticketId, {
367
373
  reason: request.reason ?? DEFAULT_CANCEL_REASON,
368
374
  actor: 'human',
369
375
  });
@@ -398,100 +404,18 @@ function handleGetResponse(ctx) {
398
404
  if (result.kind !== 'page') {
399
405
  throw new ApiError(409, 'ticket_kind_mismatch', 'unexpected result kind');
400
406
  }
401
- const productKinds = readConfig('user').page_components;
402
407
  try {
403
- const manifest = parsePage(resolved.dir, productKinds);
404
- return { status: 200, body: { ...result, answer: describePageAnswer(manifest, result.responses, { completedAt: result.completedAt, productKinds }) } };
408
+ const manifest = parsePage(resolved.dir);
409
+ return { status: 200, body: { ...result, answer: describePageAnswer(manifest, result.responses, { completedAt: result.completedAt }) } };
405
410
  }
406
411
  catch {
407
412
  throw new ApiError(500, 'internal', 'the ticket answer could not be read');
408
413
  }
409
414
  }
410
- // ===========================================================================
411
- // GET /v1/human/inbox/:ticket_id/render — the page's HTML payload
412
- // ===========================================================================
413
- function handleRenderPage(ctx) {
414
- const ticketId = requireTicketId(ctx.params);
415
- const resolved = resolveTicket(ticketId);
416
- if (resolved === null)
417
- throw new ApiError(404, 'ticket_not_found', 'no matching ticket');
418
- if (resolved.kind === 'review') {
419
- throw new ApiError(409, 'ticket_kind_unsupported', 'review tickets have no page render operation');
420
- }
421
- const productKinds = readConfig('user').page_components;
422
- // Addressable for pending, resolved, and canceled tickets alike — the same
423
- // read rule as the detail route: a page stays readable after it resolves.
424
- let ticket;
425
- try {
426
- ticket = readPageTicket(resolved.dir, productKinds);
427
- }
428
- catch {
429
- throw new ApiError(500, 'internal', 'the ticket could not be read');
430
- }
431
- try {
432
- return { status: 200, body: { html: renderPageHtml(ticket.manifest, ticket.document, productKinds) } };
433
- }
434
- catch {
435
- throw new ApiError(500, 'internal', 'the page could not be rendered');
436
- }
437
- }
438
- // ===========================================================================
439
- // GET /v1/human/pages/bundle.js|.css — the page runtime assets
440
- // ===========================================================================
441
- const BUNDLE_MEDIA_TYPES = {
442
- 'bundle.js': 'text/javascript; charset=utf-8',
443
- 'bundle.css': 'text/css; charset=utf-8',
444
- };
445
- const bundleCache = new Map();
446
- /** Read one built bundle asset, reusing the cached bytes while the file on disk
447
- * is unchanged. A runtime generation swap changes mtime, so the next request
448
- * re-reads and re-hashes. */
449
- function readBundleAsset(name) {
450
- let path;
451
- let stat;
452
- try {
453
- path = join(assetRoot(), 'pages', name);
454
- stat = statSync(path);
455
- }
456
- catch {
457
- throw new ApiError(500, 'page_bundle_unavailable', `${name} is not present in this runtime generation; the page runtime is built by \`npm run build\``);
458
- }
459
- const cached = bundleCache.get(name);
460
- if (cached !== undefined && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
461
- return cached;
462
- const bytes = readFileSync(path);
463
- const asset = {
464
- mtimeMs: stat.mtimeMs,
465
- size: stat.size,
466
- bytes,
467
- etag: `"${createHash('sha256').update(bytes).digest('hex').slice(0, 32)}"`,
468
- };
469
- bundleCache.set(name, asset);
470
- return asset;
471
- }
472
- function etagMatches(header, etag) {
473
- if (header === undefined)
474
- return false;
475
- return header.split(',').map((candidate) => candidate.trim()).some((candidate) => candidate === '*' || candidate === etag || candidate === `W/${etag}`);
476
- }
477
- function handleBundleAsset(name) {
478
- return (ctx) => {
479
- const asset = readBundleAsset(name);
480
- const headers = { etag: asset.etag, 'cache-control': 'no-cache' };
481
- const ifNoneMatch = ctx.req.headers['if-none-match'];
482
- if (etagMatches(Array.isArray(ifNoneMatch) ? ifNoneMatch.join(',') : ifNoneMatch, asset.etag)) {
483
- return { status: 304, headers };
484
- }
485
- return { status: 200, rawBody: asset.bytes, headers: { ...headers, 'content-type': BUNDLE_MEDIA_TYPES[name] } };
486
- };
487
- }
488
415
  export const inboxRoutes = [
489
- { method: 'GET', pattern: '/v1/human/pages/bundle.js', handler: handleBundleAsset('bundle.js') },
490
- { method: 'GET', pattern: '/v1/human/pages/bundle.css', handler: handleBundleAsset('bundle.css') },
491
416
  { method: 'GET', pattern: '/v1/human/inbox', handler: handleList },
492
417
  { method: 'GET', pattern: '/v1/human/inbox/history', handler: handleHistory },
493
418
  { method: 'GET', pattern: '/v1/human/inbox/:ticket_id', handler: handleGetPage },
494
- { method: 'GET', pattern: '/v1/human/inbox/:ticket_id/render', handler: handleRenderPage },
495
419
  { method: 'GET', pattern: '/v1/human/inbox/:ticket_id/response', handler: handleGetResponse },
496
420
  { method: 'POST', pattern: '/v1/human/inbox/:ticket_id/respond', handler: handleRespond },
497
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
+ }