@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
@@ -105,3 +105,25 @@ test('cycleAwareMessages: a yielded cycle keeps terminal assistant content witho
105
105
  assert.equal(yieldedAssistant.errorMessage, undefined, 'the provider abort text is omitted with the yield boundary');
106
106
  assert.equal(out.filter((message) => message.customType === 'crtr-cycle-divider').length, 1);
107
107
  });
108
+ // Observed shape: the yield's self-abort landed as the NEXT request was
109
+ // starting, so the provider caught a bare AbortError instead of seeing its own
110
+ // signal and stamped `stopReason: 'error'` on an empty turn. Pi's assistant
111
+ // component reserves a red `Error: …` line for exactly that stopReason, so the
112
+ // annotation must come off here too — a yield is not a failure.
113
+ test('cycleAwareMessages: a yield abort recorded as a provider error is still not a failed turn', () => {
114
+ const m1 = userMsg('m1', null, 'yield after this turn');
115
+ const m2 = assistantMsg('m2', 'm1', [], 'error', 'This operation was aborted');
116
+ const cy1 = {
117
+ type: 'custom',
118
+ id: 'cy1',
119
+ parentId: null,
120
+ timestamp: new Date(1).toISOString(),
121
+ customType: CRTR_CYCLE_CUSTOM_TYPE,
122
+ data: { cycle: 2, fromLeaf: 'm2' },
123
+ };
124
+ const m3 = userMsg('m3', 'cy1', 'fresh cycle');
125
+ const out = cycleAwareMessages(cycleManager([m1, m2, cy1, m3], 'm3'));
126
+ const yieldedAssistant = out.find((message) => message.role === 'assistant');
127
+ assert.equal(yieldedAssistant.stopReason, undefined, 'yield is not presented as a failed assistant turn');
128
+ assert.equal(yieldedAssistant.errorMessage, undefined, 'the aborted-request text is omitted with the yield boundary');
129
+ });
@@ -28,7 +28,7 @@ function node(id, over = {}) {
28
28
  ...over,
29
29
  };
30
30
  }
