@crouton-kit/crouter 0.3.28 → 0.3.29

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 (322) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +5 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  49. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  50. package/dist/clients/attach/attach-cmd.js +282 -46
  51. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  52. package/dist/clients/attach/auth-pickers.js +194 -0
  53. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  54. package/dist/clients/attach/canvas-panels.js +45 -17
  55. package/dist/clients/attach/chat-view.d.ts +27 -2
  56. package/dist/clients/attach/chat-view.js +98 -9
  57. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  58. package/dist/clients/attach/clipboard-image.js +150 -26
  59. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  60. package/dist/clients/attach/clipboard-text.js +39 -0
  61. package/dist/clients/attach/config-load.d.ts +54 -7
  62. package/dist/clients/attach/config-load.js +99 -3
  63. package/dist/clients/attach/context-message.d.ts +12 -0
  64. package/dist/clients/attach/context-message.js +72 -0
  65. package/dist/clients/attach/git-info.d.ts +20 -0
  66. package/dist/clients/attach/git-info.js +44 -0
  67. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  68. package/dist/clients/attach/graph-overlay.js +59 -19
  69. package/dist/clients/attach/input-controller.d.ts +75 -13
  70. package/dist/clients/attach/input-controller.js +305 -67
  71. package/dist/clients/attach/pickers.d.ts +58 -0
  72. package/dist/clients/attach/pickers.js +171 -0
  73. package/dist/clients/attach/slash-commands.d.ts +22 -0
  74. package/dist/clients/attach/slash-commands.js +91 -25
  75. package/dist/clients/attach/titled-editor.d.ts +33 -0
  76. package/dist/clients/attach/titled-editor.js +84 -0
  77. package/dist/clients/attach/view-socket.d.ts +18 -1
  78. package/dist/clients/attach/view-socket.js +70 -1
  79. package/dist/clients/web/dev-server.d.ts +7 -0
  80. package/dist/clients/web/dev-server.js +59 -0
  81. package/dist/clients/web/events.d.ts +14 -0
  82. package/dist/clients/web/events.js +151 -0
  83. package/dist/clients/web/server.d.ts +18 -0
  84. package/dist/clients/web/server.js +450 -0
  85. package/dist/clients/web/web-cmd.d.ts +2 -0
  86. package/dist/clients/web/web-cmd.js +120 -0
  87. package/dist/commands/canvas.js +1 -2
  88. package/dist/commands/chord.js +1 -1
  89. package/dist/commands/dashboard.js +5 -1
  90. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  91. package/dist/commands/memory/lint.js +14 -2
  92. package/dist/commands/memory/read.js +2 -2
  93. package/dist/commands/memory/shared.d.ts +7 -1
  94. package/dist/commands/memory/shared.js +18 -3
  95. package/dist/commands/memory/write.js +8 -4
  96. package/dist/commands/memory.js +4 -4
  97. package/dist/commands/node.d.ts +1 -1
  98. package/dist/commands/node.js +64 -65
  99. package/dist/commands/pkg.js +1 -2
  100. package/dist/commands/revive.js +11 -3
  101. package/dist/commands/sys/sync.d.ts +1 -0
  102. package/dist/commands/sys/sync.js +187 -0
  103. package/dist/commands/sys.js +3 -2
  104. package/dist/commands/view-cycle.js +2 -2
  105. package/dist/commands/view-list.js +8 -8
  106. package/dist/commands/view-new.js +21 -17
  107. package/dist/commands/view-pick.js +1 -1
  108. package/dist/commands/view-run.js +35 -14
  109. package/dist/commands/view.js +8 -6
  110. package/dist/commands/workspace.d.ts +2 -0
  111. package/dist/commands/workspace.js +161 -0
  112. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  114. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  116. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  117. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  118. package/dist/core/__tests__/close.test.js +14 -20
  119. package/dist/core/__tests__/context-intro.test.js +19 -19
  120. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  121. package/dist/core/__tests__/draw-style.test.js +1 -1
  122. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  123. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  124. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  125. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  126. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  127. package/dist/core/__tests__/focuses.test.js +25 -13
  128. package/dist/core/__tests__/fork.test.js +22 -5
  129. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  130. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  131. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  133. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  135. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  136. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  137. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  138. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  139. package/dist/core/__tests__/helpers/harness.js +19 -36
  140. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  141. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  142. package/dist/core/__tests__/lifecycle.test.js +1 -19
  143. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  144. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  145. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  146. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  147. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  148. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  149. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  150. package/dist/core/__tests__/reset.test.js +19 -79
  151. package/dist/core/__tests__/steer-note.test.js +9 -7
  152. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  153. package/dist/core/bootstrap.d.ts +0 -3
  154. package/dist/core/bootstrap.js +1 -143
  155. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  156. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  157. package/dist/core/canvas/browse/app.js +131 -8
  158. package/dist/core/canvas/browse/model.d.ts +23 -5
  159. package/dist/core/canvas/browse/model.js +68 -13
  160. package/dist/core/canvas/browse/render.d.ts +7 -1
  161. package/dist/core/canvas/browse/render.js +178 -47
  162. package/dist/core/canvas/canvas.js +1 -1
  163. package/dist/core/canvas/focuses.d.ts +4 -4
  164. package/dist/core/canvas/focuses.js +7 -7
  165. package/dist/core/canvas/nav-model.js +8 -7
  166. package/dist/core/canvas/render.d.ts +68 -9
  167. package/dist/core/canvas/render.js +195 -61
  168. package/dist/core/canvas/types.d.ts +16 -23
  169. package/dist/core/config.js +1 -4
  170. package/dist/core/memory-resolver.js +43 -5
  171. package/dist/core/runtime/bearings.d.ts +1 -1
  172. package/dist/core/runtime/bearings.js +8 -8
  173. package/dist/core/runtime/branded-host.d.ts +16 -0
  174. package/dist/core/runtime/branded-host.js +127 -0
  175. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  176. package/dist/core/runtime/broker.d.ts +4 -1
  177. package/dist/core/runtime/broker.js +494 -58
  178. package/dist/core/runtime/close.d.ts +12 -2
  179. package/dist/core/runtime/close.js +37 -16
  180. package/dist/core/runtime/front-door.js +6 -14
  181. package/dist/core/runtime/host.d.ts +14 -34
  182. package/dist/core/runtime/host.js +14 -50
  183. package/dist/core/runtime/launch.d.ts +12 -9
  184. package/dist/core/runtime/launch.js +27 -15
  185. package/dist/core/runtime/lifecycle.d.ts +1 -1
  186. package/dist/core/runtime/lifecycle.js +15 -19
  187. package/dist/core/runtime/nodes.d.ts +0 -55
  188. package/dist/core/runtime/nodes.js +6 -74
  189. package/dist/core/runtime/placement.d.ts +91 -335
  190. package/dist/core/runtime/placement.js +262 -840
  191. package/dist/core/runtime/promote.d.ts +2 -0
  192. package/dist/core/runtime/promote.js +24 -6
  193. package/dist/core/runtime/recap.d.ts +8 -0
  194. package/dist/core/runtime/recap.js +107 -0
  195. package/dist/core/runtime/recycle.js +25 -61
  196. package/dist/core/runtime/reset.d.ts +43 -37
  197. package/dist/core/runtime/reset.js +131 -218
  198. package/dist/core/runtime/revive.d.ts +9 -29
  199. package/dist/core/runtime/revive.js +40 -139
  200. package/dist/core/runtime/spawn.d.ts +15 -16
  201. package/dist/core/runtime/spawn.js +165 -194
  202. package/dist/core/runtime/surface-bg.d.ts +11 -0
  203. package/dist/core/runtime/surface-bg.js +68 -0
  204. package/dist/core/runtime/tmux.d.ts +13 -50
  205. package/dist/core/runtime/tmux.js +33 -87
  206. package/dist/core/scope.d.ts +1 -2
  207. package/dist/core/scope.js +1 -5
  208. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  209. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  210. package/dist/core/skill-sync/builtins.d.ts +42 -0
  211. package/dist/core/skill-sync/builtins.js +112 -0
  212. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  213. package/dist/core/skill-sync/claude-plugins.js +71 -0
  214. package/dist/core/skill-sync/engine.d.ts +42 -0
  215. package/dist/core/skill-sync/engine.js +633 -0
  216. package/dist/core/skill-sync/export.d.ts +23 -0
  217. package/dist/core/skill-sync/export.js +86 -0
  218. package/dist/core/skill-sync/manifest.d.ts +64 -0
  219. package/dist/core/skill-sync/manifest.js +181 -0
  220. package/dist/core/skill-sync/profile.d.ts +76 -0
  221. package/dist/core/skill-sync/profile.js +173 -0
  222. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  223. package/dist/core/skill-sync/snapshot.js +120 -0
  224. package/dist/core/substrate/index.d.ts +1 -1
  225. package/dist/core/substrate/index.js +1 -1
  226. package/dist/core/substrate/injected-store.js +3 -3
  227. package/dist/core/substrate/on-read.js +68 -6
  228. package/dist/core/substrate/render.d.ts +8 -11
  229. package/dist/core/substrate/render.js +29 -43
  230. package/dist/core/substrate/schema.d.ts +10 -3
  231. package/dist/core/substrate/schema.js +6 -3
  232. package/dist/core/tui/host.d.ts +12 -4
  233. package/dist/core/tui/host.js +280 -149
  234. package/dist/core/view/bridge.d.ts +10 -0
  235. package/dist/core/view/bridge.js +31 -0
  236. package/dist/core/view/chrome.d.ts +9 -0
  237. package/dist/core/view/chrome.js +22 -0
  238. package/dist/core/view/contract.d.ts +171 -0
  239. package/dist/core/view/contract.js +23 -0
  240. package/dist/core/view/loader.d.ts +31 -0
  241. package/dist/core/view/loader.js +188 -0
  242. package/dist/core/view/transport-local.d.ts +7 -0
  243. package/dist/core/view/transport-local.js +70 -0
  244. package/dist/core/view/transport.d.ts +4 -0
  245. package/dist/core/view/transport.js +15 -0
  246. package/dist/daemon/crtrd.d.ts +38 -27
  247. package/dist/daemon/crtrd.js +303 -376
  248. package/dist/daemon/manage.js +6 -1
  249. package/dist/index.js +1 -1
  250. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  251. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  252. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  253. package/dist/pi-extensions/canvas-nav.js +5 -0
  254. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  255. package/dist/pi-extensions/canvas-recap.js +236 -0
  256. package/dist/pi-extensions/canvas-resume.js +2 -1
  257. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  258. package/dist/pi-extensions/canvas-stophook.js +87 -101
  259. package/dist/pi-extensions/canvas-view.js +2 -1
  260. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  261. package/dist/pi-extensions/widget-order-bus.js +34 -0
  262. package/dist/prompts/view.d.ts +2 -2
  263. package/dist/prompts/view.js +145 -73
  264. package/dist/types.d.ts +1 -6
  265. package/dist/types.js +1 -3
  266. package/dist/web/ViewChrome.d.ts +7 -0
  267. package/dist/web/ViewChrome.js +28 -0
  268. package/dist/web/ViewPane.d.ts +39 -0
  269. package/dist/web/ViewPane.js +48 -0
  270. package/dist/web/index.d.ts +6 -0
  271. package/dist/web/index.js +16 -0
  272. package/dist/web/runtime.d.ts +39 -0
  273. package/dist/web/runtime.js +133 -0
  274. package/dist/web/states.d.ts +24 -0
  275. package/dist/web/states.js +24 -0
  276. package/dist/web/transport-http.d.ts +5 -0
  277. package/dist/web/transport-http.js +28 -0
  278. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  279. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  280. package/dist/web-client/index.html +13 -0
  281. package/package.json +20 -6
  282. package/dist/builtin-views/canvas/client.mjs +0 -303
  283. package/dist/builtin-views/canvas/view.mjs +0 -576
  284. package/dist/builtin-views/git-pr/client.mjs +0 -440
  285. package/dist/builtin-views/git-pr/view.mjs +0 -675
  286. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  287. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  288. package/dist/builtin-views/inbox/view.mjs +0 -889
  289. package/dist/builtin-views/linkedin/client.mjs +0 -610
  290. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  291. package/dist/commands/pkg/bridge.d.ts +0 -1
  292. package/dist/commands/pkg/bridge.js +0 -137
  293. package/dist/commands/skill/author.d.ts +0 -3
  294. package/dist/commands/skill/author.js +0 -140
  295. package/dist/commands/skill/shared.d.ts +0 -3
  296. package/dist/commands/skill/shared.js +0 -19
  297. package/dist/commands/skill.d.ts +0 -2
  298. package/dist/commands/skill.js +0 -21
  299. package/dist/commands/tmux-spread.d.ts +0 -2
  300. package/dist/commands/tmux-spread.js +0 -144
  301. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  302. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  303. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  304. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  305. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  306. package/dist/core/__tests__/home-session.test.js +0 -186
  307. package/dist/core/__tests__/relaunch.test.js +0 -335
  308. package/dist/core/bridge-map.d.ts +0 -19
  309. package/dist/core/bridge-map.js +0 -73
  310. package/dist/core/tui/contract.d.ts +0 -83
  311. package/dist/core/tui/contract.js +0 -8
  312. package/dist/core/tui/loader.d.ts +0 -16
  313. package/dist/core/tui/loader.js +0 -94
  314. package/dist/prompts/skill.d.ts +0 -2
  315. package/dist/prompts/skill.js +0 -650
  316. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  317. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  318. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -1,14 +1,14 @@
