@north-light/crouter 0.3.197 → 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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  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 +126 -33
  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 +8 -8
  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 +3 -3
  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/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,12 +1,11 @@
1
1
  import { test, before, after, beforeEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, existsSync, readdirSync, statSync, writeFileSync, chmodSync } from 'node:fs';
3
+ import { mkdtempSync, mkdirSync, rmSync, existsSync, statSync, writeFileSync, chmodSync } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { gitSync } from '../../git.js';
8
- import { push } from '../../feed/feed.js';
9
- import { createManagedWorktree, closeManagedWorktree, managedWorktreePath, rollbackManagedWorktree, setWorktreeCleanupRunnerForTest } from '../../worktree.js';
8
+ import { createManagedWorktree, closeManagedWorktree, managedWorktreePath, rollbackManagedWorktree } from '../../worktree.js';
10
9
  import { createNode, getNode } from '../../canvas/canvas.js';
11
10
  import { closeDb } from '../../canvas/db.js';
12
11
  let home;
@@ -60,14 +59,9 @@ after(() => {
60
59
  rmSync(sourceDir, { recursive: true, force: true });
61
60
  delete process.env['CRTR_HOME'];
62
61
  });
63
- // Regression for #120: `git worktree remove` deletes the checkout directory
64
- // that IS the calling node's own pi-process cwd. A process can never chdir
65
- // itself out of a directory that vanishes out from under it, so once that
66
- // dir is gone every subsequent bash in that node fails permanently and the
67
- // mandated post-close `crtr push final` can never run. closeManagedWorktree
68
- // must leave the path resolvable (as an empty tombstone dir) even though the
69
- // git worktree registration itself is gone.
70
- test('closeManagedWorktree recreates the worktree path as a tombstone dir so the node cwd still resolves', () => {
62
+ // Regression for #710: close lands from a live caller, so it must preserve the
63
+ // caller checkout rather than starting Git's recursive removal beneath it.
64
+ test('closeManagedWorktree leaves the active caller checkout intact with named deferred cleanup', () => {
71
65
  const nodeId = 'wt-close-1';
72
66
  const wt = createManagedWorktree(sourceDir, nodeId);
73
67
  assert.equal(wt.base_ref, 'main', 'default base must remain backward-compatible with persisted main worktrees');
@@ -75,22 +69,16 @@ test('closeManagedWorktree recreates the worktree path as a tombstone dir so the
75
69
  assert.ok(existsSync(wt.path), 'worktree checkout should exist after create');
76
70
  createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
77
71
  const result = closeManagedWorktree(nodeId);
78
- assert.equal(result.worktree_removed, true);
79
- // The path must still resolve as a directory — this is the actual fix: a
80
- // node whose cwd was wt.path can still chdir there / run bash / push final.
81
- assert.ok(existsSync(wt.path), 'worktree path must still exist after close (tombstone)');
82
- assert.ok(statSync(wt.path).isDirectory(), 'recreated path must be a directory');
83
- assert.deepEqual(readdirSync(wt.path), [], 'tombstone dir should be empty — git already removed the real checkout');
84
- // git itself no longer tracks it as a worktree.
72
+ assert.equal(result.worktree_removed, false);
73
+ assert.match(result.worktree_remove_error ?? '', /cleanup pending/);
74
+ assert.ok(existsSync(join(wt.path, 'README.md')), 'the caller checkout keeps its tracked files');
75
+ assert.ok(statSync(wt.path).isDirectory(), 'the caller cwd remains a directory');
85
76
  const list = gitSync(['worktree', 'list', '--porcelain'], sourceDir);
86
- assert.ok(!list.stdout.includes(wt.path), 'git should no longer register the removed worktree');
77
+ assert.ok(list.stdout.includes(wt.path), 'Git keeps the intact caller checkout registered');
87
78
  const meta = getNode(nodeId);
88
79
  assert.equal(meta?.managed_worktree?.state, 'closed');
89
- // Regression for #519: a later park/revive must relaunch from the durable
90
- // base checkout, not the torn-down worktree dir (best-effort tombstone —
91
- // reapable by concurrent git GC, and not a real repo checkout even while
92
- // it survives). closeManagedWorktree must repoint the persisted cwd.
93
- assert.equal(meta?.cwd, wt.repo_root, 'closed node cwd must repoint to the repo root, not the removed worktree path');
80
+ assert.equal(meta?.managed_worktree?.cleanup, 'pending');
81
+ assert.equal(meta?.cwd, wt.repo_root, 'a later revive uses the durable base checkout');
94
82
  });
95
83
  test('two concurrent creates from linked worktrees serialize on their shared repository', async () => {
96
84
  const worktreeModule = new URL('../../worktree.ts', import.meta.url).href;
@@ -372,7 +360,8 @@ test('close ignores crtr\'s own untracked .crouter/config.json and state.json bo
372
360
  // reports a clean tree, not just that close happens not to notice.
373
361
  assert.equal(gitSync(['status', '--porcelain'], wt.path).stdout.trim(), '', 'git status must already be clean with only crtr bookkeeping present');
374
362
  const result = closeManagedWorktree(nodeId);
375
- assert.equal(result.worktree_removed, true);
363
+ assert.equal(result.worktree_removed, false);
364
+ assert.match(result.worktree_remove_error ?? '', /cleanup pending/);
376
365
  });
377
366
  // The exclude must be narrow: it must NEVER hide real, committable node work
378
367
  // that merely happens to live under `.crouter/` — this mirrors crouter's OWN
@@ -416,21 +405,15 @@ test('autoDropCleanManagedWorktree drops a clean, zero-commit worktree', async (
416
405
  createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
417
406
  const result = autoDropCleanManagedWorktree(nodeId);
418
407
  assert.ok(result !== null, 'a worktree with zero commits and a clean tree must be droppable');
419
- assert.equal(result?.worktree_removed, true);
408
+ assert.equal(result?.worktree_removed, false);
409
+ assert.match(result?.worktree_remove_error ?? '', /cleanup pending/);
420
410
  const meta = getNode(nodeId);
421
411
  assert.equal(meta?.managed_worktree?.state, 'closed');
422
- assert.ok(!gitSync(['worktree', 'list', '--porcelain'], sourceDir).stdout.includes(wt.path), 'git must no longer register the dropped worktree');
423
- // Regression for #519: auto-drop must persist the SAME closed-worktree
424
- // transition explicit close does — repointing `cwd` to the durable base
425
- // checkout, atomically with `state: 'closed'`. Before the fix, auto-drop
426
- // left `cwd` at the removed worktree path; that path is recreated as an
427
- // empty tombstone dir (see `bestEffortRemoveWorktreeCheckout`), so a later
428
- // `--reopen` revive's `existsSync(meta.cwd)` preflight passed anyway and
429
- // launched into a non-checkout instead of failing loud or landing somewhere
430
- // real. `cwd` must be durable: a real git checkout, not a reapable stand-in.
431
- assert.equal(meta?.cwd, wt.repo_root, 'auto-dropped node cwd must repoint to the repo root, not the removed worktree path');
432
- assert.equal(gitSync(['rev-parse', '--git-dir'], meta.cwd).status, 0, 'repointed cwd must resolve to a real git checkout a reopen/revive can launch from');
433
- assert.equal(existsSync(join(wt.path, '.git')), false, 'the tombstoned worktree path must NOT itself resolve as a git checkout');
412
+ assert.equal(meta?.managed_worktree?.cleanup, 'pending');
413
+ assert.ok(gitSync(['worktree', 'list', '--porcelain'], sourceDir).stdout.includes(wt.path), 'Git keeps the caller checkout registered until it exits');
414
+ assert.equal(meta?.cwd, wt.repo_root, 'auto-dropped node cwd repoints to the durable base checkout');
415
+ assert.equal(gitSync(['rev-parse', '--git-dir'], meta.cwd).status, 0, 'repointed cwd resolves to a real git checkout');
416
+ assert.ok(existsSync(join(wt.path, '.git')), 'the intact caller worktree remains a git checkout');
434
417
  });
435
418
  // The same real-commit case close already protects: a worktree with actual
436
419
  // unlanded work must NEVER be auto-dropped by push-final.
@@ -570,9 +553,10 @@ test('autoDropCleanManagedWorktree drops a branch with real commits already cont
570
553
  assert.equal(gitSync(['rev-parse', 'main'], repo).stdout.trim(), branchSha);
571
554
  const result = autoDropCleanManagedWorktree(nodeId);
572
555
  assert.ok(result !== null, 'a branch whose commits are already contained in a moved base has nothing left to land');
573
- assert.equal(result?.branch_deleted, true);
556
+ assert.equal(result?.branch_deleted, false);
574
557
  assert.equal(getNode(nodeId)?.managed_worktree?.state, 'closed');
575
- assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], repo).status, 1, 'the now-redundant branch ref must actually be gone');
558
+ assert.equal(getNode(nodeId)?.managed_worktree?.cleanup, 'pending');
559
+ assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], repo).status, 0, 'the caller branch remains checked out until cleanup runs after exit');
576
560
  // The work itself is NOT lost — it stays reachable from main, which is the point.
