@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
@@ -0,0 +1,306 @@
1
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { cronIdsForProfileDeletion, cronIdsForProfileDetach, deleteCronsForProfile, deleteNodes, getNode, getRow, globalizeCronsForProfile, listNodesByProfile, nodesRoot, reviewDir, } from '../core/canvas/index.js';
4
+ import { usage } from '../core/errors.js';
5
+ import { emitEvent } from '../core/events/emit.js';
6
+ import { clearDefaultProfileId, defaultProfileDirs, } from '../core/profiles/default-binding.js';
7
+ import { reserveProfileDeletion } from '../core/profiles/deletion-reservation.js';
8
+ import { deleteProfile, loadProfileManifest, } from '../core/profiles/manifest.js';
9
+ import { deleteReviewsByOriginNodes, reviewIdsByOriginNodes, } from '../core/review/store.js';
10
+ import { boundFleet } from '../core/runtime/fleet.js';
11
+ import { headlessBrokerHost } from '../core/runtime/host.js';
12
+ import { transition } from '../core/runtime/lifecycle.js';
13
+ import { tearDownNode } from '../core/runtime/placement.js';
14
+ import { reshapeNode } from '../core/runtime/promote.js';
15
+ import { reviveNode } from '../core/runtime/revive.js';
16
+ const SELF_DELETE_GRACE_MS = 1_000;
17
+ const TICKET_FILES = new Set([
18
+ 'page.json',
19
+ 'page.tsx',
20
+ 'page.html',
21
+ 'page.js',
22
+ 'review.json',
23
+ 'response.json',
24
+ 'progress.json',
25
+ 'claim.json',
26
+ 'reply-route.json',
27
+ 'feedback-comments.json',
28
+ 'feedback.json',
29
+ '.ticket-lock',
30
+ ]);
31
+ const pending = new Map();
32
+ function requestObject(body) {
33
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
34
+ throw usage('profile deletion requires a JSON request body', {
35
+ error: 'force_required',
36
+ message: 'profile deletion requires --force',
37
+ next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
38
+ });
39
+ }
40
+ return body;
41
+ }
42
+ function validateRequest(body) {
43
+ const value = requestObject(body);
44
+ if (value['force'] !== true) {
45
+ throw usage('profile deletion requires --force', {
46
+ error: 'force_required',
47
+ message: 'profile deletion requires --force',
48
+ next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
49
+ });
50
+ }
51
+ if (value['detach'] !== undefined && typeof value['detach'] !== 'boolean') {
52
+ throw usage('detach must be a boolean', {
53
+ error: 'invalid_detach',
54
+ message: 'detach must be a boolean',
55
+ next: 'Use --detach as a presence-only flag, or omit it.',
56
+ });
57
+ }
58
+ const caller = value['caller_node_id'];
59
+ if (caller !== undefined && caller !== null && (typeof caller !== 'string' || caller === '')) {
60
+ throw usage('caller_node_id must be a nonempty string or null');
61
+ }
62
+ return {
63
+ force: true,
64
+ ...(value['detach'] === true ? { detach: true } : {}),
65
+ ...(caller === undefined ? {} : { caller_node_id: caller }),
66
+ };
67
+ }
68
+ function manifestSourceNodeId(path) {
69
+ if (!existsSync(path))
70
+ return null;
71
+ try {
72
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
73
+ return typeof parsed.source?.nodeId === 'string' && parsed.source.nodeId !== ''
74
+ ? parsed.source.nodeId
75
+ : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ function ticketCleanupForNodes(nodeIds) {
82
+ const wanted = new Set(nodeIds);
83
+ const root = nodesRoot();
84
+ if (wanted.size === 0 || !existsSync(root))
85
+ return [];
86
+ const cleanup = [];
87
+ for (const dirent of readdirSync(root, { withFileTypes: true })) {
88
+ if (!dirent.isDirectory())
89
+ continue;
90
+ const dir = join(root, dirent.name);
91
+ const sourceIds = [
92
+ manifestSourceNodeId(join(dir, 'page.json')),
93
+ manifestSourceNodeId(join(dir, 'review.json')),
94
+ ];
95
+ if (!sourceIds.some((sourceId) => sourceId !== null && wanted.has(sourceId)))
96
+ continue;
97
+ cleanup.push({
98
+ dir,
99
+ wholeDirectory: wanted.has(dirent.name) || getRow(dirent.name) === null,
100
+ });
101
+ }
102
+ return cleanup.sort((a, b) => a.dir.localeCompare(b.dir));
103
+ }
104
+ function isTicketOwnedResidue(name) {
105
+ if (TICKET_FILES.has(name))
106
+ return true;
107
+ for (const base of TICKET_FILES) {
108
+ if (name.startsWith(`${base}.`) && (name.endsWith('.tmp') || name.endsWith('.lock'))) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+ function cleanTickets(tickets) {
115
+ let cleaned = 0;
116
+ for (const ticket of tickets) {
117
+ if (!existsSync(ticket.dir))
118
+ continue;
119
+ if (ticket.wholeDirectory) {
120
+ rmSync(ticket.dir, { recursive: true, force: true });
121
+ cleaned += 1;
122
+ continue;
123
+ }
124
+ for (const dirent of readdirSync(ticket.dir, { withFileTypes: true })) {
125
+ if (!isTicketOwnedResidue(dirent.name))
126
+ continue;
127
+ rmSync(join(ticket.dir, dirent.name), { recursive: true, force: true });
128
+ }
129
+ cleaned += 1;
130
+ }
131
+ return cleaned;
132
+ }
133
+ function openWorktreeError(nodes) {
134
+ const blocked = nodes
135
+ .filter((node) => node.managed_worktree?.state === 'open')
136
+ .map((node) => ({ node_id: node.node_id, path: node.managed_worktree.path }));
137
+ if (blocked.length === 0)
138
+ return null;
139
+ const message = `profile deletion is blocked by ${blocked.length} open managed worktree${blocked.length === 1 ? '' : 's'}`;
140
+ return usage(message, {
141
+ error: 'open_managed_worktree',
142
+ message,
143
+ nodes: blocked,
144
+ next: `Close ${blocked.map(({ node_id }) => node_id).join(', ')} with \`crtr node worktree close <node>\`, then retry.`,
145
+ });
146
+ }
147
+ function prepareDeletion(entry, request) {
148
+ const nodes = listNodesByProfile(entry.profileId);
149
+ const blocked = openWorktreeError(nodes);
150
+ if (blocked !== null)
151
+ throw blocked;
152
+ const nodeIds = nodes.map(({ node_id }) => node_id);
153
+ const mode = request.detach === true ? 'detached' : 'deleted';
154
+ return {
155
+ entry,
156
+ mode,
157
+ nodes,
158
+ nodeIds,
159
+ tickets: mode === 'deleted' ? ticketCleanupForNodes(nodeIds) : [],
160
+ cronIds: mode === 'deleted'
161
+ ? cronIdsForProfileDeletion(entry.profileId, nodeIds)
162
+ : cronIdsForProfileDetach(entry.profileId),
163
+ reviewIds: mode === 'deleted' ? reviewIdsByOriginNodes(nodeIds) : [],
164
+ defaultDirs: defaultProfileDirs(entry.profileId),
165
+ };
166
+ }
167
+ function assertDeletionPlanPreconditions(plan) {
168
+ const currentNodes = plan.nodeIds.flatMap((nodeId) => {
169
+ const node = getNode(nodeId);
170
+ return node === null ? [] : [node];
171
+ });
172
+ const blocked = openWorktreeError(currentNodes);
173
+ if (blocked !== null)
174
+ throw blocked;
175
+ }
176
+ function resultForPlan(plan) {
177
+ return {
178
+ profile_id: plan.entry.profileId,
179
+ name: plan.entry.manifest.name,
180
+ mode: plan.mode,
181
+ nodes: plan.nodeIds.length,
182
+ crons: plan.cronIds.length,
183
+ tickets: plan.tickets.length,
184
+ reviews: plan.reviewIds.length,
185
+ default_pins: plan.defaultDirs.length,
186
+ };
187
+ }
188
+ async function detachProfile(plan) {
189
+ assertDeletionPlanPreconditions(plan);
190
+ const existingNodeIds = plan.nodeIds.filter((nodeId) => getNode(nodeId) !== null);
191
+ const fleet = boundFleet();
192
+ const live = existingNodeIds.flatMap((nodeId) => {
193
+ const entry = fleet.get(nodeId);
194
+ return entry === undefined ? [] : [{ nodeId, exited: entry.handle.exited }];
195
+ });
196
+ const restartNodeIds = [];
197
+ for (const { nodeId } of live) {
198
+ const current = getNode(nodeId);
199
+ if (current?.status === 'active' || current?.status === 'idle') {
200
+ transition(nodeId, 'release');
201
+ restartNodeIds.push(nodeId);
202
+ }
203
+ headlessBrokerHost.teardown(nodeId);
204
+ }
205
+ await Promise.all(live.map(({ exited }) => exited));
206
+ const detachedNodeIds = [];
207
+ for (const nodeId of existingNodeIds) {
208
+ if (getNode(nodeId) === null)
209
+ continue;
210
+ reshapeNode(nodeId, { profileId: null });
211
+ detachedNodeIds.push(nodeId);
212
+ }
213
+ const cronIds = globalizeCronsForProfile(plan.entry.profileId);
214
+ const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
215
+ deleteProfile(plan.entry.profileId);
216
+ const detached = new Set(detachedNodeIds);
217
+ for (const nodeId of restartNodeIds) {
218
+ if (detached.has(nodeId))
219
+ reviveNode(nodeId, { resume: true, recovery: true });
220
+ }
221
+ return {
222
+ ...resultForPlan(plan),
223
+ nodes: detachedNodeIds.length,
224
+ crons: cronIds.length,
225
+ default_pins: defaultDirs.length,
226
+ };
227
+ }
228
+ function hardDeleteProfile(plan) {
229
+ assertDeletionPlanPreconditions(plan);
230
+ const cronIds = deleteCronsForProfile(plan.entry.profileId, plan.nodeIds);
231
+ for (const nodeId of plan.nodeIds) {
232
+ headlessBrokerHost.teardown(nodeId);
233
+ tearDownNode(nodeId);
234
+ }
235
+ const reviewIds = deleteReviewsByOriginNodes(plan.nodeIds);
236
+ for (const reviewId of reviewIds)
237
+ rmSync(reviewDir(reviewId), { recursive: true, force: true });
238
+ const tickets = cleanTickets(plan.tickets);
239
+ const deletedNodeIds = deleteNodes(plan.nodeIds);
240
+ const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
241
+ deleteProfile(plan.entry.profileId);
242
+ return {
243
+ ...resultForPlan(plan),
244
+ nodes: deletedNodeIds.length,
245
+ crons: cronIds.length,
246
+ tickets,
247
+ reviews: reviewIds.length,
248
+ default_pins: defaultDirs.length,
249
+ };
250
+ }
251
+ async function executePlan(plan) {
252
+ return plan.mode === 'detached' ? detachProfile(plan) : hardDeleteProfile(plan);
253
+ }
254
+ function scheduleSelfDeletion(plan, releaseReservation) {
255
+ const result = {
256
+ ...resultForPlan(plan),
257
+ accepted: true,
258
+ grace_ms: SELF_DELETE_GRACE_MS,
259
+ };
260
+ pending.set(plan.entry.profileId, { result });
261
+ setTimeout(() => {
262
+ void (async () => {
263
+ try {
264
+ await executePlan(plan);
265
+ }
266
+ catch (error) {
267
+ emitEvent({
268
+ level: 'error',
269
+ event: 'profile.delete.deferred_failed',
270
+ error: error instanceof Error ? error : new Error(String(error)),
271
+ fields: { profile_id: plan.entry.profileId, mode: plan.mode },
272
+ });
273
+ }
274
+ finally {
275
+ pending.delete(plan.entry.profileId);
276
+ releaseReservation();
277
+ }
278
+ })();
279
+ }, SELF_DELETE_GRACE_MS);
280
+ return result;
281
+ }
282
+ /** Validate and enact one forced profile deletion. A request from a node that
283
+ * belongs to the target profile is acknowledged before its broker is stopped. */
284
+ export async function requestProfileDeletion(name, body) {
285
+ const request = validateRequest(body);
286
+ const entry = loadProfileManifest(name);
287
+ const existing = pending.get(entry.profileId);
288
+ if (existing !== undefined)
289
+ return { status: 202, body: existing.result };
290
+ const releaseReservation = reserveProfileDeletion(entry.profileId);
291
+ let deferred = false;
292
+ try {
293
+ const plan = prepareDeletion(entry, request);
294
+ const selfDeleting = request.caller_node_id != null && plan.nodeIds.includes(request.caller_node_id);
295
+ if (selfDeleting) {
296
+ const result = scheduleSelfDeletion(plan, releaseReservation);
297
+ deferred = true;
298
+ return { status: 202, body: result };
299
+ }
300
+ return { status: 200, body: await executePlan(plan) };
301
+ }
302
+ finally {
303
+ if (!deferred)
304
+ releaseReservation();
305
+ }
306
+ }
@@ -20,6 +20,7 @@ import { operationIdContext } from '../../core/events/operation-id.js';
20
20
  import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
21
21
  import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
22
22
  import { focusOf } from '../../core/runtime/placement.js';
23
+ import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
23
24
  import { parkEvent } from './live-obligation.js';
24
25
  const execFileAsync = promisify(execFile);
25
26
  function configuredPositiveInteger(name, fallback) {
@@ -191,7 +192,8 @@ export class BrokerSupervisionReconciler {
191
192
  || row.intent !== null
192
193
  || isBusy(id)
193
194
  || activeBackgroundBashJobs(contextDir(id)).length > 0
194
- || focusOf(id) !== null) {
195
+ || focusOf(id) !== null
196
+ || hasAttachedBrokerViewers(id)) {
195
197
  this.unattendedSince.delete(id);
196
198
  return;
197
199
  }
@@ -3,8 +3,8 @@ import { formatReviewApproval } from '../../shared/generated-context.js';
3
3
  import { deliverNodeMessage } from '../messaging/node-message.js';
4
4
  function approvalBody(review, resultPath) {
5
5
  const summary = review.changed
6
- ? `The human reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
7
- : `The human reviewed and approved \`${review.file}\`. The document was not modified.`;
6
+ ? `The user reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
7
+ : `The user reviewed and approved \`${review.file}\`. The document was not modified.`;
8
8
  return `${summary}\n\nDurable review result: ${resultPath}`;
9
9
  }
10
10
  /** Render the immutable approval fact without replaying comment text. */
@@ -25,7 +25,7 @@ export async function deliverApproval(review, resultPath) {
25
25
  const delivery = await deliverNodeMessage({
26
26
  node_id: review.origin_node_id,
27
27
  body,
28
- label: 'human review approved',
28
+ label: 'review approved by the user',
29
29
  from: 'crtrd',
30
30
  mode: 'interactive',
31
31
  data: { review_id: review.review_id, result_path: resultPath },
@@ -18,8 +18,6 @@ export interface ReviewCancellationFinishOutcome {
18
18
  ticket_result?: TicketResult;
19
19
  }
20
20
  export type ReviewFinishOutcome = ReviewApprovalFinishOutcome | ReviewCancellationFinishOutcome;
21
- /** Retire a companion only through a lifecycle transition legal for its current status. */
22
- export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
23
21
  /** Approve on the human's behalf, waiting first when the companion is still
24
22
  * working. Waiting is the only new outcome: an idle companion approves in the
25
23
  * same pass it always did, and the wait never freezes the review — comments
@@ -1,5 +1,4 @@
1
1
  import { withCanvasWrite } from '../../core/canvas/db.js';
2
- import { getNode } from '../../core/canvas/canvas.js';
3
2
  import { responsePath } from '../../core/human/convention.js';
4
3
  import { ticketDir } from '../../core/human/root.js';
5
4
  import { readTicketResult } from '../../core/human/tickets.js';
@@ -10,9 +9,8 @@ import { readReviewSource } from '../../core/review/document.js';
10
9
  import { signalReviewActivity } from '../../core/review/signal.js';
11
10
  import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
12
11
  import { ReviewOperationError } from '../../core/review/types.js';
13
- import { transition } from '../../core/runtime/lifecycle.js';
14
- import { headlessBrokerHost } from '../../core/runtime/host.js';
15
12
  import { emitEvent } from '../../core/events/emit.js';
13
+ import { retireCompanion } from '../companion-retire.js';
16
14
  import { deliverNodeMessage } from '../messaging/node-message.js';
17
15
  import { cancelHumanTicket, deliverTerminalResult } from '../human/finish.js';
18
16
  import { companionHasQueuedWork, companionMidTurn, companionStillWorking } from './companion.js';
@@ -69,32 +67,6 @@ async function serializedFinish(reviewId, work) {
69
67
  finishChains.delete(reviewId);
70
68
  }
71
69
  }
72
- /** Retire a companion only through a lifecycle transition legal for its current status. */
73
- export function retireCompanion(nodeId, outcome) {
74
- const node = getNode(nodeId);
75
- if (node === null)
76
- return 'missing';
77
- if (node.status === 'done' || node.status === 'canceled')
78
- return 'already';
79
- if (outcome === 'approved' && (node.status === 'active' || node.status === 'idle')) {
80
- transition(nodeId, 'finish');
81
- try {
82
- headlessBrokerHost.teardown(nodeId);
83
- }
84
- catch { /* the terminal transition is authoritative */ }
85
- return 'retired';
86
- }
87
- // Cancellation is legal from every status. An already-dead companion cannot
88
- // be finished, so it is retired as canceled even when approval won.
89
- transition(nodeId, 'cancel');
90
- if (node.status === 'active' || node.status === 'idle') {
91
- try {
92
- headlessBrokerHost.teardown(nodeId);
93
- }
94
- catch { /* the terminal transition is authoritative */ }
95
- }
96
- return 'retired';
97
- }
98
70
  async function finishApproved(review, result) {
99
71
  let current = requireVisibleReview(review.review_id);
100
72
  if (current.projected_at === null) {
@@ -4,7 +4,7 @@
4
4
  // still spawned commands with that deleted directory as cwd. child_process
5
5
  // then failed before bash could run, leaving the node unable to use its shell.
6
6
  import assert from 'node:assert/strict';
7
- import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
7
+ import { chmodSync, existsSync, mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
8
8
  import { homedir, tmpdir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import { test } from 'node:test';
@@ -29,3 +29,58 @@ test('bash falls back to home after its worktree cwd was removed', { timeout: 5_
29
29
  rmSync(root, { recursive: true, force: true });
30
30
  }
31
31
  });
32
+ test('bash valve resolves an admitted relative PATH entry from the execution cwd', { timeout: 5_000 }, async () => {
33
+ const root = mkdtempSync(join(tmpdir(), 'crtr-bash-valve-relative-host-path-'));
34
+ const contextDir = join(root, 'context');
35
+ const hostBin = join(root, 'host-bin');
36
+ const shim = join(root, 'shim');
37
+ const marker = join(root, 'contributed-bash-ran');
38
+ mkdirSync(contextDir);
39
+ mkdirSync(hostBin);
40
+ mkdirSync(shim);
41
+ symlinkSync('/bin/bash', join(hostBin, 'bash'));
42
+ writeFileSync(join(shim, 'bash'), `#!/bin/sh\nprintf contributed > ${JSON.stringify(marker)}\nexit 97\n`);
43
+ writeFileSync(join(shim, 'tool'), '#!/bin/sh\nprintf tool\n');
44
+ chmodSync(join(shim, 'bash'), 0o755);
45
+ chmodSync(join(shim, 'tool'), 0o755);
46
+ try {
47
+ const output = [];
48
+ const result = await createValveOperations('test-node', contextDir).exec('tool; command -v tool', root, {
49
+ env: { PATH: `${shim}:host-bin:/bin:/usr/bin`, CRTR_ADMITTED_HOST_PATH: 'host-bin:/bin:/usr/bin' },
50
+ onData: (data) => output.push(data),
51
+ });
52
+ assert.equal(result.exitCode, 0);
53
+ assert.equal(Buffer.concat(output).toString(), `tool${shim}/tool\n`);
54
+ assert.equal(existsSync(marker), false, 'the contributed bash must never be crouter\'s interpreter');
55
+ }
56
+ finally {
57
+ rmSync(root, { recursive: true, force: true });
58
+ }
59
+ });
60
+ test('bash valve uses the admitted host shell while commands keep shim precedence', { timeout: 5_000 }, async () => {
61
+ const root = mkdtempSync(join(tmpdir(), 'crtr-bash-valve-shim-'));
62
+ const contextDir = join(root, 'context');
63
+ const shim = join(root, 'shim');
64
+ const marker = join(root, 'contributed-bash-ran');
65
+ mkdirSync(contextDir);
66
+ mkdirSync(shim);
67
+ writeFileSync(join(shim, 'bash'), `#!/bin/sh\nprintf contributed > ${JSON.stringify(marker)}\nexit 97\n`);
68
+ writeFileSync(join(shim, 'tool'), '#!/bin/sh\nprintf tool\n');
69
+ chmodSync(join(shim, 'bash'), 0o755);
70
+ chmodSync(join(shim, 'tool'), 0o755);
71
+ try {
72
+ const output = [];
73
+ const hostPath = process.env['PATH'];
74
+ assert.notEqual(hostPath, undefined);
75
+ const result = await createValveOperations('test-node', contextDir).exec('tool; command -v tool', root, {
76
+ env: { PATH: `${shim}:${hostPath}`, CRTR_ADMITTED_HOST_PATH: hostPath },
77
+ onData: (data) => output.push(data),
78
+ });
79
+ assert.equal(result.exitCode, 0);
80
+ assert.equal(Buffer.concat(output).toString(), `tool${shim}/tool\n`);
81
+ assert.equal(existsSync(marker), false, 'the contributed bash must never be crouter\'s interpreter');
82
+ }
83
+ finally {
84
+ rmSync(root, { recursive: true, force: true });
85
+ }
86
+ });
@@ -363,8 +363,8 @@ test('turn_end still clears a daemon→node wedge fault (existing behavior, unch
363
363
  pi.fire('turn_end', turnEndEvent('turn completed'), {});
364
364
  assert.equal(readFault('leaf'), null, 'the daemon→node wedge fault is still cleared at turn_end');
365
365
  });
366
- test('a successful final or human ask suppresses the stop reprompt without a canvas read', async () => {
367
- for (const [signal, shutsDown] of [['pushedFinal', true], ['askedHuman', false]]) {
366
+ test('a successful final or requested human reply suppresses the stop reprompt without a canvas read', async () => {
367
+ for (const [signal, shutsDown] of [['pushedFinal', true], ['requestedHumanReply', false]]) {
368
368
  process.env['CRTR_NODE_ID'] = 'signal-only';
369
369
  markStopSignal('signal-only', signal);
370
370
  const pi = makeFakePi();
@@ -389,12 +389,12 @@ test('an in-run retry (agent_start with no new input) does not erase this run\'s
389
389
  const ctx = { shutdown: () => { } };
390
390
  pi.fire('input', { type: 'input' }, ctx);
391
391
  pi.fire('agent_start', { type: 'agent_start' }, ctx);
392
- markStopSignal('leaf', 'askedHuman'); // the node ran `crtr human ask` mid-run
392
+ markStopSignal('leaf', 'requestedHumanReply'); // the node ran `crtr human send` mid-run
393
393
  pi.fire('agent_start', { type: 'agent_start' }, ctx); // pi auto-retried after a provider error
394
394
  await settle(pi, stopEvent('waiting on the human now'), ctx);
395
395
  assert.equal(pi.injected.length, 0, 'the ask survives the in-run retry — no stall reprompt');
396
396
  // ...and the NEXT user-directed run does start clean.
397
- markStopSignal('leaf', 'askedHuman');
397
+ markStopSignal('leaf', 'requestedHumanReply');
398
398
  pi.fire('input', { type: 'input' }, ctx);
399
399
  await settle(pi, stopEvent('I think I am basically done here'), ctx);
400
400
  assert.equal(pi.injected.length, 1, 'a new input discards a leftover marker from a prior run');
@@ -29,6 +29,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmS
29
29
  import { homedir } from 'node:os';
30
30
  import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
31
31
  import { readConfig } from '../core/config.js';
32
+ import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
32
33
  import { Type } from 'typebox';
33
34
  import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
34
35
  // ---------------------------------------------------------------------------
@@ -52,19 +53,31 @@ function resolveValveMs() {
52
53
  // Positional argv (never interpolated into the script — the command text
53
54
  // lives only in cmd.sh):
54
55
  // $1 cmd.sh $2 job.log $3 job.exit $4 job.run
55
- // $5 job.done $6 job.bg $7 jobId $8 nodeId
56
+ // $5 job.done $6 job.bg $7 jobId $8 nodeId $9 shell path
56
57
  //
57
58
  // `--` before the paths is a dummy $0 (a standard `bash -c script -- args`
58
59
  // idiom) so the real payload lands at $1 onward.
59
60
  // ---------------------------------------------------------------------------
61
+ // The elapsed rendering mirrors formatBashElapsed() in core/bash-jobs.ts
62
+ // (`12s` / `3m 40s` / `1h 04m`) so the completion message reads the same as the
63
+ // handoff notice; it measures TOTAL runtime, foreground portion included.
60
64
  const SUPERVISOR_SCRIPT = [
61
- 'bash "$1" > "$2" 2>&1',
65
+ 'SECONDS=0',
66
+ '"${9}" "$1" > "$2" 2>&1',
62
67
  'ec=$?',
68
+ 'd=$SECONDS',
63
69
  'echo "$ec" > "$3"',
70
+ 'if [ "$d" -lt 60 ]; then',
71
+ ' el="${d}s"',
72
+ 'elif [ "$d" -lt 3600 ]; then',
73
+ ' el="$((d / 60))m $(printf "%02d" "$((d % 60))")s"',
74
+ 'else',
75
+ ' el="$((d / 3600))h $(printf "%02d" "$((d / 60 % 60))")m"',
76
+ 'fi',
64
77
  'if mv "$4" "$5" 2>/dev/null; then',
65
78
  ' :',
66
79
  'elif [ -f "$6" ]; then',
67
- ' crtr node message send "Background bash job $7 finished (exit $ec). Log: $2 Command: $1" --to "$8" --tier urgent || true',
80
+ ' crtr node message send "Background bash job $7 finished (exit $ec) after $el. Log: $2 Command: $1" --to "$8" --tier urgent || true',
68
81
  'fi',
69
82
  ].join('\n');
70
83
  function makeJobPaths(contextDir) {
@@ -183,7 +196,7 @@ function longSleepRefusal(command) {
183
196
  `Holding a live window on a clock is never how you wait here. Instead:\n` +
184
197
  ` - schedule the wake and end your turn: crtr cron -h (one-shot at a time, or recurring)\n` +
185
198
  ` - race an inbox message against a deadline: crtr node wait deadline -h\n` +
186
- ` - waiting on a child, a human ask, or any spine event: just stop — the runtime wakes you.\n` +
199
+ ` - waiting on a child, a reply from the user, or any spine event: just stop — the runtime wakes you.\n` +
187
200
  `If you truly need a short pause inline, sleep ${MAX_INLINE_SLEEP_SECONDS}s or less.`);
188
201
  }
189
202
  /** A node can legitimately remove its own merged worktree. Never recreate that
@@ -296,7 +309,8 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
296
309
  const execution = resolveExecutionCwd(cwd);
297
310
  if (execution.warning)
298
311
  onData(Buffer.from(execution.warning));
299
- const child = spawn('bash', ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId], {
312
+ const shellPath = resolveAdmittedHostCommand('bash', env ?? process.env, execution.cwd);
313
+ const child = spawn(shellPath, ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId, shellPath], {
300
314
  cwd: execution.cwd,
301
315
  detached: true,
302
316
  stdio: 'ignore',
@@ -306,7 +306,7 @@ class CanvasInboxWatcher {
306
306
  }
307
307
  /** True when delivery must wait a tick for a report-node projection refresh.
308
308
  * The projection captured at session_start omits every node created since —
309
- * each `crtr human ask` bridge and each child this node spawns — so their
309
+ * each reply-bearing `crtr human send` bridge and each child this node spawns — so their
310
310
  * reports would render as bare refs instead of inlined bodies. One refresh
311
311
  * per unknown node id keeps a genuinely missing node from stalling delivery. */
312
312
  refreshForUnknownReportRefs() {
@@ -19,6 +19,10 @@ interface CustomMessageLike {
19
19
  reviewId: string;
20
20
  originNodeId: string;
21
21
  targetFile: string;
22
+ /** Which binding flavor authored this marker — lets a display surface
23
+ * (e.g. Northlight's native boundary) pick its copy without parsing the
24
+ * agent-facing prose. Absent on markers written before this field. */
25
+ kind: 'review' | 'page_feedback';
22
26
  };
23
27
  }
24
28
  interface RenderedMessageLike {
@@ -55,19 +55,28 @@ export function registerCanvasReviewBoundary(pi) {
55
55
  const alreadyMarked = scan.some((entry) => isReviewBoundary(entry, node.review_binding?.review_id));
56
56
  if (alreadyMarked)
57
57
  return;
58
- const { review_id: reviewId, origin_node_id: originNodeId, target_file: targetFile, } = node.review_binding;
59
- const content = [
60
- `Reviewed file: ${targetFile}`,
61
- `Review ID: ${reviewId}`,
62
- `Origin node: ${originNodeId}`,
63
- '',
64
- "The person's visible transcript starts here. Everything above this marker is inherited context they have not seen.",
65
- ].join('\n');
58
+ const binding = node.review_binding;
59
+ const { review_id: reviewId, origin_node_id: originNodeId, target_file: targetFile } = binding;
60
+ const content = binding.kind === 'page_feedback'
61
+ ? [
62
+ `This conversation began from feedback on page ${reviewId}.`,
63
+ `Page source: ${targetFile}`,
64
+ `Origin node: ${originNodeId}`,
65
+ '',
66
+ "The person's visible transcript starts here. Everything above this marker is the origin node's work — inherited context they have not seen.",
67
+ ].join('\n')
68
+ : [
69
+ `Reviewed file: ${targetFile}`,
70
+ `Review ID: ${reviewId}`,
71
+ `Origin node: ${originNodeId}`,
72
+ '',
73
+ "The person's visible transcript starts here. Everything above this marker is inherited context they have not seen.",
74
+ ].join('\n');
66
75
  pi.sendMessage({
67
76
  customType: REVIEW_BOUNDARY_CUSTOM_TYPE,
68
77
  content,
69
78
  display: true,
70
- details: { reviewId, originNodeId, targetFile },
79
+ details: { reviewId, originNodeId, targetFile, kind: binding.kind ?? 'review' },
71
80
  });
72
81
  });
73
82
  }
@@ -407,7 +407,7 @@ export function registerCanvasStophook(pi) {
407
407
  // input — a new user-directed run is beginning. This is the ONLY place stop
408
408
  // signals are cleared: `agent_start` re-fires for pi's automatic retries and
409
409
  // post-compaction continuations within the SAME run, so clearing there would
410
- // erase the `crtr human ask` / `crtr push final` marker written earlier in the
410
+ // erase the `crtr human send` / `crtr push final` marker written earlier in the
411
411
  // run and make the eventual settlement fall back to a canvas read (exactly the
412
412
  // read the marker exists to avoid). One run's markers survive until the one
413
413
  // `agent_settled` consumes them; a leftover from an errored or interrupted run
@@ -508,7 +508,7 @@ export function registerCanvasStophook(pi) {
508
508
  stopReason: 'quit',
509
509
  backgroundJobsRunning: backgroundJobsRunning(),
510
510
  pushedFinal: false,
511
- askedHuman: false,
511
+ requestedHumanReply: false,
512
512
  });
513
513
  enactSettleDirective(directive, ctx);
514
514
  }
@@ -637,14 +637,14 @@ export function registerCanvasStophook(pi) {
637
637
  const last = lastAssistantMessage(messages);
638
638
  const stopReason = typeof last?.stopReason === 'string' ? last.stopReason : '';
639
639
  const stopSignals = event === null
640
- ? { pushedFinal: false, askedHuman: false }
640
+ ? { pushedFinal: false, requestedHumanReply: false }
641
641
  : consumeStopSignals(nodeId);
642
642
  try {
643
643
  const directive = await settleBroker(nodeId, {
644
644
  stopReason,
645
645
  backgroundJobsRunning: backgroundJobsRunning(),
646
646
  pushedFinal: stopSignals.pushedFinal,
647
- askedHuman: stopSignals.askedHuman,
647
+ requestedHumanReply: stopSignals.requestedHumanReply,
648
648
  });
649
649
  enactSettleDirective(directive, ctx);
650
650
  }