1
1
  // Run with: node --import tsx/esm --test src/core/__tests__/reset.test.ts
2
+ //
3
+ // resetRoot now handles ONLY the non-root child `/new` (a session-id refresh).
4
+ // A root's `/new` mints a fresh node via relaunchRoot — see relaunch-root.test.ts.
2
5
  import { test, before, after, beforeEach } from 'node:test';
3
6
  import assert from 'node:assert/strict';
4
- import { mkdtempSync, rmSync, existsSync, writeFileSync } from 'node:fs';
7
+ import { mkdtempSync, rmSync } from 'node:fs';
5
8
  import { tmpdir } from 'node:os';
6
9
  import { join } from 'node:path';
7
- import { createNode, getNode, subscribe, setStatus, subscriptionsOf, view, } from '../canvas/canvas.js';
8
- import { openFocusRow, getFocusByNode } from '../canvas/focuses.js';
10
+ import { createNode, getNode, subscribe } from '../canvas/canvas.js';
9
11
  import { closeDb } from '../canvas/db.js';
10
- import { reportsDir, inboxPath } from '../canvas/paths.js';
11
- import { roadmapPath } from '../runtime/roadmap.js';
12
12
  import { resetRoot } from '../runtime/reset.js';
13
13
  let home;
14
14
  function node(id, over = {}) {
@@ -37,76 +37,29 @@ after(() => {
37
37
  rmSync(home, { recursive: true, force: true });
38
38
  delete process.env['CRTR_HOME'];
39
39
  });
40
- test('resetRoot empties the root view, reaps descendants, and wipes working state', () => {
41
- // root → child → grandchild (mirrors a parent that subscribes to its workers)
42
- createNode(node('root', { parent: null, lifecycle: 'resident', mode: 'orchestrator', pi_session_id: 'old-sess' }));
43
- createNode(node('child', { parent: 'root' }));
44
- createNode(node('grand', { parent: 'child' }));
45
- subscribe('root', 'child', true);
46
- subscribe('child', 'grand', true);
47
- // Root accumulated working state.
48
- writeFileSync(roadmapPath('root'), '# Roadmap\nold goal\n');
49
- writeFileSync(inboxPath('root'), '{"ts":"x","from":"child","tier":"normal","kind":"update","label":"hi"}\n');
50
- writeFileSync(join(reportsDir('root'), '20260101T000000-update.md'), 'stale report');
51
- assert.equal(view('root').length, 2, 'precondition: root sees 2 descendants');
52
- const res = resetRoot('root', 'new-sess', '/abs/sessions/new.jsonl');
53
- assert.equal(res.reset, true);
54
- assert.deepEqual(res.detached, ['child'], 'root detaches its direct subscription');
55
- assert.deepEqual(res.reaped.sort(), ['child', 'grand'], 'whole sub-DAG reaped');
56
- // Graph is empty from the root's view.
57
- assert.equal(view('root').length, 0, 'root view is empty after reset');
58
- assert.equal(subscriptionsOf('root').length, 0, 'no outgoing edges remain');
59
- // Descendants are CANCELED (A5, human-confirmed 2026-06-06): an externally-
60
- // reaped node — via reset/relaunch OR close — did not finish its OWN work, so
61
- // it unifies on `canceled`; `done` is reserved for finalize. Daemon skips them.
62
- assert.equal(getNode('child')?.status, 'canceled');
63
- assert.equal(getNode('grand')?.status, 'canceled');
64
- // Regression: a reset-reaped descendant ends EXACTLY {canceled, null} — byte-
65
- // for-byte identical to the close path (mirrors cascade-close.test.ts's tuple).
66
- assert.deepEqual({ status: getNode('grand')?.status, intent: getNode('grand')?.intent ?? null }, { status: 'canceled', intent: null }, 'reset-reaped descendant: (status,intent) === (canceled, null), same as close');
67
- // Working state wiped.
68
- assert.equal(existsSync(roadmapPath('root')), false, 'roadmap wiped');
69
- assert.equal(existsSync(inboxPath('root')), false, 'inbox wiped');
70
- // Root reset to a pristine base resident, rebound to the new session id.
71
- const root = getNode('root');
72
- assert.equal(root?.mode, 'base');
73
- assert.equal(root?.lifecycle, 'resident');
74
- assert.equal(root?.status, 'active');
75
- assert.equal(root?.intent, null);
76
- assert.equal(root?.pi_session_id, 'new-sess');
77
- assert.equal(root?.pi_session_file, '/abs/sessions/new.jsonl', 'session FILE rebound too');
78
- assert.ok(root?.launch, 'a fresh base launch spec was written');
79
- });
80
- test('resetRoot on a non-root only refreshes the session id (no reap)', () => {
40
+ test('resetRoot on a non-root child refreshes the session id (no reap, no reset)', () => {
81
41
  createNode(node('root', { parent: null }));
82
42
  createNode(node('child', { parent: 'root', pi_session_id: 'old' }));
83
43
  subscribe('root', 'child', true);
84
- subscribe('child', 'root', false); // contrived: ensure child has an outgoing edge
85
44
  const res = resetRoot('child', 'fresh', '/abs/sessions/fresh.jsonl');
86
- assert.equal(res.reset, false, 'a non-root is not a graph reset');
45
+ assert.equal(res.reset, false, 'a child `/new` is not a graph reset');
87
46
  assert.deepEqual(res.reaped, []);
88
47
  assert.deepEqual(res.detached, []);
89
- assert.equal(getNode('child')?.pi_session_id, 'fresh', 'session id still refreshed');
48
+ assert.equal(getNode('child')?.pi_session_id, 'fresh', 'session id refreshed');
90
49
  assert.equal(getNode('child')?.pi_session_file, '/abs/sessions/fresh.jsonl', 'session FILE refreshed too');
91
50
  assert.equal(getNode('child')?.status, 'active', 'child not reaped');
92
- // The root that subscribes to the child is untouched.
93
- assert.equal(getNode('root')?.status, 'active');
51
+ assert.equal(getNode('root')?.status, 'active', 'the subscribing root is untouched');
94
52
  });
95
- test('Step 7: resetRoot reaps a FOCUSED descendant through tearDownNode (closes its focus row + nulls presence)', () => {
96
- createNode(node('root', { parent: null, lifecycle: 'resident', mode: 'orchestrator' }));
97
- createNode(node('desc', { parent: 'root', pane: '%d' }));
98
- subscribe('root', 'desc', true);
99
- openFocusRow('fD', '%d', 'Suser', 'desc');
100
- resetRoot('root', 'new-sess');
101
- // reapDescendants now routes each descendant through tearDownNode, so a focused
102
- // descendant's focus row is closed and its LOCATION nulled. Non-vacuous:
103
- // pre-Step-7 reap used closeWindow and never touched the focuses table, so
104
- // getFocusByNode('desc') would still return fD.
105
- assert.equal(getFocusByNode('desc'), null, 'descendant focus row closed by tearDownNode');
106
- const d = getNode('desc');
107
- assert.equal(d.status, 'canceled', 'descendant reaped (canceled — A5 unified)');
108
- assert.equal(d.pane ?? null, null, 'descendant pane nulled');
109
- assert.equal(d.tmux_session ?? null, null, 'descendant session nulled');
53
+ test('resetRoot on a ROOT is a no-op (roots route to relaunchRoot, not here)', () => {
54
+ createNode(node('root', { parent: null, lifecycle: 'resident', pi_session_id: 'keep' }));
55
+ createNode(node('child', { parent: 'root' }));
56
+ subscribe('root', 'child', true);
57
+ const res = resetRoot('root', 'new-sess');
58
+ assert.equal(res.reset, false);
59
+ assert.deepEqual(res.reaped, [], 'a root `/new` reaps nothing here');
60
+ assert.equal(getNode('root')?.status, 'active', 'root left untouched');
61
+ assert.equal(getNode('root')?.pi_session_id, 'keep', 'a root session id is NOT refreshed here');
62
+ assert.equal(getNode('child')?.status, 'active', 'descendant left untouched');
110
63
  });
111
64
  test('resetRoot is a no-op for an unknown node', () => {
112
65
  const res = resetRoot('ghost', 'x');
@@ -114,16 +67,3 @@ test('resetRoot is a no-op for an unknown node', () => {
114
67
  assert.deepEqual(res.reaped, []);
115
68
  assert.deepEqual(res.detached, []);
116
69
  });
117
- test('reaped descendants keep their meta on disk (orphaned, not deleted)', () => {
118
- createNode(node('root', { parent: null }));
119
- createNode(node('child', { parent: 'root' }));
120
- subscribe('root', 'child', true);
121
- setStatus('child', 'idle');
122
- resetRoot('root', 'new');
123
- // The node record persists (we detach + mark canceled, we don't delete the node).
124
- const child = getNode('child');
125
- assert.ok(child, 'child meta still on disk');
126
- assert.equal(child?.status, 'canceled');
127
- // It is just unreachable from the root.
128
- assert.equal(view('root').length, 0);
129
- });
@@ -18,16 +18,18 @@ test('an orchestrator (resident) gets the same roadmap/yield steering as a termi
18
18
  const r = steerNote(150_000, 'resident', 'orchestrator');
19
19
  assert.equal(r, t, 'mode drives the message, not lifecycle');
20
20
  });
21
- test('a terminal BASE worker is steered to promote / push final', () => {
21
+ test('a terminal BASE worker is steered to yield-or-finish, never at a bare promote', () => {
22
22
  const msg = steerNote(170_000, 'terminal', 'base');
23
- assert.match(msg, /node promote/, 'a base worker is told it can promote');
24
- assert.match(msg, /push final/, 'and to finish with push final when nearly done');
25
- assert.doesNotMatch(msg, /roadmap\.md/, 'a base worker has no roadmap to checkpoint');
23
+ assert.match(msg, /node yield/, 'a base worker is told to yield to continue (which auto-promotes under the hood)');
24
+ assert.match(msg, /push final/, 'and to finish with push final when close');
25
+ assert.doesNotMatch(msg, /node promote/, 'never the heavy "become an orchestrator" framing that bred yield-avoidance');
26
+ assert.doesNotMatch(msg, /roadmap\.md/, 'a base worker has no roadmap.md to checkpoint');
26
27
  });
27
- test('a resident BASE root is steered to promote-or-wrap-up, never at a roadmap', () => {
28
+ test('a resident BASE root is steered to yield-or-wrap-up, never at a bare promote or roadmap', () => {
28
29
  const msg = steerNote(150_000, 'resident', 'base');
29
- assert.match(msg, /node promote/, 'a root growing into a job is told to promote');
30
- assert.doesNotMatch(msg, /roadmap\.md/, 'a root has no roadmap to point at');
30
+ assert.match(msg, /node yield/, 'a root growing into a job is told to yield (which auto-promotes)');
31
+ assert.doesNotMatch(msg, /node promote/, 'never the heavy "become an orchestrator" framing');
32
+ assert.doesNotMatch(msg, /roadmap\.md/, 'a root has no roadmap.md to point at');
31
33
  assert.doesNotMatch(msg, /push final/, 'a resident node never finishes with push final');
32
34
  });
33
35
  test('below the first band there is no nudge boundary issue — pushy escalation kicks in at 185k', () => {
@@ -3,8 +3,10 @@
3
3
  // STEP 2 of the placement/focus migration: guards on the tmux Surface (driver).
4
4
  //
5
5
  // 1. The §2.2 HARD DRIVER INVARIANT (GREEN now): every create/placement verb
6
- // in the driver — new-window / split-window / swap-pane / break-pane /
7
- // join-pane / move-pane / respawn-pane — MUST pass an explicit `-t` target.
6
+ // in the driver — after the broker-host cut gutted the engine-in-pane
7
+ // relocate verbs (U9 deleted swap-pane/break-pane/join-pane/move-pane),
8
+ // the survivors are new-window / split-window / respawn-pane — MUST pass an
9
+ // explicit `-t` target.
8
10
  // Omitting `-t` lets tmux resolve against its GLOBAL current session, which
9
11
  // can leak a pane into a user session — the exact unbidden-window bug this
10
12
  // redesign kills. This guards the bug's blast radius and should pass today.
@@ -21,7 +23,10 @@ import { dirname, join, basename } from 'node:path';
21
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
24
  const SRC_ROOT = join(__dirname, '..', '..'); // .../src
23
25
  const TMUX_TS = join(SRC_ROOT, 'core', 'runtime', 'tmux.ts');
24
- /** The placement verbs the §2.2 invariant governs. */
26
+ /** The placement verbs the §2.2 invariant governs. (swap-pane/break-pane/
27
+ * join-pane/move-pane were deleted with the engine-in-pane host in the broker
28
+ * cut; kept in the match set so a regression that re-introduces an untargeted
29
+ * one is still caught.) */
25
30
  const PLACEMENT_VERBS = [
26
31
  'new-window',
27
32
  'split-window',
@@ -67,8 +72,9 @@ test('§2.2 driver invariant: every placement verb in tmux.ts passes an explicit
67
72
  `instance of the unbidden-window bug (§2.2). Offending args array:\n${arr}`);
68
73
  }
69
74
  // Sanity: the driver really does contain placement verbs (so a refactor that
70
- // renames them can't make this assertion vacuously pass).
71
- assert.ok(found >= 4, `expected to scan ≥4 placement verbs, saw ${found}`);
75
+ // renames them can't make this assertion vacuously pass). The broker cut left
76
+ // three (new-window / split-window / respawn-pane).
77
+ assert.ok(found >= 3, `expected to scan ≥3 placement verbs, saw ${found}`);
72
78
  });
73
79
  // ---------------------------------------------------------------------------
74
80
  // Lint guard — §5.1, ENFORCED. The driver (tmux.ts) is imported ONLY by
@@ -92,8 +98,8 @@ function allTsFiles(dir) {
92
98
  const ALLOWED_IMPORTERS = new Set(['tmux.ts', 'placement.ts', 'tmux-chrome.ts']);
93
99
  function importsDriver(file) {
94
100
  const src = readFileSync(file, 'utf8');
95
- // A specifier whose basename is exactly `tmux.js` (so `tmux-chrome.js` and
96
- // `tmux-spread.js` are NOT matched). Covers `from '...'` and `import('...')`.
101
+ // A specifier whose basename is exactly `tmux.js` (so `tmux-chrome.js` is NOT
102
+ // matched). Covers `from '...'` and `import('...')`.
97
103
  return [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].some((m) => basename(m[1]) === 'tmux.js');
98
104
  }
99
105
  test('§5.1 lint: only placement.ts / tmux-chrome.ts import the tmux driver', () => {
@@ -1,8 +1,5 @@
1
- import type { RootDef } from './command.js';
2
1
  export declare const OFFICIAL_MARKETPLACE_NAME = "crouter-official-marketplace";
3
2
  export declare const OFFICIAL_MARKETPLACE_URL = "https://github.com/crouton-labs/crouter-official-marketplace.git";
4
3
  export declare const OFFICIAL_MARKETPLACE_REF = "main";
5
- export declare function ensureBootSkill(argv: string[]): void;
6
- export declare function ensureSlashCommands(root: RootDef, argv: string[]): void;
7
4
  export declare function ensureOfficialMarketplace(argv: string[]): void;
8
5
  export declare function ensureProjectScope(argv: string[]): void;
@@ -1,12 +1,10 @@
1
- import { writeFileSync } from 'node:fs';
2
1
  import { homedir } from 'node:os';
3
2
  import { join } from 'node:path';
4
3
  import { findProjectScopeRoot, resetScopeCache, userScopeRoot } from './scope.js';
5
- import { ensureDir, pathExists, readText, removePath, nowIso } from './fs-utils.js';
4
+ import { ensureDir, pathExists, removePath, nowIso } from './fs-utils.js';
6
5
  import { readConfig, readState, updateConfig, updateState, ensureScopeInitialized } from './config.js';
7
6
  import { clone } from './git.js';
8
7
  import { readMarketplaceManifest } from './manifest.js';
9
- import { collectSlashSpecs } from './command.js';
10
8
  import { CRTR_DIR_NAME } from '../types.js';
11
9
  export const OFFICIAL_MARKETPLACE_NAME = 'crouter-official-marketplace';
12
10
  export const OFFICIAL_MARKETPLACE_URL = 'https://github.com/crouton-labs/crouter-official-marketplace.git';
@@ -24,146 +22,6 @@ function shouldSkipForArgv(argv) {
24
22
  return true;
25
23
  return SKIP_SUBCOMMANDS.has(sub);
26
24
  }
27
- const BOOT_SKILL_NAME = 'crtr-skills';
28
- const BOOT_SKILL_MARKER = '<!-- crtr-boot-skill v2 -->';
29
- const BOOT_SKILL_MARKER_PREFIX = '<!-- crtr-boot-skill v';
30
- function bootSkillBody() {
31
- return `---
32
- name: crtr-skills
33
- description: 'Author, list, search, and load crtr memory documents (skills, references, preferences) via the crtr CLI. These are durable agent memory — markdown future LLM sessions load on demand. Use when the user wants to remember/save knowledge, build a context primer, or recall a previously saved doc. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".'
34
- argument-hint: [topic or verb]
35
- ---
36
-
37
- ${BOOT_SKILL_MARKER}
38
-
39
- # /crtr-skills — crtr memory router
40
-
41
- crtr memory documents = durable agent memory (kind: skill, reference, or
42
- preference). Written for **future LLM sessions**, not the user. \`crtr memory\`
43
- is the surface — author, discover, and load them.
44
-
45
- ## Route by intent
46
-
47
- - **Capture** ("save", "remember", "build context for", "make a skill"):
48
- \`crtr memory write <name> --kind <skill|reference|preference>\` with the
49
- body piped on stdin — e.g.
50
- \`crtr memory write $ARGUMENTS --kind skill <<'EOF' … EOF\`. Run
51
- \`crtr memory write -h\` for the full frontmatter schema
52
- (--when-and-why-to-read, --short-form, --scope, visibility rungs), then
53
- \`crtr memory lint\` to validate what you wrote.
54
- - **Find** ("what do we have on X"): \`crtr memory find "$ARGUMENTS"\` →
55
- \`crtr memory read <name>\` on the best hit (add --body/--grep to search
56
- bodies).
57
- - **Load by name**: \`crtr memory read <name>\`.
58
- - **List all**: \`crtr memory list\`.
59
-
60
- If \`$ARGUMENTS\` is empty, ask the user what they want before running.
61
-
62
- ## Rules
63
-
64
- - CLI stdout is the prompt — act on it, don't paraphrase to the user.
65
- - Append \`-h\` at any leaf (\`crtr memory write -h\`) for its full schema
66
- before authoring.
67
- - If \`crtr\` is not on PATH, tell the user and stop.
68
- `;
69
- }
70
- export function ensureBootSkill(argv) {
71
- try {
72
- if (process.env.CRTR_NO_BOOT_SKILL === '1')
73
- return;
74
- if (shouldSkipForArgv(argv))
75
- return;
76
- const claudeSkillsRoot = join(homedir(), '.claude', 'skills');
77
- // Only install if the user actually uses Claude Code (the dir exists or is
78
- // creatable). We won't create ~/.claude itself; that's not our directory.
79
- if (!pathExists(join(homedir(), '.claude')))
80
- return;
81
- const skillDir = join(claudeSkillsRoot, BOOT_SKILL_NAME);
82
- const skillFile = join(skillDir, 'SKILL.md');
83
- if (pathExists(skillFile)) {
84
- const existing = readText(skillFile);
85
- // If the user customized (no boot-skill marker at all), don't clobber.
86
- if (!existing.includes(BOOT_SKILL_MARKER_PREFIX))
87
- return;
88
- // Any marker version present → roll forward to current. Skip if identical.
89
- if (existing === bootSkillBody())
90
- return;
91
- }
92
- ensureDir(skillDir);
93
- writeFileSync(skillFile, bootSkillBody(), 'utf8');
94
- }
95
- catch (e) {
96
- if (process.env.CRTR_DEBUG === '1') {
97
- const msg = e instanceof Error ? e.message : String(e);
98
- process.stderr.write(`crtr: boot-skill error: ${msg}\n`);
99
- }
100
- }
101
- }
102
- // ---------------------------------------------------------------------------
103
- // Slash commands (editor prompt templates) auto-installed for opted-in nodes.
104
- //
105
- // Any command that declares a `slash` SlashSpec is rendered to a markdown
106
- // template and dropped into the host's command dirs on each crtr run — pi reads
107
- // `~/.pi/agent/prompts/<name>.md`, Claude Code reads `~/.claude/commands/<name>.md`,
108
- // so `/name` becomes available. Marker-guarded (never clobbers a user-edited
109
- // file) and version-rolled like the boot skill. Kill switch: CRTR_NO_MODE_CMDS=1.
110
- // ---------------------------------------------------------------------------
111
- const SLASH_CMD_MARKER = '<!-- crtr-mode-cmd v1 -->';
112
- const SLASH_CMD_MARKER_PREFIX = '<!-- crtr-mode-cmd v';
113
- /** Render a SlashSpec to a full template file (frontmatter + marker + body). */
114
- function renderSlashTemplate(spec) {
115
- const hint = spec.argumentHint !== undefined
116
- ? `argument-hint: ${JSON.stringify(spec.argumentHint)}\n`
117
- : '';
118
- return `---\ndescription: ${spec.description}\n${hint}---\n\n${SLASH_CMD_MARKER}\n\n${spec.body}\n`;
119
- }
120
- /** Write `content` to `file` unless a user-customized file is already there.
121
- * Rolls forward our own (marker-bearing) versions; skips if identical. */
122
- function writeSlashFileIfOurs(dir, name, content) {
123
- const file = join(dir, `${name}.md`);
124
- if (pathExists(file)) {
125
- const existing = readText(file);
126
- if (!existing.includes(SLASH_CMD_MARKER_PREFIX))
127
- return; // user's own file
128
- if (existing === content)
129
- return; // already current
130
- }
131
- ensureDir(dir);
132
- writeFileSync(file, content, 'utf8');
133
- }
134
- export function ensureSlashCommands(root, argv) {
135
- try {
136
- if (process.env.CRTR_NO_MODE_CMDS === '1')
137
- return;
138
- if (shouldSkipForArgv(argv))
139
- return;
140
- const specs = collectSlashSpecs(root);
141
- if (specs.length === 0)
142
- return;
143
- // Target each host's command dir, but only when that host is actually in use
144
- // (its root dir exists). We never create ~/.pi or ~/.claude ourselves.
145
- const targets = [];
146
- if (pathExists(join(homedir(), '.pi', 'agent'))) {
147
- targets.push(join(homedir(), '.pi', 'agent', 'prompts'));
148
- }
149
- if (pathExists(join(homedir(), '.claude'))) {
150
- targets.push(join(homedir(), '.claude', 'commands'));
151
- }
152
- if (targets.length === 0)
153
- return;
154
- for (const spec of specs) {
155
- const content = renderSlashTemplate(spec);
156
- for (const dir of targets)
157
- writeSlashFileIfOurs(dir, spec.name, content);
158
- }
159
- }
160
- catch (e) {
161
- if (process.env.CRTR_DEBUG === '1') {
162
- const msg = e instanceof Error ? e.message : String(e);
163
- process.stderr.write(`crtr: slash-command error: ${msg}\n`);
164
- }
165
- }
166
- }
167
25
  export function ensureOfficialMarketplace(argv) {
168
26
  try {
169
27
  if (process.env.CRTR_NO_BOOTSTRAP === '1')
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { buildTree, flatten, fuzzyMatch, tabPredicate, TABS } from '../model.js';
3
+ import { buildTree, flatten, fuzzyMatch, tabPredicate, TABS, attentionTier, attentionMtime } from '../model.js';
4
4
  // ── Fixture canvas ───────────────────────────────────────────────────────────
5
5
  // root1 (active) ← rank 0
6
6
  // child-a (idle)
@@ -12,6 +12,9 @@ import { buildTree, flatten, fuzzyMatch, tabPredicate, TABS } from '../model.js'
12
12
  function row(node_id, name, status, asks = 0) {
13
13
  return { node_id, name, status, kind: 'general', mode: 'base', ctx_tokens: 0, asks, cwd: '/tmp/proj', created: '2026-01-01T00:00:00.000Z' };
14
14
  }
15
+ function lrow(node_id, name, status, lifecycle) {
16
+ return { ...row(node_id, name, status), lifecycle };
17
+ }
15
18
  const ROWS = [
16
19
  row('root1', 'root-one', 'active'),
17
20
  row('child-a', 'child-a', 'idle'),
@@ -140,3 +143,85 @@ test('flatten: query with no matches yields nothing', () => {
140
143
  const v = flatten(tree(), { collapsed: allCollapsed(), tab: 'All', query: 'zzzznope' });
141
144
  assert.deepEqual(v, []);
142
145
  });
146
+ // ── flatten: residentsOnly fold-reveal ───────────────────────────────────────
147
+ // resident-root → terminal-worker (child) → terminal-grand (grandchild)
148
+ // resident-only must hide the terminals from the TOP LEVEL + flat search, yet
149
+ // still reveal them once you expand the resident fold that owns them.
150
+ const RES_ROWS = [
151
+ lrow('res-root', 'resident-root', 'idle', 'resident'),
152
+ lrow('term-a', 'worker-a', 'active', 'terminal'),
153
+ lrow('term-g', 'worker-grand', 'active', 'terminal'),
154
+ lrow('term-root', 'lone-worker', 'active', 'terminal'), // pure-terminal root
155
+ ];
156
+ const RES_CHILDREN = { 'res-root': ['term-a'], 'term-a': ['term-g'] };
157
+ function resTree() {
158
+ return buildTree(RES_ROWS, ['res-root', 'term-root'], (id) => RES_CHILDREN[id] ?? []);
159
+ }
160
+ test('flatten: residentsOnly hides terminal top-level rows (incl. terminal roots)', () => {
161
+ // Everything collapsed → only the resident root shows; the lone terminal root
162
+ // and the worker subtree are hidden at the top level.
163
+ const v = flatten(resTree(), { collapsed: new Set(['res-root', 'term-a']), tab: 'All', query: '', residentsOnly: true });
164
+ assert.deepEqual(v.map((r) => r.id), ['res-root']);
165
+ });
166
+ test('flatten: residentsOnly reveals terminal children once their fold is expanded', () => {
167
+ // Expand the resident root → its terminal worker appears (still collapsed, so
168
+ // its own terminal grandchild stays hidden).
169
+ const v1 = flatten(resTree(), { collapsed: new Set(['term-a']), tab: 'All', query: '', residentsOnly: true });
170
+ assert.deepEqual(v1.map((r) => r.id), ['res-root', 'term-a']);
171
+ // Expand the worker too → the terminal grandchild is revealed.
172
+ const v2 = flatten(resTree(), { collapsed: new Set(), tab: 'All', query: '', residentsOnly: true });
173
+ assert.deepEqual(v2.map((r) => r.id), ['res-root', 'term-a', 'term-g']);
174
+ });
175
+ test('flatten: residentsOnly keeps terminals out of flat (relevance) search results', () => {
176
+ // A query that matches the workers must not surface them in the flat search —
177
+ // resident-only de-clutters search even though the fold would reveal them.
178
+ const v = flatten(resTree(), { collapsed: new Set(), tab: 'All', query: 'worker', residentsOnly: true, sort: 'relevance' });
179
+ assert.deepEqual(v.map((r) => r.id), []);
180
+ });
181
+ test('flatten: residentsOnly off shows every lifecycle at the top level', () => {
182
+ // Roots are live-first: term-root (active) ranks ahead of res-root (idle).
183
+ const v = flatten(resTree(), { collapsed: new Set(['res-root', 'term-a']), tab: 'All', query: '', residentsOnly: false });
184
+ assert.deepEqual(v.map((r) => r.id), ['term-root', 'res-root']);
185
+ });
186
+ function arow(node_id, status, opts) {
187
+ return { ...row(node_id, node_id, status), ...opts };
188
+ }
189
+ test('attentionTier: classifies each cheap-field combination', () => {
190
+ assert.equal(attentionTier(arow('x', 'active', { viewed: true, streaming: true })), 0);
191
+ assert.equal(attentionTier(arow('x', 'active', { viewed: true })), 1);
192
+ assert.equal(attentionTier(arow('x', 'active', { streaming: true, viewed: false })), 2);
193
+ assert.equal(attentionTier(arow('x', 'active', {})), 3);
194
+ assert.equal(attentionTier(arow('x', 'idle', {})), 3);
195
+ assert.equal(attentionTier(arow('x', 'done', {})), 4);
196
+ assert.equal(attentionTier(arow('x', 'dead', {})), 4);
197
+ assert.equal(attentionTier(arow('x', 'canceled', {})), 4);
198
+ });
199
+ test('attentionMtime: prefers mtimeMs, falls back to created', () => {
200
+ assert.equal(attentionMtime(arow('x', 'active', { mtimeMs: 1234 })), 1234);
201
+ // No mtimeMs → epoch-ms of the ISO `created` (fixture is 2026-01-01T00:00:00Z).
202
+ assert.equal(attentionMtime(arow('x', 'active', {})), Date.parse('2026-01-01T00:00:00.000Z'));
203
+ });
204
+ test('flatten: attention sort tiers rows (attached/streaming/live first)', () => {
205
+ // One row per tier, deliberately shuffled in input order.
206
+ const rows = [
207
+ arow('dormant', 'done', { mtimeMs: 100 }), // T4
208
+ arow('live', 'active', { mtimeMs: 100 }), // T3
209
+ arow('streaming', 'active', { streaming: true, mtimeMs: 100 }), // T2
210
+ arow('attached', 'idle', { viewed: true, mtimeMs: 100 }), // T1
211
+ arow('both', 'active', { viewed: true, streaming: true, mtimeMs: 100 }), // T0
212
+ ];
213
+ const t = buildTree(rows, [], () => []);
214
+ const v = flatten(t, { collapsed: new Set(), tab: 'All', query: '', sort: 'attention' });
215
+ assert.deepEqual(v.map((r) => r.id), ['both', 'attached', 'streaming', 'live', 'dormant']);
216
+ });
217
+ test('flatten: attention sort orders newest-message-first WITHIN a tier', () => {
218
+ // All dormant (T4) → ordering is purely by mtime, newest first.
219
+ const rows = [
220
+ arow('old', 'done', { mtimeMs: 1000 }),
221
+ arow('newest', 'done', { mtimeMs: 3000 }),
222
+ arow('mid', 'done', { mtimeMs: 2000 }),
223
+ ];
224
+ const t = buildTree(rows, [], () => []);
225
+ const v = flatten(t, { collapsed: new Set(), tab: 'All', query: '', sort: 'attention' });
226
+ assert.deepEqual(v.map((r) => r.id), ['newest', 'mid', 'old']);
227
+ });
@@ -55,7 +55,8 @@ test('color on: each status glyph carries its hue, glyph kept', () => {
55
55
  });
56
56
  test('color on: ctx ≥100k row is red; asks are bright-yellow', () => {
57
57
  const out = renderFrame(state({ cursor: -1 }), SIZE, ON);
58
- assert.ok(out.includes(`${ESC}31m120k`), 'ctx ≥100k red');
58
+ // ctx is a right-aligned fixed-width column now, so the red span carries padding.
59
+ assert.ok(out.includes(`${ESC}31m`) && out.includes('120k'), 'ctx ≥100k → red');
59
60
  assert.ok(out.includes(`${ESC}93m`), 'asks → bright-yellow');
60
61
  assert.ok(out.includes('⚑3'), 'flag glyph kept');
61
62
  });