577
561
  assert.equal(gitSync(['merge-base', '--is-ancestor', branchSha, 'main'], repo).status, 0, 'the landed commit must remain reachable from main after the drop');
578
562
  rmSync(repo, { recursive: true, force: true });
@@ -687,63 +671,6 @@ test('the re-verified base containment refuses when the base is rewritten betwee
687
671
  // for the branch-only race, and the same one the independent re-review used
688
672
  // to reproduce this exact Critical against `6de1c8d`.
689
673
  // ---------------------------------------------------------------------------
690
- test('auto-drop atomic delete refuses when the base is force-reset between the live ancestry check and the ref transaction', async () => {
691
- const { proveCleanEmptyManagedWorktree, autoDropCleanManagedWorktreeLocked } = await import('../../worktree.js');
692
- const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-auto-drop-base-window-'));
693
- run(['init', '-b', 'main'], repo);
694
- run(['config', 'user.email', 'test@example.com'], repo);
695
- run(['config', 'user.name', 'Test'], repo);
696
- writeFileSync(join(repo, 'README.md'), 'hello\n');
697
- run(['add', '.'], repo);
698
- run(['commit', '-m', 'init'], repo);
699
- const preMergeMainSha = gitSync(['rev-parse', 'main'], repo).stdout.trim();
700
- const nodeId = 'wt-auto-drop-base-window';
701
- const wt = createManagedWorktree(repo, nodeId);
702
- createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
703
- writeFileSync(join(wt.path, 'feature.txt'), 'landed elsewhere\n');
704
- run(['add', '.'], wt.path);
705
- run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'feature'], wt.path);
706
- const branchSha = gitSync(['rev-parse', 'HEAD'], wt.path).stdout.trim();
707
- // A manager fast-forwards main past this branch's own tip, exactly like
708
- // the #327 positive case, so the branch proves clean-and-contained.
709
- run(['merge', '--ff-only', branchSha], repo);
710
- assert.equal(gitSync(['rev-parse', 'main'], repo).stdout.trim(), branchSha);
711
- const proof = proveCleanEmptyManagedWorktree(wt);
712
- assert.ok(proof !== null, 'the branch, now contained in the moved base, must prove droppable');
713
- // Wrap `git worktree remove` — the call `autoDropCleanManagedWorktreeLocked`
714
- // makes strictly AFTER its own live ancestry re-check and strictly BEFORE
715
- // its ref-deletion transaction — to force-reset main back to its pre-merge
716
- // tip right before the real removal runs. A sequential re-check placed
717
- // anywhere before this point cannot see this rewrite; only binding the
718
- // base's live value into the delete transaction itself can.
719
- const wrapperDir = mkdtempSync(join(tmpdir(), 'crtr-git-wrapper-'));
720
- const wrapperPath = join(wrapperDir, 'git');
721
- const originalPath = process.env['PATH'] ?? '';
722
- writeFileSync(wrapperPath, `#!/bin/sh
723
- if [ "$1" = "worktree" ] && [ "$2" = "remove" ] && [ "$3" = "${wt.path}" ]; then
724
- PATH="$CRTR_TEST_REAL_PATH" git -C "${repo}" update-ref refs/heads/main "${preMergeMainSha}"
725
- fi
726
- PATH="$CRTR_TEST_REAL_PATH" exec git "$@"
727
- `);
728
- chmodSync(wrapperPath, 0o755);
729
- process.env['CRTR_TEST_REAL_PATH'] = originalPath;
730
- process.env['PATH'] = `${wrapperDir}:${originalPath}`;
731
- let result;
732
- try {
733
- result = autoDropCleanManagedWorktreeLocked(nodeId, wt, proof);
734
- }
735
- finally {
736
- process.env['PATH'] = originalPath;
737
- delete process.env['CRTR_TEST_REAL_PATH'];
738
- rmSync(wrapperDir, { recursive: true, force: true });
739
- }
740
- assert.equal(result, null, 'a base rewritten strictly after the live ancestry check must still refuse the atomic delete');
741
- assert.equal(getNode(nodeId)?.managed_worktree?.state, 'open', 'state must stay open — nothing may be marked safely dropped');
742
- const refCheck = gitSync(['rev-parse', '--verify', `refs/heads/${wt.branch}`], repo);
743
- assert.equal(refCheck.status, 0, 'the source branch must be preserved');
744
- assert.equal(refCheck.stdout.trim(), branchSha, 'the branch must still name the proven commit — the source commit remains reachable from this head');
745
- rmSync(repo, { recursive: true, force: true });
746
- });
747
674
  test('close-landed atomic delete refuses when the base is force-reset between landing and the cleanup delete', () => {
748
675
  const repo = mkdtempSync(join(tmpdir(), 'crtr-worktree-close-base-window-'));
749
676
  run(['init', '-b', 'main'], repo);
@@ -817,107 +744,6 @@ test('auto-drop refuses and preserves the branch and checkout when checkout remo
817
744
  assert.ok(list.stdout.includes(wt.path), 'the checkout must remain registered and recoverable');
818
745
  assert.ok(existsSync(join(wt.path, 'late-arrival.txt')), 'the live checkout, including its uncommitted file, must remain intact');
819
746
  });
820
- test('post-land cleanup timeout preserves the land and releases the repository lock', async () => {
821
- const nodeId = 'wt-close-cleanup-timeout';
822
- const wt = createManagedWorktree(sourceDir, nodeId);
823
- createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
824
- writeFileSync(join(wt.path, 'cleanup-timeout-work.txt'), 'work\n');
825
- run(['add', '.'], wt.path);
826
- run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'cleanup timeout work'], wt.path);
827
- const baseBefore = gitSync(['rev-parse', 'main'], sourceDir).stdout.trim();
828
- setWorktreeCleanupRunnerForTest((args, cwd, options) => {
829
- if (args[0] === 'worktree' && args[1] === 'remove' && args[2] === wt.path) {
830
- assert.equal(options.timeoutMs, 5_000, 'only post-land cleanup gets the finite deadline');
831
- return { status: 1, stdout: '', stderr: '', timedOut: true };
832
- }
833
- if (args[0] === 'worktree' && args[1] === 'list') {
834
- assert.equal(options.timeoutMs, 5_000, 'residual inspection gets the same finite deadline');
835
- const inspection = gitSync(args, cwd, undefined, options);
836
- return {
837
- ...inspection,
838
- // Branch identity is not checkout-validity: a switch/detach racing the
839
- // timed-out remove still leaves a live registration that needs --force.
840
- stdout: inspection.stdout.replace(`branch refs/heads/${wt.branch}`, 'branch refs/heads/externally-switched'),
841
- };
842
- }
843
- return gitSync(args, cwd, undefined, options);
844
- });
845
- try {
846
- const started = Date.now();
847
- const result = closeManagedWorktree(nodeId);
848
- const elapsed = Date.now() - started;
849
- const cleanupDeadlineMs = 5_000;
850
- const schedulingMarginMs = 500;
851
- assert.ok(elapsed < cleanupDeadlineMs - schedulingMarginMs, `cleanup timeout must return promptly, took ${elapsed}ms`);
852
- assert.notEqual(result.landed_sha, baseBefore, 'the land must contain a new commit');
853
- assert.equal(gitSync(['rev-parse', 'main'], sourceDir).stdout.trim(), result.landed_sha, 'the base must contain the landed commit');
854
- assert.equal(getNode(nodeId)?.managed_worktree?.state, 'closed', 'timeout must preserve the closed state');
855
- assert.equal(result.worktree_removed, false);
856
- assert.equal(result.branch_deleted, false);
857
- assert.match(result.worktree_remove_error ?? '', /timed out/);
858
- assert.match(result.worktree_remove_error ?? '', /git -C .* worktree remove --force/);
859
- assert.doesNotMatch(result.worktree_remove_error ?? '', /worktree prune/, 'a valid registered checkout needs force-removal even if its branch changed');
860
- assert.equal(gitSync(['show-ref', '--verify', '--quiet', `refs/heads/${wt.branch}`], sourceDir).status, 0, 'the source branch must remain reachable');
861
- // The node that timed out must still be able to complete its own finalization
862
- // from the recreated cwd; a different-node lock check alone is insufficient.
863
- await push(nodeId, { kind: 'final', body: 'timed-out cleanup node finalized' });
864
- assert.equal(getNode(nodeId)?.status, 'done', 'the timed-out node must finalize itself successfully');
865
- assert.ok(getNode(nodeId)?.final_report, 'the timed-out node must record its canonical final');
866
- // A second close must acquire the same repository lock and complete, proving
867
- // the timed-out cleanup did not strand the lock around the finished land.
868
- const followupId = 'wt-close-after-cleanup-timeout';
869
- const followup = createManagedWorktree(sourceDir, followupId);
870
- createNode(node(followupId, { cwd: followup.path, managed_worktree: followup }));
871
- writeFileSync(join(followup.path, 'followup-work.txt'), 'followup\n');
872
- run(['add', '.'], followup.path);
873
- run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'followup work'], followup.path);
874
- const followupResult = closeManagedWorktree(followupId);
875
- assert.equal(followupResult.worktree_removed, true, 'subsequent finalization must complete');
876
- assert.equal(getNode(followupId)?.managed_worktree?.state, 'closed');
877
- }
878
- finally {
879
- setWorktreeCleanupRunnerForTest();
880
- }
881
- });
882
- test('post-land cleanup timeout gives prune guidance for a tombstone residual', () => {
883
- const nodeId = 'wt-close-cleanup-timeout-tombstone';
884
- const wt = createManagedWorktree(sourceDir, nodeId);
885
- createNode(node(nodeId, { cwd: wt.path, managed_worktree: wt }));
886
- writeFileSync(join(wt.path, 'cleanup-tombstone-work.txt'), 'work\n');
887
- run(['add', '.'], wt.path);
888
- run(['-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', 'cleanup tombstone work'], wt.path);
889
- setWorktreeCleanupRunnerForTest((args, cwd, options) => {
890
- if (args[0] === 'worktree' && args[1] === 'remove' && args[2] === wt.path) {
891
- assert.equal(options.timeoutMs, 5_000, 'the timed-out remove gets the finite deadline');
892
- rmSync(wt.path, { recursive: true, force: true });
893
- mkdirSync(wt.path, { recursive: true });
894
- return { status: 1, stdout: '', stderr: '', timedOut: true };
895
- }
896
- if (args[0] === 'worktree' && args[1] === 'list') {
897
- assert.equal(options.timeoutMs, 5_000, 'residual inspection gets the same finite deadline');
898
- const branchSha = gitSync(['rev-parse', `refs/heads/${wt.branch}`], sourceDir).stdout.trim();
899
- return {
900
- status: 0,
901
- stdout: `worktree ${wt.path}\nHEAD ${branchSha}\nbranch refs/heads/${wt.branch}\nprunable gitdir file points to non-existent location\n`,
902
- stderr: '',
903
- };
904
- }
905
- return gitSync(args, cwd, undefined, options);
906
- });
907
- try {
908
- const result = closeManagedWorktree(nodeId);
909
- const guidance = result.worktree_remove_error ?? '';
910
- assert.equal(result.worktree_removed, false);
911
- assert.equal(result.branch_deleted, false);
912
- assert.match(guidance, /git -C .* worktree prune/);
913
- assert.match(guidance, /rm -rf/);
914
- assert.doesNotMatch(guidance, /worktree remove --force/);
915
- assert.ok(existsSync(wt.path), 'the tombstone residual must remain available for guided cleanup');
916
- }
917
- finally {
918
- setWorktreeCleanupRunnerForTest();
919
- }
920
- });
921
747
  test('close-landed never low-level-deletes the source branch when checkout removal is refused', () => {
922
748
  const nodeId = 'wt-close-removal-refused';
923
749
  const wt = createManagedWorktree(sourceDir, nodeId);
@@ -105,3 +105,25 @@ test('cycleAwareMessages: a yielded cycle keeps terminal assistant content witho
105
105
  assert.equal(yieldedAssistant.errorMessage, undefined, 'the provider abort text is omitted with the yield boundary');
106
106
  assert.equal(out.filter((message) => message.customType === 'crtr-cycle-divider').length, 1);
107
107
  });
108
+ // Observed shape: the yield's self-abort landed as the NEXT request was
109
+ // starting, so the provider caught a bare AbortError instead of seeing its own
110
+ // signal and stamped `stopReason: 'error'` on an empty turn. Pi's assistant
111
+ // component reserves a red `Error: …` line for exactly that stopReason, so the
112
+ // annotation must come off here too — a yield is not a failure.
113
+ test('cycleAwareMessages: a yield abort recorded as a provider error is still not a failed turn', () => {
114
+ const m1 = userMsg('m1', null, 'yield after this turn');
115
+ const m2 = assistantMsg('m2', 'm1', [], 'error', 'This operation was aborted');
116
+ const cy1 = {
117
+ type: 'custom',
118
+ id: 'cy1',
119
+ parentId: null,
120
+ timestamp: new Date(1).toISOString(),
121
+ customType: CRTR_CYCLE_CUSTOM_TYPE,
122
+ data: { cycle: 2, fromLeaf: 'm2' },
123
+ };
124
+ const m3 = userMsg('m3', 'cy1', 'fresh cycle');
125
+ const out = cycleAwareMessages(cycleManager([m1, m2, cy1, m3], 'm3'));
126
+ const yieldedAssistant = out.find((message) => message.role === 'assistant');
127
+ assert.equal(yieldedAssistant.stopReason, undefined, 'yield is not presented as a failed assistant turn');
128
+ assert.equal(yieldedAssistant.errorMessage, undefined, 'the aborted-request text is omitted with the yield boundary');
129
+ });
@@ -28,7 +28,7 @@ function node(id, over = {}) {
28
28
  ...over,
29
29
  };
30
30
  }
