@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) 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/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -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
  *
@@ -124,19 +124,133 @@ export function matchIndices(query, text) {
124
124
  out.clear(); // no full match → no highlight
125
125
  return out;
126
126
  }
127
+ /** Chars of leading context kept before the match start when windowing. */
128
+ const SNIPPET_LEAD = 12;
129
+ /** Highlight indices for the preview: the literal case-insensitive SUBSTRING span
130
+ * when present (so "where does this text appear?" is answered exactly), else the
131
+ * scattered subsequence indices, else empty (empty query). This is a different,
132
+ * stricter model than the subsequence super-search on purpose — a contiguous span
133
+ * is what reads as a highlight. */
134
+ export function highlightIndices(query, text) {
135
+ if (query === '')
136
+ return new Set();
137
+ const lower = text.toLowerCase();
138
+ const q = query.toLowerCase();
139
+ const sub = lower.indexOf(q);
140
+ if (sub >= 0) {
141
+ const out = new Set();
142
+ for (let i = sub; i < sub + q.length; i++)
143
+ out.add(i);
144
+ return out;
145
+ }
146
+ return matchIndices(query, text);
147
+ }
148
+ /** Greedy word-wrap of single-spaced `s` (caller normalizes whitespace) to `width`
149
+ * cols × `maxLines` lines, returning each line's text AND its start offset in `s`.
150
+ * Only the single space at each wrap break is dropped, so a highlight index maps
151
+ * to a line column as `idx - line.start` with no drift across the wrap. */
152
+ function wrapTracked(s, width, maxLines) {
153
+ const out = [];
154
+ if (width <= 0 || maxLines <= 0)
155
+ return out;
156
+ const n = s.length;
157
+ let i = 0;
158
+ while (i < n && out.length < maxLines) {
159
+ const lineStart = i;
160
+ let lineEnd = i; // exclusive end of committed words
161
+ let j = i;
162
+ while (j < n) {
163
+ let ws = j;
164
+ while (ws < n && s[ws] === ' ')
165
+ ws++;
166
+ let we = ws;
167
+ while (we < n && s[we] !== ' ')
168
+ we++;
169
+ if (ws === we)
170
+ break; // only trailing spaces remain
171
+ if (we - lineStart <= width || lineEnd === lineStart) {
172
+ lineEnd = we; // include this word (always take ≥1; a huge word is clipped below)
173
+ j = we;
174
+ }
175
+ else
176
+ break;
177
+ }
178
+ let text = s.slice(lineStart, lineEnd);
179
+ if (text.length > width)
180
+ text = text.slice(0, width);
181
+ out.push({ text, start: lineStart });
182
+ i = lineEnd;
183
+ if (i < n && s[i] === ' ')
184
+ i++; // drop the single break space
185
+ }
186
+ return out;
187
+ }
188
+ /**
189
+ * Build the preview snippet for `text` under the live `query`: up to `maxLines`
190
+ * word-wrapped lines (each ≤ `width` cols), WINDOWED so the best match is visible
191
+ * (long conversations: the match can be thousands of chars in), with the matched
192
+ * columns flagged for highlight. Empty query (or empty text) → a plain wrap from
193
+ * the start with no highlight. The snippet string itself is what carries the
194
+ * highlight indices, so they never drift across the windowing.
195
+ */
196
+ export function previewSnippet(query, text, width, maxLines) {
197
+ const norm = text.replace(/\s+/g, ' ').trim();
198
+ if (norm === '' || width <= 0 || maxLines <= 0)
199
+ return [];
200
+ // 1. Locate the match in the normalized text + choose a window start that keeps
201
+ // a little leading context, snapped to a word boundary.
202
+ let matchStart = -1;
203
+ if (query !== '') {
204
+ const sub = norm.toLowerCase().indexOf(query.toLowerCase());
205
+ if (sub >= 0)
206
+ matchStart = sub;
207
+ else {
208
+ const ix = matchIndices(query, norm);
209
+ if (ix.size > 0)
210
+ matchStart = Math.min(...ix);
211
+ }
212
+ }
213
+ let windowStart = 0;
214
+ if (matchStart > SNIPPET_LEAD) {
215
+ let s = matchStart - SNIPPET_LEAD;
216
+ while (s < matchStart && norm[s] !== ' ')
217
+ s++; // forward to the next word boundary
218
+ windowStart = s < matchStart ? s + 1 : matchStart;
219
+ }
220
+ const snippet = (windowStart > 0 ? '… ' : '') + norm.slice(windowStart);
221
+ // 2. Recompute highlight indices on the FINAL snippet string (no drift), then
222
+ // word-wrap with offset tracking and split the highlight into per-line cols.
223
+ const hi = highlightIndices(query, snippet);
224
+ return wrapTracked(snippet, width, maxLines).map(({ text: lt, start }) => {
225
+ const lhi = new Set();
226
+ for (const idx of hi)
227
+ if (idx >= start && idx < start + lt.length)
228
+ lhi.add(idx - start);
229
+ return { text: lt, hi: lhi };
230
+ });
231
+ }
127
232
  function shortId(id) {
128
233
  return id.slice(0, 8);
129
234
  }
