@crouton-kit/crouter 0.3.18 → 0.3.19

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 (257) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,133 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/review-render-pane-regression.test.ts
2
+ //
3
+ // BUG REGRESSION: `crtr human review` showed the human RAW markdown — literal
4
+ // `:::callout{...}` / `#` / `**bold**` source — instead of the termrender-
5
+ // rendered document the leaf help promises ("directive-flavored markdown
6
+ // rendered by termrender (panels, columns, trees, callouts, mermaid)").
7
+ //
8
+ // Root cause: the review surface was ONLY the read-only nvim buffer. nvim must
9
+ // show the raw source (anchored comments hang off source line/col numbers), and
10
+ // `-u NONE` treesitter/render-markdown styling never interprets ::: directives —
11
+ // so nothing anywhere rendered the doc. There was no termrender invocation in
12
+ // the entire review path (crouter `_run` review branch + humanloop
13
+ // launchReview): the managed venv binary was healthy and never asked.
14
+ //
15
+ // THE FIX (src/commands/human/queue.ts, `_run` review branch): open a live
16
+ // termrender watch pane via humanloop `display()` BESIDE the editor, record its
17
+ // pane id as `render_pane_id` on run.json (so `human cancel` can clear it), and
18
+ // kill it in a `finally` when the editor exits. The editor keeps the raw
19
+ // source; the human reads the rendered pane.
20
+ //
21
+ // This test drives the REAL `crtr human _run` worker (review mode) in a REAL
22
+ // but isolated tmux session — no mocks — and asserts:
23
+ // (a) a second pane appears beside the editor running termrender on the file,
24
+ // (b) that pane's content is RENDERED ANSI (box-drawing panels), not raw `:::`,
25
+ // (c) run.json carries render_pane_id for the cancel path,
26
+ // (d) the render pane is torn down when the editor exits.
27
+ import { test } from 'node:test';
28
+ import assert from 'node:assert/strict';
29
+ import { spawnSync } from 'node:child_process';
30
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
31
+ import { tmpdir } from 'node:os';
32
+ import { join, dirname } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+ import { createRequire } from 'node:module';
35
+ import { hasTmux } from './helpers/harness.js';
36
+ const CROUTER = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
37
+ const CLI_SRC = join(CROUTER, 'src', 'cli.ts');
38
+ const TSX_ESM = createRequire(import.meta.url).resolve('tsx/esm');
39
+ function tmux(args) {
40
+ const r = spawnSync('tmux', args, { encoding: 'utf8' });
41
+ return { code: r.status ?? -1, out: (r.stdout ?? '').trim(), err: (r.stderr ?? '').trim() };
42
+ }
43
+ function sessionPanes(session) {
44
+ const r = tmux(['list-panes', '-s', '-t', session, '-F', '#{pane_id}\t#{pane_current_command}']);
45
+ if (r.code !== 0)
46
+ return [];
47
+ return r.out
48
+ .split('\n')
49
+ .filter((l) => l.trim() !== '')
50
+ .map((l) => {
51
+ const [id, cmd] = l.split('\t');
52
+ return { id: id ?? '', cmd: cmd ?? '' };
53
+ });
54
+ }
55
+ function paneContent(paneId) {
56
+ return tmux(['capture-pane', '-p', '-t', paneId]).out;
57
+ }
58
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
59
+ async function waitFor(fn, timeoutMs, label) {
60
+ const deadline = Date.now() + timeoutMs;
61
+ for (;;) {
62
+ const v = fn();
63
+ if (v !== undefined)
64
+ return v;
65
+ if (Date.now() > deadline)
66
+ throw new Error(`timeout waiting for ${label}`);
67
+ await sleep(300);
68
+ }
69
+ }
70
+ const SKIP = !hasTmux();
71
+ test('review-render-pane regression: `human _run` (review) opens a live termrender pane showing RENDERED panels, not raw ::: source', { skip: SKIP ? 'tmux unavailable' : false, timeout: 90_000 }, async () => {
72
+ const dir = mkdtempSync(join(tmpdir(), 'crtr-review-render-'));
73
+ const session = `crtr-revrender-${process.pid}-${Date.now().toString(36)}`;
74
+ const mdPath = join(dir, 'doc.md');
75
+ // A directive doc: rendered output has box-drawing panels; raw output has `:::`.
76
+ writeFileSync(mdPath, '# Render check\n\n:::callout{type="info"}\nRENDER-MARKER body\n:::\n');
77
+ writeFileSync(join(dir, 'run.json'), JSON.stringify({ mode: 'review', job_id: '', file: mdPath, output: join(dir, 'feedback.json') }));
78
+ try {
79
+ // The worker runs the real `crtr human _run` from src via tsx, exactly the
80
+ // command shape detachHumanTui bakes into the spawned pane. job_id is ''
81
+ // so pushFinal is skipped (no canvas home needed) — the surface under
82
+ // test is the panes, not the report fan-out.
83
+ const workerCmd = `CRTR_HUMAN_DIR='${dir}' '${process.execPath}' --import '${TSX_ESM}' '${CLI_SRC}' human _run; sleep 120`;
84
+ const created = tmux(['new-session', '-d', '-s', session, '-x', '200', '-y', '50', workerCmd]);
85
+ assert.equal(created.code, 0, `create isolated session failed: ${created.err}`);
86
+ // (a) a SECOND pane appears: the termrender watch pane beside the editor.
87
+ const renderPane = await waitFor(() => {
88
+ const panes = sessionPanes(session);
89
+ if (panes.length < 2)
90
+ return undefined;
91
+ // The render pane is the one NOT hosting the editor/worker shell.
92
+ const candidates = panes.filter((p) => !/zsh|bash|sh|node|nvim|vim/i.test(p.cmd));
93
+ return candidates[0]?.id;
94
+ }, 30_000, 'termrender render pane to open');
95
+ // (b) its content is RENDERED: panel box-drawing present, raw ::: absent.
96
+ const rendered = await waitFor(() => {
97
+ const c = paneContent(renderPane);
98
+ return c.includes('RENDER-MARKER') ? c : undefined;
99
+ }, 30_000, 'rendered body to paint');
100
+ assert.ok(/[┌│└]/.test(rendered), `render pane draws panels (box chars): ${rendered.slice(0, 200)}`);
101
+ assert.ok(!rendered.includes(':::'), 'render pane does NOT show raw ::: directive source');
102
+ // (c) run.json carries render_pane_id so `human cancel` can clear the pane.
103
+ const rc = JSON.parse(readFileSync(join(dir, 'run.json'), 'utf8'));
104
+ assert.equal(rc.render_pane_id, renderPane, 'run.json records the render pane id');
105
+ // (d) editor exit (any quit submits) tears the render pane down. The
106
+ // editor pane is the OTHER pane (nvim runs as a child of the worker, so
107
+ // #{pane_current_command} may report node/zsh — match on its content:
108
+ // the read-only buffer shows the RAW ::: source).
109
+ const editorPane = await waitFor(() => {
110
+ const other = sessionPanes(session).find((p) => p.id !== renderPane);
111
+ if (other === undefined)
112
+ return undefined;
113
+ return paneContent(other.id).includes(':::') ? other.id : undefined;
114
+ }, 30_000, 'editor pane showing the raw source buffer');
115
+ tmux(['send-keys', '-t', editorPane, 'Space', 's']);
116
+ await waitFor(() => (existsSync(join(dir, 'feedback.json')) ? true : undefined), 30_000, 'feedback.json after submit');
117
+ await waitFor(() => (sessionPanes(session).some((p) => p.id === renderPane) ? undefined : true), 15_000, 'render pane to be killed after editor exit');
118
+ }
119
+ finally {
120
+ tmux(['kill-session', '-t', session]);
121
+ // The worker may still be flushing job files as it exits; retry the rm
122
+ // briefly so a write racing the first attempt can't fail the test.
123
+ for (let i = 0; i < 10; i++) {
124
+ try {
125
+ rmSync(dir, { recursive: true, force: true });
126
+ break;
127
+ }
128
+ catch {
129
+ await sleep(300);
130
+ }
131
+ }
132
+ }
133
+ });
@@ -10,6 +10,7 @@ import { createNode, getNode, getRow, subscribersOf } from '../canvas/canvas.js'
10
10
  import { closeDb } from '../canvas/db.js';