31
- const noSignals = { pushedFinal: false, askedHuman: false };
31
+ const noSignals = { pushedFinal: false, requestedHumanReply: false };
32
32
  before(() => {
33
33
  home = mkdtempSync(join(tmpdir(), 'crtr-stopguard-'));
34
34
  process.env['CRTR_HOME'] = home;
@@ -132,10 +132,10 @@ test('a TERMINAL node whose only worker is dead is stalled (no LIVE subscription
132
132
  assert.equal(d.action, 'reprompt');
133
133
  assert.equal(d.reason, 'stalled');
134
134
  });
135
- test('pushedFinal → finished; askedHuman → escalated (both short-circuit before lifecycle)', () => {
135
+ test('pushedFinal → finished; requestedHumanReply → escalated (both short-circuit before lifecycle)', () => {
136
136
  createNode(node('t', { parent: 'mgr', lifecycle: 'terminal' }));
137
- assert.deepEqual(evaluateStop('t', { pushedFinal: true, askedHuman: false }), { action: 'allow', reason: 'finished' });
138
- assert.deepEqual(evaluateStop('t', { pushedFinal: false, askedHuman: true }), { action: 'allow', reason: 'escalated' });
137
+ assert.deepEqual(evaluateStop('t', { pushedFinal: true, requestedHumanReply: false }), { action: 'allow', reason: 'finished' });
138
+ assert.deepEqual(evaluateStop('t', { pushedFinal: false, requestedHumanReply: true }), { action: 'allow', reason: 'escalated' });
139
139
  });
140
140
  test('an active follow-up cycle with a canonical final is allowed without another final', () => {
141
141
  createNode(node('followUp', {
@@ -14,7 +14,7 @@ after(() => {
14
14
  rmSync(home, { recursive: true, force: true });
15
15
  });
16
16
  test('only a watchdog-stamped aborted settlement records an automatic connection fault', () => {
17
- const settleAbort = (nodeId, stagedWatchdogAbort) => {
17
+ const settleAbort = (nodeId, stagedWatchdogAbort, stagedRefreshAbort, stopReason = 'aborted') => {
18
18
  const session = {
19
19
  sessionManager: { getLeafId: () => 'leaf-entry' },
20
20
  model: undefined,
@@ -24,9 +24,14 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
24
24
  session,
25
25
  fallback: { blocked: false },
26
26
  stagedProviderAgentEnd: {
27
- messages: [{ role: 'assistant', stopReason: 'aborted', errorMessage: 'Request was aborted' }],
27
+ messages: [{
28
+ role: 'assistant',
29
+ stopReason,
30
+ errorMessage: stopReason === 'error' ? 'This operation was aborted' : 'Request was aborted',
31
+ }],
28
32
  },
29
33
  stagedWatchdogAbort,
34
+ stagedRefreshAbort,
30
35
  stagedOverflowFailure: null,
31
36
  executingTools: new Set(),
32
37
  };
@@ -38,15 +43,24 @@ test('only a watchdog-stamped aborted settlement records an automatic connection
38
43
  retry.publishSettledProviderFault(generation);
39
44
  return { retry, generation };
40
45
  };
41
- const watchdog = settleAbort('watchdog-node', true);
46
+ const watchdog = settleAbort('watchdog-node', true, false);
42
47
  const fault = readFault('watchdog-node');
43
48
  assert.equal(fault?.link, 'pi→provider');
44
49
  assert.equal(fault?.kind, 'connection');
45
50
  assert.deepEqual(fault?.retry, { disposition: 'auto', by: 'daemon' });
46
51
  assert.equal(watchdog.retry.isActiveAutoFault(fault), true);
47
52
  assert.equal(watchdog.generation.stagedWatchdogAbort, false, 'settlement consumes the one-shot provenance');
48
- settleAbort('human-node', false);
53
+ settleAbort('human-node', false, false);
49
54
  assert.equal(readFault('human-node'), null, 'an otherwise-identical human abort must not arm recovery');
55
+ // Pi 0.84 currently serializes a host abort from `node yield` as an `error`
56
+ // settlement. Only the refresh-abort provenance suppresses that provider-looking error.
57
+ const refresh = settleAbort('refresh-node', false, true, 'error');
58
+ assert.equal(readFault('refresh-node'), null, 'a refresh-aborted error settlement must not enter provider fault recovery');
59
+ assert.equal(refresh.generation.stagedRefreshAbort, false, 'settlement consumes the one-shot refresh provenance');
60
+ settleAbort('provider-node', false, false, 'error');
61
+ const providerFault = readFault('provider-node');
62
+ assert.equal(providerFault?.kind, 'other', 'the same provider error without refresh provenance still classifies');
63
+ assert.deepEqual(providerFault?.retry, { disposition: 'fatal' }, 'the same provider error without refresh provenance still records its fault');
50
64
  });
51
65
  test('a failed retry settlement carries the episode forward instead of restarting it', () => {
52
66
  // The episode marker a daemon retry records before re-prompting: original
@@ -68,6 +82,7 @@ test('a failed retry settlement carries the episode forward instead of restartin
68
82
  messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'fetch failed' }],
69
83
  },
70
84
  stagedWatchdogAbort: false,
85
+ stagedRefreshAbort: false,
71
86
  stagedOverflowFailure: null,
72
87
  executingTools: new Set(),
73
88
  };
@@ -77,6 +77,12 @@ export declare function jobStartedAtMs(jobId: string): number | undefined;
77
77
  * notice, the `node bash background` result/toast, and the attach cockpit)
78
78
  * shows it: `<60s -> "12s"`; `60s-59m -> "3m 40s"`; `>=60m -> "1h 04m"`. */
79
79
  export declare function formatBashElapsed(ms: number): string;
80
+ /** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
81
+ * injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
82
+ * literal first line names no command at all: skip leading assignment lines and
83
+ * return the first real one, whitespace-collapsed to a single line. A job whose
84
+ * command is nothing but assignments falls back to its first non-empty line. */
85
+ export declare function bashJobCommandLine(command: string): string;
80
86
  export interface BashJobStatus {
81
87
  jobId: string;
82
88
  /** Full text of cmd.sh. */
@@ -189,6 +189,16 @@ export function formatBashElapsed(ms) {
189
189
  const minutes = totalMinutes % 60;
190
190
  return `${hours}h ${String(minutes).padStart(2, '0')}m`;
191
191
  }
192
+ /** The command line worth SHOWING for a job. `cmd.sh` opens with the harness's
193
+ * injected environment preamble (`export CRTR_PREVIEW_RESULT_PATH=…`), so its
194
+ * literal first line names no command at all: skip leading assignment lines and
195
+ * return the first real one, whitespace-collapsed to a single line. A job whose
196
+ * command is nothing but assignments falls back to its first non-empty line. */
197
+ export function bashJobCommandLine(command) {
198
+ const lines = command.split('\n').map((line) => line.trim()).filter((line) => line !== '');
199
+ const real = lines.find((line) => !/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*=/.test(line));
200
+ return (real ?? lines[0] ?? '').replace(/\s+/g, ' ');
201
+ }
192
202
  /** Every live backgrounded bash job for this node — job.bg present, job.exit
193
203
  * absent — whether handed off by the menu, the valve deadline, or broker
194
204
  * termination. Read-only; never mutates. Skips malformed/partially-written
@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
4
4
  import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
- import { atomicWriteJson, pageManifestPath } from '../../human/convention.js';
7
+ import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../../human/convention.js';
8
8
  import { createNode } from '../canvas.js';
9
9
  import { closeDb } from '../db.js';
10
10
  import { nodeDir } from '../paths.js';
@@ -22,16 +22,19 @@ function meta(id, cwd, extra) {
22
22
  ...extra,
23
23
  };
24
24
  }
25
+ // A ticket only counts as attention when it routes a reply back, so the fixture binds
26
+ // the reply route the same way `human send` does before writing the page manifest.
25
27
  function seedPage(bridgeId) {
26
28
  const dir = nodeDir(bridgeId);
27
29
  mkdirSync(dir, { recursive: true });
30
+ bindTicketReplyRoute(dir, bridgeId);
28
31
  atomicWriteJson(pageManifestPath(dir), {
29
- schema: 'crtr.page/v1',
30
- dialect: 'md',
32
+ schema: 'crtr.page/v2',
33
+ dialect: 'jsx',
31
34
  title: bridgeId,
32
35
  subtitle: 'Acknowledge this ticket so the request can proceed.',
33
- placement: 'inline',
34
- document: 'page.md',
36
+ delivery: { placement: 'inline', inbox: true, reply: true },
37
+ document: 'page.tsx',
35
38
  steps: 1,
36
39
  slots: [],
37
40
  });
@@ -8,7 +8,7 @@
8
8
  // This is adversarial, not merely a clean-machine assertion: BEFORE calling
9
9
  // the render functions, the test seeds real, CONFLICTING local state for the
10
10
  // same node id `a` (a local focus, an active fault, telemetry, a goal file, a
11
- // session transcript, and a pending human ask) — every historically-broken
11
+ // session transcript, and a pending human reply) — every historically-broken
12
12
  // local read this suppression covers. If the production suppression ever
13
13
  // regressed back to consulting local disk/SQLite for a remote source, the
14
14
  // assertions below would observe the poisoned values instead of the safe
@@ -114,12 +114,12 @@ function seedPoisonedLocalState() {
114
114
  const askDir = nodeDir('poison-ask');
115
115
  mkdirSync(askDir, { recursive: true });
116
116
  atomicWriteJson(pageManifestPath(askDir), {
117
- schema: 'crtr.page/v1',
118
- dialect: 'md',
117
+ schema: 'crtr.page/v2',
118
+ dialect: 'jsx',
119
119
  title: 'poison',
120
120
  subtitle: 'This local ticket must not affect the remote rendering.',
121
- placement: 'inline',
122
- document: 'page.md',
121
+ delivery: { placement: 'inline', inbox: true, reply: true },
122
+ document: 'page.tsx',
123
123
  steps: 1,
124
124
  slots: [],
125
125
  });
@@ -1,15 +1,17 @@
1
1
  // attention.ts — pending human-ticket counters across the canvas.
2
2
  //
3
- // Every ticket is a direct child of the single registered `<canvas>/nodes` root,
4
- // named for its terminal human bridge node. The bridge's parent is the asking
5
- // node, so storage identity and attribution come from the canvas graph rather
6
- // than duplicated attribution fields in page/review data.
7
- import { basename } from 'node:path';
3
+ // Reply-bearing tickets carry an explicit bridge route. The bridge's parent is
4
+ // the asking node; standalone pages have no bridge and do not contribute to a
5
+ // node's pending-reply count.
8
6
  import { scanInbox } from '../human/scan.js';
9
7
  import { filterTerminalReviewTickets } from '../review/ticket-filter.js';
8
+ import { readTicketReplyRoute } from '../human/convention.js';
10
9
  import { getNode, listNodes, view } from './canvas.js';
11
10
  function askingNodeId(ticketDir) {
12
- const bridge = getNode(basename(ticketDir));
11
+ const replyRoute = readTicketReplyRoute(ticketDir);
12
+ if (replyRoute === null)
13
+ return null;
14
+ const bridge = getNode(replyRoute.bridge_node_id);
13
15
  if (bridge === null)
14
16
  return null;
15
17
  return bridge.parent ?? bridge.node_id;
@@ -1,6 +1,6 @@
1
1
  import type { CanvasSource } from '../source.js';
2
2
  import { type BindingId, type BindingResolution, type RawTerminalInput } from '../../keybindings/index.js';
3
- declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background"];
3
+ declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.profile", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus", "crtr.browse.focus-background", "crtr.browse.yank-id"];
4
4
  declare const BROWSE_SEARCH_ACTIONS: readonly ["crtr.browse.search.cancel", "crtr.browse.search.commit", "crtr.browse.search.delete", "crtr.browse.search.quit"];
5
5
  export declare function resolveBrowseNavAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_NAV_ACTIONS)[number] | null;
6
6
  export declare function resolveBrowseSearchAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_SEARCH_ACTIONS)[number] | null;
@@ -20,7 +20,7 @@ const BROWSE_NAV_ACTIONS = [
20
20
  'crtr.browse.tab-next', 'crtr.browse.tab-previous', 'crtr.browse.tab.1', 'crtr.browse.tab.2',
21
21
  'crtr.browse.tab.3', 'crtr.browse.tab.4', 'crtr.browse.tab.5', 'crtr.browse.close-subtree', 'crtr.browse.sort',
22
22
  'crtr.browse.scope', 'crtr.browse.profile', 'crtr.browse.residents', 'crtr.browse.preview', 'crtr.browse.search',
23
- 'crtr.browse.revive-now', 'crtr.browse.focus', 'crtr.browse.focus-background',
23
+ 'crtr.browse.revive-now', 'crtr.browse.focus', 'crtr.browse.focus-background', 'crtr.browse.yank-id',
24
24
  ];
25
25
  const BROWSE_SEARCH_ACTIONS = [
26
26
  'crtr.browse.search.cancel', 'crtr.browse.search.commit', 'crtr.browse.search.delete',
@@ -39,6 +39,7 @@ export function resolveBrowseSearchAction(bindings, raw) {
39
39
  // press restores the structural `tree` view, then relevance/recency, then back.
40
40
  const SORT_CYCLE = ['attention', 'tree', 'relevance', 'recency'];
41
41
  import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './render.js';
42
+ import { writeClipboardTextEverywhere } from '../../clipboard-text.js';
42
43
  /** Viewport (body) height = total rows minus the header renderFrame draws (see
43
44
  * render.ts headerHeight), the footer, and the preview panel when shown. Kept
44
45
  * in lockstep with render.ts via the shared headerHeight/PREVIEW_HEIGHT. */
@@ -102,7 +103,9 @@ export async function runBrowse(opts) {
102
103
  // canvas-wide because there is no truthful profile to select.
103
104
  const launchProfileId = opts.profileId;
104
105
  const state = {
105
- tab: 'All',
106
+ // Open on Live (active|idle): the navigator's job is getting you back to a
107
+ // node you are working with, and All buries those under every dormant row.
108
+ tab: 'Live',
106
109
  cursor: 0,
107
110
  // Initial collapse = every node with children → only roots/top-level show.
108
111
  collapsed: new Set([...tree.nodes.entries()].filter(([, n]) => n.childIds.length > 0).map(([id]) => id)),
@@ -115,6 +118,7 @@ export async function runBrowse(opts) {
115
118
  preview: true, // default ON (decision)
116
119
  residentsOnly: true, // default ON: hide one-shot workers (decision)
117
120
  pendingClose: null,
121
+ flash: null,
118
122
  pinned: loadPins(),
119
123
  help: false,
120
124
  rename: false,
@@ -242,7 +246,7 @@ export async function runBrowse(opts) {
242
246
  tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
243
247
  query: state.query, search: state.search, totalNodes,
244
248
  cwdScope: state.cwdScope, profileScope: state.profileScope, sort: state.sort, preview: state.preview,
245
- residentsOnly: state.residentsOnly, pendingClose: state.pendingClose,
249
+ residentsOnly: state.residentsOnly, pendingClose: state.pendingClose, flash: state.flash,
246
250
  pinned: state.pinned, help: state.help,
247
251
  rename: state.rename, renameBuf: state.renameBuf,
248
252
  message: state.message, messageBuf: state.messageBuf,
@@ -542,6 +546,8 @@ export async function runBrowse(opts) {
542
546
  const onKeyNav = (input, key) => {
543
547
  const raw = { input, ...key };
544
548
  const action = resolveBrowseNavAction(bindings, raw);
549
+ // Any keystroke retires the previous one-shot footer confirmation.
550
+ state.flash = null;
545
551
  // Unbound ctrl-combos are swallowed so they never masquerade as printable commands.
546
552
  if (key.ctrl && action === null)
547
553
  return;
@@ -756,6 +762,18 @@ export async function runBrowse(opts) {
756
762
  scheduleFlush();
757
763
  return;
758
764
  }
765
+ // Yank the hovered node's id to the system clipboard, so it can be pasted into
766
+ // a prompt or another command without retyping it off the screen.
767
+ if (action === 'crtr.browse.yank-id') {
768
+ if (row !== undefined) {
769
+ const { system, tmux } = writeClipboardTextEverywhere(row.id);
770
+ state.flash = system || tmux
771
+ ? `Copied ${row.id}${system ? '' : ' to the tmux buffer'}`
772
+ : 'No clipboard tool available — nothing copied';
773
+ }
774
+ scheduleFlush();
775
+ return;
776
+ }
759
777
  // Resume the selected node.
760
778
  if (action === 'crtr.browse.focus') {
761
779
  if (row !== undefined)
@@ -11,8 +11,14 @@ export type SortMode = 'tree' | 'attention' | 'relevance' | 'recency';
11
11
  export declare const SORTS: readonly SortMode[];
12
12
  /** Does a node belong to this tab's slice?
13
13
  * All — every node.
14
- * Live — active | idle.
15
- * Dormant — done | dead | canceled.
14
+ * Live — what you can pick up right now: active | idle, PLUS a resident
15
+ * agent that stopped without finishing (its broker exited, but
16
+ * Enter resumes the conversation in place — see `isFinishedWork`).
17
+ * Finished work never appears here.
18
+ * Dormant — done | dead | canceled: literally "no engine running". A stopped
19
+ * resident agent is deliberately on BOTH tabs — it is resumable
20
+ * (Live) and it is not running (Dormant); both are true and each
21
+ * tab is the one a different search starts from.
16
22
  * Attention — needs a human: has pending asks OR is parked on a fault.
17
23
  * Pinned — user-starred (a local, view-only preference; `isPinned`). */
18
24
  export declare function tabPredicate(tab: Tab, row: DashboardRow, isPinned?: boolean): boolean;