31
- const noSignals = { pushedFinal: false, askedHuman: false };
31
+ const noSignals = { pushedFinal: false, requestedHumanReply: false };
32
32
  before(() => {
33
33
  home = mkdtempSync(join(tmpdir(), 'crtr-stopguard-'));
34
34
  process.env['CRTR_HOME'] = home;
@@ -132,10 +132,10 @@ test('a TERMINAL node whose only worker is dead is stalled (no LIVE subscription
132
132
  assert.equal(d.action, 'reprompt');
133
133
  assert.equal(d.reason, 'stalled');
134
134
  });
135
- test('pushedFinal → finished; askedHuman → escalated (both short-circuit before lifecycle)', () => {
135
+ test('pushedFinal → finished; requestedHumanReply → escalated (both short-circuit before lifecycle)', () => {
136
136
  createNode(node('t', { parent: 'mgr', lifecycle: 'terminal' }));
137
- assert.deepEqual(evaluateStop('t', { pushedFinal: true, askedHuman: false }), { action: 'allow', reason: 'finished' });
138
- assert.deepEqual(evaluateStop('t', { pushedFinal: false, askedHuman: true }), { action: 'allow', reason: 'escalated' });
137
+ assert.deepEqual(evaluateStop('t', { pushedFinal: true, requestedHumanReply: false }), { action: 'allow', reason: 'finished' });
138
+ assert.deepEqual(evaluateStop('t', { pushedFinal: false, requestedHumanReply: true }), { action: 'allow', reason: 'escalated' });
139
139
  });
140
140
  test('an active follow-up cycle with a canonical final is allowed without another final', () => {
141
141
  createNode(node('followUp', {
@@ -14,7 +14,7 @@ after(() => {
14
14
  rmSync(home, { recursive: true, force: true });
15
15
  });
16
16
  test('only a watchdog-stamped aborted settlement records an automatic connection fault', () => {
17
- const settleAbort = (nodeId, stagedWatchdogAbort) => {
17
+ const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted') => {
18
18
  const session = {
19
19
  sessionManager: { getLeafId: () => 'leaf-entry' },
20
20
  model: undefined,
@@ -24,9 +24,14 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
24
24
  session,
25
25
  fallback: { blocked: false },
26
26
  stagedProviderAgentEnd: {
27
- messages: [{ role: 'assistant', stopReason: 'aborted', errorMessage: 'Request was aborted' }],
27
+ messages: [{
28
+ role: 'assistant',
29
+ stopReason,
30
+ errorMessage: stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted',
31
+ }],
28
32
  },
29
33
  stagedWatchdogAbort,
34
+ stagedRefreshAbort,
30
35
  stagedOverflowFailure: null,
31
36
  executingTools: new Set(),
32
37
  };
@@ -38,15 +43,24 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
38
43
  retry.publishSettledProviderFault(generation);
39
44
  return { retry, generation };
40
45
  };
41
- const watchdog = settleAbort('watchdog-node', true);
46
+ const watchdog = settleAbort('watchdog-node', true, false);
42
47
  const fault = readFault('watchdog-node');
43
48
  assert.equal(fault?.link, 'pi→provider');
44
49
  assert.equal(fault?.kind, 'connection');
45
50
  assert.deepEqual(fault?.retry, { disposition: 'auto', by: 'daemon' });
46
51
  assert.equal(watchdog.retry.isActiveAutoFault(fault), true);
47
52
  assert.equal(watchdog.generation.stagedWatchdogAbort, false, 'settlement consumes the one-shot provenance');
48
- settleAbort('human-node', false);
53
+ settleAbort('human-node', false, false);
49
54
  assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
55
+ // Pi 0.84 currently serializes a host abort from `node yield` as an `error`
56
+ // settlement. Only the refresh-abort provenance suppresses that provider-looking error.
57
+ const refresh = settleAbort('refresh-node', false, true, 'error');
58
+ assert.equal(readFault('refresh-node'), null, 'a refresh-aborted error settlement must not enter provider fault recovery');
59
+ assert.equal(refresh.generation.stagedRefreshAbort, false, 'settlement consumes the one-shot refresh provenance');
60
+ settleAbort('provider-node', false, false, 'error');
61
+ const providerFault = readFault('provider-node');
62
+ assert.equal(providerFault?.kind, 'other', 'the same provider error without refresh provenance still classifies');
63
+ assert.deepEqual(providerFault?.retry, { disposition: 'fatal' }, 'the same provider error without refresh provenance still records its fault');
50
64
  });
51
65
  test('a failed retry settlement carries the episode forward instead of restarting it', () => {
52
66
  // The episode marker a daemon retry records before re-prompting: original
@@ -68,6 +82,7 @@ test('a failed retry settlement carries the episode forward instead of restartin
68
82
  messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'fetch failed' }],
69
83
  },
70
84
  stagedWatchdogAbort: false,
85
+ stagedRefreshAbort: false,
71
86
  stagedOverflowFailure: null,
72
87
  executingTools: new Set(),
73
88
  };
@@ -77,6 +77,12 @@ export declare function jobStartedAtMs(jobId: string): number | undefined;
77
77
  * notice, the `node bash background` result/toast, and the attach cockpit)
78
78
  * shows it: `<60s -> "12s"`; `60s-59m -> "3m 40s"`; `>=60m -> "1h 04m"`. */
79
79
  export declare function formatBashElapsed(ms: number): string;
80
+ /** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
81
+ * injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
82
+ * literal first line names no command at all: skip leading assignment lines and
83
+ * return the first real one, whitespace-collapsed to a single line. A job whose
84
+ * command is nothing but assignments falls back to its first non-empty line. */
85
+ export declare function bashJobCommandLine(command: string): string;
80
86
  export interface BashJobStatus {
81
87
  jobId: string;
82
88
  /** Full text of cmd.sh. */
@@ -189,6 +189,16 @@ export function formatBashElapsed(ms) {
189
189
  const minutes = totalMinutes % 60;
190
190
  return `${hours}h ${String(minutes).padStart(2, '0')}m`;
191
191
  }
192
+ /** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
193
+ * injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
194
+ * literal first line names no command at all: skip leading assignment lines and
195
+ * return the first real one, whitespace-collapsed to a single line. A job whose
196
+ * command is nothing but assignments falls back to its first non-empty line. */
197
+ export function bashJobCommandLine(command) {
198
+ const lines = command.split('\n').map((line) => line.trim()).filter((line) => line !== '');
199
+ const real = lines.find((line) => !/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*=/.test(line));
200
+ return (real ?? lines[0] ?? '').replace(/\s+/g, ' ');
201
+ }
192
202
  /** Every live backgrounded bash job for this node — job.bg present, job.exit
193
203
  * absent — whether handed off by the menu, the valve deadline, or broker
194
204
  * termination. Read-only; never mutates. Skips malformed/partially-written
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
4
4
  import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
- import { atomicWriteJson, pageManifestPath } from '../../human/convention.js';
7
+ import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../../human/convention.js';
8
8
  import { createNode } from '../canvas.js';
9
9
  import { closeDb } from '../db.js';
10
10
  import { nodeDir } from '../paths.js';
@@ -22,16 +22,19 @@ function meta(id, cwd, extra) {
22
22
  ...extra,
23
23
  };
24
24
  }
25
+ // A ticket only counts as attention when it routes a reply back, so the fixture binds
26
+ // the reply route the same way `human send` does before writing the page manifest.
25
27
  function seedPage(bridgeId) {
26
28
  const dir = nodeDir(bridgeId);
27
29
  mkdirSync(dir, { recursive: true });
30
+ bindTicketReplyRoute(dir, bridgeId);
28
31
  atomicWriteJson(pageManifestPath(dir), {
29
- schema: 'crtr.page/v1',
30
- dialect: 'html',
32
+ schema: 'crtr.page/v2',
33
+ dialect: 'jsx',
31
34
  title: bridgeId,
32
35
  subtitle: 'Acknowledge this ticket so the request can proceed.',
33
- placement: 'inline',
34
- document: 'page.html',
36
+ delivery: { placement: 'inline', inbox: true, reply: true },
37
+ document: 'page.tsx',
35
38
  steps: 1,
36
39
  slots: [],
37
40
  });
@@ -8,7 +8,7 @@
8
8
  // This is adversarial, not merely a clean-machine assertion: BEFORE calling
9
9
  // the render functions, the test seeds real, CONFLICTING local state for the
10
10
  // same node id `a` (a local focus, an active fault, telemetry, a goal file, a
11
- // session transcript, and a pending human ask) — every historically-broken
11
+ // session transcript, and a pending human reply) — every historically-broken
12
12
  // local read this suppression covers. If the production suppression ever
13
13
  // regressed back to consulting local disk/SQLite for a remote source, the
14
14
  // assertions below would observe the poisoned values instead of the safe
@@ -114,12 +114,12 @@ function seedPoisonedLocalState() {
114
114
  const askDir = nodeDir('poison-ask');
115
115
  mkdirSync(askDir, { recursive: true });
116
116
  atomicWriteJson(pageManifestPath(askDir), {
117
- schema: 'crtr.page/v1',
118
- dialect: 'html',
117
+ schema: 'crtr.page/v2',
118
+ dialect: 'jsx',
119
119
  title: 'poison',
120
120
  subtitle: 'This local ticket must not affect the remote rendering.',
121
- placement: 'inline',
122
- document: 'page.html',
121
+ delivery: { placement: 'inline', inbox: true, reply: true },
122
+ document: 'page.tsx',
123
123
  steps: 1,
124
124
  slots: [],
125
125
  });
@@ -1,15 +1,17 @@
1
1
  // attention.ts — pending human-ticket counters across the canvas.
2
2
  //
3
- // Every ticket is a direct child of the single registered `<canvas>/nodes` root,
4
- // named for its terminal human bridge node. The bridge's parent is the asking
5
- // node, so storage identity and attribution come from the canvas graph rather
6
- // than duplicated attribution fields in page/review data.
7
- import { basename } from 'node:path';
3
+ // Reply-bearing tickets carry an explicit bridge route. The bridge's parent is
4
+ // the asking node; standalone pages have no bridge and do not contribute to a
5
+ // node's pending-reply count.
8
6
  import { scanInbox } from '../human/scan.js';
9
7
  import { filterTerminalReviewTickets } from '../review/ticket-filter.js';
8
+ import { readTicketReplyRoute } from '../human/convention.js';
10
9
  import { getNode, listNodes, view } from './canvas.js';
11
10
  function askingNodeId(ticketDir) {
12
- const bridge = getNode(basename(ticketDir));
11
+ const replyRoute = readTicketReplyRoute(ticketDir);
12
+ if (replyRoute === null)
13
+ return null;
14
+ const bridge = getNode(replyRoute.bridge_node_id);
13
15
  if (bridge === null)
14
16
  return null;
15
17
  return bridge.parent ?? bridge.node_id;
@@ -11,8 +11,14 @@ export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency';
11
11
  export declare const SORTS: readonly SortMode[];
12
12
  /** Does a node belong to this tab's slice?
13
13
  * All — every node.
14
- * Live — active | idle.
15
- * Dormant — done | dead | canceled.
14
+ * Live — what you can pick up right now: active | idle, PLUS a resident
15
+ * agent that stopped without finishing (its broker exited, but
16
+ * Enter resumes the conversation in place — see `isFinishedWork`).
17
+ * Finished work never appears here.
18
+ * Dormant — done | dead | canceled: literally "no engine running". A stopped
19
+ * resident agent is deliberately on BOTH tabs — it is resumable
20
+ * (Live) and it is not running (Dormant); both are true and each
21
+ * tab is the one a different search starts from.
16
22
  * Attention — needs a human: has pending asks OR is parked on a fault.
17
23
  * Pinned — user-starred (a local, view-only preference; `isPinned`). */
18
24
  export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean;
@@ -4,19 +4,26 @@
4
4
  // it is exhaustively unit-testable. The app layer (app.ts) wires the canvas data
5
5
  // access (dashboardRowsAll / listNodes / subscriptionsOf) into buildTree, then
6
6
  // drives flatten() on each keystroke.
7
- import { statusRank } from '../node-order.js';
7
+ import { isFinishedWork, isLiveStatus, statusRank } from '../node-order.js';
8
8
  export const TABS = ['All', 'Live', 'Dormant', 'Attention', 'Pinned'];
9
9
  export const SORTS = ['tree', 'attention', 'relevance', 'recency'];
10
10
  /** Does a node belong to this tab's slice?
11
11
  * All — every node.
12
- * Live — active | idle.
13
- * Dormant — done | dead | canceled.
12
+ * Live — what you can pick up right now: active | idle, PLUS a resident
13
+ * agent that stopped without finishing (its broker exited, but
14
+ * Enter resumes the conversation in place — see `isFinishedWork`).
15
+ * Finished work never appears here.
16
+ * Dormant — done | dead | canceled: literally "no engine running". A stopped
17
+ * resident agent is deliberately on BOTH tabs — it is resumable
18
+ * (Live) and it is not running (Dormant); both are true and each
19
+ * tab is the one a different search starts from.
14
20
  * Attention — needs a human: has pending asks OR is parked on a fault.
15
21
  * Pinned — user-starred (a local, view-only preference; `isPinned`). */
16
22
  export function tabPredicate(tab, row, isPinned = false) {
17
23
  switch (tab) {
18
24
  case 'All': return true;
19
- case 'Live': return row.status === 'active' || row.status === 'idle';
25
+ case 'Live': return isLiveStatus(row.status)
26
+ || !isFinishedWork({ lifecycle: row.lifecycle, finalized: row.finalized === true });
20
27
  case 'Dormant': return row.status === 'done' || row.status === 'dead' || row.status === 'canceled';
21
28
  case 'Attention': return row.asks > 0 || row.hanging != null;
22
29
  case 'Pinned': return isPinned;
@@ -90,6 +90,10 @@ export declare function clearPid(nodeId: string): void;
90
90
  export declare function migrateLegacyPidIdentities(captureIdentity?: (pid: number) => string | undefined): {
91
91
  migrated: string[];
92
92
  };
93
+ /** Every hydrated node whose durable profile identity equals `profileId`.
94
+ * Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
95
+ * deletion owns every matching row, whether or not it is user-visible. */
96
+ export declare function listNodesByProfile(profileId: string): NodeMeta[];
93
97
  /** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
94
98
  export declare function listNodes(filter?: {
95
99
  status?: NodeStatus | NodeStatus[];
@@ -306,6 +310,9 @@ export interface DeleteNodeResult {
306
310
  * state are removed in one canvas write boundary. Best-effort dir removal
307
311
  * follows the commit. */
308
312
  export declare function deleteNode(nodeId: string): DeleteNodeResult;
313
+ /** Hard-delete a validated set of nodes in one row transaction, then remove
314
+ * their directories. The caller must tear down engines and viewers first. */
315
+ export declare function deleteNodes(nodeIds: readonly string[]): string[];
309
316
  /** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
310
317
  * `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
311
318
  * node rows + dirs. Open managed-worktree nodes are retained so their checkout
@@ -336,6 +336,21 @@ export function migrateLegacyPidIdentities(captureIdentity = (pid) => capturePid
336
336
  * yet — surfacing it would put a phantom empty conversation in the user's
337
337
  * sidebar. Claiming deletes the marker, which is what makes the node visible. */
338
338
  const NOT_A_SPARE = 'node_id NOT IN (SELECT node_id FROM warm_pool)';
339
+ /** Every hydrated node whose durable profile identity equals `profileId`.
340
+ * Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
341
+ * deletion owns every matching row, whether or not it is user-visible. */
342
+ export function listNodesByProfile(profileId) {
343
+ const ids = openDb()
344
+ .prepare('SELECT node_id FROM nodes WHERE profile_id = ? ORDER BY created')
345
+ .all(profileId);
346
+ return ids.map(({ node_id }) => {
347
+ const meta = getNode(node_id);
348
+ if (meta === null) {
349
+ throw new Error(`profile deletion cannot hydrate indexed node ${node_id}`);
350
+ }
351
+ return meta;
352
+ });
353
+ }
339
354
  /** All rows, optionally filtered by status. Unclaimed warm spares are excluded. */
340
355
  export function listNodes(filter) {
341
356
  const db = openDb();
@@ -744,6 +759,28 @@ export function deleteNode(nodeId) {
744
759
  rmSync(nodeDir(nodeId), { recursive: true, force: true });
745
760
  return result;
746
761
  }
762
+ /** Hard-delete a validated set of nodes in one row transaction, then remove
763
+ * their directories. The caller must tear down engines and viewers first. */
764
+ export function deleteNodes(nodeIds) {
765
+ const ids = [...new Set(nodeIds)];
766
+ if (ids.length === 0)
767
+ return [];
768
+ const blocked = ids.filter(hasOpenManagedWorktree);
769
+ if (blocked.length > 0) {
770
+ throw new Error(`refusing to delete nodes with open managed worktrees: ${blocked.join(', ')}`);
771
+ }
772
+ const placeholders = ids.map(() => '?').join(',');
773
+ const deleted = withCanvasWrite((db) => {
774
+ const existing = db
775
+ .prepare(`SELECT node_id FROM nodes WHERE node_id IN (${placeholders}) ORDER BY created`)
776
+ .all(...ids).map(({ node_id }) => node_id);
777
+ db.prepare(`DELETE FROM nodes WHERE node_id IN (${placeholders})`).run(...ids);
778
+ return existing;
779
+ });
780
+ for (const id of deleted)
781
+ rmSync(nodeDir(id), { recursive: true, force: true });
782
+ return deleted;
783
+ }
747
784
  /** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
748
785
  * `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
749
786
  * node rows + dirs. Open managed-worktree nodes are retained so their checkout
@@ -159,6 +159,15 @@ export declare function listCrons(scope: {
159
159
  export declare function cancelCron(cron_id: string, options?: {
160
160
  preserveRunningProcess?: boolean;
161
161
  }): boolean;
162
+ /** Cron ids a hard profile deletion will remove: matching execution profile,
163
+ * plus every row anchored to a node being deleted. */
164
+ export declare function cronIdsForProfileDeletion(profileId: string, anchorNodeIds: readonly string[]): string[];
165
+ /** Kill any leased matching/anchored runs, then delete their rows together. */
166
+ export declare function deleteCronsForProfile(profileId: string, anchorNodeIds: readonly string[]): string[];
167
+ /** Cron ids whose snapshotted profile will be detached. */
168
+ export declare function cronIdsForProfileDetach(profileId: string): string[];
169
+ /** Preserve matching crons while removing their deleted profile dependency. */
170
+ export declare function globalizeCronsForProfile(profileId: string): string[];
162
171
  /** DELETE one cron by id — one-shot consumption after its run settles.
163
172
  * Returns whether a live row was removed, so a consumer racing an explicit
164
173
  * cancel can no-op instead of double-settling. */
@@ -191,6 +191,57 @@ export function cancelCron(cron_id, options) {
191
191
  return true;
192
192
  });
193
193
  }
194
+ function profileCronWhere(profileId, anchorNodeIds) {
195
+ const anchors = [...new Set(anchorNodeIds)];
196
+ if (anchors.length === 0)
197
+ return { sql: 'profile = ?', params: [profileId] };
198
+ return {
199
+ sql: `profile = ? OR anchor_node IN (${anchors.map(() => '?').join(',')})`,
200
+ params: [profileId, ...anchors],
201
+ };
202
+ }
203
+ /** Cron ids a hard profile deletion will remove: matching execution profile,
204
+ * plus every row anchored to a node being deleted. */
205
+ export function cronIdsForProfileDeletion(profileId, anchorNodeIds) {
206
+ const where = profileCronWhere(profileId, anchorNodeIds);
207
+ return openDb()
208
+ .prepare(`SELECT cron_id FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
209
+ .all(...where.params).map(({ cron_id }) => cron_id);
210
+ }
211
+ /** Kill any leased matching/anchored runs, then delete their rows together. */
212
+ export function deleteCronsForProfile(profileId, anchorNodeIds) {
213
+ return withCanvasWrite((db) => {
214
+ const where = profileCronWhere(profileId, anchorNodeIds);
215
+ const rows = db
216
+ .prepare(`SELECT * FROM crons WHERE ${where.sql} ORDER BY created, cron_id`)
217
+ .all(...where.params).map(cronFrom);
218
+ for (const cron of rows) {
219
+ if (cron.run_state === 'running')
220
+ killLeasedCronRun(cron);
221
+ }
222
+ db.prepare(`DELETE FROM crons WHERE ${where.sql}`).run(...where.params);
223
+ return rows.map(({ cron_id }) => cron_id);
224
+ });
225
+ }
226
+ /** Cron ids whose snapshotted profile will be detached. */
227
+ export function cronIdsForProfileDetach(profileId) {
228
+ return openDb()
229
+ .prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
230
+ .all(profileId).map(({ cron_id }) => cron_id);
231
+ }
232
+ /** Preserve matching crons while removing their deleted profile dependency. */
233
+ export function globalizeCronsForProfile(profileId) {
234
+ return withCanvasWrite((db) => {
235
+ const ids = db
236
+ .prepare('SELECT cron_id FROM crons WHERE profile = ? ORDER BY created, cron_id')
237
+ .all(profileId).map(({ cron_id }) => cron_id);
238
+ if (ids.length > 0) {
239
+ db.prepare("UPDATE crons SET profile = NULL, scope = 'global', updated = ? WHERE profile = ?")
240
+ .run(new Date().toISOString(), profileId);
241
+ }
242
+ return ids;
243
+ });
244
+ }
194
245
  /** DELETE one cron by id — one-shot consumption after its run settles.
195
246
  * Returns whether a live row was removed, so a consumer racing an explicit
196
247
  * cancel can no-op instead of double-settling. */
@@ -1,3 +1,24 @@
1
- import type { NodeStatus } from './types.js';
1
+ import type { NodeStatus, Lifecycle } from './types.js';
2
2
  /** Sort live work before terminal work; unknown statuses sort last. */
3
3
  export declare function statusRank(status: NodeStatus | undefined): number;
4
+ /** True when the node has a broker the daemon supervises — the statuses that
5
+ * mean "a live engine is (or should be) running right now". */
6
+ export declare function isLiveStatus(status: NodeStatus | undefined): boolean;
7
+ /** True when a node's work is FINISHED — a terminal one-shot worker, or any node
8
+ * that pushed a final report.
9
+ *
10
+ * This is deliberately NOT "is it running": a non-live status alone conflates two
11
+ * unrelated situations. Finished work is archive — opening it is inspection, and
12
+ * relaunching it puts a completed worker back to work (the stop-guard reprompts a
13
+ * terminal node that never pushed a final). A RESIDENT node with no final report
14
+ * is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
15
+ * exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
16
+ * so it never finished anything and resuming it is simply reopening a conversation.
17
+ *
18
+ * `lifecycle` is optional because DashboardRow only carries it on the full browser
19
+ * snapshot; absent, this answers `true` (treat as finished), keeping the caller on
20
+ * the conservative inspect-don't-revive path. */
21
+ export declare function isFinishedWork(node: {
22
+ lifecycle?: Lifecycle;
23
+ finalized: boolean;
24
+ }): boolean;
@@ -1,4 +1,7 @@
1
- // node-order.ts — canonical live-first ordering for canvas node statuses.
1
+ // node-order.ts — canonical ordering and status/lifecycle classification for
2
+ // canvas nodes. Pure (type-only imports) so every surface — the browser model,
3
+ // command leaves, renderers — can share one answer to "is this node live?" and
4
+ // "did this node finish?" without reaching the db.
2
5
  /** Sort live work before terminal work; unknown statuses sort last. */
3
6
  export function statusRank(status) {
4
7
  switch (status) {
@@ -10,3 +13,25 @@ export function statusRank(status) {
10
13
  default: return 5;
11
14
  }
12
15
  }
16
+ /** True when the node has a broker the daemon supervises — the statuses that
17
+ * mean "a live engine is (or should be) running right now". */
18
+ export function isLiveStatus(status) {
19
+ return status === 'active' || status === 'idle';
20
+ }
21
+ /** True when a node's work is FINISHED — a terminal one-shot worker, or any node
22
+ * that pushed a final report.
23
+ *
24
+ * This is deliberately NOT "is it running": a non-live status alone conflates two
25
+ * unrelated situations. Finished work is archive — opening it is inspection, and
26
+ * relaunching it puts a completed worker back to work (the stop-guard reprompts a
27
+ * terminal node that never pushed a final). A RESIDENT node with no final report
28
+ * is the opposite: its `done` is only `markCleanExitDone` recording a clean broker
29
+ * exit, which the stop-guard itself rules a legitimate stop (`reason: 'dormant'`),
30
+ * so it never finished anything and resuming it is simply reopening a conversation.
31
+ *
32
+ * `lifecycle` is optional because DashboardRow only carries it on the full browser
33
+ * snapshot; absent, this answers `true` (treat as finished), keeping the caller on
34
+ * the conservative inspect-don't-revive path. */
35
+ export function isFinishedWork(node) {
36
+ return node.lifecycle !== 'resident' || node.finalized;
37
+ }
@@ -34,6 +34,11 @@ export interface DashboardRow {
34
34
  * agent you come back to. Drives the browser's resident-only lifecycle filter.
35
35
  * Only populated by dashboardRowsAll (the browser snapshot). */
36
36
  lifecycle?: Lifecycle;
37
+ /** True when the node pushed a final report. With `lifecycle` this separates
38
+ * work that FINISHED from a resident agent that merely stopped, which the
39
+ * status alone cannot (see `isFinishedWork` in node-order.ts). Only populated
40
+ * by dashboardRowsAll (the browser snapshot). */
41
+ finalized?: boolean;
37
42
  /** "Most recent activity" sort key — epoch ms. The cheap boot stats the node's
38
43
  * job/telemetry.json (rewritten on every turn_end at a DETERMINISTIC path, so
39
44
  * no meta read), falling back to `created`'s epoch when a node never ran a turn.
@@ -372,6 +372,7 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
372
372
  profile_id: row.profile_id,
373
373
  created: row.created,
374
374
  lifecycle: row.lifecycle,
375
+ finalized: row.final_report != null,
375
376
  mtimeMs: sessionMtime(row.node_id, row.created, remote),
376
377
  streaming: !remote && hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
377
378
  hanging,
@@ -49,6 +49,9 @@ export interface LaunchSpec {
49
49
  }
50
50
  export interface ManagedWorktree {
51
51
  state: 'open' | 'closed';
52
+ /** A closed worktree remains registered until its owner has exited, so Git
53
+ * never deletes the caller's live checkout beneath it. */
54
+ cleanup?: 'pending' | 'complete';
52
55
  path: string;
53
56
  branch: string;
54
57
  repo_root: string;
@@ -57,10 +60,13 @@ export interface ManagedWorktree {
57
60
  created: string;
58
61
  closed?: string;
59
62
  }
60
- /** Immutable review-companion provenance. The origin session/leaf/model
61
- * coordinates remain canonical in the review row rather than being duplicated
62
- * into node metadata. */
63
+ /** Immutable companion provenance. The origin session/leaf/model coordinates
64
+ * remain canonical in the review row rather than being duplicated into node
65
+ * metadata. `kind` names the surface the companion serves: absent or `review`
66
+ * is a human document review; `page_feedback` binds a page ticket's feedback
67
+ * conversation, with review_id = ticket id and target_file = the page source. */
63
68
  export interface ReviewBinding {
69
+ kind?: 'review' | 'page_feedback';
64
70
  review_id: string;
65
71
  origin_node_id: string;
66
72
  branch_file: string;
@@ -1,5 +1,5 @@
1
1
  import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
2
- import type { KindConfig } from '../../types.js';
2
+ import type { KindConfig, PageComponentRegistration } from '../../types.js';
3
3
  export interface BundleIssue {
4
4
  code: string;
5
5
  path?: string;
@@ -15,6 +15,11 @@ export interface ValidatedBundle {
15
15
  * manifest, where `readMergedLaunchConfig` layers it — see
16
16
  * `PluginManifest.kinds`. */
17
17
  kinds?: Record<string, Partial<KindConfig>>;
18
+ /** Page-component contributions declared in `bundle.json` (optional). The
19
+ * installer copies this validated block into the synthesized plugin
20
+ * manifest, where `resolvePageComponents` reads it — see
21
+ * `PluginManifest.page_components`. */
22
+ page_components?: PageComponentRegistration[];
18
23
  commands: ValidatedCommandManifest;
19
24
  commandsBytes: Uint8Array;
20
25
  directories: ReadonlyArray<{
@@ -1,6 +1,7 @@
1
1
  import { Parser } from 'tar';
2
2
  import { validateCommandManifest } from '../command-manifests/manifest.js';
3
3
  import { invalidPluginKindsReasons } from '../config.js';
4
+ import { invalidPageComponentsReasons, normalizePageComponents } from '../human/page-catalog.js';
4
5
  function issue(code, message, received, expected, next, path) {
5
6
  return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
6
7
  }
@@ -143,24 +144,35 @@ function parseBundleMetadata(bytes) {
143
144
  };
144
145
  }
145
146
  const record = raw;
146
- const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds');
147
+ const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds' && key !== 'page_components');
147
148
  if (record['bundleVersion'] !== 1 || unknownKeys.length > 0) {
148
149
  return {
149
- issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most a kinds block', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optionally kinds', 'Set bundle.json to {"bundleVersion":1} plus an optional kinds object.', 'bundle.json'),
150
+ issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most kinds and page_components blocks', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optionally kinds and page_components', 'Set bundle.json to {"bundleVersion":1} plus an optional kinds object and page_components array.', 'bundle.json'),
150
151
  };
151
152
  }
152
- if (record['kinds'] === undefined)
153
- return {};
154
- // Install-time strictness: a bad kinds block fails the install loudly here,
155
- // because the read side (`mergeKinds`) drops invalid entries silently and
156
- // would otherwise ship a kind that never registers.
157
- const reasons = invalidPluginKindsReasons(record['kinds']);
158
- if (reasons.length > 0) {
159
- return {
160
- issue: bundleInvalid('bundle.json kinds block is invalid', reasons.join('; '), 'each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields', 'Fix the kinds entries in bundle.json.', 'bundle.json'),
161
- };
153
+ // Install-time strictness: a bad block fails the install loudly here, because
154
+ // the read side (`mergeKinds`) drops invalid kind entries silently and would
155
+ // otherwise ship a kind that never registers.
156
+ if (record['kinds'] !== undefined) {
157
+ const reasons = invalidPluginKindsReasons(record['kinds']);
158
+ if (reasons.length > 0) {
159
+ return {
160
+ issue: bundleInvalid('bundle.json kinds block is invalid', reasons.join('; '), 'each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields', 'Fix the kinds entries in bundle.json.', 'bundle.json'),
161
+ };
162
+ }
162
163
  }
163
- return { kinds: record['kinds'] };
164
+ if (record['page_components'] !== undefined) {
165
+ const reasons = invalidPageComponentsReasons(record['page_components']);
166
+ if (reasons.length > 0) {
167
+ return {
168
+ issue: bundleInvalid('bundle.json page_components block is invalid', reasons.join('; '), 'an array of page component registrations, each a kind string or {kind, description?, useWhen?, doc?, display?}', 'Fix the page_components entries in bundle.json.', 'bundle.json'),
169
+ };
170
+ }
171
+ }
172
+ return {
173
+ ...(record['kinds'] !== undefined ? { kinds: record['kinds'] } : {}),
174
+ ...(record['page_components'] !== undefined ? { page_components: normalizePageComponents(record['page_components']) } : {}),
175
+ };
164
176
  }
165
177
  function parseCommands(bytes, reservedCoreNames, coreCommandPaths) {
166
178
  let raw;
@@ -206,6 +218,7 @@ export async function validatePluginBundle(archive, options) {
206
218
  bundle: {
207
219
  bundleVersion: 1,
208
220
  ...(metadata.kinds !== undefined ? { kinds: metadata.kinds } : {}),
221
+ ...(metadata.page_components !== undefined ? { page_components: metadata.page_components } : {}),
209
222
  commands: commandValidation.commands,
210
223
  commandsBytes: commands.bytes,
211
224
  directories: parsed.members