@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
@@ -2,17 +2,12 @@ import stringWidth from 'string-width';
2
2
  import { isResponseBearingSlot } from '../../../core/human/page-schema.js';
3
3
  import { answerCommentAnchorLabel, describePageAnswer } from '../../../core/human/answer.js';
4
4
  import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
5
- import { renderHtmlProseAsMarkdown } from '../../../core/human/html-markdown.js';
6
- import { describeSlot, pageComplete, slotActions, slotAnswered, tableMarkdown, terminalAnswerable } from './slots.js';
7
- import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, hardWrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
5
+ import { commentAnchors, describeSlot, pageComplete, slotActions, slotAnswered, tableMarkdown, terminalAnswerable } from './slots.js';
6
+ import { RESET, BOLD, DIM, GREEN, YELLOW, CYAN, REVERSE, sanitize, singleLine, truncate, hline, wrap, centerHorizontal, clipLine, panLine, visibleWidth } from './ansi.js';
8
7
  const PAN_STEP = 8;
9
8
  const mdLines = (md, maxW, paneW) => renderMarkdownBlockAwareLines(md, maxW, paneW);
10
- function proseLines(blocks, maxW, paneW, dialect) {
11
- const rendered = blocks.map((block) => dialect === 'html' ? renderHtmlProseAsMarkdown(block) : block).filter((block) => block.trim() !== '');
12
- return rendered.flatMap((block, index) => [...(index > 0 ? [''] : []), ...mdLines(block, maxW, paneW)]);
13
- }
14
9
  function pageAnswer(state) {
15
- return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: '', productKinds: state.productKinds });
10
+ return describePageAnswer(state.manifest, Object.fromEntries(state.responses), { completedAt: '' });
16
11
  }