11
11
  import { nodeDir } from '../canvas/paths.js';
12
12
  import { spawnNode } from '../runtime/nodes.js';
13
+ import { resolveSpawner } from '../runtime/spawn.js';
13
14
  let home;
14
15
  function spawner(id) {
15
16
  return {
@@ -62,6 +63,15 @@ test('independent root: provenance only, no parent, no subscription', () => {
62
63
  assert.equal(meta.lifecycle, 'resident', 'a root is resident');
63
64
  assert.equal(subscribersOf(root.node_id).length, 0, 'nobody is subscribed to an independent root — the spawner is not woken by it');
64
65
  });
66
+ test('resolveSpawner: a --root spawn needs no caller; a managed child does (regression)', () => {
67
+ // Bug-regression: `crtr node new --root` from a human shell (no CRTR_NODE_ID,
68
+ // no --parent) threw "spawnChild requires a calling node" even though a root
69
+ // has no spine parent and the spawner id is provenance only.
70
+ assert.equal(resolveSpawner(undefined, null, true), null, 'human root spawn: no caller is fine');
71
+ assert.equal(resolveSpawner(undefined, 'A', true), 'A', 'node root spawn: provenance recorded');
72
+ assert.equal(resolveSpawner('A', null, false), 'A', 'explicit parent satisfies a child spawn');
73
+ assert.throws(() => resolveSpawner(undefined, null, false), /requires a calling node/, 'a managed child with no caller still throws');
74
+ });
65
75
  test('unknown parent: spawnNode throws and mints NO node dir / row (validate before create)', () => {
66
76
  // Pre-allocate the would-be id so we can prove nothing was scaffolded for it.
67
77
  const orphanId = 'orphan-under-ghost';
@@ -46,6 +46,7 @@ const CANVAS_ENV_KEYS = [
46
46
  'CRTR_NODE_ID',
47
47
  'CRTR_HOME',
48
48
  'CRTR_ROOT_SESSION',
49
+ 'CRTR_SUBTREE',
49
50
  'CRTR_NODE_SESSION',
50
51
  'CRTR_PARENT_NODE_ID',
51
52
  'CRTR_FRONT_DOOR',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/wake-bearings.test.ts
2
+ //
3
+ // Wake-origin self-knowledge (the <crtr-wake> provenance block). Regression
4
+ // guard for the observed gap: a node woken or BORN by a scheduled TIMER could
5
+ // not tell it came from a timer — a scheduled wake arrived indistinguishable
6
+ // from an ordinary refresh/message (the Invariant B/D anti-goal). These lock:
7
+ // • buildWakeBearings renders the right decision-first block per wake kind /
8
+ // cadence, and never crashes when the armer was reaped (no resolved name);
9
+ // • the bare-wake revive kickoff carries that block (so a scheduled alarm is
10
+ // distinguishable from a generic context-refresh), placed AFTER the kickoff
11
+ // sentinel (goal-capture keys on it) and BEFORE the roadmap/disk bearings;
12
+ // • an ordinary fresh revive (no wakeReason) carries NO block.
13
+ import { test, before, after, beforeEach } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import { mkdtempSync, rmSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { createNode } from '../canvas/canvas.js';
19
+ import { closeDb } from '../canvas/db.js';
20
+ import { buildWakeBearings } from '../runtime/bearings.js';
21
+ import { buildReviveKickoff, drainBearings, REVIVE_KICKOFF_SENTINEL, } from '../runtime/kickoff.js';
22
+ let home;
23
+ function node(id) {
24
+ return {
25
+ node_id: id,
26
+ name: id,
27
+ created: new Date().toISOString(),
28
+ cwd: '/tmp/work',
29
+ kind: 'general',
30
+ mode: 'base',
31
+ lifecycle: 'terminal',
32
+ status: 'active',
33
+ };
34
+ }
35
+ const ARMED = '2026-06-08T13:30:00.000Z';
36
+ before(() => {
37
+ home = mkdtempSync(join(tmpdir(), 'crtr-wake-bearings-'));
38
+ process.env['CRTR_HOME'] = home;
39
+ });
40
+ beforeEach(() => {
41
+ closeDb();
42
+ rmSync(home, { recursive: true, force: true });
43
+ });
44
+ after(() => {
45
+ closeDb();
46
+ rmSync(home, { recursive: true, force: true });
47
+ delete process.env['CRTR_HOME'];
48
+ });
49
+ // --- buildWakeBearings: one per kind / cadence variant ----------------------
50
+ test('spawn one-shot: names a deferred birth + the armer, and says it does not recur', () => {
51
+ const o = {
52
+ kind: 'spawn',
53
+ ownerId: 'arm-1',
54
+ ownerName: 'release-orch',
55
+ armedAt: ARMED,
56
+ recur: null,
57
+ };
58
+ const b = buildWakeBearings(o);
59
+ assert.ok(b.startsWith('<crtr-wake>') && b.endsWith('</crtr-wake>'));
60
+ assert.match(b, /BORN by a scheduled wake/);
61
+ assert.match(b, /deferred birth/);
62
+ // Armer named ROLE-explicitly so the newborn never reads the id as its own.
63
+ assert.match(b, /armed by node arm-1 \("release-orch"\)/);
64
+ assert.match(b, /only run, not a recurring job/);
65
+ assert.match(b, /Your task follows\./);
66
+ });
67
+ test('spawn cron: surfaces the cadence + frames it as one run of a standing job', () => {
68
+ const o = {
69
+ kind: 'spawn',
70
+ ownerId: 'arm-1',
71
+ ownerName: 'nightly',
72
+ armedAt: ARMED,
73
+ recur: JSON.stringify({ cron: '0 9 * * *', tz: 'America/New_York' }),
74
+ };
75
+ const b = buildWakeBearings(o);
76
+ assert.match(b, /recurring spawn-cron armed by node arm-1 \("nightly"\)/);
77
+ // The cadence renders via the shared cadenceDisplay (matches `wake list`).
78
+ assert.match(b, /firing cron `0 9 \* \* \*` \(America\/New_York\)/);
79
+ assert.match(b, /one run of a standing job, not a one-off/);
80
+ assert.match(b, /inherit nothing from prior runs but this task/);
81
+ });
82
+ test('spawn interval cron: renders the every-N cadence', () => {
83
+ const o = {
84
+ kind: 'spawn',
85
+ ownerId: 'arm-1',
86
+ ownerName: 'poller',
87
+ armedAt: ARMED,
88
+ recur: JSON.stringify({ every: '6h' }),
89
+ };
90
+ assert.match(buildWakeBearings(o), /firing every 6h/);
91
+ });
92
+ test('spawn with a reaped armer: renders the bare id, no name, never crashes', () => {
93
+ const o = {
94
+ kind: 'spawn',
95
+ ownerId: 'gone-9',
96
+ ownerName: undefined, // armer no longer exists → no resolved name
97
+ armedAt: ARMED,
98
+ recur: JSON.stringify({ every: '12h' }),
99
+ };
100
+ const b = buildWakeBearings(o);
101
+ assert.match(b, /armed by node gone-9 \(now gone\)/); // bare id, flagged reaped
102
+ assert.doesNotMatch(b, /\("/); // never an empty/dangling name
103
+ });
104
+ test('bare one-shot: frames a timed re-check, NOT a new request', () => {
105
+ const o = { kind: 'bare', ownerId: 'n1', ownerName: 'n1', armedAt: ARMED, recur: null };
106
+ const b = buildWakeBearings(o);
107
+ assert.match(b, /a scheduled alarm fired/);
108
+ assert.match(b, /a timer, NOT a new message or request/);
109
+ assert.match(b, /timed re-check, not a new task/);
110
+ assert.match(b, /re-read your roadmap/);
111
+ // No armer attribution on bare (could be `--node`-armed by another), no ISO.
112
+ assert.doesNotMatch(b, /you set/);
113
+ assert.doesNotMatch(b, new RegExp(ARMED));
114
+ });
115
+ test('bare recurring: frames one tick of a standing re-check + the cadence', () => {
116
+ const o = {
117
+ kind: 'bare',
118
+ ownerId: 'n1',
119
+ ownerName: 'n1',
120
+ armedAt: ARMED,
121
+ recur: JSON.stringify({ every: '6h' }),
122
+ };
123
+ const b = buildWakeBearings(o);
124
+ assert.match(b, /recurring scheduled alarm fired \(every 6h\)/);
125
+ assert.match(b, /one tick of a standing re-check/);
126
+ });
127
+ // --- the bare-wake revive seam (Seam 2) -------------------------------------
128
+ test('a bare scheduled wake makes the fresh-revive kickoff distinguishable from a refresh', () => {
129
+ const id = 'b1';
130
+ const meta = createNode(node(id));
131
+ const wakeReason = {
132
+ kind: 'bare',
133
+ ownerId: id,
134
+ ownerName: id,
135
+ armedAt: ARMED,
136
+ recur: null,
137
+ };
138
+ const msg = buildReviveKickoff(meta, drainBearings(meta), wakeReason);
139
+ // The block is present, and the kickoff STILL starts with the sentinel that
140
+ // goal-capture keys on (so the prompt is never mistaken for a user mandate).
141
+ assert.ok(msg.startsWith(REVIVE_KICKOFF_SENTINEL), 'kickoff still leads with the sentinel');
142
+ assert.match(msg, /<crtr-wake>/);
143
+ assert.match(msg, /a scheduled alarm fired/);
144
+ // Placement: sentinel < <crtr-wake> < <roadmap> — "why you woke" precedes
145
+ // "what to rebuild from".
146
+ const iSentinel = msg.indexOf(REVIVE_KICKOFF_SENTINEL);
147
+ const iWake = msg.indexOf('<crtr-wake>');
148
+ const iRoadmap = msg.indexOf('<roadmap');
149
+ assert.ok(iSentinel === 0 && iSentinel < iWake && iWake < iRoadmap, 'block sits after sentinel, before roadmap');
150
+ });
151
+ test('an ordinary fresh revive (no wakeReason) carries NO wake block', () => {
152
+ const id = 'b2';
153
+ const meta = createNode(node(id));
154
+ const msg = buildReviveKickoff(meta, drainBearings(meta));
155
+ assert.doesNotMatch(msg, /<crtr-wake>/);
156
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/wake-origin.test.ts
2
+ //
3
+ // Wake-origin self-knowledge at the DAEMON FIRING seams (the other half of the
4
+ // regression — wake-bearings.test.ts covers the pure block + the bare-revive
5
+ // kickoff). The observed gap: a scheduled wake arrived indistinguishable from an
6
+ // ordinary message/spawn (Invariant D). These lock the daemon's third pass:
7
+ // • noted — the delivered inbox label carries the ⏰ scheduled-wake marker, so
8
+ // a timed note is distinguishable from a plain `node msg` (no tmux needed).
9
+ // • spawn — a node BORN by a spawn wake gets the <crtr-wake> block prepended
10
+ // to its kickoff prompt (naming the armer + cadence); an ordinary `node new`
11
+ // spawn does NOT. The headline. (tmux-gated integration via the harness.)
12
+ import { test, before, after, beforeEach } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { mkdtempSync, rmSync, readdirSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { createNode } from '../canvas/canvas.js';
19
+ import { armWake } from '../canvas/wakeups.js';
20
+ import { closeDb } from '../canvas/db.js';
21
+ import { readInboxSince } from '../feed/inbox.js';
22
+ import { superviseTick } from '../../daemon/crtrd.js';
23
+ import { hasTmux, createHarness } from './helpers/harness.js';
24
+ const CROUTER = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
25
+ function node(id) {
26
+ return {
27
+ node_id: id,
28
+ name: id,
29
+ created: new Date().toISOString(),
30
+ cwd: '/tmp/work',
31
+ kind: 'general',
32
+ mode: 'base',
33
+ lifecycle: 'terminal',
34
+ status: 'active',
35
+ parent: null,
36
+ };
37
+ }
38
+ // --- noted seam (no tmux) ---------------------------------------------------
39
+ let home;
40
+ before(() => {
41
+ home = mkdtempSync(join(tmpdir(), 'crtr-wake-origin-'));
42
+ process.env['CRTR_HOME'] = home;
43
+ });
44
+ beforeEach(() => {
45
+ closeDb();
46
+ rmSync(home, { recursive: true, force: true });
47
+ });
48
+ after(() => {
49
+ closeDb();
50
+ rmSync(home, { recursive: true, force: true });
51
+ delete process.env['CRTR_HOME'];
52
+ });
53
+ test('a noted scheduled wake delivers a ⏰-marked inbox label, distinct from a plain message', async () => {
54
+ const armer = createNode(node('armer'));
55
+ const target = createNode(node('tgt')); // no tmux placement → pass 1 skips it
56
+ armWake({
57
+ wakeup_id: 'wk-noted-1',
58
+ node_id: target.node_id,
59
+ owner_id: armer.node_id,
60
+ fire_at: new Date(Date.now() - 1000).toISOString(),
61
+ kind: 'noted',
62
+ recur: null,
63
+ payload: { body: 'CI should be green by now — check it', label: 'check CI' },
64
+ });
65
+ closeDb();
66
+ await superviseTick(Date.now());
67
+ closeDb();
68
+ const entries = readInboxSince(target.node_id);
69
+ assert.equal(entries.length, 1, 'the noted wake delivered exactly one inbox entry');
70
+ // Invariant D: the timer signal rides the VISIBLE label (digests never surface
71
+ // arbitrary data keys), so this is distinguishable from a plain `node msg`.
72
+ assert.equal(entries[0].label, '⏰ scheduled wake — check CI');
73
+ assert.notEqual(entries[0].label, 'check CI', 'a plain message would carry the bare label');
74
+ });
75
+ // --- spawn seam (the headline; tmux-gated integration) ----------------------
76
+ test('a spawn scheduled wake births a node whose kickoff carries <crtr-wake>; node new does NOT', { skip: hasTmux() ? false : 'tmux unavailable' }, async () => {
77
+ const h = await createHarness();
78
+ try {
79
+ const armer = h.spawnRoot('armer');
80
+ // Contrast: an ordinary `node new` spawn — its kickoff has NO wake block.
81
+ const plainChild = await h.spawnChild(armer, 'plain task');
82
+ const plainBoot = await h.awaitBoot(plainChild);
83
+ assert.doesNotMatch(plainBoot.prompt ?? '', /<crtr-wake>/, 'node new carries no wake block');
84
+ // Arm a recurring spawn-cron (detached), fire one tick, inspect the BORN
85
+ // node's kickoff prompt (the message it actually wakes on).
86
+ const before = new Set(readdirSync(join(h.home, 'nodes')));
87
+ armWake({
88
+ wakeup_id: 'wk-spawn-1',
89
+ node_id: null, // canvas-detached deferred birth
90
+ owner_id: armer,
91
+ fire_at: new Date(Date.now() - 1000).toISOString(),
92
+ kind: 'spawn',
93
+ recur: JSON.stringify({ every: '6h' }),
94
+ payload: { kind: 'general', cwd: CROUTER, prompt: 'do the recurring job', parent: armer },
95
+ });
96
+ await h.tick(Date.now());
97
+ const born = readdirSync(join(h.home, 'nodes')).filter((d) => !before.has(d));
98
+ assert.equal(born.length, 1, 'exactly one node born from the spawn wake');
99
+ const bornBoot = await h.awaitBoot(born[0]);
100
+ const prompt = bornBoot.prompt ?? '';
101
+ assert.match(prompt, /<crtr-wake>/, 'the born node learns a timer birthed it');
102
+ assert.match(prompt, /recurring spawn-cron armed by node /, 'names the armer role-explicitly');
103
+ assert.match(prompt, /firing every 6h/, 'surfaces the cadence');
104
+ assert.match(prompt, new RegExp(armer), 'the still-alive armer is named');
105
+ assert.match(prompt, /do the recurring job/, 'the real task follows the block');
106
+ }
107
+ finally {
108
+ await h.dispose();
109
+ }
110
+ });
@@ -30,7 +30,7 @@ const BOOT_SKILL_MARKER_PREFIX = '<!-- crtr-boot-skill v';
30
30
  function bootSkillBody() {
31
31
  return `---
32
32
  name: crtr-skills
33
- description: Capture, list, search, and load skills via the crtr CLI. Skills 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 skill. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".
33
+ description: 'Capture, list, search, and load skills via the crtr CLI. Skills 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 skill. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".'
34
34
  argument-hint: [topic or verb]
35
35
  ---
36
36
 
@@ -36,6 +36,7 @@ function state(over = {}) {
36
36
  cwdScope: null,
37
37
  sort: 'tree',
38
38
  preview: false,
39
+ residentsOnly: false,
39
40
  ...over,
40
41
  };
41
42
  }
@@ -11,7 +11,7 @@ import { execFileSync } from 'node:child_process';
11
11
  import { resolve } from 'node:path';
12
12
  import { dashboardRowsAll, renderForest } from '../render.js';
13
13
  import { listNodes, subscriptionsOf } from '../canvas.js';
14
- import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from './terminal.js';
14
+ import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../../tui/terminal.js';
15
15
  import { buildTree, flatten, TABS, SORTS } from './model.js';
16
16
  import { renderFrame, detectColorCaps, headerHeight, PREVIEW_HEIGHT } from './render.js';
17
17
  /** Viewport (body) height = total rows minus the header renderFrame draws (see
@@ -52,6 +52,7 @@ export async function runBrowse(opts = {}) {
52
52
  cwdScope: launchCwd, // default: this dir
53
53
  sort: 'tree',
54
54
  preview: true, // default ON (decision)
55
+ residentsOnly: true, // default ON: hide one-shot workers (decision)
55
56
  };
56
57
  let visible = [];
57
58
  // Color capability is fixed for the session (it's a property of the tty/env).
@@ -89,6 +90,7 @@ export async function runBrowse(opts = {}) {
89
90
  tab: state.tab,
90
91
  query: state.query,
91
92
  cwdScope: state.cwdScope,
93
+ residentsOnly: state.residentsOnly,
92
94
  sort: state.sort,
93
95
  });
94
96
  if (keepId !== undefined) {
@@ -116,6 +118,7 @@ export async function runBrowse(opts = {}) {
116
118
  tree, visible, tab: state.tab, cursor: state.cursor, scrollOffset: state.scrollOffset,
117
119
  query: state.query, search: state.search, totalNodes,
118
120
  cwdScope: state.cwdScope, sort: state.sort, preview: state.preview,
121
+ residentsOnly: state.residentsOnly,
119
122
  }, size, caps);
120
123
  process.stdout.write(frame);
121
124
  };
@@ -146,6 +149,13 @@ export async function runBrowse(opts = {}) {
146
149
  state.cwdScope = state.cwdScope === null ? launchCwd : null;
147
150
  recompute(keep);
148
151
  };
152
+ // Toggle the resident-only lifecycle filter (hide/show one-shot worker nodes),
153
+ // keeping the selected node put when it survives the toggle.
154
+ const toggleResidents = () => {
155
+ const keep = curRow()?.id;
156
+ state.residentsOnly = !state.residentsOnly;
157
+ recompute(keep);
158
+ };
149
159
  const onKeySearch = (input, key) => {
150
160
  if (key.escape) {
151
161
  // Cancel the search: drop the query AND the relevance ranking it switched
@@ -284,7 +294,7 @@ export async function runBrowse(opts = {}) {
284
294
  flush();
285
295
  return;
286
296
  }
287
- // Sort / scope / preview.
297
+ // Sort / scope / residents / preview.
288
298
  if (input === 's') {
289
299
  cycleSort();
290
300
  flush();
@@ -295,6 +305,11 @@ export async function runBrowse(opts = {}) {
295
305
  flush();
296
306
  return;
297
307
  }
308
+ if (input === 'r') {
309
+ toggleResidents();
310
+ flush();
311
+ return;
312
+ }
298
313
  if (input === 'p') {
299
314
  state.preview = !state.preview;
300
315
  flush();
@@ -322,6 +337,13 @@ export async function runBrowse(opts = {}) {
322
337
  // Boot. If the launch dir holds NO nodes, the default this-dir scope would show
323
338
  // a blank canvas — fall back to All dirs so browse is never empty on open.
324
339
  recompute();
340
+ // Relax the resident-only filter first (you're in this dir for a reason), then
341
+ // the cwd scope — so browse is never blank on open even if the launch dir holds
342
+ // only one-shot workers, or only nodes from other dirs.
343
+ if (visible.length === 0 && state.residentsOnly) {
344
+ state.residentsOnly = false;
345
+ recompute();
346
+ }
325
347
  if (visible.length === 0 && state.cwdScope !== null) {
326
348
  state.cwdScope = null;
327
349
  recompute();
@@ -50,9 +50,36 @@ export declare function fuzzyMatch(query: string, text: string): boolean;
50
50
  * the renderer can highlight them. Empty set when `query` is empty OR does not
51
51
  * fully match (no partial highlights). */
52
52
  export declare function matchIndices(query: string, text: string): Set<number>;
53
+ /** One word-wrapped preview line: its text + the column indices WITHIN that text
54
+ * to highlight (the query match). */
55
+ export interface SnippetLine {
56
+ text: string;
57
+ hi: Set<number>;
58
+ }
59
+ /** Highlight indices for the preview: the literal case-insensitive SUBSTRING span
60
+ * when present (so "where does this text appear?" is answered exactly), else the
61
+ * scattered subsequence indices, else empty (empty query). This is a different,
62
+ * stricter model than the subsequence super-search on purpose — a contiguous span
63
+ * is what reads as a highlight. */
64
+ export declare function highlightIndices(query: string, text: string): Set<number>;
65
+ /**
66
+ * Build the preview snippet for `text` under the live `query`: up to `maxLines`
67
+ * word-wrapped lines (each ≤ `width` cols), WINDOWED so the best match is visible
68
+ * (long conversations: the match can be thousands of chars in), with the matched
69
+ * columns flagged for highlight. Empty query (or empty text) → a plain wrap from
70
+ * the start with no highlight. The snippet string itself is what carries the
71
+ * highlight indices, so they never drift across the windowing.
72
+ */
73
+ export declare function previewSnippet(query: string, text: string, width: number, maxLines: number): SnippetLine[];
74
+ /** The searchable conversation text for a row: EVERY user prompt across the pi
75
+ * session (`prompts`) when present, else the spawn prompt (`goal`) for a
76
+ * never-revived node that has no session yet. Searched by super-search and
77
+ * windowed in the preview, so search matches a prompt from ANYWHERE in the
78
+ * conversation, not just the first one. */
79
+ export declare function promptText(row: DashboardRow): string;
53
80
  /** Does this row match the live query? Super-search spans name (which already
54
- * folds in the pi-generated description), kind, short-id, AND the spawn prompt
55
- * (`row.goal`). Empty query matches everything. */
81
+ * folds in the pi-generated description), kind, short-id, AND every user prompt
82
+ * in the conversation (`promptText`). Empty query matches everything. */
56
83
  export declare function queryMatch(query: string, row: DashboardRow): boolean;
57
84
  /** Score how well `query` matches one field, 0 (no match) → 1 (exact). Tiers:
58
85
  * exact > prefix > word-boundary substring > interior substring > subsequence.
@@ -76,12 +103,21 @@ export interface FlattenOpts {
76
103
  * undefined = All dirs (no cwd filter). Like the tab predicate, it gates the
77
104
  * matched set — ancestors from other dirs still render dimmed for tree context. */
78
105
  cwdScope?: string | null;
106
+ /** Lifecycle filter: when true, hide `terminal` (one-shot worker) nodes so only
107
+ * the persistent `resident` agents you come back to show. The resume picker
108
+ * defaults this ON. Undefined/false = every lifecycle. Gates the matched set
109
+ * like the tab + cwd predicates (ancestors of a match still render for context). */
110
+ residentsOnly?: boolean;
79
111
  /** Ordering. `tree` keeps the spanning tree + ancestor context; `relevance` /
80
112
  * `recency` produce a FLAT ranked list of directly-matched rows. */
81
113
  sort?: SortMode;
82
114
  }
83
115
  /** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
84
116
  export declare function cwdMatch(scope: string | null | undefined, row: DashboardRow): boolean;
117
+ /** Is this row inside the active lifecycle filter? `residentsOnly` hides `terminal`
118
+ * nodes; off (false/undefined) = every lifecycle. A row with an unknown lifecycle
119
+ * (older snapshot field absent) is treated as resident so it is never hidden. */
120
+ export declare function lifecycleMatch(residentsOnly: boolean | undefined, row: DashboardRow): boolean;
85
121
  /**
86
122
  * Flatten the tree to the ordered list of currently-visible rows.
87
123
  *