@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
@@ -1,15 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
3
2
  import { ApiError } from '../../api/index.js';
4
3
  import { defineLeaf } from '../../core/command.js';
5
4
  import { ticketDir } from '../../core/human/root.js';
6
5
  import { scanInbox } from '../../core/human/scan.js';
7
- import { pageManifestPath, isResolved, reviewPath } from '../../core/human/convention.js';
6
+ import { pageManifestPath, isResolved, readTicketReplyRoute, reviewPath } from '../../core/human/convention.js';
8
7
  import { InputError } from '../../core/io.js';
9
8
  import { paginate } from '../../core/pagination.js';
10
9
  import { cliClient, rethrowAsCliError } from '../api-client.js';
11
10
  import { filterVisibleReviewTickets } from '../../clients/inbox/review-visibility.js';
12
- import { readJsonOrNull } from '../../core/fs-utils.js';
13
11
  function toRef(n) {
14
12
  return { node_id: n.node_id, name: n.name, cwd: n.cwd, parent: n.parent, status: n.status };
15
13
  }
@@ -20,17 +18,6 @@ function toRef(n) {
20
18
  async function fetchNodeRefs() {
21
19
  return (await cliClient().listNodes()).map(toRef);
22
20
  }
23
- /** Look up one node by id, mapping the API's 404 to `null`. */
24
- async function getNodeOrNull(id) {
25
- try {
26
- return await cliClient().getNode(id);
27
- }
28
- catch (err) {
29
- if (err instanceof ApiError && err.status === 404)
30
- return null;
31
- rethrowAsCliError(err);
32
- }
33
- }
34
21
  function resolveConversation(asking, nodes) {
35
22
  if (asking === undefined)
36
23
  return undefined;
@@ -59,43 +46,41 @@ function askingNodeFor(jobId, byId) {
59
46
  return byId.get(bridge.parent) ?? bridge;
60
47
  return bridge;
61
48
  }
62
- /** `notify` enqueues a durable page with no bridge node at all — its run.json
63
- * is the manifest-based truth that distinguishes that intentional, workerless
64
- * ticket from an `ask`/`review` ticket whose bridge node was reaped. */
65
- function isWorkerlessTicket(dir) {
66
- return readJsonOrNull(join(dir, 'run.json'))?.mode === 'notify';
49
+ /** A missing reply route is the complete representation of a standalone page;
50
+ * a route whose bridge disappeared is an orphaned reply-bearing ticket. */
51
+ function replyBridgeId(dir) {
52
+ return readTicketReplyRoute(dir)?.bridge_node_id ?? null;
67
53
  }
68
54
  function summarizeTicket(summary, nodes, byId) {
69
- const jobId = basename(summary.dir);
70
- const workerless = isWorkerlessTicket(summary.dir);
71
- // A ticket that expects a bridge node but has none is a stale ticket whose
72
- // node was reaped, intentionally absent from the canvas view. A workerless
73
- // notify ticket never had one, so it renders with its node-derived fields
74
- // omitted rather than being dropped as an orphan.
75
- if (!workerless && !byId.has(jobId))
55
+ const bridgeNodeId = replyBridgeId(summary.dir);
56
+ // A reply-bearing ticket whose bridge was reaped is stale and absent from the
57
+ // canvas view. A standalone page has no bridge by construction.
58
+ if (bridgeNodeId !== null && !byId.has(bridgeNodeId))
76
59
  return null;
77
- const asking = workerless ? undefined : askingNodeFor(jobId, byId);
60
+ const asking = bridgeNodeId === null ? undefined : askingNodeFor(bridgeNodeId, byId);
78
61
  const conversation = resolveConversation(asking, nodes);
79
62
  const sourceLabel = readableSourceLabel(summary.source);
80
63
  return {
81
- id: summary.id,
82
- ...(workerless ? {} : { job_id: jobId }),
64
+ ticket_id: summary.id,
65
+ ...(bridgeNodeId === null ? {} : { bridge_node_id: bridgeNodeId }),
83
66
  dir: summary.dir,
84
67
  title: summary.title,
85
68
  subtitle: summary.subtitle,
86
69
  kind: summary.kind,
87
- blocked_since: summary.blockedSince,
70
+ ...(summary.kind === 'page' ? { emitted_at: summary.emittedAt } : { blocked_since: summary.blockedSince }),
88
71
  ...(summary.claim !== undefined ? { claim_owner: summary.claim.owner } : {}),
89
72
  ...(sourceLabel !== undefined ? { source_label: sourceLabel } : {}),
90
73
  ...(asking !== undefined ? { asking_node_id: asking.node_id, asking_node_name: asking.name, cwd: asking.cwd } : {}),
91
74
  ...(conversation !== undefined ? { conversation_id: conversation.node_id, conversation_title: conversation.name } : {}),
92
75
  };
93
76
  }
94
- function interactionDirForJob(jobId, byId) {
95
- const node = byId.get(jobId);
96
- if (node !== undefined)
97
- return { dir: ticketDir(jobId), node };
98
- throw new InputError({ error: 'not_found', message: `no interaction node: ${jobId}`, field: 'job_id', next: 'Pass a job_id from `crtr human list`.' });
77
+ function interactionDirForTicket(ticketId, byId) {
78
+ const dir = ticketDir(ticketId);
79
+ if (!existsSync(pageManifestPath(dir)) && !existsSync(reviewPath(dir))) {
80
+ throw new InputError({ error: 'not_found', message: `no interaction ticket: ${ticketId}`, field: 'ticket_id', next: 'Pass a ticket_id from `crtr human list`.' });
81
+ }
82
+ const bridgeNodeId = replyBridgeId(dir);
83
+ return { dir, bridge: bridgeNodeId === null ? null : byId.get(bridgeNodeId) ?? null };
99
84
  }
100
85
  function isTerminalBridge(status) {
101
86
  return status === 'done' || status === 'dead' || status === 'canceled';
@@ -109,7 +94,7 @@ function isAlreadyResolvedConflict(error) {
109
94
  export const humanList = defineLeaf({
110
95
  name: 'list',
111
96
  description: 'enumerate pending interactions',
112
- whenToUse: 'discovering what is blocked on a human',
97
+ whenToUse: 'discovering what is blocked on the user',
113
98
  help: {
114
99
  name: 'human list',
115
100
  summary: 'paginated list of pending interactions, oldest first',
@@ -118,7 +103,7 @@ export const humanList = defineLeaf({
118
103
  { kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: "Opaque token from a previous response's next_cursor. Omit on first call." },
119
104
  ],
120
105
  output: [
121
- { name: 'items', type: 'object[]', required: true, constraint: 'Each: {id, dir, title, subtitle, kind, blocked_since}; subtitle is the required authored one-sentence summary. Enriched when derivable with claim_owner, source_label, asking_node_id/name, conversation_id/title. Oldest first.' },
106
+ { name: 'items', type: 'object[]', required: true, constraint: 'Each: {ticket_id, dir, title, subtitle, kind, emitted_at? | blocked_since?}; pages use emitted_at, reviews use blocked_since. Enriched when derivable with bridge_node_id, claim_owner, source_label, asking_node_id/name, conversation_id/title. Oldest first.' },
122
107
  { name: 'next_cursor', type: 'string | null', required: true, constraint: 'Pass on the next call to continue. null means no more items.' },
123
108
  { name: 'total', type: 'integer | null', required: true, constraint: 'Total pending interactions.' },
124
109
  ],
@@ -139,14 +124,14 @@ export const humanList = defineLeaf({
139
124
  return item === null ? [] : [item];
140
125
  })
141
126
  .sort((a, b) => {
142
- const ka = `${a.blocked_since}|${a.id}`;
143
- const kb = `${b.blocked_since}|${b.id}`;
127
+ const ka = `${a.emitted_at ?? a.blocked_since}|${a.ticket_id}`;
128
+ const kb = `${b.emitted_at ?? b.blocked_since}|${b.ticket_id}`;
144
129
  return ka < kb ? -1 : ka > kb ? 1 : 0;
145
130
  });
146
131
  const page = paginate(items, { limit, cursor }, {
147
132
  defaultLimit: 20,
148
133
  maxLimit: 100,
149
- keyOf: (i) => `${i.blocked_since}|${i.id}`,
134
+ keyOf: (i) => `${i.emitted_at ?? i.blocked_since}|${i.ticket_id}`,
150
135
  total: 'count',
151
136
  });
152
137
  return { items: page.items, next_cursor: page.next_cursor, total: page.total };
@@ -162,11 +147,11 @@ function parseResolveBody(raw) {
162
147
  parsed = JSON.parse(raw);
163
148
  }
164
149
  catch (e) {
165
- throw new InputError({ error: 'invalid_json', message: `stdin is not valid JSON: ${String(e)}`, field: 'stdin', next: 'Pass {"responses":{"<slot_id>":{...}}} on stdin.' });
150
+ throw new InputError({ error: 'invalid_json', message: `stdin is not valid JSON: ${String(e)}`, field: 'stdin', next: 'Pass {"responses":{"<component_id>":{...}}} on stdin.' });
166
151
  }
167
152
  const responses = parsed.responses;
168
153
  if (typeof responses !== 'object' || responses === null || Array.isArray(responses)) {
169
- throw new InputError({ error: 'invalid_field', message: 'responses must be an object keyed by slot id', field: 'responses', next: 'Pass {"responses":{"<slot_id>":{...}}}, or {"responses":{}} to acknowledge a page with no response slots.' });
154
+ throw new InputError({ error: 'invalid_field', message: 'responses must be an object keyed by the id of a response-bearing component', field: 'responses', next: 'Pass {"responses":{"<component_id>":{...}}}, or {"responses":{}} to acknowledge a page with no response-bearing components.' });
170
155
  }
171
156
  return responses;
172
157
  }
@@ -177,101 +162,85 @@ export const humanResolve = defineLeaf({
177
162
  tier: 'hidden',
178
163
  help: {
179
164
  name: 'human resolve',
180
- summary: 'resolve a pending page with the given answers and deliver them to the asking node',
165
+ summary: 'resolve a pending page with the given answers',
181
166
  params: [
182
- { kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'job_id from human ask/review/list.' },
183
- { kind: 'stdin', name: 'body', required: true, constraint: 'JSON {responses:{<slot_id>:{...}}} — one typed response object per response-bearing slot of the page.' },
167
+ { kind: 'positional', name: 'ticket_id', type: 'string', required: true, constraint: 'page_id from human send, or a page ticket_id from human list.' },
168
+ { kind: 'stdin', name: 'body', required: true, constraint: 'JSON {responses:{<component_id>:{...}}} — one typed response object per response-bearing component of the page.' },
184
169
  ],
185
170
  output: [
186
- { name: 'resolved', type: 'boolean', required: true, constraint: 'True when this call answered and delivered the page to the asking node.' },
187
- { name: 'job_id', type: 'string', required: true, constraint: 'Interaction job id.' },
171
+ { name: 'resolved', type: 'boolean', required: true, constraint: 'True when this call completed the page.' },
172
+ { name: 'ticket_id', type: 'string', required: true, constraint: 'Interaction ticket id.' },
188
173
  { name: 'reason', type: 'string', required: false, constraint: 'already_resolved or claimed when resolved is false.' },
189
174
  ],
190
175
  outputKind: 'object',
191
- effects: ['The daemon publishes the answer, delivers it to the asking node, and retires the bridge before this call returns.'],
176
+ effects: ['The daemon publishes the answer and, when the ticket has a reply route, retires its bridge and returns the completion to subscribers.'],
192
177
  },
193
178
  run: async (input) => {
194
- const jobId = input['job_id'];
179
+ const ticketId = input['ticket_id'];
195
180
  const nodes = await fetchNodeRefs();
196
181
  const byId = new Map(nodes.map((n) => [n.node_id, n]));
197
- const { dir, node } = interactionDirForJob(jobId, byId);
182
+ const { dir, bridge } = interactionDirForTicket(ticketId, byId);
198
183
  if (!existsSync(pageManifestPath(dir))) {
199
- throw new InputError({ error: 'not_found', message: `no page for interaction: ${jobId}`, field: 'job_id', next: 'Pass a pending job_id from `crtr human list`.' });
184
+ throw new InputError({ error: 'not_found', message: `no page for interaction: ${ticketId}`, field: 'ticket_id', next: 'Pass a pending ticket_id from `crtr human list`.' });
200
185
  }
201
186
  const responses = parseResolveBody(input['body']);
202
187
  // A published result on a live bridge may be an interrupted delivery. The
203
- // daemon re-drives that delivery before it returns its 409, so only bypass
204
- // it when the bridge is already terminal.
205
- if (isResolved(dir) && isTerminalBridge(node.status)) {
206
- return { resolved: false, job_id: jobId, reason: 'already_resolved' };
188
+ // daemon re-drives that delivery before it returns its 409. A standalone
189
+ // ticket or terminal bridge has nothing left to deliver.
190
+ if (isResolved(dir) && (bridge === null || isTerminalBridge(bridge.status))) {
191
+ return { resolved: false, ticket_id: ticketId, reason: 'already_resolved' };
207
192
  }
208
193
  try {
209
- await cliClient().resolveHumanTicket(jobId, { responses });
194
+ await cliClient().resolveHumanTicket(ticketId, { responses });
210
195
  }
211
196
  catch (e) {
212
197
  if (isAlreadyResolvedConflict(e))
213
- return { resolved: false, job_id: jobId, reason: 'already_resolved' };
198
+ return { resolved: false, ticket_id: ticketId, reason: 'already_resolved' };
214
199
  rethrowAsCliError(e);
215
200
  }
216
- return { resolved: true, job_id: jobId };
201
+ return { resolved: true, ticket_id: ticketId };
217
202
  },
218
203
  });
219
204
  // ---------------------------------------------------------------------------
220
- // cancel — retract a pending ask/review
205
+ // cancel — retract a pending page/review
221
206
  // ---------------------------------------------------------------------------
222
207
  export const humanCancel = defineLeaf({
223
208
  name: 'cancel',
224
- description: 'retract a pending ask/review',
225
- whenToUse: 'a question went stale before the human answered',
209
+ description: 'retract a pending page/review',
210
+ whenToUse: 'a page or review went stale before the user completed it',
226
211
  help: {
227
212
  name: 'human cancel',
228
- summary: 'retract a pending ask/review you posed — drops it from the human inbox and retires the bridge node. Reach for this the moment a question goes stale (you answered it yourself, the situation changed) so a human is not left resolving a prompt whose answer no longer matters',
229
- guide: 'Pass the job_id returned by `human ask`/`review`. Best-effort and idempotent: if the human already answered, or it was already canceled, it reports canceled:false with reason "already_resolved" and changes nothing. Cancellation races the human\'s submit; whichever wins is authoritative. The agent that posed the question is almost always the one canceling it, so the caller is never messaged — only OTHER subscribers get a quiet deferred note that no answer is coming.',
213
+ summary: 'retract a pending page/review — drops it from the human inbox and retires its reply bridge when one exists. Reach for this the moment an interaction goes stale so the user is not left completing something that no longer matters',
214
+ guide: 'Pass the page_id returned by `human send`, or a ticket_id from `human list`. Best-effort and idempotent: if the user already completed or canceled it, canceled:false with reason "already_resolved" changes nothing. Cancellation races the user\'s submit; whichever wins is authoritative. For a reply-bearing ticket, only subscribers other than the caller receive the cancellation notice.',
230
215
  params: [
231
- { kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Node id of the interaction to cancel — the job_id returned by ask/review.' },
216
+ { kind: 'positional', name: 'ticket_id', type: 'string', required: true, constraint: 'page_id returned by human send, or ticket_id returned by human list.' },
232
217
  { kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Optional short note delivered to subscribers explaining why it was retracted.' },
233
218
  ],
234
219
  output: [
235
220
  { name: 'canceled', type: 'boolean', required: true, constraint: 'True when the interaction was retracted; false when there was nothing live to cancel (already answered/canceled).' },
236
- { name: 'job_id', type: 'string', required: true, constraint: 'The interaction node id.' },
221
+ { name: 'ticket_id', type: 'string', required: true, constraint: 'The interaction ticket id.' },
237
222
  { name: 'reason', type: 'string', required: false, constraint: 'Why nothing was canceled (e.g. "already_resolved"), present when canceled is false.' },
238
223
  ],
239
224
  outputKind: 'object',
240
225
  effects: [
241
- 'The daemon retires the bridge and notifies subscribers other than the caller that no answer is coming.',
226
+ 'The daemon removes the ticket from the inbox; when it has a reply bridge, the bridge retires and subscribers other than the caller are notified that no answer is coming.',
242
227
  ],
243
228
  },
244
229
  run: async (input) => {
245
- const jobId = input['job_id'];
230
+ const ticketId = input['ticket_id'];
246
231
  const reason = input['reason'];
247
- const node = await getNodeOrNull(jobId);
248
- if (node === null) {
249
- throw new InputError({
250
- error: 'not_found',
251
- message: `no interaction node: ${jobId}`,
252
- field: 'job_id',
253
- next: 'Pass the job_id from human ask/review, or list pending with `crtr human list`.',
254
- });
255
- }
256
- const idir = ticketDir(jobId);
257
- // Nothing live to cancel: the bridge is already terminal, or the ticket
258
- // never carried a request descriptor.
259
- const hasDescriptor = existsSync(pageManifestPath(idir)) || existsSync(reviewPath(idir));
260
- if (isTerminalBridge(node.status) || !hasDescriptor) {
261
- return { canceled: false, job_id: jobId, reason: 'already_resolved' };
262
- }
263
232
  const actor = process.env['CRTR_NODE_ID'] ?? 'human';
264
233
  try {
265
- await cliClient().cancelHumanTicket(jobId, { ...(reason !== undefined && reason !== '' ? { reason } : {}), actor });
234
+ await cliClient().cancelHumanTicket(ticketId, { ...(reason !== undefined && reason !== '' ? { reason } : {}), actor });
266
235
  }
267
236
  catch (e) {
268
237
  if (isAlreadyResolvedConflict(e))
269
- return { canceled: false, job_id: jobId, reason: 'already_resolved' };
238
+ return { canceled: false, ticket_id: ticketId, reason: 'already_resolved' };
270
239
  rethrowAsCliError(e);
271
240
  }
272
- return { canceled: true, job_id: jobId };
241
+ return { canceled: true, ticket_id: ticketId };
273
242
  },
274
243
  render: (r) => r['canceled'] === true
275
- ? `Canceled human interaction ${r['job_id']} — it left the human inbox and subscribers were notified no answer is coming.`
276
- : `Nothing to cancel for ${r['job_id']} — ${r['reason'] ?? 'nothing to cancel'}.`,
244
+ ? `Canceled human interaction ${r['ticket_id']} — it left the human inbox.`
245
+ : `Nothing to cancel for ${r['ticket_id']} — ${r['reason'] ?? 'nothing to cancel'}.`,
277
246
  });
@@ -4,16 +4,6 @@ export interface PageHelpText {
4
4
  pageHint: string;
5
5
  rootUseWhen: string;
6
6
  }
7
- /** The one HTML authoring model for `human -h`. */
8
- export declare function pageHelpText(htmlSurface?: boolean): PageHelpText;
9
- /**
10
- * Crouter's private per-interaction record. Reduced to the delivery essentials:
11
- * the mode that identifies the ticket and the bridge node id it delivers back to.
12
- * The ticket store owns every public request/result/output path (page.html,
13
- * page.json, response.json, review.json); crouter never records pane ids or output paths.
14
- */
15
- export interface RunRecord {
16
- mode: 'ask' | 'notify' | 'review';
17
- job_id?: string;
18
- }
7
+ /** The one page authoring model for `human -h`. */
8
+ export declare function pageHelpText(pageSurface?: boolean): PageHelpText;
19
9
  export declare function resolveMaxPanes(): number;
@@ -1,16 +1,22 @@
1
1
  import { readConfig } from '../../core/config.js';
2
- import { BUILTIN_COMPONENT_SELECTION_LINES } from '../../core/human/component-docs.js';
3
- /** The one HTML authoring model for `human -h`. */
4
- export function pageHelpText(htmlSurface = false) {
2
+ import { CORE_COMPONENT_SELECTION_LINES } from '../../core/human/component-docs.js';
3
+ const PAGE_AUTHORING = 'Write one `.tsx` module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx` that default-exports a component whose root element is `<Page title="…" subtitle="…">`; a multi-step page puts its content in `<Step>` children. Or send a complete `.html` file with a nonempty `<title>`; HTML is delivered verbatim and collects no response. Delivery is chosen by `human send` flags, never a Page prop. Registry components and React are already in scope, so the module contains no `import` and no `require`. Full JS is yours: state, expressions, `.map` over your data, event handlers, plain elements with Tailwind classes. `usePageHost` is a reserved hook whose product-supplied members are opaque here. Never render submit or dismiss chrome — the host owns it. Display-only pages are first-class: a page with no questions may be sent without inbox or reply delivery. The module is compiled when you submit it and a compile error rejects the submit; there is no pre-check.';
4
+ const DISPLAY_SET_LINE = 'The shadcn display set — `Card`, `Badge`, `Button`, `Table`, `Tabs`, `Separator`, `Alert`, `Progress`, `Accordion` and their parts — is in scope too, for presentation only.';
5
+ /** The one page authoring model for `human -h`. */
6
+ export function pageHelpText(pageSurface = false) {
5
7
  return {
6
- authoring: htmlSurface
7
- ? '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`.'
8
- : '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`.',
9
- componentSelection: BUILTIN_COMPONENT_SELECTION_LINES,
10
- pageHint: 'Write the page to `$CRTR_CONTEXT_DIR/pages/<name>.html`; pass that path.',
11
- rootUseWhen: htmlSurface
12
- ? '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.'
13
- : 'when a person must decide, review a document with you, or receive a live display.',
8
+ authoring: pageSurface
9
+ ? `Use inline pages as the default way to put structured or interactive content in front of the user; the environment renders rich components, so a page is not a special occasion. ${PAGE_AUTHORING}`
10
+ : PAGE_AUTHORING,
11
+ componentSelection: [...CORE_COMPONENT_SELECTION_LINES, DISPLAY_SET_LINE],
12
+ pageHint: 'Write the page module to `$CRTR_CONTEXT_DIR/pages/<name>.tsx`; pass that path.',
13
+ // The page-surface wording names no document review and no live display:
14
+ // `human review` and `human show` are terminal-only and leave the tree on
15
+ // such a host (see `registerHuman`).
16
+ // No leading "when": the root renderer already emits `use when ${useWhen}`.
17
+ rootUseWhen: pageSurface
18
+ ? 'a person must decide something or receive structured content from you; use inline pages by default because the environment renders rich components.'
19
+ : 'a person must decide, review a document with you, or receive a live display.',
14
20
  };
15
21
  }
16
22
  export function resolveMaxPanes() {
@@ -1,47 +1,55 @@
1
- // `crtr human` creates bridge nodes and durable tickets for human decisions.
1
+ // `crtr human` creates durable pages and review tickets for human decisions.
2
2
  //
3
- // Ask creates a bridge node and submits a ticket to the store. Review creation
4
- // asks crtrd to create a ticket, daemon-owned review, and companion. notify
5
- // enqueues a durable page with no node; show is passive display.
3
+ // Send optionally creates a reply bridge; review creation asks crtrd to create
4
+ // a ticket, daemon-owned review, and companion. Show is passive display.
6
5
  import { defineBranch } from '../core/command.js';
7
6
  import { readConfig } from '../core/config.js';
8
- import { humanAsk, humanNotify, humanShow } from './human/prompts.js';
7
+ import { humanSend, humanShow } from './human/prompts.js';
9
8
  import { humanList, humanResolve, humanCancel } from './human/queue.js';
10
9
  import { humanReviewBranch } from './human/review.js';
11
- import { humanInbox } from './human/inbox.js';
12
- import { humanDoc } from './human/doc.js';
13
- import { humanHtml } from './human/html.js';
10
+ import { humanFeedbackBranch } from './human/feedback.js';
11
+ import { humanComponents } from './human/components.js';
14
12
  import { pageHelpText } from './human/shared.js';
15
13
  export function registerHuman() {
16
- const pageHelp = pageHelpText(readConfig('user').html_surface);
14
+ // `page_surface` marks a host whose human surface is a rendered page product
15
+ // rather than a terminal. Review and show are terminal-only: a review is a
16
+ // live editor session with anchored comments driven by a companion node, and
17
+ // show is a passive tmux live-watch. Such a host can render neither, so both
18
+ // leave the surface entirely instead of filing tickets nobody can open.
19
+ const pageSurface = readConfig('user').page_surface;
20
+ const pageHelp = pageHelpText(pageSurface);
21
+ const concept = pageSurface
22
+ ? 'human-in-the-loop decisions and structured pages'
23
+ : 'human-in-the-loop decisions, document review, and live display';
24
+ const ticketModel = pageSurface
25
+ ? 'Tickets queue in the human inbox; nothing opens on screen. `send` routes completion back when the page carries response-bearing components, and publishes a standalone announcement when it does not.'
26
+ : 'Tickets queue in the human inbox; nothing opens on screen. `send` routes completion back when the page carries response-bearing components, and publishes a standalone announcement when it does not. `review` is for live document review with anchored comments; `show` is a passive tmux live-watch rather than a ticket.';
17
27
  return defineBranch({
18
28
  name: 'human',
19
29
  rootEntry: {
20
- concept: 'human-in-the-loop decisions, document review, and live display',
21
- desc: 'ask, review new/comment, notify, show, cancel, list',
30
+ concept,
31
+ desc: pageSurface ? 'send, components, feedback, cancel, list' : 'send, review new/comment, show, cancel, list',
22
32
  useWhen: pageHelp.rootUseWhen,
23
33
  },
24
34
  help: {
25
35
  name: 'human',
26
- summary: 'human-in-the-loop decisions, document review, and live display',
36
+ summary: concept,
27
37
  model: [
28
- "Tickets queue in the human inbox; nothing opens on screen. `ask` returns immediately and its answer is pushed to the asking node, while `notify` expects no reply. `review` is for live document review with anchored comments; `show` is a passive tmux live-watch rather than a ticket. Validate authored pages with `crtr human doc check`.",
38
+ ticketModel,
29
39
  pageHelp.authoring,
30
40
  pageHelp.componentSelection.join('\n'),
31
- 'Component contracts: `crtr human html`.',
41
+ 'Props, response contracts, and the full component catalog: `crtr human components`.',
32
42
  ].join('\n\n'),
33
43
  },
34
44
  children: [
35
- humanHtml,
36
- humanAsk,
37
- humanReviewBranch,
38
- humanNotify,
39
- humanShow,
45
+ humanComponents,
46
+ humanSend,
47
+ ...(pageSurface ? [] : [humanReviewBranch]),
48
+ humanFeedbackBranch,
49
+ ...(pageSurface ? [] : [humanShow]),
40
50
  humanCancel,
41
51
  humanList,
42
52
  humanResolve,
43
- humanInbox,
44
- humanDoc,
45
53
  ],
46
54
  });
47
55
  }
@@ -20,7 +20,7 @@ export const originLeaf = defineLeaf({
20
20
  { name: 'origin_recorded', type: 'boolean', required: true, constraint: 'true when the doc carries an `origin` provenance block; false for hand-authored or pre-provenance docs.' },
21
21
  { name: 'created', type: 'string', required: false, constraint: 'ISO timestamp the doc was created. Absent when origin_recorded is false.' },
22
22
  { name: 'cwd', type: 'string', required: false, constraint: 'Working directory the authoring conversation ran in.' },
23
- { name: 'node', type: 'string', required: false, constraint: 'Node id that authored the doc, or null when written by a human at the CLI (no node).' },
23
+ { name: 'node', type: 'string', required: false, constraint: 'Node id that authored the doc, or null when written by the user at the CLI (no node).' },
24
24
  { name: 'node_present', type: 'boolean', required: false, constraint: 'true when that node still exists on the canvas (deref below is live); false when it has been pruned.' },
25
25
  { name: 'node_kind', type: 'string', required: false, constraint: 'Kind of the authoring node (only when node_present).' },
26
26
  { name: 'node_mode', type: 'string', required: false, constraint: 'Mode of the authoring node (only when node_present).' },
@@ -79,7 +79,7 @@ export const originLeaf = defineLeaf({
79
79
  ? `Read the originating conversation at \`${sessionFile ?? '(no session captured)'}\`, browse its shared docs at \`${contextDir(nodeId)}\`, or attach live with \`crtr surface attach ${nodeId}\`.`
80
80
  : nodeId !== null
81
81
  ? `The authoring node \`${nodeId}\` has been pruned from the canvas; its context dir may persist at \`${contextDir(nodeId)}\`.`
82
- : 'Authored by a human at the CLI \u2014 no node conversation to deref.';
82
+ : 'Authored by the user at the CLI \u2014 no node conversation to deref.';
83
83
  return {
84
84
  name: doc.name,
85
85
  path: doc.path,
@@ -3,7 +3,7 @@ import { defineBranch, defineLeaf } from '../../core/command.js';
3
3
  import { InputError } from '../../core/io.js';
4
4
  import { displayMessage } from '../../core/runtime/tmux-chrome.js';
5
5
  import { contextDir } from '../../core/canvas/paths.js';
6
- import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobPaths, formatBashElapsed, isSafeBashJobId, jobStartedAtMs, readJobPgid, stopBackgroundBashJob, tailJobLog } from '../../core/bash-jobs.js';
6
+ import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobCommandLine, bashJobPaths, formatBashElapsed, isSafeBashJobId, jobStartedAtMs, readJobPgid, stopBackgroundBashJob, tailJobLog } from '../../core/bash-jobs.js';
7
7
  import { cliClient, getNodeOrNull } from '../api-client.js';
8
8
  import { nodeInPane } from '../surface/node/placement.js';
9
9
  function resolveBashTarget(input, missingMessage) {
@@ -108,13 +108,13 @@ const nodeBashList = defineLeaf({
108
108
  const jobs = result['jobs'];
109
109
  if (jobs.length === 0)
110
110
  return `No background bash jobs for ${result['node_id']}.`;
111
- return jobs.map((job) => `${job.job_id} · ${formatBashElapsed(job.elapsed_ms)} · ${job.command.split('\n')[0]}${job.purpose === null ? '' : `\n purpose: ${job.purpose}`}\n log: ${job.log_path}`).join('\n');
111
+ return jobs.map((job) => `${job.job_id} · ${formatBashElapsed(job.elapsed_ms)} · ${bashJobCommandLine(job.command)}${job.purpose === null ? '' : `\n purpose: ${job.purpose}`}\n log: ${job.log_path}`).join('\n');
112
112
  },
113
113
  });
114
114
  const nodeBashKill = defineLeaf({
115
115
  name: 'kill',
116
116
  description: 'stop one of a node’s background bash jobs',
117
- whenToUse: 'a human watching the Inspector or the attach cockpit’s bash row wants a long-running background job to stop now, or an agent is abandoning work it backgrounded',
117
+ whenToUse: 'the user watching the Inspector or the attach cockpit’s bash row wants a long-running background job to stop now, or an agent is abandoning work it backgrounded',
118
118
  tier: 'hidden',
119
119
  help: {
120
120
  name: 'node bash kill',
@@ -166,7 +166,7 @@ const nodeBashKill = defineLeaf({
166
166
  });
167
167
  }
168
168
  await cliClient().sendMessage(node.node_id, {
169
- body: `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
169
+ body: `Background bash job ${jobId} was stopped by the user before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
170
170
  tier: 'urgent',
171
171
  });
172
172
  return { node_id: node.node_id, job_id: jobId, signaled: stopped.signaled };
@@ -17,7 +17,7 @@ import { focusViewer, nodeInPane } from '../surface/node/placement.js';
17
17
  const nodeList = defineLeaf({
18
18
  name: 'list',
19
19
  description: 'list nodes on the canvas',
20
- whenToUse: 'you want a flat roster of the nodes on the canvas, sliced by displayed name, status, kind, mode, lifecycle, profile, cwd, and graph scope (--under a node, for that node plus its subscription descendants). The filters AND together. Use `node inspect show` instead to drill into one node and its spine neighbors, `canvas dashboard` for the tree shape, and `canvas attention` to find nodes blocked on a human',
20
+ whenToUse: 'you want a flat roster of the nodes on the canvas, sliced by displayed name, status, kind, mode, lifecycle, profile, cwd, and graph scope (--under a node, for that node plus its subscription descendants). The filters AND together. Use `node inspect show` instead to drill into one node and its spine neighbors, `canvas dashboard` for the tree shape, and `canvas attention` to find nodes blocked on the user',
21
21
  help: {
22
22
  name: 'node inspect list',
23
23
  summary: 'list nodes on the canvas, filtered by any combination of attribute slices and a graph-scope (--under) that AND together',
@@ -239,7 +239,7 @@ const nodeShow = defineLeaf({
239
239
  export const nodeFork = defineLeaf({
240
240
  name: 'fork',
241
241
  description: 'fork a node into an independent idle root with the same conversation and durable context',
242
- whenToUse: 'a conversation has already accumulated the context you want, and you want a new human-driven branch beside it without sending a prompt or inheriting any graph-management responsibility. This copies the conversation and durable context, then waits for the fork’s first real user message to supply its mandate. Use `node new --fork-from` instead when spawning a managed worker with an immediate task',
242
+ whenToUse: 'a conversation has already accumulated the context you want, and you want a new user-driven branch beside it without sending a prompt or inheriting any graph-management responsibility. This copies the conversation and durable context, then waits for the fork’s first real user message to supply its mandate. Use `node new --fork-from` instead when spawning a managed worker with an immediate task',
243
243
  help: {
244
244
  name: 'node fork',
245
245
  summary: 'create an independent resident root from a node’s conversation and durable context, with no kickoff prompt or model turn',
@@ -293,7 +293,7 @@ export const nodeFork = defineLeaf({
293
293
  export const nodeInspect = defineBranch({
294
294
  name: 'inspect',
295
295
  description: 'read one node — its graph position, context, and artifacts',
296
- whenToUse: 'reading everything about ONE node you can name: enumerate the roster (`list`), inspect a node and its spine neighbors (`show`), browse its on-disk context dir (`context`), or list its reports/docs as refs (`artifacts`). Use `canvas dashboard` instead to render the whole tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
296
+ whenToUse: 'reading everything about ONE node you can name: enumerate the roster (`list`), inspect a node and its spine neighbors (`show`), browse its on-disk context dir (`context`), or list its reports/docs as refs (`artifacts`). Use `canvas dashboard` instead to render the whole tree SHAPE, or `canvas attention` to find which nodes are blocked on the user',
297
297
  help: {
298
298
  name: 'node inspect',
299
299
  summary: 'read one node (or the roster) — topology, context dir, and artifacts',
@@ -8,7 +8,7 @@ import { closePane, paneExists, respawnPaneSync, viewerEnv, viewerPaneOf, nodeIn
8
8
  const nodeRecycle = defineLeaf({
9
9
  name: 'recycle',
10
10
  description: 'finish the agent in your pane + recycle it into a fresh root',
11
- whenToUse: 'you are at an agent\'s pane and done with it: finish it cleanly and recycle the pane in one move — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The human-driver way to end an agent and immediately start over in place. Use `node lifecycle demote` instead to put it on a finishing track IN PLACE (flip terminal, keep it running) without ending it now, `node lifecycle close` to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn',
11
+ whenToUse: 'you are at an agent\'s pane and done with it: finish it cleanly and recycle the pane in one move — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The hands-on way to end an agent and immediately start over in place. Use `node lifecycle demote` instead to put it on a finishing track IN PLACE (flip terminal, keep it running) without ending it now, `node lifecycle close` to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn',
12
12
  help: {
13
13
  name: 'node lifecycle recycle',
14
14
  summary: 'finish the agent in your current pane and recycle the pane — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane',
@@ -49,7 +49,7 @@ function messageSendParams() {
49
49
  { kind: 'flag', name: 'self', type: 'bool', required: false, constraint: 'Target the calling node. Exactly one of --to or --self is required.' },
50
50
  { kind: 'flag', name: 'tier', type: 'enum', choices: ['critical', 'urgent', 'normal', 'deferred'], required: false, default: 'normal', constraint: 'Delivery urgency. Deferred never wakes an idle node.' },
51
51
  { kind: 'flag', name: 'situational-context', type: 'string', required: false, constraint: 'Non-empty hidden ambient context delivered beside the message, or alone.' },
52
- { kind: 'flag', name: 'reopen', type: 'bool', required: false, hidden: true, constraint: 'Clear a finalized target latch before delivery or fresh revive.' },
52
+ { kind: 'flag', name: 'reopen', type: 'bool', required: false, constraint: 'Required to re-task a finalized target: clears its finalization latch before delivery. Rejected when the target is not finalized.' },
53
53
  ];
54
54
  }
55
55
  function messageRequestParams() {
@@ -132,7 +132,7 @@ const nodeMessageSend = defineLeaf({
132
132
  params: messageSendParams(),
133
133
  output: messageOutput(),
134
134
  outputKind: 'object',
135
- effects: ['Appends one inbox entry. Critical, urgent, and normal delivery may revive a dormant target; deferred waits for its next natural cycle.'],
135
+ effects: ['Appends one inbox entry. Critical, urgent, and normal delivery may revive a dormant target; deferred waits for its next natural cycle.', '--reopen clears a finalized target\'s finalization latch before delivery, allowing it to take a new mandate. Without it, a finalized target rejects delivery before an inbox entry is appended or a revive is attempted.'],
136
136
  },
137
137
  run: (input) => runMessageEngine(input),
138
138
  render: (r) => String(r['guidance']),
@@ -41,7 +41,7 @@ function mapWorktreeApiError(err) {
41
41
  const worktreeClose = defineLeaf({
42
42
  name: 'close',
43
43
  description: 'land and close the current node-managed worktree',
44
- whenToUse: 'you are inside the node that owns an open managed worktree and are ready to rebase it onto its recorded local base branch, fast-forward that base onto it, and (best-effort) remove the worktree and delete the branch before finishing',
44
+ whenToUse: 'you are inside the node that owns an open managed worktree and are ready to rebase it onto its recorded local base branch and fast-forward that base before finishing; its active checkout is retained for deferred cleanup',
45
45
  help: {
46
46
  name: 'node worktree close',
47
47
  summary: 'land and close the current node-managed worktree',
@@ -50,15 +50,15 @@ const worktreeClose = defineLeaf({
50
50
  { name: 'node_id', type: 'string', required: true, constraint: 'The caller node id whose managed worktree was closed.' },
51
51
  { name: 'branch', type: 'string', required: true, constraint: 'The worktree branch that was fast-forwarded into its recorded local base branch.' },
52
52
  { name: 'landed_sha', type: 'string', required: true, constraint: 'The commit SHA that the recorded local base branch was fast-forwarded onto.' },
53
- { name: 'worktree_path', type: 'string', required: true, constraint: 'The managed worktree path that was closed (best-effort removed; see worktree_removed).' },
54
- { name: 'worktree_removed', type: 'boolean', required: true, constraint: 'True when the worktree directory was removed; false means the landing still happened (the close is done) but automatic removal failed and needs a manual follow-up.' },
55
- { name: 'worktree_remove_error', type: 'string', required: false, constraint: 'Present only when worktree_removed is false: the manual cleanup command to run.' },
56
- { name: 'branch_deleted', type: 'boolean', required: true, constraint: 'True when the local branch was deleted; false means the landing still happened (the close is done) but the local branch cleanup failed and needs a manual follow-up.' },
57
- { name: 'branch_delete_error', type: 'string', required: false, constraint: 'Present only when branch_deleted is false: the manual cleanup command to run.' },
53
+ { name: 'worktree_path', type: 'string', required: true, constraint: 'The managed worktree path whose cleanup is deferred until its caller exits.' },
54
+ { name: 'worktree_removed', type: 'boolean', required: true, constraint: 'Always false for an active caller: its checkout remains intact rather than being removed beneath the node.' },
55
+ { name: 'worktree_remove_error', type: 'string', required: true, constraint: 'The named deferred-cleanup state and safe manual follow-up after the caller exits.' },
56
+ { name: 'branch_deleted', type: 'boolean', required: true, constraint: 'False while the deferred-cleanup checkout still has this branch checked out.' },
57
+ { name: 'branch_delete_error', type: 'string', required: true, constraint: 'The safe branch-cleanup follow-up after the checkout is removed.' }
58
58
  ],
59
59
  outputKind: 'object',
60
60
  effects: [
61
- 'Rebases the worktree onto its recorded local base branch, fast-forwards that base onto it (never pushes to origin), marks the node-managed worktree closed, then attempts (best-effort) to remove the worktree and delete the local branch.',
61
+ 'Rebases the worktree onto its recorded local base branch, fast-forwards that base onto it (never pushes to origin), marks the node-managed worktree closed with cleanup pending, and leaves the active caller checkout and branch intact.',
62
62
  ],
63
63
  },
64
64
  run: async () => {
@@ -93,7 +93,7 @@ const worktreeClose = defineLeaf({
93
93
  export const nodeWorktreeBranch = defineBranch({
94
94
  name: 'worktree',
95
95
  description: 'manage a node-owned git worktree — close the one born by `node new --worktree <base>`',
96
- whenToUse: 'you are inside a node that was spawned with `node new --worktree <base>` and are ready to land it: rebase onto its recorded local base branch, fast-forward that base onto it, and close the worktree. Symmetric with `node new --worktree <base>`, which births it',
96
+ whenToUse: 'you are inside a node that was spawned with `node new --worktree <base>` and are ready to land it: rebase it onto its recorded local base branch, fast-forward that base onto it, and close the worktree with cleanup deferred until the node exits. Symmetric with `node new --worktree <base>`, which births it',
97
97
  help: {
98
98
  name: 'node worktree',
99
99
  summary: 'manage a node-owned git worktree — birth is `node new --worktree <base>`; this branch closes it',
@@ -8,7 +8,7 @@ import { defineLeaf } from '../../core/command.js';
8
8
  export const pkgBrowseLeaf = defineLeaf({
9
9
  name: 'browse',
10
10
  description: 'open the interactive plugin browser',
11
- whenToUse: 'a human wants to browse registered marketplaces and inspect or manage plugins interactively. Agents should use pkg market browse or pkg plugin list, which are scriptable.',
11
+ whenToUse: 'the user wants to browse registered marketplaces and inspect or manage plugins interactively. Agents should use pkg market browse or pkg plugin list, which are scriptable.',
12
12
  help: {
13
13
  name: 'pkg browse',
14
14
  summary: 'open the interactive plugin browser',