235
+ /** The searchable conversation text for a row: EVERY user prompt across the pi
236
+ * session (`prompts`) when present, else the spawn prompt (`goal`) for a
237
+ * never-revived node that has no session yet. Searched by super-search and
238
+ * windowed in the preview, so search matches a prompt from ANYWHERE in the
239
+ * conversation, not just the first one. */
240
+ export function promptText(row) {
241
+ return row.prompts ?? row.goal ?? '';
242
+ }
130
243
  /** Does this row match the live query? Super-search spans name (which already
131
- * folds in the pi-generated description), kind, short-id, AND the spawn prompt
132
- * (`row.goal`). Empty query matches everything. */
244
+ * folds in the pi-generated description), kind, short-id, AND every user prompt
245
+ * in the conversation (`promptText`). Empty query matches everything. */
133
246
  export function queryMatch(query, row) {
134
247
  if (query === '')
135
248
  return true;
249
+ const prompt = promptText(row);
136
250
  return (fuzzyMatch(query, row.name) ||
137
251
  fuzzyMatch(query, row.kind) ||
138
252
  fuzzyMatch(query, shortId(row.node_id)) ||
139
- (row.goal !== undefined && fuzzyMatch(query, row.goal)));
253
+ (prompt !== '' && fuzzyMatch(query, prompt)));
140
254
  }
141
255
  // ---------------------------------------------------------------------------
142
256
  // Relevance scoring (super-search)
@@ -159,23 +273,30 @@ export function fieldScore(query, text) {
159
273
  }
160
274
  return fuzzyMatch(q, t) ? 0.2 : 0; // scattered subsequence
161
275
  }
162
- /** Per-field weights — name (handle + description) dominates, the spawn prompt
163
- * is the long-tail super-search field. */
164
- const FIELD_WEIGHTS = { name: 4, kind: 2, id: 1, goal: 1.5 };
276
+ /** Per-field weights — name (handle + description) dominates, the conversation
277
+ * prompts are the long-tail super-search field. */
278
+ const FIELD_WEIGHTS = { name: 4, kind: 2, id: 1, prompt: 1.5 };
165
279
  /** Weighted relevance of a row to the query across all searched fields. 0 means
166
280
  * no field matched (excluded from relevance results, same as `queryMatch`). */
167
281
  export function scoreRow(query, row) {
168
282
  if (query === '')
169
283
  return 0;
284
+ const prompt = promptText(row);
170
285
  return (FIELD_WEIGHTS.name * fieldScore(query, row.name) +
171
286
  FIELD_WEIGHTS.kind * fieldScore(query, row.kind) +
172
287
  FIELD_WEIGHTS.id * fieldScore(query, shortId(row.node_id)) +
173
- FIELD_WEIGHTS.goal * (row.goal !== undefined ? fieldScore(query, row.goal) : 0));
288
+ FIELD_WEIGHTS.prompt * (prompt !== '' ? fieldScore(query, prompt) : 0));
174
289
  }