17
12
  function answerSlot(answer, slot) {
18
13
  return slot.id === undefined ? undefined : answer.slots.find((candidate) => candidate.id === slot.id);
@@ -36,10 +31,11 @@ export function renderOverview(state, cols, rows) {
36
31
  const maxW = Math.min(Math.max(20, cols - 4), 120);
37
32
  const lines = ['', ` ${BOLD}${CYAN}${sanitize(state.manifest.title)}${RESET}`];
38
33
  if (state.manifest.subtitle)
39
- lines.push(` ${DIM}${sanitize(state.manifest.subtitle)}${RESET}`);
40
- lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
41
- for (const line of proseLines(state.overviewProse, maxW, Math.max(20, cols - 4), state.manifest.dialect))
42
- lines.push(` ${line}`);
34
+ for (const line of wrap(sanitize(state.manifest.subtitle), maxW))
35
+ lines.push(` ${DIM}${line}${RESET}`);
36
+ const hasBody = state.slots.length > 0;
37
+ if (hasBody)
38
+ lines.push(` ${DIM}${hline(maxW)}${RESET}`, '');
43
39
  const answer = pageAnswer(state);
44
40
  let priorStep = -1;
45
41
  for (let i = 0; i < state.slots.length; i++) {
@@ -57,18 +53,64 @@ export function renderOverview(state, cols, rows) {
57
53
  if (response !== undefined)
58
54
  lines.push(` ${DIM}${truncate(singleLine(answerSummary(answerSlot(answer, slot))), Math.max(10, cols - 10))}${RESET}`);
59
55
  }
60
- if (state.slots.length === 0)
61
- lines.push(` ${DIM}No interactive slots${RESET}`);
62
- lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
56
+ if (hasBody)
57
+ lines.push('');
58
+ lines.push(` ${DIM}${hline(maxW)}${RESET}`);
63
59
  const display = !state.manifest.slots.some(isResponseBearingSlot);
64
60
  lines.push(display ? ` ${DIM}q${RESET} dismiss` : ` ${DIM}enter${RESET} review ${DIM}j/k${RESET} navigate ${DIM}q${RESET} close`);
65
61
  return centeredWindow(lines, state, cols, rows, maxW + 2);
66
62
  }
63
+ /** Wrap an input buffer and paint the cursor the way a terminal does: the
64
+ * character under it is drawn in reverse video, occupying its own cell rather
65
+ * than pushing the rest of the line across by one. Only past the end of a row
66
+ * does the cursor need a cell of its own, and there it is a reversed space.
67
+ * Wrapping runs over the plain buffer, so the rows here are exactly the rows
68
+ * `verticalCursor` moves between. */
67
69
  export function renderInputBuffer(buffer, cursor, maxWidth) {
68
70
  const chars = [...buffer];
69
71
  const at = Math.max(0, Math.min(cursor, chars.length));
70
- const withCaret = [...chars.slice(0, at), '\uE000', ...chars.slice(at)].join('');
71
- return hardWrap(withCaret, maxWidth).map((line) => line.includes('\uE000') ? line.replace('\uE000', `${REVERSE} ${RESET}`) : line);
72
+ const rows = wrappedRowBounds(buffer, maxWidth);
73
+ let cursorRow = 0;
74
+ for (let i = 0; i < rows.length; i++)
75
+ if (rows[i].start <= at)
76
+ cursorRow = i;
77
+ const lines = rows.map((row, index) => {
78
+ if (index !== cursorRow)
79
+ return chars.slice(row.start, row.end).join('');
80
+ const head = chars.slice(row.start, at).join('');
81
+ const under = at < row.end ? chars[at] : ' ';
82
+ const tail = at < row.end ? chars.slice(at + 1, row.end).join('') : '';
83
+ return `${head}${REVERSE}${under}${RESET}${tail}`;
84
+ });
85
+ // A cursor past the end of an already-full row has no cell left on it, so it
86
+ // takes the first cell of the next row instead of overflowing the wrap width.
87
+ const last = rows[cursorRow];
88
+ if (at === last.end && stringWidth(chars.slice(last.start, last.end).join('')) >= maxWidth) {
89
+ lines[cursorRow] = chars.slice(last.start, last.end).join('');
90
+ lines.splice(cursorRow + 1, 0, `${REVERSE} ${RESET}`);
91
+ }
92
+ return lines;
93
+ }
94
+ /** Start of the word before the cursor (alt+left / readline alt-b). The one
95
+ * implementation of word motion for every inbox input buffer. */
96
+ export function wordLeftIndex(buffer, cursor) {
97
+ const chars = [...buffer];
98
+ let i = Math.max(0, Math.min(cursor, chars.length));
99
+ while (i > 0 && /\s/.test(chars[i - 1]))
100
+ i--;
101
+ while (i > 0 && !/\s/.test(chars[i - 1]))
102
+ i--;
103
+ return i;
104
+ }
105
+ /** Just past the end of the word after the cursor (alt+right / readline alt-f). */
106
+ export function wordRightIndex(buffer, cursor) {
107
+ const chars = [...buffer];
108
+ let i = Math.max(0, Math.min(cursor, chars.length));
109
+ while (i < chars.length && /\s/.test(chars[i]))
110
+ i++;
111
+ while (i < chars.length && !/\s/.test(chars[i]))
112
+ i++;
113
+ return i;
72
114
  }
73
115
  export function wrappedRowBounds(buffer, width) {
74
116
  const chars = [...buffer];
@@ -129,10 +171,12 @@ function itemLines(state, cols) {
129
171
  const maxW = Math.min(Math.max(20, cols - 4), 120);
130
172
  const paneW = Math.max(20, cols - 4);
131
173
  const lines = ['', ` ${BOLD}${CYAN}[${state.currentIndex + 1}/${state.slots.length}]${RESET} ${DIM}${sanitize(state.manifest.title)}${RESET}`, ` ${DIM}${hline(maxW)}${RESET}`, '', ` ${BOLD}${sanitize(describeSlot(slot))}${RESET}`];
132
- // A single-slot page opens straight into item-review, so its leading prose has nowhere else to render.
133
- const prose = state.slots.length === 1 ? [...state.overviewProse, ...(state.slotProse.get(0) ?? [])] : state.slotProse.get(state.currentIndex) ?? [];
134
- for (const line of proseLines(prose, maxW, paneW, state.manifest.dialect))
135
- lines.push(` ${line}`);
174
+ const body = slot.config.body;
175
+ if (typeof body === 'string' && body.length > 0) {
176
+ lines.push('');
177
+ for (const line of mdLines(body, maxW, paneW))
178
+ lines.push(` ${line}`);
179
+ }
136
180
  if (!terminalAnswerable(slot))
137
181
  lines.push('', ` ${DIM}${sanitize(slot.kind)} — cannot be answered in the terminal; open in Northlight${RESET}`);
138
182
  else if (slot.kind === 'chart') {
@@ -157,7 +201,7 @@ function itemLines(state, cols) {
157
201
  }
158
202
  lines.push('', ` ${DIM}${hline(maxW)}${RESET}`);
159
203
  if (state.inputMode !== null) {
160
- const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor ?? { kind: 'whole' })}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
204
+ const label = state.inputMode.kind === 'comment' ? `Comment on ${commentLabel(slot, state.inputMode.anchor)}` : state.inputMode.kind === 'text' ? 'Edit text' : 'Other response';
161
205
  lines.push(` ${BOLD}${label}${RESET}`);
162
206
  for (const line of renderInputBuffer(state.inputMode.buffer, state.inputMode.cursor, Math.max(10, maxW - 4)))
163
207
  lines.push(` ${line}`);
@@ -171,7 +215,8 @@ function itemLines(state, cols) {
171
215
  const mark = selected(state, slot, action.group, action.id) ? `${GREEN}✓${RESET}` : ' ';
172
216
  lines.push(` ${cursor} [${mark}] ${action.shortcut ? `[${action.shortcut}] ` : ''}${sanitize(action.label)}`);
173
217
  if (action.description)
174
- lines.push(` ${DIM}${sanitize(action.description)}${RESET}`);
218
+ for (const line of wrap(sanitize(action.description), Math.max(10, maxW - 6)))
219
+ lines.push(` ${DIM}${line}${RESET}`);
175
220
  }
176
221
  else {
177
222
  const key = action.kind === 'freetext' ? 'r' : action.kind === 'edit' ? 'e' : 'a';
@@ -179,10 +224,11 @@ function itemLines(state, cols) {
179
224
  }
180
225
  });
181
226
  for (const comment of responseComments(state, slot))
182
- lines.push(` ${DIM}attached: ${commentLabel(slot, comment.anchor)} — ${sanitize(comment.text)}${RESET}`);
227
+ for (const line of wrap(`attached: ${commentLabel(slot, comment.anchor)} — ${sanitize(comment.text)}`, Math.max(10, maxW - 2)))
228
+ lines.push(` ${DIM}${line}${RESET}`);
183
229
  if (state.hint)
184
230
  lines.push(` ${YELLOW}${sanitize(state.hint)}${RESET}`);
185
- lines.push(` ${DIM}n/p next/previous j/k actions c comment u/d scroll h/l pan q close${RESET}`);
231
+ lines.push(` ${DIM}n/p next/previous j/k actions${commentAnchors(slot).length > 0 ? ' c comment' : ''} u/d scroll h/l pan q close${RESET}`);
186
232
  }
187
233
  const maxLine = Math.max(0, ...lines.map(visibleWidth));
188
234
  return { lines, maxW, maxH: Math.max(0, maxLine - Math.max(1, cols)) };
@@ -221,8 +267,6 @@ export function renderFinal(state, cols, rows) {
221
267
  const mark = answered ? `${GREEN}✓${RESET}` : `${YELLOW}○${RESET}`;
222
268
  lines.push(` ${mark} ${BOLD}${sanitize(manifestSlot === undefined ? slot.kind : describeSlot(manifestSlot))}${RESET} ${DIM}${sanitize(answerSummary(slot))}${RESET}`);
223
269
  }
224
- for (const line of proseLines(state.finalProse, maxW, Math.max(20, cols - 4), state.manifest.dialect))
225
- lines.push(` ${line}`);
226
270
  const complete = pageComplete(state);
227
271
  if (!complete) {
228
272
  lines.push('', ` ${YELLOW}Cannot submit yet.${RESET}`);
@@ -20,11 +20,13 @@ export type ActionRow = {
20
20
  label: string;
21
21
  };
22
22
  export declare function describeSlot(slot: PageSlot): string;
23
- /** False when the slot's response cannot be authored here: an unvalidated product kind, or a source-backed cards slot whose cards the terminal never sees. */
23
+ /** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
24
24
  export declare function terminalAnswerable(slot: PageSlot): boolean;
25
25
  export declare function slotActions(slot: PageSlot): ActionRow[];
26
26
  export declare function tableMarkdown(config: TableConfig): string;
27
27
  export declare function slotAnswered(state: TuiState, slot: PageSlot): boolean;
28
28
  export declare function pageComplete(state: Pick<TuiState, 'manifest' | 'responses'>): boolean;
29
29
  export declare function commentIdFor(anchor: CommentAnchor): string;
30
+ /** The response-comment anchors this component may author: options take per-option, tables take rows only. */
31
+ export declare function commentAnchors(slot: PageSlot): CommentAnchor[];
30
32
  export declare function awaitsResponse(manifest: PageManifest): boolean;
@@ -18,7 +18,7 @@ export function describeSlot(slot) {
18
18
  return 'Chart';
19
19
  return slot.kind;
20
20
  }
21
- /** False when the slot's response cannot be authored here: an unvalidated product kind, or a source-backed cards slot whose cards the terminal never sees. */
21
+ /** False when this component's response cannot be authored here: an unvalidated product kind, or a source-backed cards component whose cards the terminal never sees. */
22
22
  export function terminalAnswerable(slot) {
23
23
  if (slot.unvalidated === true)
24
24
  return false;
@@ -34,18 +34,16 @@ export function slotActions(slot) {
34
34
  const config = slot.config;
35
35
  rows = config.options.map((option) => ({ kind: 'select', group: 'option', id: option.id, label: option.label, ...(option.description === undefined ? {} : { description: option.description }), mode: config.mode }));
36
36
  if (config.allowFreetext === true)
37
- rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Other' });
37
+ rows.push({ kind: 'freetext', label: config.freetextLabel ?? 'Something else' });
38
38
  }
39
39
  else if (slot.kind === 'cards') {
40
40
  const config = slot.config;
41
41
  rows = (config.cards ?? []).map((card) => ({ kind: 'select', group: 'card', id: card.id, label: card.subtitle === undefined ? card.title : `${card.title} — ${card.subtitle}`, ...(card.body === undefined ? {} : { description: card.body }), mode: config.mode }));
42
42
  }
43
43
  else if (slot.kind === 'text') {
44
- const config = slot.config;
45
- // Text is a writing surface unless the author explicitly made it review-only, and a
46
- // one-line field is an input by definition — the same contract the page kit presents.
47
- if (config.singleLine === true || config.editable !== false)
48
- rows.push({ kind: 'edit', label: 'Edit' });
44
+ // Text is always a writing surface — the same contract the page kit presents. Accepting
45
+ // the draft as written is the other half of that answer, not a separate read-only mode.
46
+ rows.push({ kind: 'edit', label: 'Edit' });
49
47
  rows.push({ kind: 'accept', label: 'Accept as-is' });
50
48
  }
51
49
  else if (slot.kind === 'table') {
@@ -114,14 +112,20 @@ export function pageComplete(state) {
114
112
  }
115
113
  export function commentIdFor(anchor) {
116
114
  switch (anchor.kind) {
117
- case 'whole': return 'whole';
118
115
  case 'option': return `option:${anchor.optionId}`;
119
- case 'card': return `card:${anchor.cardId}`;
120
116
  case 'row': return `row:${anchor.rowId}`;
121
- case 'column': return `column:${anchor.columnId}`;
122
- case 'range': return `range:${anchor.start}:${anchor.end}`;
123
117
  }
124
118
  }
119
+ /** The response-comment anchors this component may author: options take per-option, tables take rows only. */
120
+ export function commentAnchors(slot) {
121
+ if (!terminalAnswerable(slot))
122
+ return [];
123
+ if (slot.kind === 'options')
124
+ return slot.config.options.map((option) => ({ kind: 'option', optionId: option.id }));
125
+ if (slot.kind === 'table')
126
+ return (slot.config.rows ?? []).map((row) => ({ kind: 'row', rowId: row.id }));
127
+ return [];
128
+ }
125
129
  export function awaitsResponse(manifest) {
126
- return manifest.slots.some(isResponseBearingSlot);
130
+ return manifest.delivery.reply;
127
131
  }
@@ -1,13 +1,12 @@
1
1
  import type { Key } from './keys.js';
2
2
  import type { ProductPageComponents } from '../../../types.js';
3
3
  import type { CommentAnchor, PageManifest, PageResponses, PageSlot, SlotResponse } from '../../../core/human/page-schema.js';
4
- import type { PageSourceProjection } from '../../../core/human/page.js';
5
4
  export type Phase = 'overview' | 'item-review' | 'final';
6
5
  export type InputMode = null | {
7
6
  kind: 'comment';
8
7
  buffer: string;
9
8
  cursor: number;
10
- anchor?: CommentAnchor;
9
+ anchor: CommentAnchor;
11
10
  } | {
12
11
  kind: 'freetext';
13
12
  buffer: string;
@@ -22,9 +21,6 @@ export interface TuiState {
22
21
  currentIndex: number;
23
22
  manifest: PageManifest;
24
23
  slots: PageSlot[];
25
- overviewProse: string[];
26
- slotProse: Map<number, string[]>;
27
- finalProse: string[];
28
24
  responses: Map<string, SlotResponse>;
29
25
  productKinds: ProductPageComponents;
30
26
  inputMode: InputMode;
@@ -43,7 +39,6 @@ export interface TuiState {
43
39
  }
44
40
  export interface MountedPanelOpts {
45
41
  manifest: PageManifest;
46
- projection: PageSourceProjection;
47
42
  productKinds?: ProductPageComponents;
48
43
  initialResponses?: PageResponses | null;
49
44
  cols: number;
@@ -59,7 +54,7 @@ export interface MountedPanel {
59
54
  render(): string[];
60
55
  handleResize(cols: number, rows: number): string[];
61
56
  unmount(): void;
62
- loadPage(manifest: PageManifest, projection: PageSourceProjection, opts?: {
57
+ loadPage(manifest: PageManifest, opts?: {
63
58
  initialResponses?: PageResponses | null;
64
59
  productKinds?: ProductPageComponents;
65
60
  }): void;
@@ -36,12 +36,12 @@ export function buildInboxLines(items, width, selectedIndex, height = Number.MAX
36
36
  }
37
37
  function buildItemLines(item, index, selectedIndex, contentWidth) {
38
38
  const kind = item.kind === 'page'
39
- ? item.awaitsResponse === false ? 'display' : item.dialect === 'html' ? 'rich' : 'interactive'
39
+ ? item.awaitsResponse === false ? 'display' : 'rich'
40
40
  : item.kind;
41
41
  const icon = KIND_ICON[kind] ?? '·';
42
42
  // The inbox is a title-first triage surface. Origin details remain available
43
43
  // after opening a ticket, rather than consuming the list's scarce row width.
44
- const age = formatTimeAgo(item.blockedSince);
44
+ const age = formatTimeAgo(item.kind === 'page' ? item.emittedAt : item.blockedSince);
45
45
  const cursor = index === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
46
46
  const titleWidth = Math.max(1, contentWidth - age.length - 6);
47
47
  const row = `${cursor}${ansiColor(icon, KIND_COLOR[kind] ?? 'cyan')} ${BOLD}${truncateRow(item.title || `(${item.id.slice(0, 8)})`, titleWidth)}${RESET} ${DIM}${age}${RESET}`;
@@ -7,9 +7,9 @@ import { join } from 'node:path';
7
7
  import { Readable } from 'node:stream';
8
8
  import { parseArgv } from '../../core/command.js';
9
9
  import { renderLeafArgv } from '../../core/help.js';
10
- import { renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
11
- import { humanHtml } from '../human/html.js';
12
- import { humanAsk, humanNotify } from '../human/prompts.js';
10
+ import { componentDoc, renderComponentCatalog, renderComponentContract } from '../../core/human/component-docs.js';
11
+ import { humanComponents } from '../human/components.js';
12
+ import { humanSend } from '../human/prompts.js';
13
13
  import { registerHuman } from '../human.js';
14
14
  import { pageHelpText } from '../human/shared.js';
15
15
  const nlThreadRegistration = {
@@ -17,62 +17,7 @@ const nlThreadRegistration = {
17
17
  description: 'message conversation with participants and metadata',
18
18
  useWhen: 'use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide',
19
19
  display: true,
20
- doc: [
21
- '`nl-thread` renders a conversation as bubbles down one column: your own messages on the right in the accent, the other party on the left, consecutive messages from one sender grouped under a single avatar.',
22
- '',
23
- 'Display only: it contributes nothing to the response, stays readable and expandable on a resolved page, and does not require a slot `id`.',
24
- '',
25
- '## Config',
26
- '',
27
- '| field | type | required | meaning |',
28
- '| --- | --- | --- | --- |',
29
- '| `label` | string | no | the heading above the thread - what this conversation is, in your own words |',
30
- '| `platform` | string | no | where it happened (`email`, `linkedin`, `imessage`, `slack`); shown as a small chip |',
31
- '| `messages` | message[] | no | the conversation, oldest first; an empty or absent list renders "No messages in this thread." |',
32
- '| `participants` | participant[] | no | display identities, joined to messages by handle |',
33
- '| `collapsedByDefault` | boolean | no | open on the last 3 messages, the rest behind a "Show N earlier messages" control |',
34
- '',
35
- 'Unknown config properties are rejected: the slot then renders an unavailable notice naming the offending key.',
36
- '',
37
- '### message',
38
- '',
39
- '| field | type | required | meaning |',
40
- '| --- | --- | --- | --- |',
41
- '| `sender` | string | yes | `self` is the viewer, `them` the unnamed other party; any other value is a named sender |',
42
- '| `body` | string | yes | plain text; authored newlines are preserved, markdown is NOT rendered |',
43
- '| `senderName` | string | no | display name for this message, winning over the participant roster |',
44
- '| `senderHandle` | string | no | the raw address - phone, email, profile id - joined against `participants` |',
45
- '| `timestamp` | string | no | ISO 8601; a clock within today, date + time before that. Anything unparseable is shown verbatim |',
46
- '',
47
- '### participant',
48
- '',
49
- '| field | type | required | meaning |',
50
- '| --- | --- | --- | --- |',
51
- '| `handle` | string | yes | matches the `senderHandle` on a message |',
52
- '| `name` | string | no | display name for every message from that handle |',
53
- '| `avatarDataUrl` | string | no | a `data:image/...` URL ONLY. A page never fetches, so an http(s) avatar is rejected - omit it and initials are drawn instead |',
54
- '',
55
- 'There is no `source` field and no artifact reference: messages are always inline in the config.',
56
- '',
57
- '## Example',
58
- '',
59
- '```html',
60
- '<crtr-slot kind="nl-thread" slot-id="conversation">',
61
- ' <script type="application/json">',
62
- ' {',
63
- ' "label": "Your thread with Dana Whitfield",',
64
- ' "platform": "linkedin",',
65
- ' "collapsedByDefault": true,',
66
- ' "participants": [{ "handle": "dana-whitfield", "name": "Dana Whitfield" }],',
67
- ' "messages": [',
68
- ' { "sender": "them", "senderHandle": "dana-whitfield", "timestamp": "2026-03-02T15:04:00Z", "body": "Saw the launch, congrats." },',
69
- ' { "sender": "self", "timestamp": "2026-03-02T16:20:00Z", "body": "Thank you! Want a walkthrough next week?" }',
70
- ' ]',
71
- ' }',
72
- ' </script>',
73
- '</crtr-slot>',
74
- '```',
75
- ].join('\n'),
20
+ doc: '`nl-thread` renders a conversation as bubbles down one column. Display only: it contributes nothing to the response.',
76
21
  };
77
22
  async function withPipedStdin(body, fn) {
78
23
  const original = process.stdin;
@@ -86,12 +31,12 @@ async function withPipedStdin(body, fn) {
86
31
  Object.defineProperty(process, 'stdin', { value: original, configurable: true });
87
32
  }
88
33
  }
89
- function withHtmlSurface(htmlSurface, run) {
34
+ function withPageSurface(pageSurface, run) {
90
35
  const home = mkdtempSync(join(tmpdir(), 'crtr-human-help-'));
91
36
  const originalHome = process.env['HOME'];
92
37
  process.env['HOME'] = home;
93
38
  mkdirSync(join(home, '.crouter'), { recursive: true });
94
- writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({ html_surface: htmlSurface }));
39
+ writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({ page_surface: pageSurface }));
95
40
  try {
96
41
  return run();
97
42
  }
@@ -105,80 +50,92 @@ function withHtmlSurface(htmlSurface, run) {
105
50
  }
106
51
  describe('human page input', () => {
107
52
  test('stdin supplies the authored page document', async () => {
108
- const document = '<!doctype html><html><head><meta name="crtr-page-title" content="Question"></head><body></body></html>';
109
- const result = await withPipedStdin(document, () => parseArgv(humanAsk.help.params ?? [], []));
53
+ const document = 'export default () => <Page title="Question" />;\n';
54
+ const result = await withPipedStdin(document, () => parseArgv(humanSend.help.params ?? [], []));
110
55
  assert.equal(result['document'], document);
111
56
  });
112
57
  test('empty stdin remains empty alongside --page', async () => {
113
- const result = await withPipedStdin('', () => parseArgv(humanAsk.help.params ?? [], ['--page', 'page.html']));
114
- assert.equal(result['page'], 'page.html');
58
+ const result = await withPipedStdin('', () => parseArgv(humanSend.help.params ?? [], ['--page', 'page.tsx']));
59
+ assert.equal(result['page'], 'page.tsx');
115
60
  assert.equal(result['document'], '');
116
61
  });
117
62
  test('a positional document is rejected', async () => {
118
- await assert.rejects(() => parseArgv(humanAsk.help.params ?? [], ['page.html']), (err) => {
63
+ await assert.rejects(() => parseArgv(humanSend.help.params ?? [], ['page.tsx']), (err) => {
119
64
  assert.equal(err.code, 'bad_invocation');
120
65
  assert.match(err.message, /takes no positional arguments/);
121
66
  assert.match(err.details?.next ?? '', /schema/);
122
67
  return true;
123
68
  });
124
69
  });
125
- test('ask and notify help keep page authoring at the human branch', () => {
126
- const expected = (name, summary, output, effects) => `human ${name}: ${summary}.\n\nPage authoring: \`crtr human -h\`.\n\nInput\n stdin optional. Page bytes from stdin. Provide exactly one of stdin or --page PATH.\n --page PAGE path, optional. Write the page to \`$CRTR_CONTEXT_DIR/pages/<name>.html\`; pass that path. Provide exactly one of --page or stdin.\n\nOutput (fields carried in the rendered result)\n${output}\n\nEffects\n${effects}`;
127
- assert.equal(renderLeafArgv(humanAsk.help), expected('ask', "submit a decision ticket to crouter's inbox; returns a job handle immediately", ' job_id string. Node id of this human interaction. Its answer is pushed to your inbox when the human responds.\n dir string. Interaction directory holding page.html, page.json, run.json, and response.json.\n follow_up string. A non-blocking status peek; the answer arrives via your inbox.', ' Creates a kind:"human" node under you and enqueues a page in the human inbox.\n Returns immediately; nothing opens on screen. The human answers from their inbox on their own time.'));
128
- assert.equal(renderLeafArgv(humanNotify.help), expected('notify', 'enqueue a fire-and-forget acknowledgement in the human inbox; creates no job', ' queued boolean. Always true — the notify page is durable inbox state the human acknowledges from the inbox.\n dir string. Interaction directory holding page.html, page.json, and run.json.', ' Enqueues a durable display page in the human inbox. Creates no node; nothing opens on screen.'));
70
+ test('send help keeps page authoring at the human branch and makes reply routing explicit', () => {
71
+ const rendered = renderLeafArgv(humanSend.help);
72
+ assert.match(rendered, /^human send: send a durable TSX or HTML page\./);
73
+ assert.match(rendered, /Page authoring: `crtr human -h`\./);
74
+ assert.doesNotMatch(rendered, /--no-reply|--reply/);
75
+ assert.match(rendered, /page_id/);
76
+ assert.match(rendered, /reply/);
77
+ assert.doesNotMatch(rendered, /human notify|run\.json|job_id/);
129
78
  });
130
79
  });
131
- test('human help pins HTML prompting to the html_surface setting', () => {
132
- const originalAuthoring = 'Write one complete HTML document to `$CRTR_CONTEXT_DIR/pages/<name>.html`. Its `<head>` has required `<meta name="crtr-page-title" content="…">`, optional `<meta name="crtr-page-subtitle" content="…">`, and optional `<meta name="crtr-page-placement" content="inline|panel">` (default `panel`); interactive or multi-step pages need one `<crtr-pages>` wrapper with `<crtr-page>` steps, and each component config is its first `<script type="application/json">` child. Display-only pages are first-class: a page with no questions is a legitimate, encouraged way to show structured UI with `notify --page`.';
133
- const originalRootUseWhen = 'when a person must decide, review a document with you, or receive a live display.';
134
- const htmlAuthoring = 'Use inline HTML pages as the default way to put structured or interactive content in front of the human; the environment renders web content, so a page is not a special occasion. Write one complete HTML document to `$CRTR_CONTEXT_DIR/pages/<name>.html`. Its `<head>` has required `<meta name="crtr-page-title" content="…">`, optional `<meta name="crtr-page-subtitle" content="…">`, and optional `<meta name="crtr-page-placement" content="inline|panel">` (default `panel`); interactive or multi-step pages need one `<crtr-pages>` wrapper with `<crtr-page>` steps, and each component config is its first `<script type="application/json">` child. Display-only pages are first-class: a page with no questions is a legitimate, encouraged way to show structured UI with `notify --page`.';
135
- const htmlRootUseWhen = 'when a person must decide, review a document with you, or receive a live display; use inline HTML pages by default for structured or interactive content because the environment renders web content.';
136
- withHtmlSurface(false, () => {
80
+ test('human help carries the page authoring model and follows the page-surface setting', () => {
81
+ withPageSurface(false, () => {
137
82
  const human = registerHuman();
138
- assert.equal(pageHelpText(false).authoring, originalAuthoring);
139
- assert.equal(human.rootEntry?.useWhen, originalRootUseWhen);
140
- assert.ok(human.help.model?.includes(originalAuthoring));
83
+ const help = pageHelpText(false);
84
+ assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
85
+ assert.ok(human.help.model?.includes(help.authoring));
86
+ assert.ok(human.help.model?.includes('`crtr human components`'));
87
+ assert.doesNotMatch(human.help.model ?? '', /doc check/);
88
+ assert.match(human.help.model ?? '', /\.html/);
141
89
  });
142
- withHtmlSurface(true, () => {
90
+ withPageSurface(true, () => {
143
91
  const human = registerHuman();
144
- assert.equal(pageHelpText(true).authoring, htmlAuthoring);
145
- assert.equal(human.rootEntry?.useWhen, htmlRootUseWhen);
146
- assert.ok(human.help.model?.includes(htmlAuthoring));
92
+ const help = pageHelpText(true);
93
+ assert.notEqual(help.authoring, pageHelpText(false).authoring);
94
+ assert.equal(human.rootEntry?.useWhen, help.rootUseWhen);
95
+ assert.ok(human.help.model?.includes(help.authoring));
147
96
  });
148
97
  });
149
- test('human html catalog and contracts teach product registrations', () => {
98
+ // The gate itself, not its prose: a page-surface host must not carry the two
99
+ // terminal-only surfaces, because a review ticket filed there reaches a person
100
+ // who has no editor to open it in.
101
+ test('page_surface removes the terminal-only human surfaces from the tree', () => {
102
+ const names = () => registerHuman().children.map((child) => child.name);
103
+ withPageSurface(false, () => {
104
+ assert.deepEqual(names(), ['components', 'send', 'review', 'feedback', 'show', 'cancel', 'list', 'resolve']);
105
+ });
106
+ withPageSurface(true, () => {
107
+ const children = names();
108
+ assert.deepEqual(children, ['components', 'send', 'feedback', 'cancel', 'list', 'resolve']);
109
+ const human = registerHuman();
110
+ assert.doesNotMatch(human.help.model ?? '', /`review`|`show`/);
111
+ assert.doesNotMatch(human.rootEntry?.useWhen ?? '', /review a document|live display/);
112
+ });
113
+ });
114
+ test('human components catalog and contracts teach product registrations', () => {
150
115
  const legacyRegistration = { kind: 'product-card' };
151
116
  const catalog = renderComponentCatalog([nlThreadRegistration, legacyRegistration]);
152
- assert.match(catalog, /^<components count="9">\n`<crtr-pages>`/);
153
- assert.match(catalog, /\n`nl-thread` — message conversation with participants and metadata\. Use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide\. Display-only\n/);
154
- assert.match(catalog, /\n`product-card`\nproduct-registered\n<\/components>$/);
155
- assert.doesNotMatch(catalog, /Config JSON|Response rules/);
156
- assert.equal(renderComponentContract('nl-thread', [nlThreadRegistration]), `\`nl-thread\`\n\n${nlThreadRegistration.doc}`);
157
- assert.equal(renderComponentContract('product-card', [legacyRegistration]), "`product-card`\n\nproduct-registered; config is passed through unvalidated (unvalidated: true) — see the product's own docs");
117
+ assert.match(catalog, /^<components count="\d+">\nStructural — /);
118
+ assert.match(catalog, /\n`Page` — /);
119
+ assert.match(catalog, /\n`NlThread` \(kind `nl-thread`\) — message conversation with participants and metadata\. Use when the human needs to read a conversation - an email chain, a LinkedIn DM, an iMessage exchange - in message order, usually above whatever the page asks them to decide\. Display-only\n/);
120
+ assert.match(catalog, /\n`ProductCard` \(kind `product-card`\) — product-registered\n<\/components>$/);
121
+ assert.doesNotMatch(catalog, /Props\n|Response rules/);
122
+ // A product component is addressed by the JSX tag page-catalog derives from its registered
123
+ // kind; the contract heading names the wire kind that tag maps to.
124
+ assert.equal(renderComponentContract('nl-thread', [nlThreadRegistration]), `\`<NlThread>\` — product-registered, wire kind \`nl-thread\`\n\n${nlThreadRegistration.doc}`);
125
+ assert.equal(renderComponentContract('ProductCard', [legacyRegistration]), "`<ProductCard>` — product-registered, wire kind `product-card`\n\nproduct-registered; config is passed through unvalidated (unvalidated: true) — see the product's own docs");
158
126
  });
159
- test('human html show options output is byte-stable', async () => {
160
- const show = humanHtml.children.find((child) => child.name === 'show');
127
+ test('human components show resolves every registry name, families included', async () => {
128
+ const show = humanComponents.children.find((child) => child.name === 'show');
161
129
  assert.ok(show !== undefined && show.kind === 'leaf');
162
- const result = await show.run({ kind: 'options' });
163
- assert.ok(result !== undefined);
164
- assert.equal(show.render?.(result), `\`<crtr-options>\`
165
-
166
- Response-bearing. The response is \`{selectedOptionIds:string[], comments:Comment[], freetext?:string}\`.
167
-
168
- Element contract
169
- - Put the JSON config in the first direct \`<script type="application/json">\` child of the component element. It must contain one JSON object.
170
- - \`id\` must match \`^[A-Za-z0-9_-]{1,64}$\`, be unique across the page when present, and is required only for response-bearing components. Display-only components do not need an id.
171
- - When a page has two or more response-bearing components, every one needs a nonempty \`config.label\`.
172
- - Config objects and every documented nested object reject unknown fields.
173
-
174
- Config JSON
175
- - \`options\` — required nonempty array of \`{id:string (nonempty, unique), label:string (nonempty), description?:string}\`.
176
- - \`mode\` — required: \`single\` or \`multi\`.
177
- - \`label?:string\` — slot label; required by the multi-response page rule.
178
- - \`allowFreetext?:boolean\`, \`freetextLabel?:string\`, \`freetextPlaceholder?:string\` — optional free-text affordance fields.
179
-
180
- Response rules
181
- - \`selectedOptionIds\` contains unique known option ids. A \`single\` response has at most one selected id and requires exactly one selection or nonempty allowed \`freetext\`; a \`multi\` response requires a selection, comment, or nonempty allowed \`freetext\`.
182
- - \`freetext\` is accepted only when \`allowFreetext:true\`.
183
- - Comments are \`{id:string (nonempty), text:string, anchor:Anchor}\`. Every response carries \`comments\` as an array. \`Anchor\` is one of \`{kind:"whole"}\`, \`{kind:"option",optionId:string}\`, \`{kind:"row",rowId:string}\`, \`{kind:"column",columnId:string}\`, \`{kind:"card",cardId:string}\`, or \`{kind:"range",start:nonnegative integer,end:nonnegative integer >= start,quote:string}\`; each component accepts only the anchors named below. Options accept only \`whole\` and \`option\` anchors; \`optionId\` must name a configured option.`);
130
+ const question = await show.run({ component: 'UserQuestion' });
131
+ assert.ok(question !== undefined);
132
+ assert.equal(question['contract'], componentDoc('UserQuestion')?.contract);
133
+ assert.match(String(question['contract']), /wire kind `options`/);
134
+ // A shadcn family member resolves to the family contract that documents it.
135
+ const cardHeader = await show.run({ component: 'CardHeader' });
136
+ assert.equal(cardHeader?.['contract'], componentDoc('Card')?.contract);
137
+ await assert.rejects(() => show.run({ component: 'crtr-options' }), (err) => {
138
+ assert.equal(err.code, 'unknown_component');
139
+ return true;
140
+ });
184
141
  });
@@ -0,0 +1,14 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { renderLeafArgv } from '../../core/help.js';
4
+ import { nodeMessage } from '../node/message.js';
5
+ test('node message send help documents the finalized-node re-task gate', () => {
6
+ const send = nodeMessage.children.find((child) => child.kind === 'leaf' && child.name === 'send');
7
+ if (send === undefined || send.kind !== 'leaf')
8
+ assert.fail('node message send leaf is missing');
9
+ const rendered = renderLeafArgv(send.help);
10
+ assert.match(rendered, /--reopen/);
11
+ assert.match(rendered, /Required to re-task a finalized target/);
12
+ assert.match(rendered, /clears a finalized target's finalization latch before delivery/);
13
+ assert.match(rendered, /Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted/);
14
+ });
@@ -1,6 +1,6 @@
1
1
  // `crtr canvas attention` — aggregate pending human tickets across the canvas.
2
2
  //
3
- // A pending "ticket" is any humanloop interaction awaiting a human: a page or
3
+ // A pending "ticket" is any humanloop interaction awaiting the user: a page or
4
4
  // an anchored review. Each lives in its human bridge node
5
5
  // directory and is attributed through that bridge's parent relation. This
6
6
  // subtree surfaces a direct node projection; cwd is display metadata only.
@@ -93,7 +93,7 @@ const attentionCount = defineLeaf({
93
93
  const attentionList = defineLeaf({
94
94
  name: 'list',
95
95
  description: 'itemised list of cwds with pending human tickets',
96
- whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
96
+ whenToUse: 'finding which agents are blocked waiting on the user — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
97
97
  help: {
98
98
  name: 'canvas attention list',
99
99
  summary: 'list nodes that asked pending human tickets',
@@ -213,11 +213,11 @@ const attentionMap = defineLeaf({
213
213
  export const attentionBranch = defineBranch({
214
214
  name: 'attention',
215
215
  description: 'count/list pending human tickets across the graph',
216
- whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
216
+ whenToUse: 'checking whether any agent on the canvas is blocked waiting on the user, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
217
217
  help: {
218
218
  name: 'canvas attention',
219
219
  summary: 'aggregate pending human tickets across the canvas',
220
- model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting a human: an ask, a review, or a notification.',
220
+ model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised asking-node entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting the user: an ask, a review, or a notification.',
221
221
  },
222
222
  children: [attentionCount, attentionList, attentionMap],
223
223
  });
@@ -15,7 +15,7 @@ export const canvasSnapshotLeaf = defineLeaf({
15
15
  { name: 'subscriptions', type: 'object', required: true, constraint: 'Authoritative subscribes_to adjacency map: each node id maps to its publisher/child node ids in edge order.' },
16
16
  ],
17
17
  outputKind: 'object',
18
- effects: ['Read-only: queries canvas.db, focus rows, telemetry files, and human ask counts.'],
18
+ effects: ['Read-only: queries canvas.db, focus rows, telemetry files, and pending human-reply counts.'],
19
19
  // Machine-readable dump for the browser canvas, not for an agent/human to
20
20
  // glance at — collapse it in the attach viewer by default.
21
21
  preview: { suppressOutput: true },
@@ -43,7 +43,7 @@ export function registerCanvas() {
43
43
  help: {
44
44
  name: 'canvas',
45
45
  summary: 'observe and supervise the whole agent graph',
46
- model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine write verb (`push`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive --all` mass-reconnects every disconnected node (bringing back ONE named node is `node lifecycle revive <id>` instead); `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `prune` bounds growth by deleting terminal nodes past a TTL. `config` registers/lists remote canvas targets (preview endpoint + relay token); `use` switches remote-capable read commands such as `node inspect` onto a configured target or back to local.',
46
+ model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine write verb (`push`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending replies from the user across the graph; `revive --all` mass-reconnects every disconnected node (bringing back ONE named node is `node lifecycle revive <id>` instead); `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `prune` bounds growth by deleting terminal nodes past a TTL. `config` registers/lists remote canvas targets (preview endpoint + relay token); `use` switches remote-capable read commands such as `node inspect` onto a configured target or back to local.',
47
47
  },
48
48
  children: [dashboardLeaf, canvasSnapshotLeaf, canvasRosterLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, chordLeaf, issueBranch, canvasPruneLeaf, canvasRebuildIndexLeaf, canvasConfigBranch, canvasUseLeaf],
49
49
  });