@north-light/crouter 0.3.198 → 0.3.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/dist/api/client.d.ts +18 -18
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +81 -34
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +6 -6
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/helpers/harness.js +17 -55
  99. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  100. package/dist/core/__tests__/human-deliver.test.js +31 -26
  101. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  102. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  103. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +4 -5
  104. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  105. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  106. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  107. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  108. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  109. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  110. package/dist/core/__tests__/session-cycles.test.js +22 -0
  111. package/dist/core/__tests__/stop-guard.test.js +4 -4
  112. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  113. package/dist/core/bash-jobs.d.ts +6 -0
  114. package/dist/core/bash-jobs.js +10 -0
  115. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  116. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  117. package/dist/core/canvas/attention.js +8 -6
  118. package/dist/core/canvas/browse/model.d.ts +8 -2
  119. package/dist/core/canvas/browse/model.js +11 -4
  120. package/dist/core/canvas/canvas.d.ts +7 -0
  121. package/dist/core/canvas/canvas.js +37 -0
  122. package/dist/core/canvas/crons.d.ts +9 -0
  123. package/dist/core/canvas/crons.js +51 -0
  124. package/dist/core/canvas/node-order.d.ts +22 -1
  125. package/dist/core/canvas/node-order.js +26 -1
  126. package/dist/core/canvas/render-source.d.ts +5 -0
  127. package/dist/core/canvas/render-source.js +1 -0
  128. package/dist/core/canvas/types.d.ts +9 -3
  129. package/dist/core/command-plugins/bundle.d.ts +6 -1
  130. package/dist/core/command-plugins/bundle.js +26 -13
  131. package/dist/core/command.js +2 -1
  132. package/dist/core/config.d.ts +47 -1
  133. package/dist/core/config.js +137 -4
  134. package/dist/core/help.js +1 -1
  135. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  136. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  137. package/dist/core/human/__tests__/page.test.js +56 -98
  138. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  139. package/dist/core/human/answer-text.js +3 -3
  140. package/dist/core/human/answer.d.ts +6 -4
  141. package/dist/core/human/answer.js +4 -10
  142. package/dist/core/human/component-docs.d.ts +11 -7
  143. package/dist/core/human/component-docs.js +361 -88
  144. package/dist/core/human/convention.d.ts +17 -3
  145. package/dist/core/human/convention.js +36 -5
  146. package/dist/core/human/feedback-companion.d.ts +20 -0
  147. package/dist/core/human/feedback-companion.js +98 -0
  148. package/dist/core/human/feedback.d.ts +65 -0
  149. package/dist/core/human/feedback.js +116 -0
  150. package/dist/core/human/page-catalog.d.ts +40 -5
  151. package/dist/core/human/page-catalog.js +102 -30
  152. package/dist/core/human/page-errors.d.ts +4 -0
  153. package/dist/core/human/page-errors.js +7 -0
  154. package/dist/core/human/page-eval.d.ts +20 -0
  155. package/dist/core/human/page-eval.js +123 -0
  156. package/dist/core/human/page-schema.d.ts +23 -191
  157. package/dist/core/human/page-schema.js +60 -89
  158. package/dist/core/human/page-synth.d.ts +1 -1
  159. package/dist/core/human/page-synth.js +15 -18
  160. package/dist/core/human/page.d.ts +12 -27
  161. package/dist/core/human/page.js +142 -474
  162. package/dist/core/human/root.d.ts +6 -2
  163. package/dist/core/human/root.js +12 -4
  164. package/dist/core/human/scan.d.ts +3 -4
  165. package/dist/core/human/scan.js +16 -15
  166. package/dist/core/human/summary.d.ts +1 -2
  167. package/dist/core/human/summary.js +2 -2
  168. package/dist/core/human/tickets.d.ts +20 -14
  169. package/dist/core/human/tickets.js +91 -31
  170. package/dist/core/human/types.d.ts +10 -2
  171. package/dist/core/preview-registry.js +8 -10
  172. package/dist/core/profiles/default-binding.d.ts +4 -0
  173. package/dist/core/profiles/default-binding.js +24 -0
  174. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  175. package/dist/core/profiles/deletion-reservation.js +38 -0
  176. package/dist/core/profiles/manifest.d.ts +3 -0
  177. package/dist/core/profiles/manifest.js +12 -0
  178. package/dist/core/profiles/select.js +2 -2
  179. package/dist/core/review/realize.js +3 -3
  180. package/dist/core/review/store.d.ts +6 -0
  181. package/dist/core/review/store.js +31 -0
  182. package/dist/core/runtime/bearings.d.ts +0 -4
  183. package/dist/core/runtime/bearings.js +4 -62
  184. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  185. package/dist/core/runtime/bin-contributions.js +186 -0
  186. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  187. package/dist/core/runtime/broker/client-registry.js +13 -1
  188. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  189. package/dist/core/runtime/broker/extension-abort.js +4 -2
  190. package/dist/core/runtime/broker/fault-retry.js +4 -0
  191. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  192. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  193. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  194. package/dist/core/runtime/broker/read-ops.js +2 -2
  195. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  196. package/dist/core/runtime/broker/rebind.js +6 -1
  197. package/dist/core/runtime/broker-extension-render.js +19 -0
  198. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  199. package/dist/core/runtime/broker.d.ts +1 -1
  200. package/dist/core/runtime/broker.js +17 -6
  201. package/dist/core/runtime/front-door-env.d.ts +5 -0
  202. package/dist/core/runtime/front-door-env.js +16 -0
  203. package/dist/core/runtime/front-door.d.ts +1 -5
  204. package/dist/core/runtime/front-door.js +2 -5
  205. package/dist/core/runtime/host.d.ts +1 -1
  206. package/dist/core/runtime/invocation.d.ts +6 -0
  207. package/dist/core/runtime/invocation.js +10 -0
  208. package/dist/core/runtime/kickoff.js +34 -0
  209. package/dist/core/runtime/launch.d.ts +2 -6
  210. package/dist/core/runtime/node-read.js +4 -1
  211. package/dist/core/runtime/nodes.js +2 -0
  212. package/dist/core/runtime/promote.d.ts +1 -0
  213. package/dist/core/runtime/promote.js +3 -1
  214. package/dist/core/runtime/session-cycles.d.ts +6 -0
  215. package/dist/core/runtime/session-cycles.js +15 -1
  216. package/dist/core/runtime/session-visibility.d.ts +17 -8
  217. package/dist/core/runtime/session-visibility.js +19 -10
  218. package/dist/core/runtime/spawn-env.d.ts +9 -1
  219. package/dist/core/runtime/spawn-env.js +62 -2
  220. package/dist/core/runtime/stop-guard.d.ts +2 -2
  221. package/dist/core/runtime/stop-guard.js +2 -2
  222. package/dist/core/runtime/stop-signals.d.ts +1 -1
  223. package/dist/core/runtime/stop-signals.js +2 -2
  224. package/dist/core/runtime/tmux-bindings.js +1 -1
  225. package/dist/core/session-model/session-state.d.ts +4 -3
  226. package/dist/core/session-model/session-state.js +14 -1
  227. package/dist/core/user-settings.d.ts +21 -2
  228. package/dist/core/user-settings.js +32 -11
  229. package/dist/core/worktree.d.ts +15 -45
  230. package/dist/core/worktree.js +59 -261
  231. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  232. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  233. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  234. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  235. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  236. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  237. package/dist/daemon/api/handlers/human.js +24 -20
  238. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  239. package/dist/daemon/api/handlers/inbox.js +39 -115
  240. package/dist/daemon/api/handlers/profiles.js +11 -29
  241. package/dist/daemon/api/map.d.ts +6 -1
  242. package/dist/daemon/api/map.js +23 -0
  243. package/dist/daemon/api/server.js +3 -1
  244. package/dist/daemon/companion-retire.d.ts +2 -0
  245. package/dist/daemon/companion-retire.js +33 -0
  246. package/dist/daemon/cron-run.js +14 -16
  247. package/dist/daemon/human/finish.d.ts +12 -7
  248. package/dist/daemon/human/finish.js +101 -38
  249. package/dist/daemon/human/sweep.js +5 -1
  250. package/dist/daemon/profile-delete.d.ts +7 -0
  251. package/dist/daemon/profile-delete.js +306 -0
  252. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  253. package/dist/daemon/review/deliver.js +3 -3
  254. package/dist/daemon/review/finish.d.ts +0 -2
  255. package/dist/daemon/review/finish.js +1 -29
  256. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  257. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  258. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  259. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  260. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  261. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  262. package/dist/pi-extensions/canvas-stophook.js +4 -4
  263. package/dist/shared/generated-context.js +2 -2
  264. package/dist/types.d.ts +44 -2
  265. package/dist/types.js +7 -2
  266. package/package.json +6 -5
  267. package/runtime.lock.json +5 -33
  268. package/dist/builtin-memory/init.md +0 -38
  269. package/dist/builtin-memory/plan.md +0 -19
  270. package/dist/builtin-memory/spec.md +0 -19
  271. package/dist/commands/human/doc.d.ts +0 -2
  272. package/dist/commands/human/doc.js +0 -91
  273. package/dist/commands/human/html.js +0 -81
  274. package/dist/commands/human/inbox.d.ts +0 -2
  275. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  276. package/dist/core/human/__tests__/page-render.test.js +0 -66
  277. package/dist/core/human/html-markdown.d.ts +0 -3
  278. package/dist/core/human/html-markdown.js +0 -288
  279. package/dist/core/human/page-render.d.ts +0 -4
  280. package/dist/core/human/page-render.js +0 -105
  281. package/dist/pages/bundle.css +0 -1
  282. package/dist/pages/bundle.js +0 -1778
  283. package/dist/pages/comments.d.ts +0 -72
  284. package/dist/pages/comments.js +0 -176
  285. package/dist/pages/controls.d.ts +0 -98
  286. package/dist/pages/controls.js +0 -261
  287. package/dist/pages/elements/cards.d.ts +0 -28
  288. package/dist/pages/elements/cards.js +0 -915
  289. package/dist/pages/elements/chart.d.ts +0 -23
  290. package/dist/pages/elements/chart.js +0 -664
  291. package/dist/pages/elements/options.d.ts +0 -30
  292. package/dist/pages/elements/options.js +0 -742
  293. package/dist/pages/elements/pages.d.ts +0 -30
  294. package/dist/pages/elements/pages.js +0 -377
  295. package/dist/pages/elements/slot.d.ts +0 -26
  296. package/dist/pages/elements/slot.js +0 -112
  297. package/dist/pages/elements/table.d.ts +0 -36
  298. package/dist/pages/elements/table.js +0 -1043
  299. package/dist/pages/elements/text.d.ts +0 -31
  300. package/dist/pages/elements/text.js +0 -1175
  301. package/dist/pages/entry.d.ts +0 -8
  302. package/dist/pages/entry.js +0 -8
  303. package/dist/pages/host.d.ts +0 -134
  304. package/dist/pages/host.js +0 -165
  305. package/dist/pages/readonly.d.ts +0 -24
  306. package/dist/pages/readonly.js +0 -31
  307. package/dist/pages/register.d.ts +0 -2
  308. package/dist/pages/register.js +0 -3
  309. package/dist/pages/responses.d.ts +0 -37
  310. package/dist/pages/responses.js +0 -56
  311. package/dist/pages/slot-config.d.ts +0 -36
  312. package/dist/pages/slot-config.js +0 -50
  313. package/dist/pages/types.d.ts +0 -98
  314. package/dist/pages/types.js +0 -23
  315. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  316. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -8,12 +8,13 @@ import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
  import { createApiServer } from '../../daemon/api/server.js';