175
290
  /** Is this row inside the active cwd scope? No scope (null/undefined) = All dirs. */
176
291
  export function cwdMatch(scope, row) {
177
292
  return scope === null || scope === undefined || row.cwd === scope;
178
293
  }
294
+ /** Is this row inside the active lifecycle filter? `residentsOnly` hides `terminal`
295
+ * nodes; off (false/undefined) = every lifecycle. A row with an unknown lifecycle
296
+ * (older snapshot field absent) is treated as resident so it is never hidden. */
297
+ export function lifecycleMatch(residentsOnly, row) {
298
+ return residentsOnly !== true || row.lifecycle !== 'terminal';
299
+ }
179
300
  /**
180
301
  * Flatten the tree to the ordered list of currently-visible rows.
181
302
  *
@@ -189,11 +310,14 @@ export function cwdMatch(scope, row) {
189
310
  * always reachable.
190
311
  */
191
312
  export function flatten(tree, opts) {
192
- const { collapsed, tab, query, cwdScope, sort = 'tree' } = opts;
193
- // 1. Directly-matched nodes: tab predicate AND cwd scope AND query.
313
+ const { collapsed, tab, query, cwdScope, residentsOnly, sort = 'tree' } = opts;
314
+ // 1. Directly-matched nodes: tab predicate AND cwd scope AND lifecycle AND query.
194
315
  const matched = new Set();
195
316
  for (const [id, node] of tree.nodes) {
196
- if (tabPredicate(tab, node.row) && cwdMatch(cwdScope, node.row) && queryMatch(query, node.row)) {
317
+ if (tabPredicate(tab, node.row) &&
318
+ cwdMatch(cwdScope, node.row) &&
319
+ lifecycleMatch(residentsOnly, node.row) &&
320
+ queryMatch(query, node.row)) {
197
321
  matched.add(id);
198
322
  }
199
323
  }
@@ -1,19 +1,10 @@
1
1
  import type { Tab, Tree, VisibleRow, SortMode } from './model.js';
2
+ import { detectColorCaps, type ColorCaps } from '../../tui/draw.js';
3
+ export { detectColorCaps };
4
+ export type { ColorCaps };
2
5
  export declare const PREVIEW_BODY = 5;
3
6
  export declare const PREVIEW_HEIGHT: number;
4
7
  export declare function headerHeight(search: boolean): number;
5
- export interface ColorCaps {
6
- /** Any hue (fg/bg color) allowed. */
7
- color: boolean;
8
- /** 256-color bg allowed — drives the subtle cursor-row background. */
9
- color256: boolean;
10
- }
11
- /** Detect color capability. Honors `NO_COLOR` and `TERM=dumb`, and only emits
12
- * hue when stdout is a TTY. `color256` additionally requires a 256/truecolor
13
- * terminal (for the cursor-row background; otherwise we fall back to reverse). */
14
- export declare function detectColorCaps(stream?: {
15
- isTTY?: boolean;
16
- }, env?: NodeJS.ProcessEnv): ColorCaps;
17
8
  export interface RenderState {
18
9
  tree: Tree;
19
10
  visible: VisibleRow[];
@@ -29,6 +20,9 @@ export interface RenderState {
29
20
  sort: SortMode;
30
21
  /** Whether the bottom preview panel is drawn. */
31
22
  preview: boolean;
23
+ /** Lifecycle filter: when true, `terminal` (one-shot worker) nodes are hidden
24
+ * — surfaced as a status-line cue. */
25
+ residentsOnly: boolean;
32
26
  }
33
27
  /**
34
28
  * Render the whole frame. Returns a single string that, written as-is, repaints