10
10
  import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket } from '../../daemon/human/finish.js';
11
- import { humanAsk, humanNotify } from '../../commands/human/prompts.js';
11
+ import { humanSend } from '../../commands/human/prompts.js';
12
12
  import { getNode, listNodes, closeDb } from '../canvas/index.js';
13
13
  import { readInboxSince } from '../feed/inbox.js';
14
14
  import { resolveRef } from '../canvas/history.js';
15
15
  import { apiSocketPath } from '../canvas/paths.js';
16
16
  import { spawnNode } from '../runtime/nodes.js';
17
+ import { bindTicketReplyRoute } from '../human/convention.js';
17
18
  import { ticketDir } from '../human/root.js';
18
19
  import { readTicketResult, submitPage } from '../human/tickets.js';
19
20
  let home;
@@ -39,16 +40,17 @@ async function withMutedStdout(fn) {
39
40
  }
40
41
  function bridgeUnder() {
41
42
  const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
42
- const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-ask', lifecycle: 'terminal' }).node_id;
43
+ const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
43
44
  return { parent, bridge };
44
45
  }
45
46
  function submitBridgePage(bridge) {
46
47
  const dir = ticketDir(bridge);
47
48
  const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-deliver-page-'));
48
- const sourceFile = join(sourceDir, 'page.html');
49
- writeFileSync(sourceFile, '<!doctype html><html><head><meta name="crtr-page-title" content="Ship it?"><meta name="crtr-page-subtitle" content="Decide whether to ship now so the release can proceed."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
49
+ const sourceFile = join(sourceDir, 'page.tsx');
50
+ writeFileSync(sourceFile, 'export default () => (\n <Page title="Ship it?" subtitle="Decide whether to ship now so the release can proceed.">\n <UserText id="question" initialText="" />\n </Page>\n);\n');
50
51
  try {
51
- submitPage({ dir, sourceFile, dialect: 'html' });
52
+ bindTicketReplyRoute(dir, bridge);
53
+ submitPage({ dir, sourceFile, delivery: { placement: 'inline', inbox: true, reply: true } });
52
54
  }
53
55
  finally {
54
56
  rmSync(sourceDir, { recursive: true, force: true });
@@ -87,40 +89,41 @@ after(() => {
87
89
  delete process.env['CRTR_NODE_ID'];
88
90
  });
89
91
  describe('enqueue is producer-only', () => {
90
- test('ask preserves title and subtitle from the authored page', async () => {
92
+ test('send preserves title and subtitle from the authored page', async () => {
91
93
  const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
92
94
  process.env['CRTR_NODE_ID'] = parent;
93
- const page = join(home, 'release-decision.html');
94
- writeFileSync(page, '<!doctype html><html><head><meta name="crtr-page-title" content="Release decision"><meta name="crtr-page-subtitle" content="Decide whether to release now so the launch can proceed."></head><body><p>This first line stays page content.</p><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
95
- await assert.rejects(() => humanAsk.run({}), /a page source is required/);
96
- await assert.rejects(() => humanAsk.run({ page, document: '<!doctype html><html></html>' }), /received both --page and a piped page document/);
97
- const result = (await humanAsk.run({ page, document: '' }));
98
- assert.equal(typeof result['job_id'], 'string');
95
+ const page = join(home, 'release-decision.tsx');
96
+ writeFileSync(page, 'export default () => (\n <Page title="Release decision" subtitle="Decide whether to release now so the launch can proceed.">\n <p>This first line stays page content.</p>\n <UserText id="question" initialText="" />\n </Page>\n);\n');
97
+ await assert.rejects(() => humanSend.run({}), /a page source is required/);
98
+ await assert.rejects(() => humanSend.run({ page, document: 'export default () => <Page title="Dup" />;' }), /received both --page and a piped page document/);
99
+ const result = (await humanSend.run({ page, document: '' }));
100
+ assert.equal(typeof result['page_id'], 'string');
101
+ assert.equal(result['reply'], true);
99
102
  assert.equal(typeof result['dir'], 'string');
100
103
  for (const forbidden of ['pane_id', 'render_pane_id', 'pid', 'worker_pid'])
101
- assert.ok(!(forbidden in result), `ask result must not expose ${forbidden}`);
104
+ assert.ok(!(forbidden in result), `send result must not expose ${forbidden}`);
102
105
  const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
103
106
  assert.equal(manifest.title, 'Release decision');
104
107
  assert.equal(manifest.subtitle, 'Decide whether to release now so the launch can proceed.');
105
- const document = readFileSync(join(result['dir'], 'page.html'), 'utf8');
108
+ const document = readFileSync(join(result['dir'], 'page.tsx'), 'utf8');
106
109
  assert.ok(document.includes('This first line stays page content.'));
107
110
  assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1);
108
111
  });
109
- test('ask materializes a piped page before validation and publication', async () => {
112
+ test('send materializes a piped page before validation and publication', async () => {
110
113
  const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
111
114
  process.env['CRTR_NODE_ID'] = parent;
112
- const document = '<!doctype html><html><head><meta name="crtr-page-title" content="Piped question"><meta name="crtr-page-subtitle" content="Answer the question to unblock this task."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":""}</script></crtr-text></crtr-page></crtr-pages></body></html>';
113
- const result = (await humanAsk.run({ document }));
115
+ const document = 'export default () => (\n <Page title="Piped question" subtitle="Answer the question to unblock this task.">\n <UserText id="question" initialText="" />\n </Page>\n);\n';
116
+ const result = (await humanSend.run({ document }));
114
117
  const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
115
118
  assert.equal(manifest.title, 'Piped question');
116
- assert.equal(readFileSync(join(result['dir'], 'page.html'), 'utf8'), document);
119
+ assert.equal(readFileSync(join(result['dir'], 'page.tsx'), 'utf8'), document);
117
120
  });
118
121
  });
119
122
  describe('terminal ticket delivery', () => {
120
123
  test('resolvePageTicket pushFinals the readable answer once and wakes the subscribed parent', async () => {
121
124
  const { parent, bridge } = bridgeUnder();
122
125
  const dir = submitBridgePage(bridge);
123
- const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true, comments: [] } });
126
+ const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true } });
124
127
  assert.equal(result.kind, 'page');
125
128
  closeDb();
126
129
  assert.equal(getNode(bridge)?.status, 'done');
@@ -133,7 +136,7 @@ describe('terminal ticket delivery', () => {
133
136
  test('a replay against a terminal bridge is a no-op', async () => {
134
137
  const { parent, bridge } = bridgeUnder();
135
138
  submitBridgePage(bridge);
136
- await resolvePageTicket(bridge, { question: { text: 'yes', edited: true, comments: [] } });
139
+ await resolvePageTicket(bridge, { question: { text: 'yes', edited: true } });
137
140
  assert.equal(await deliverTerminalResult(bridge), true);
138
141
  closeDb();
139
142
  const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
@@ -141,7 +144,7 @@ describe('terminal ticket delivery', () => {
141
144
  });
142
145
  test('cancelHumanTicket finishes the bridge and notifies non-actor subscribers', async () => {
143
146
  const actor = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'actor' }).node_id;
144
- const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-ask', lifecycle: 'terminal' }).node_id;
147
+ const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
145
148
  const other = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'other' }).node_id;
146
149
  const { subscribe } = await import('../canvas/canvas.js');
147
150
  subscribe(other, bridge, true);
@@ -156,12 +159,14 @@ describe('terminal ticket delivery', () => {
156
159
  assert.match(notes[0].label, /canceled/);
157
160
  });
158
161
  });
159
- test('notify enqueues an authored page and creates no canvas node', async () => {
160
- const page = join(home, 'build-done.html');
161
- writeFileSync(page, '<!doctype html><html><head><meta name="crtr-page-title" content="Build done"><meta name="crtr-page-subtitle" content="The build passed, so the change is ready to review."></head><body><p>Build output is ready.</p></body></html>');
162
+ test('a display-only page publishes passively and creates no canvas node', async () => {
163
+ const page = join(home, 'build-done.tsx');
164
+ writeFileSync(page, 'export default () => (\n <Page title="Build done" subtitle="The build passed, so the change is ready to review.">\n <p>Build output is ready.</p>\n </Page>\n);\n');
162
165
  const before = listNodes().length;
163
- const result = (await humanNotify.run({ page }));
164
- assert.equal(result['queued'], true);
166
+ const result = (await humanSend.run({ page }));
167
+ assert.equal(result['reply'], false);
168
+ assert.equal(result['inbox'], false);
169
+ assert.equal(typeof result['page_id'], 'string');
165
170
  assert.equal(typeof result['dir'], 'string');
166
171
  assert.ok(existsSync(join(result['dir'], 'page.json')));
167
172
  assert.ok(readTicketResult(result['dir']) === null);
@@ -1,7 +1,7 @@
1
1
  // Run with: node --import tsx/esm --test src/core/__tests__/human-node-not-supervised.test.ts
2
2
  //
3
3
  // BUG REGRESSION (issue #2 — "Headless node can't reach human-ask: human-kind
4
- // TUI dies at boot"): a headless orchestrator's every `crtr human ask` node died
4
+ // TUI dies at boot"): a headless orchestrator's every reply-bearing `crtr human send` bridge died
5
5
  // at boot with "pi vehicle exited before the session came up".
6
6
  //
7
7
  // ROOT CAUSE: a kind:'human' row is NOT a broker-hosted agent node — it is a
@@ -0,0 +1,157 @@
1
+ // Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/plugin-page-components.test.ts
2
+ //
3
+ // Wiring pins for plugin-contributed page components — the layer whose absence
4
+ // is SILENT: every authoring, validation, daemon, inbox, and TUI consumer takes
5
+ // the catalog as a parameter, so if `resolvePageComponents` stopped reading
6
+ // installed plugins' manifest `page_components` blocks, every plugin-shipped
7
+ // component would simply stop being authorable with all suites green.
8
+ // Plus the archive path: `validatePluginBundle` must carry a valid bundle.json
9
+ // `page_components` block into `ValidatedBundle.page_components` (what the
10
+ // installer copies into the synthesized manifest) and reject an invalid one.
11
+ import { test, before, beforeEach, afterEach, after } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import * as tar from 'tar';
17
+ import { resetScopeCache } from '../scope.js';
18
+ import { resolvePageComponents } from '../config.js';
19
+ import { validatePluginBundle } from '../command-plugins/bundle.js';
20
+ let isolatedHome;
21
+ let isolatedCwd;
22
+ let prevHome;
23
+ let prevCwd;
24
+ before(() => {
25
+ prevHome = process.env.HOME;
26
+ prevCwd = process.cwd();
27
+ });
28
+ beforeEach(() => {
29
+ isolatedHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-page-components-home-')));
30
+ isolatedCwd = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-page-components-cwd-')));
31
+ process.env.HOME = isolatedHome;
32
+ process.chdir(isolatedCwd);
33
+ resetScopeCache();
34
+ });
35
+ afterEach(() => {
36
+ process.chdir(prevCwd);
37
+ rmSync(isolatedHome, { recursive: true, force: true });
38
+ rmSync(isolatedCwd, { recursive: true, force: true });
39
+ });
40
+ after(() => {
41
+ process.chdir(prevCwd);
42
+ process.env.HOME = prevHome;
43
+ resetScopeCache();
44
+ });
45
+ /** Plant a manifest-only plugin (page components need no commands or executable). */
46
+ function plantPlugin(scopeRootPath, name, pageComponents) {
47
+ const root = join(scopeRootPath, 'plugins', name, '.crouter-plugin');
48
+ mkdirSync(root, { recursive: true });
49
+ writeFileSync(join(root, 'plugin.json'), JSON.stringify({ name, version: '0.1.0', page_components: pageComponents }));
50
+ }
51
+ function userRoot() {
52
+ const root = join(isolatedHome, '.crouter');
53
+ mkdirSync(root, { recursive: true });
54
+ return root;
55
+ }
56
+ test('an enabled user-scope plugin contributes its page components; a disabled one contributes nothing', () => {
57
+ const root = userRoot();
58
+ plantPlugin(root, 'northlight', [
59
+ { kind: 'connector', description: 'Connector decision', useWhen: 'Use for a connector decision.', doc: 'Product contract.' },
60
+ { kind: 'applet-card', display: true },
61
+ ]);
62
+ plantPlugin(root, 'zzz-disabled', ['ghost']);
63
+ writeFileSync(join(root, 'config.json'), JSON.stringify({ plugins: { 'zzz-disabled': { enabled: false } } }));
64
+ const components = resolvePageComponents();
65
+ assert.deepEqual(components, [
66
+ { kind: 'connector', description: 'Connector decision', useWhen: 'Use for a connector decision.', doc: 'Product contract.' },
67
+ { kind: 'applet-card', display: true },
68
+ ]);
69
+ });
70
+ test('user config entries and plugin contributions resolve together, and a colliding kind is rejected', () => {
71
+ const root = userRoot();
72
+ plantPlugin(root, 'northlight', ['connector']);
73
+ writeFileSync(join(root, 'config.json'), JSON.stringify({ page_components: ['approval'] }));
74
+ assert.deepEqual(resolvePageComponents().map((component) => component.kind), ['approval', 'connector']);
75
+ writeFileSync(join(root, 'config.json'), JSON.stringify({ page_components: ['connector'] }));
76
+ assert.throws(() => resolvePageComponents(), /page component kind "connector" is registered by both config.json and plugin "northlight"/);
77
+ });
78
+ test('two plugins whose kinds derive one JSX tag are rejected, naming both sources', () => {
79
+ const root = userRoot();
80
+ plantPlugin(root, 'alpha', ['foo-bar']);
81
+ plantPlugin(root, 'beta', ['foo--bar']);
82
+ assert.throws(() => resolvePageComponents(), /kind "foo--bar" \(plugin "beta"\) derives JSX tag "FooBar", which duplicates kind "foo-bar" from plugin "alpha"/);
83
+ });
84
+ // --- archive path -----------------------------------------------------------
85
+ function minimalCommandsJson() {
86
+ return {
87
+ schemaVersion: 1,
88
+ mounts: [
89
+ {
90
+ parent: [],
91
+ node: {
92
+ kind: 'branch',
93
+ name: 'app',
94
+ description: 'application lifecycle and inspection',
95
+ whenToUse: 'you need to inspect an application',
96
+ rootEntry: {
97
+ concept: 'applications hosted by a crouter home',
98
+ description: 'application lifecycle and inspection',
99
+ whenToUse: 'you need to inspect an application',
100
+ },
101
+ summary: 'application lifecycle and inspection',
102
+ model: 'Applications are product resources owned by the selected home.',
103
+ children: [
104
+ {
105
+ kind: 'leaf',
106
+ name: 'show',
107
+ description: 'show one application',
108
+ whenToUse: 'inspect a single application by id',
109
+ tier: 'important',
110
+ summary: 'show one application details',
111
+ params: [{ kind: 'positional', name: 'app-id', required: true, constraint: 'the application id' }],
112
+ output: [{ name: 'app_id', type: 'string', required: true, constraint: 'the echoed application id' }],
113
+ effects: ['None. Read-only.'],
114
+ rest: { method: 'GET', path: '/v1/apps/{app-id}', params: { 'app-id': { in: 'path' } } },
115
+ },
116
+ ],
117
+ },
118
+ },
119
+ ],
120
+ };
121
+ }
122
+ function makeArchive(bundleJson) {
123
+ const dir = mkdtempSync(join(tmpdir(), 'crtr-page-components-tar-'));
124
+ try {
125
+ writeFileSync(join(dir, 'bundle.json'), JSON.stringify(bundleJson));
126
+ writeFileSync(join(dir, 'commands.json'), JSON.stringify(minimalCommandsJson()));
127
+ const pack = tar.create({ sync: true, cwd: dir, portable: true }, ['bundle.json', 'commands.json']);
128
+ const chunks = [];
129
+ let chunk;
130
+ while ((chunk = pack.read()) !== null)
131
+ chunks.push(chunk);
132
+ return new Uint8Array(Buffer.concat(chunks));
133
+ }
134
+ finally {
135
+ rmSync(dir, { recursive: true, force: true });
136
+ }
137
+ }
138
+ test('validatePluginBundle carries a valid bundle.json page_components block into ValidatedBundle', async () => {
139
+ const archive = makeArchive({
140
+ bundleVersion: 1,
141
+ page_components: ['connector', { kind: 'applet-card', description: 'Applet card', display: true }],
142
+ });
143
+ const validated = await validatePluginBundle(archive, { reservedCoreNames: new Set(['node']) });
144
+ assert.deepEqual(validated.issues, []);
145
+ assert.deepEqual(validated.bundle?.page_components, [
146
+ { kind: 'connector' },
147
+ { kind: 'applet-card', description: 'Applet card', display: true },
148
+ ]);
149
+ });
150
+ test('validatePluginBundle rejects an invalid page_components block loudly', async () => {
151
+ const archive = makeArchive({ bundleVersion: 1, page_components: [{ kind: 'connector', use_when: 'typo field' }] });
152
+ const validated = await validatePluginBundle(archive, { reservedCoreNames: new Set(['node']) });
153
+ assert.equal(validated.bundle, undefined);
154
+ assert.equal(validated.issues.length, 1);
155
+ assert.match(validated.issues[0].message, /page_components block is invalid/);
156
+ assert.match(validated.issues[0].received, /use_when/);
157
+ });
@@ -10,11 +10,10 @@
10
10
  // lifecycle close` then tears the replacement down (shutdown frame → exit,
11
11
  // socket unlinked, status canceled, daemon leaves it).
12
12
  //
13
- // The harness API server is a separate process (so synchronous CLI calls can be
14
- // served) whose fixture fleet is inert; spawnHeadlessChild retires its fixture
15
- // broker and reboots the node through THIS process's reviveNode, so every
16
- // broker below is harness-fleet-owned and every SIGKILL's REAL ChildProcess
17
- // exit drives policy automatically — no synthetic delivery, no tick, no grace.
13
+ // spawnHeadlessChild calls the production birth path in the harness process, so
14
+ // every broker below is harness-fleet-owned from its first and only boot and
15
+ // every SIGKILL's REAL ChildProcess exit drives policy automatically — no
16
+ // synthetic delivery, no tick, no grace.
18
17
  // Each crash has its own real broker setup, so either top-level test remains a
19
18
  // complete journey under name filtering and pays only its own first respawn
20
19
  // backoff.
@@ -19,6 +19,8 @@
19
19
  // they belong to the compiled seam tier; neither needs tmux.
20
20
  import { after, before, test } from 'node:test';
21
21
  import assert from 'node:assert/strict';
22
+ import { writeFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
22
24
  import { appendInbox, readCursor, readInboxSince } from '../../feed/inbox.js';
23
25
  import { registerWarmSpare, subscribe, updateNode } from '../../canvas/index.js';
24
26
  import { isPidAlive } from '../../canvas/pid.js';
@@ -176,6 +178,44 @@ test('an unattended resident with no live obligation completes on the daemon clo
176
178
  process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
177
179
  }
178
180
  });
181
+ test('a connected broker viewer keeps an obligation-free resident live', { timeout: 30_000 }, async () => {
182
+ const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
183
+ process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
184
+ const nodeId = h.fabricateBrokerNode({
185
+ id: 'settled-with-viewer',
186
+ parent: root,
187
+ lifecycle: 'resident',
188
+ status: 'idle',
189
+ intent: 'idle-release',
190
+ });
191
+ updateNode(nodeId, {
192
+ launch: {
193
+ extensions: [],
194
+ tools: [],
195
+ systemPrompt: '',
196
+ env: {},
197
+ },
198
+ });
199
+ reviveNode(nodeId, { resume: true });
200
+ const boot = await h.awaitBoot(nodeId);
201
+ // This is the presence written by a helloed direct attach, which has no
202
+ // focuses-table row because it did not pass through the focus opener.
203
+ writeFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), JSON.stringify({ viewers: 1 }));
204
+ try {
205
+ const seenAt = 15_000;
206
+ await h.tick(seenAt);
207
+ await h.tick(seenAt + 1500);
208
+ assert.equal(h.fleet.has(nodeId), true, 'an attached viewer keeps the broker fleet-owned');
209
+ assert.equal(isPidAlive(boot.pid), true, 'an attached viewer keeps the broker process alive');
210
+ assert.ok(['active', 'idle'].includes(h.node(nodeId)?.status ?? ''), 'an attached viewer keeps the node live');
211
+ }
212
+ finally {
213
+ if (priorParkMs === undefined)
214
+ delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
215
+ else
216
+ process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
217
+ }
218
+ });
179
219
  test('an already idle terminal node with no live obligation reconciles into canceled', async () => {
180
220
  const nodeId = h.fabricateBrokerNode({
181
221
  id: 'idle-terminal-without-obligation',
@@ -150,7 +150,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
150
150
  // ('natural stop while awaiting a live worker → idle-release …').
151
151
  // ===================================================================
152
152
  {
153
- const stop = evaluateStop(B, { pushedFinal: false, askedHuman: false });
153
+ const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
154
154
  assert.equal(stop.action, 'allow', 'awaiting terminal B → allow (legit idle-release)');
155
155
  assert.equal(stop.reason, 'awaiting', "B classified awaiting on its ACTIVE live sub to C");
156
156
  }
@@ -225,7 +225,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
225
225
  assert.deepEqual(drift?.from, { mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
226
226
  assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
227
227
  assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the steer carries the base→orchestrator guidance');
228
- const stop = evaluateStop(B, { pushedFinal: false, askedHuman: false });
228
+ const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
229
229
  assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
230
230
  assert.equal(stop.reason, 'stalled', 'classified stalled');
231
231
  assert.equal(stop.action === 'reprompt' ? stop.message : '', STALL_REPROMPT, 'the stall reprompt nudge');
@@ -11,6 +11,7 @@ import { ApiError, CrtrClient } from '../../../api/index.js';
11
11
  import { createApiServer } from '../../../daemon/api/server.js';
12
12
  import { apiSocketPath } from '../../canvas/paths.js';
13
13
  import { claimTicket, readTicketClaim } from '../../human/claim.js';
14
+ import { bindTicketReplyRoute } from '../../human/convention.js';
14
15
  import { ticketDir } from '../../human/root.js';
15
16
  import { submitPage, submitReview } from '../../human/tickets.js';
16
17
  import { spawnNode } from '../../runtime/nodes.js';
@@ -66,10 +67,14 @@ async function expectReviewSubmitGuidance(nodeId, body) {
66
67
  function submitBridgePage(bridge) {
67
68
  const dir = ticketDir(bridge);
68
69
  const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-e2e-page-'));
69
- const sourceFile = join(sourceDir, 'page.html');
70
- writeFileSync(sourceFile, '<!doctype html><html><head><meta name="crtr-page-title" content="Ship it?"><meta name="crtr-page-subtitle" content="Decide whether to ship now so the release can proceed."></head><body><crtr-pages><crtr-page><crtr-text id="question"><script type="application/json">{"initialText":"","editable":true}</script></crtr-text></crtr-page></crtr-pages></body></html>');
70
+ const sourceFile = join(sourceDir, 'page.tsx');
71
+ writeFileSync(sourceFile, 'export default () => (\n <Page title="Ship it?" subtitle="Decide whether to ship now so the release can proceed.">\n <UserText id="question" initialText="" />\n </Page>\n);\n');
71
72
  try {
72
- submitPage({ dir, sourceFile, dialect: 'html' });
73
+ bindTicketReplyRoute(dir, bridge);
74
+ // The delivery `human send` derives for a terminal bridge page: a
75
+ // response-bearing slot gives it a reply route, and a terminal sender's page
76
+ // always projects to the inbox.
77
+ submitPage({ dir, sourceFile, delivery: { placement: 'inline', inbox: true, reply: true } });
73
78
  }
74
79
  finally {
75
80
  rmSync(sourceDir, { recursive: true, force: true });
@@ -100,6 +105,7 @@ test('malformed resolves leave the mounted ticket claim in place', async () => {
100
105
  const reviewDir = ticketDir(reviewBridge);
101
106
  const file = join(home, 'review.md');
102
107
  writeFileSync(file, '# Review\n');
108
+ bindTicketReplyRoute(reviewDir, reviewBridge);
103
109
  submitReview({ dir: reviewDir, review: { title: 'Review', subtitle: 'Review this file so implementation can proceed safely.', source: {}, file } });
104
110
  const reviewClaim = claimTicket(reviewDir);
105
111
  assert.ok(reviewClaim !== null);
@@ -108,10 +114,10 @@ test('malformed resolves leave the mounted ticket claim in place', async () => {
108
114
  });
109
115
  test('the ticket resolve route pushFinals the readable answer to the parent', async () => {
110
116
  const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
111
- const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-ask', lifecycle: 'terminal' }).node_id;
117
+ const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
112
118
  const dir = submitBridgePage(bridge);
113
- const result = await client.resolveHumanTicket(bridge, { responses: { question: { text: 'yes, ship', edited: true, comments: [] } } });
114
- assert.deepEqual(result, { delivered: true, kind: 'page' });
119
+ const result = await client.resolveHumanTicket(bridge, { responses: { question: { text: 'yes, ship', edited: true } } });
120
+ assert.deepEqual(result, { completed: true, reply_delivered: true, kind: 'page' });
115
121
  assert.equal(getNode(bridge)?.status, 'done', 'bridge finalized by crtrd');
116
122
  const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
117
123
  assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
@@ -48,7 +48,7 @@ import { evaluateStop } from '../../runtime/stop-guard.js';
48
48
  function persona(m) {
49
49
  return { mode: m.mode, lifecycle: m.lifecycle };
50
50
  }
51
- const SIGNALS = { pushedFinal: false, askedHuman: false };
51
+ const SIGNALS = { pushedFinal: false, requestedHumanReply: false };
52
52
  // ===========================================================================
53
53
  // (a) LIFECYCLE FLIP — `crtr node config --lifecycle` on a LIVE node, both directions,
54
54
  // observing the idle-release DECISION change. A round-trip on ONE node
@@ -186,7 +186,7 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
186
186
  const defaultMenu = tmuxBinding(socket, 'root', 'M-c') ?? '';
187
187
  assert.match(defaultMenu, /display-menu/);
188
188
  // The inbox toggle installs at its default key (alt+i → M-i) in the same sweep.
189
- assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr human inbox toggle/);
189
+ assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr surface inbox/);
190
190
  const remapped = installTmuxBindings(resolveKeybindings({
191
191
  'crtr.tmux.menu.open': ['alt+shift+z'],
192
192
  'crtr.tmux.menu.graph': ['shift+z'],
@@ -242,11 +242,13 @@ test('tmux installer reads pair-array manifests and replaces owned pairs', (t) =
242
242
  try {
243
243
  // Current-scope releases wrote the pair array before wrappers were added.
244
244
  isolatedTmux(socket, ['set-option', '-s', '@crtr_keybindings_manifest', JSON.stringify([{ table: 'root', key: 'M-i' }])]);
245
+ // Seeded with the pre-move command path, so the assertion below distinguishes a
246
+ // genuine replacement from a no-op.
245
247
  isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-i', 'run-shell', '-b', 'crtr human inbox toggle --quiet']);
246
248
  const result = installTmuxBindings(resolveKeybindings());
247
249
  assert.equal(result.ok, true, JSON.stringify(result.diagnostics));
248
250
  assert.ok(!result.diagnostics.some((d) => d.bindingId === 'crtr.tmux.inbox.toggle'), JSON.stringify(result.diagnostics));
249
- assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr human inbox toggle/);
251
+ assert.match(tmuxBinding(socket, 'root', 'M-i') ?? '', /crtr surface inbox/);
250
252
  assert.match(tmuxBinding(socket, 'root', 'M-c') ?? '', /background running bash/);
251
253
  }
252
254
  finally {