@crouton-kit/crouter 0.3.35 → 0.3.37

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 (71) hide show
  1. package/dist/build-root.js +2 -6
  2. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  3. package/dist/builtin-personas/runtime-base.md +1 -1
  4. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
  5. package/dist/builtin-views/canvas/core.mjs +3 -2
  6. package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
  7. package/dist/clients/attach/attach-cmd.js +383 -386
  8. package/dist/clients/web/web-cmd.js +7 -7
  9. package/dist/commands/__tests__/human.test.js +1 -15
  10. package/dist/commands/attention.js +9 -6
  11. package/dist/commands/canvas-history/search.js +2 -2
  12. package/dist/commands/canvas-history/show.js +3 -3
  13. package/dist/commands/canvas-history.js +1 -1
  14. package/dist/commands/canvas-issue.js +1 -0
  15. package/dist/commands/canvas-snapshot.js +1 -0
  16. package/dist/commands/canvas.js +10 -11
  17. package/dist/commands/chord.js +1 -0
  18. package/dist/commands/human/prompts.js +0 -1
  19. package/dist/commands/human/queue.js +1 -0
  20. package/dist/commands/node-snapshot.js +1 -0
  21. package/dist/commands/node.js +12 -11
  22. package/dist/commands/push.d.ts +0 -1
  23. package/dist/commands/push.js +2 -228
  24. package/dist/commands/revive.js +2 -2
  25. package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
  26. package/dist/commands/surface.js +3 -2
  27. package/dist/commands/sys/daemon.d.ts +2 -0
  28. package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
  29. package/dist/commands/sys/doctor.js +1 -0
  30. package/dist/commands/sys.js +4 -3
  31. package/dist/core/__tests__/daemon-boot.test.js +1 -1
  32. package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
  33. package/dist/core/__tests__/daemon-wedge.test.js +87 -0
  34. package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
  35. package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
  36. package/dist/core/__tests__/history-inbox.test.js +105 -0
  37. package/dist/core/__tests__/kickoff.test.js +3 -3
  38. package/dist/core/__tests__/revive.test.js +39 -1
  39. package/dist/core/canvas/history.d.ts +1 -1
  40. package/dist/core/canvas/history.js +90 -3
  41. package/dist/core/canvas/paths.d.ts +6 -0
  42. package/dist/core/canvas/paths.js +9 -0
  43. package/dist/core/canvas/status-glyph.js +1 -0
  44. package/dist/core/fault-classifier.d.ts +2 -2
  45. package/dist/core/feed/inbox.d.ts +7 -12
  46. package/dist/core/feed/inbox.js +45 -29
  47. package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
  48. package/dist/core/preview-registry.js +14 -112
  49. package/dist/core/render.js +1 -1
  50. package/dist/core/runtime/busy.d.ts +15 -0
  51. package/dist/core/runtime/busy.js +32 -1
  52. package/dist/core/runtime/close.d.ts +10 -0
  53. package/dist/core/runtime/close.js +18 -13
  54. package/dist/core/runtime/fault-recovery.js +4 -0
  55. package/dist/core/runtime/kickoff.js +5 -7
  56. package/dist/core/runtime/placement.js +1 -1
  57. package/dist/core/runtime/revive.js +9 -1
  58. package/dist/core/runtime/tmux.d.ts +2 -2
  59. package/dist/core/runtime/tmux.js +2 -2
  60. package/dist/daemon/crtrd-cli.js +1 -1
  61. package/dist/daemon/crtrd.d.ts +24 -0
  62. package/dist/daemon/crtrd.js +213 -5
  63. package/dist/daemon/manage.js +2 -2
  64. package/dist/pi-extensions/canvas-nav.js +2 -2
  65. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  66. package/dist/pi-extensions/canvas-stophook.js +59 -2
  67. package/dist/web-client/assets/{index-IAJVtuVe.js → index-MSKSWwV0.js} +1 -1
  68. package/dist/web-client/index.html +1 -1
  69. package/package.json +1 -1
  70. package/dist/commands/daemon.d.ts +0 -2
  71. /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
@@ -2,14 +2,11 @@ import { defineRoot } from './core/command.js';
2
2
  const TAGLINE = 'crtr: agentic runtime.';
3
3
  /** Lazy registry: subtree name → dynamic importer that builds its BranchDef.
4
4
  * The whole point is that each `import()` only compiles that one subtree's
5
- * module graph. The hot path (`crtr node focus …`, `crtr feed`, …) dispatches
5
+ * module graph. The hot path (`crtr node focus …`, `crtr push …`, …) dispatches
6
6
  * into a single leaf, so it must load ONE subtree — not the attach-TUI
7
7
  * (babel/highlight.js), the web/vite command, and every other subtree it never
8
8
  * touches. `crtr` cold-start is dominated by Node module loading; keeping the
9
- * other subtrees off the path is the biggest, lowest-risk win.
10
- *
11
- * Naming note: `push` and `feed` are two subtrees from one module — importing
12
- * it once yields both registers. */
9
+ * other subtrees off the path is the biggest, lowest-risk win. */
13
10
  const SUBTREE_LOADERS = {
14
11
  memory: async () => (await import('./commands/memory.js')).registerMemory(),
15
12
  search: async () => (await import('./commands/search.js')).registerSearch(),
@@ -18,7 +15,6 @@ const SUBTREE_LOADERS = {
18
15
  sys: async () => (await import('./commands/sys.js')).registerSys(),
19
16
  node: async () => (await import('./commands/node.js')).registerNode(),
20
17
  push: async () => (await import('./commands/push.js')).registerPush(),
21
- feed: async () => (await import('./commands/push.js')).registerFeed(),
22
18
  canvas: async () => (await import('./commands/canvas.js')).registerCanvas(),
23
19
  surface: async () => (await import('./commands/surface.js')).registerSurface(),
24
20
  };
@@ -29,7 +29,7 @@ Nothing is reported automatically — the feed contains only what a node **pushe
29
29
  - `push urgent` — force-wakes every subscriber (you're blocked, scope changed, an error derails the plan).
30
30
  - `push final` — the ONLY way a terminal node finishes: writes the canonical result, marks the node done, and tears down its broker. Stopping without it is not finishing. Resident/user-facing nodes stay dormant instead of finishing unless explicitly forced.
31
31
 
32
- A push fans a ~30-token **pointer** (a ref path), not the content; subscribers dereference lazily. When a subscriber push wakes you, **the wake message already IS the coalesced digest** — don't re-run `feed read` to "open" it (the cursor already advanced); dereference the refs that matter. `feed read` is for proactively polling *before* a wake; `feed peek` shows live state of the nodes below you without draining (use it to confirm workers are alive before you yield). An empty feed while workers run is normal.
32
+ A push fans a ~30-token **pointer** (a ref path), not the content; subscribers dereference lazily. When a subscriber push wakes you, **the wake message already IS the coalesced digest** — dereference the refs that matter rather than trying to re-read it. An empty feed while workers run is normal a worker that hasn't pushed yet leaves no pointer; the wake it fires when it does push is automatic, so there's nothing to poll or check in the meantime.
33
33
 
34
34
  ## Lifecycle
35
35
 
@@ -8,7 +8,7 @@ Hand any self-contained unit of work to a child instead of doing it inline — t
8
8
 
9
9
  crtr node new "<task>" --kind <kind> # `crtr node -h` lists the kinds + the delegate→feed loop
10
10
 
11
- You auto-subscribe to every child you spawn, so you're woken when it finishes; read what they reported with `crtr feed read` and dereference the reports that matter. Prefer delegating over grinding it out yourself.
11
+ You auto-subscribe to every child you spawn, so you're woken when it finishes; the wake message already carries the digest dereference the reports that matter. Prefer delegating over grinding it out yourself.
12
12
 
13
13
  When the work depends on context you already hold — an explore report, a design, a spec, earlier findings — name those files by path in the task itself: a child starts blind and inherits only what you point it at, so unreferenced context you have is context it won't find.
14
14
 
@@ -107,8 +107,8 @@ How to operate:
107
107
  it tight and reviewable.
108
108
  - Then validate the spec with a reviewer node first: spawn one with
109
109
  \`crtr node new "Review the spec at <absolute-path>" --kind review\` — you
110
- auto-subscribe to it and are woken when it finishes. Read its verdict with
111
- \`crtr feed read\` and dereference the report path it points to. It flags
110
+ auto-subscribe to it and are woken when it finishes the wake message
111
+ already carries the digest; dereference the report path it points to. It flags
112
112
  completeness gaps, contradictions, ambiguity, scope creep, and unspecified
113
113
  failure modes on critical paths. Resolve every Issue it raises before showing
114
114
  the spec to anyone;
@@ -147,8 +147,8 @@ How to operate:
147
147
  which tests to write, plus risks and dependencies.
148
148
  - Then validate the plan with a reviewer node first: spawn one with
149
149
  \`crtr node new "Review the plan at <absolute-path>" --kind review\` — you
150
- auto-subscribe to it and are woken when it finishes. Read its verdict with
151
- \`crtr feed read\` and dereference the report path it points to. It flags
150
+ auto-subscribe to it and are woken when it finishes the wake message
151
+ already carries the digest; dereference the report path it points to. It flags
152
152
  completeness gaps, spec misalignment, unresolved decisions, buildability
153
153
  problems, quality smells
154
154
  (timelines, "for now" shortcuts, magic values, fallbacks, missing type
@@ -60,9 +60,9 @@
60
60
  */
61
61
  /**
62
62
  * @typedef {Object} Fault
63
- * @property {'pi→provider'|'viewer↔broker'|'relay↔broker'|'browser↔relay'|'cli→exa'} link
63
+ * @property {'pi→provider'|'viewer↔broker'|'relay↔broker'|'browser↔relay'|'cli→exa'|'daemon→node'} link
64
64
  * @property {string} op
65
- * @property {'rate-limit'|'overloaded'|'connection'|'auth'|'protocol'|'other'} kind
65
+ * @property {'rate-limit'|'overloaded'|'connection'|'auth'|'protocol'|'other'|'wedged'} kind
66
66
  * @property {FaultRetry} retry
67
67
  * @property {string} message The raw engine error (capped).
68
68
  * @property {string} since ISO 8601 — when the fault was first observed.
@@ -188,6 +188,7 @@ export function hangingLabel(kind) {
188
188
  if (kind === 'connection') return 'down';
189
189
  if (kind === 'auth') return 'auth';
190
190
  if (kind === 'protocol') return 'protocol';
191
+ if (kind === 'wedged') return 'wedged';
191
192
  return 'other';
192
193
  }
193
194
 
@@ -19,7 +19,7 @@ function stripAnsi(text) {
19
19
  test('detects crtr in bash command position, not heredoc body prose', () => {
20
20
  assert.equal(isCrtrBashCommand('crtr node new -h'), true);
21
21
  assert.equal(isCrtrBashCommand('crtr node inspect snapshot --node abc'), true);
22
- assert.equal(isCrtrBashCommand('crtr canvas daemon status'), true);
22
+ assert.equal(isCrtrBashCommand('crtr sys daemon status'), true);
23
23
  assert.equal(isCrtrBashCommand('npm test && /usr/local/bin/crouter surface view run inbox'), true);
24
24
  assert.equal(isCrtrBashCommand("cat <<'EOF'\nask about crtr node new\nEOF\necho done"), false);
25
25
  assert.equal(isCrtrBashCommand('echo crtr node new'), false);
@@ -161,14 +161,14 @@ test('a branch help preview with more subcommands than the line cap truncates wi
161
161
  // Contract: outer bash redirects are shell plumbing, not crtr argv, so the
162
162
  // displayed crtr path and help detection include only the command segment.
163
163
  test('a shell redirect on the outer bash call never leaks into the displayed crtr path or help detection', () => {
164
- const def = createCrtrBashToolDefinition({ command: 'crtr feed -h 2>&1' });
165
- const rendered = def.renderCall({ command: 'crtr feed -h 2>&1' }, theme).render(80).join('\n');
164
+ const def = createCrtrBashToolDefinition({ command: 'crtr push -h 2>&1' });
165
+ const rendered = def.renderCall({ command: 'crtr push -h 2>&1' }, theme).render(80).join('\n');
166
166
  assert.match(rendered, new RegExp(HELP_ICON));
167
167
  assert.match(rendered, /crtr help/);
168
- assert.match(rendered, /<b>feed<\/b>/);
168
+ assert.match(rendered, /<b>push<\/b>/);
169
169
  assert.doesNotMatch(rendered, /2>/);
170
170
  assert.doesNotMatch(rendered, />&1/);
171
- assert.equal(isCrtrHelpInvocation('crtr feed -h 2>&1'), true);
171
+ assert.equal(isCrtrHelpInvocation('crtr push -h 2>&1'), true);
172
172
  // Other redirect shapes on the outer call, and mid-argv redirects, must
173
173
  // never survive into the path either.
174
174
  const other = createCrtrBashToolDefinition({ command: 'crtr node new --name foo > /tmp/log 2>&1' });
@@ -177,9 +177,9 @@ test('a shell redirect on the outer bash call never leaks into the displayed crt
177
177
  assert.doesNotMatch(otherRendered, /tmp\/log/);
178
178
  // Redirect operators glued to their target with NO space (`2>/tmp/out`,
179
179
  // `>/tmp/log`) are still shell plumbing and never displayed as path text.
180
- const noSpace = createCrtrBashToolDefinition({ command: 'crtr feed -h 2>/tmp/out' });
181
- const noSpaceRendered = noSpace.renderCall({ command: 'crtr feed -h 2>/tmp/out' }, theme).render(80).join('\n');
182
- assert.match(noSpaceRendered, /<b>feed<\/b>/);
180
+ const noSpace = createCrtrBashToolDefinition({ command: 'crtr push -h 2>/tmp/out' });
181
+ const noSpaceRendered = noSpace.renderCall({ command: 'crtr push -h 2>/tmp/out' }, theme).render(80).join('\n');
182
+ assert.match(noSpaceRendered, /<b>push<\/b>/);
183
183
  assert.doesNotMatch(noSpaceRendered, /2>/);
184
184
  assert.doesNotMatch(noSpaceRendered, /tmp\/out/);
185
185
  const noSpaceMid = createCrtrBashToolDefinition({ command: 'crtr node new --name foo >/tmp/log' });
@@ -206,12 +206,12 @@ test('a collapsed help card never shows the help label twice (header + body)', (
206
206
  // (redirects and all) plus the exact, unsummarized output, for both help and
207
207
  // normal commands.
208
208
  test('expanded view shows the original raw command (redirects included) plus the raw output', () => {
209
- const def = createCrtrBashToolDefinition({ command: 'crtr feed -h 2>&1' });
209
+ const def = createCrtrBashToolDefinition({ command: 'crtr push -h 2>&1' });
210
210
  const expanded = def
211
211
  .renderResult({ content: [{ type: 'text', text: LEAF_SCHEMA }] }, { expanded: true }, theme, {})
212
212
  .render(80)
213
213
  .join('\n');
214
- assert.match(expanded, /\$ crtr feed -h 2>&1/);
214
+ assert.match(expanded, /\$ crtr push -h 2>&1/);
215
215
  assert.match(expanded, /render the canvas as an ASCII subscription/);
216
216
  assert.match(expanded, /Read-only: queries canvas\.db/);
217
217
  // Normal (non-help) command: expanded still carries the raw invocation.
@@ -236,15 +236,15 @@ test('a running or failed crtr -h invocation never gets the muted help status',
236
236
  // the command and the output is right there. Success (help or normal) carries
237
237
  // no body status; only running/failed do.
238
238
  test('a successful crtr call shows no "crtr done" body status line', () => {
239
- const def = createCrtrBashToolDefinition({ command: 'crtr feed' });
239
+ const def = createCrtrBashToolDefinition({ command: 'crtr push update' });
240
240
  const collapsed = def
241
- .renderResult({ content: [{ type: 'text', text: 'some feed output' }] }, { expanded: false }, theme, {})
241
+ .renderResult({ content: [{ type: 'text', text: 'some push output' }] }, { expanded: false }, theme, {})
242
242
  .render(80)
243
243
  .join('\n');
244
244
  assert.doesNotMatch(collapsed, /crtr done/);
245
- assert.match(collapsed, /some feed output/);
245
+ assert.match(collapsed, /some push output/);
246
246
  const expanded = def
247
- .renderResult({ content: [{ type: 'text', text: 'some feed output' }] }, { expanded: true }, theme, {})
247
+ .renderResult({ content: [{ type: 'text', text: 'some push output' }] }, { expanded: true }, theme, {})
248
248
  .render(80)
249
249
  .join('\n');
250
250
  assert.doesNotMatch(expanded, /crtr done/);
@@ -428,7 +428,7 @@ test('renderCall uses a mapped Nerd Font glyph and one line for every mapped roo
428
428
  const cases = [
429
429
  { command: 'crtr memory lint', iconTokens: ['memory', 'lint'], path: 'memory lint' },
430
430
  { command: 'crtr canvas dashboard', iconTokens: ['canvas', 'dashboard'], path: 'canvas dashboard' },
431
- { command: 'crtr feed read', iconTokens: ['feed', 'read'], path: 'feed read' },
431
+ { command: 'crtr node yield', iconTokens: ['node', 'yield'], path: 'node yield' },
432
432
  { command: 'crtr human ask', iconTokens: ['human', 'ask'], path: 'human ask' },
433
433
  { command: 'crtr node config set foo bar', iconTokens: ['node', 'config'], path: 'node config set foo bar' },
434
434
  { command: 'crtr pkg plugin list', iconTokens: ['pkg', 'plugin', 'list'], path: 'pkg plugin list' },
@@ -583,7 +583,7 @@ test('icon lookup uses per-leaf glyphs before family glyphs, with help unchanged
583
583
  assert.equal(HELP_ICON, String.fromCodePoint(0xf059));
584
584
  });
585
585
  // ---------------------------------------------------------------------------
586
- // canvas/node/feed runtime cockpit summaries (first non-memory batch, Silas's
586
+ // canvas/node runtime cockpit summaries (first non-memory batch, Silas's
587
587
  // 2026-07-01 reframe: collapsed cards are computed status digests, not
588
588
  // decorated stdout). Fixtures below are literal renderResult() output shapes
589
589
  // (core/render.ts: a `- field: N` scalar bullet, an `N <label>:` count
@@ -715,7 +715,7 @@ test('node inspect show is command-owned suppressOutput, never on failure', () =
715
715
  assert.match(failed.render(80).join('\n'), /huge dump on failure too/);
716
716
  });
717
717
  // ---------------------------------------------------------------------------
718
- // Batch B: canvas/node/feed/push runtime cockpit summaries (Silas's
718
+ // Batch B: canvas/node/push runtime cockpit summaries (Silas's
719
719
  // 2026-07-01 reframe continued). Fixtures below are literal render() output
720
720
  // shapes copied from the real command source (see preview-registry.ts's
721
721
  // per-summarizer doc comments for the exact file:line), not the live command
@@ -961,8 +961,8 @@ test('a crtr canvas history show card collapses to a node line + type-count dige
961
961
  .join('\n');
962
962
  assert.match(emptyExpanded, /0 artifacts\./);
963
963
  });
964
- test('crtr canvas daemon start/status/stop cards collapse to a one-line pid status', () => {
965
- const start = createCrtrBashToolDefinition({ command: 'crtr canvas daemon start' });
964
+ test('crtr sys daemon start/status/stop cards collapse to a one-line pid status', () => {
965
+ const start = createCrtrBashToolDefinition({ command: 'crtr sys daemon start' });
966
966
  assert.match(start.renderResult({ content: [{ type: 'text', text: '- started: true\n- pid: 4242' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /<success>started · pid 4242<\/success>/);
967
967
  assert.match(start.renderResult({ content: [{ type: 'text', text: '- started: false\n- existing_pid: 99' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /<muted>already running · pid 99<\/muted>/);
968
968
  assert.match(start.renderResult({ content: [{ type: 'text', text: '- started: false\n- existing_pid: 99' }] }, { expanded: true }, theme, {}).render(120).join('\n'), /existing_pid: 99/);
@@ -972,7 +972,7 @@ test('crtr canvas daemon start/status/stop cards collapse to a one-line pid stat
972
972
  .join('\n');
973
973
  assert.match(startExpanded, /started: true/);
974
974
  assert.match(startExpanded, /pid: 4242/);
975
- const status = createCrtrBashToolDefinition({ command: 'crtr canvas daemon status' });
975
+ const status = createCrtrBashToolDefinition({ command: 'crtr sys daemon status' });
976
976
  assert.match(status.renderResult({ content: [{ type: 'text', text: '- running: true\n- pid: 4242' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /<success>running · pid 4242<\/success>/);
977
977
  assert.match(status.renderResult({ content: [{ type: 'text', text: '- running: false' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /<error>not running<\/error>/);
978
978
  assert.match(status.renderResult({ content: [{ type: 'text', text: '- running: false' }] }, { expanded: true }, theme, {}).render(120).join('\n'), /running: false/);
@@ -982,7 +982,7 @@ test('crtr canvas daemon start/status/stop cards collapse to a one-line pid stat
982
982
  .join('\n');
983
983
  assert.match(statusExpanded, /running: true/);
984
984
  assert.match(statusExpanded, /pid: 4242/);
985
- const stop = createCrtrBashToolDefinition({ command: 'crtr canvas daemon stop' });
985
+ const stop = createCrtrBashToolDefinition({ command: 'crtr sys daemon stop' });
986
986
  assert.match(stop.renderResult({ content: [{ type: 'text', text: '- stopped: true\n- pid: 4242' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /stopped · pid 4242/);
987
987
  assert.match(stop.renderResult({ content: [{ type: 'text', text: '- stopped: false' }] }, { expanded: false }, theme, {}).render(120).join('\n'), /<muted>was not running<\/muted>/);
988
988
  assert.match(stop.renderResult({ content: [{ type: 'text', text: '- stopped: false' }] }, { expanded: true }, theme, {}).render(120).join('\n'), /stopped: false/);
@@ -1274,190 +1274,6 @@ test('a crtr node triggers list card collapses to a pending-count + top rows, no
1274
1274
  .join('\n');
1275
1275
  assert.match(emptyExpanded, /No pending triggers in scope self\./);
1276
1276
  });
1277
- test('a crtr feed read card collapses to a drained-pointer digest, not the raw coalesced sections', () => {
1278
- const def = createCrtrBashToolDefinition({ command: 'crtr feed read' });
1279
- const drained = def
1280
- .renderResult({
1281
- content: [
1282
- {
1283
- type: 'text',
1284
- text: [
1285
- "3 unread pointers drained from mq123's inbox.",
1286
- '',
1287
- 'From worker-a — 2 updates:',
1288
- ' [push] did a thing (ref: worker-a:reports/x.md)',
1289
- ' [push] did another (ref: worker-a:reports/y.md)',
1290
- '',
1291
- 'From worker-b — 1 update:',
1292
- ' [push] finished (ref: worker-b:reports/z.md)',
1293
- ].join('\n'),
1294
- },
1295
- ],
1296
- }, { expanded: false }, theme, {})
1297
- .render(140)
1298
- .join('\n');
1299
- assert.match(drained, /3 pointers drained from 2 nodes/);
1300
- assert.match(drained, /<muted>worker-a, worker-b<\/muted>/);
1301
- assert.doesNotMatch(drained, /ref: worker-a/);
1302
- const drainedExpanded = def
1303
- .renderResult({
1304
- content: [
1305
- {
1306
- type: 'text',
1307
- text: [
1308
- "3 unread pointers drained from mq123's inbox.",
1309
- '',
1310
- 'From worker-a — 2 updates:',
1311
- ' [push] did a thing (ref: worker-a:reports/x.md)',
1312
- ' [push] did another (ref: worker-a:reports/y.md)',
1313
- '',
1314
- 'From worker-b — 1 update:',
1315
- ' [push] finished (ref: worker-b:reports/z.md)',
1316
- ].join('\n'),
1317
- },
1318
- ],
1319
- }, { expanded: true }, theme, {})
1320
- .render(140)
1321
- .join('\n');
1322
- assert.match(drainedExpanded, /ref: worker-a:reports\/x\.md/);
1323
- const empty = def
1324
- .renderResult({ content: [{ type: 'text', text: 'No unread pointers for mq123 (unread=0).\n\nYour inbox is empty — nothing has arrived yet.' }] }, { expanded: false }, theme, {})
1325
- .render(120)
1326
- .join('\n');
1327
- assert.match(empty, /<muted>nothing unread<\/muted>/);
1328
- const emptyExpanded = def
1329
- .renderResult({ content: [{ type: 'text', text: 'No unread pointers for mq123 (unread=0).\n\nYour inbox is empty — nothing has arrived yet.' }] }, { expanded: true }, theme, {})
1330
- .render(120)
1331
- .join('\n');
1332
- assert.match(emptyExpanded, /Your inbox is empty — nothing has arrived yet\./);
1333
- });
1334
- test('a crtr feed message card previews an identity line + body teaser, not the raw label', () => {
1335
- const def = createCrtrBashToolDefinition({ command: 'crtr feed message m1' });
1336
- const found = def
1337
- .renderResult({ content: [{ type: 'text', text: ['Full message m1 (from mq123):', '', 'Line one', 'Line two', 'Line three', 'Line four'].join('\n') }] }, { expanded: false }, theme, {})
1338
- .render(120)
1339
- .join('\n');
1340
- assert.match(found, /m1 · from mq123/);
1341
- assert.match(found, /Line one/);
1342
- assert.match(found, /…/);
1343
- assert.doesNotMatch(found, /Line four/);
1344
- const foundExpanded = def
1345
- .renderResult({ content: [{ type: 'text', text: ['Full message m1 (from mq123):', '', 'Line one', 'Line two', 'Line three', 'Line four'].join('\n') }] }, { expanded: true }, theme, {})
1346
- .render(120)
1347
- .join('\n');
1348
- assert.match(foundExpanded, /Line four/);
1349
- const notFound = def
1350
- .renderResult({ content: [{ type: 'text', text: "No inbox entry with id m1 in mq123's inbox. Re-read the digest with `crtr feed read --all`." }] }, { expanded: false }, theme, {})
1351
- .render(120)
1352
- .join('\n');
1353
- assert.match(notFound, /No inbox entry with id m1/);
1354
- const notFoundExpanded = def
1355
- .renderResult({ content: [{ type: 'text', text: "No inbox entry with id m1 in mq123's inbox. Re-read the digest with `crtr feed read --all`." }] }, { expanded: true }, theme, {})
1356
- .render(120)
1357
- .join('\n');
1358
- assert.match(notFoundExpanded, /Re-read the digest with `crtr feed read --all`\./);
1359
- });
1360
- test('a crtr feed peek card collapses to a verdict + working/done/dead counts, not the raw rows', () => {
1361
- const def = createCrtrBashToolDefinition({ command: 'crtr feed peek' });
1362
- const zeroClear = def
1363
- .renderResult({ content: [{ type: 'text', text: 'No nodes below you (0 subscriptions, 0 unread). Nothing will wake you.' }] }, { expanded: false }, theme, {})
1364
- .render(120)
1365
- .join('\n');
1366
- assert.match(zeroClear, /<success>PASS · nothing below<\/success>/);
1367
- const zeroClearExpanded = def
1368
- .renderResult({ content: [{ type: 'text', text: 'No nodes below you (0 subscriptions, 0 unread). Nothing will wake you.' }] }, { expanded: true }, theme, {})
1369
- .render(120)
1370
- .join('\n');
1371
- assert.match(zeroClearExpanded, /Nothing will wake you\./);
1372
- const zeroUnread = def
1373
- .renderResult({ content: [{ type: 'text', text: 'No nodes below you (0 subscriptions, 4 unread). 4 unread reports sit in your inbox.' }] }, { expanded: false }, theme, {})
1374
- .render(120)
1375
- .join('\n');
1376
- assert.match(zeroUnread, /<warning>0 below · 4 unread<\/warning>/);
1377
- const zeroUnreadExpanded = def
1378
- .renderResult({ content: [{ type: 'text', text: 'No nodes below you (0 subscriptions, 4 unread). 4 unread reports sit in your inbox.' }] }, { expanded: true }, theme, {})
1379
- .render(120)
1380
- .join('\n');
1381
- assert.match(zeroUnreadExpanded, /4 unread reports sit in your inbox\./);
1382
- const withDead = def
1383
- .renderResult({
1384
- content: [
1385
- {
1386
- type: 'text',
1387
- text: [
1388
- '⚠ 1 below you is dead and will NOT wake you.',
1389
- '',
1390
- '3 nodes below you — 1 working, 1 done, 1 dead, 2 unread:',
1391
- '- ● worker-a (mq1) · developer · active · spawned 5m ago · cyc 2 · pushed 1m ago [push]',
1392
- '- ✓ worker-b (mq2) · developer · done · spawned 10m ago · cyc 3 · pushed 2m ago [push]',
1393
- '- ✗ worker-c (mq3) · developer · dead · spawned 20m ago · cyc 1 · no push yet',
1394
- ].join('\n'),
1395
- },
1396
- ],
1397
- }, { expanded: false }, theme, {})
1398
- .render(140)
1399
- .join('\n');
1400
- assert.match(withDead, /1 working · 1 done · 1 dead/);
1401
- assert.match(withDead, /<warning>1 dead — will not wake you<\/warning>/);
1402
- assert.doesNotMatch(withDead, /pushed 1m ago/);
1403
- const withDeadExpanded = def
1404
- .renderResult({
1405
- content: [
1406
- {
1407
- type: 'text',
1408
- text: [
1409
- '⚠ 1 below you is dead and will NOT wake you.',
1410
- '',
1411
- '3 nodes below you — 1 working, 1 done, 1 dead, 2 unread:',
1412
- '- ● worker-a (mq1) · developer · active · spawned 5m ago · cyc 2 · pushed 1m ago [push]',
1413
- '- ✓ worker-b (mq2) · developer · done · spawned 10m ago · cyc 3 · pushed 2m ago [push]',
1414
- '- ✗ worker-c (mq3) · developer · dead · spawned 20m ago · cyc 1 · no push yet',
1415
- ].join('\n'),
1416
- },
1417
- ],
1418
- }, { expanded: true }, theme, {})
1419
- .render(140)
1420
- .join('\n');
1421
- assert.match(withDeadExpanded, /pushed 1m ago/);
1422
- const noDead = def
1423
- .renderResult({
1424
- content: [
1425
- {
1426
- type: 'text',
1427
- text: [
1428
- 'Safe to yield — 1 worker running async will wake you.',
1429
- '',
1430
- '2 nodes below you — 1 working, 1 done, 0 dead, 3 unread:',
1431
- '- ● worker-a (mq1) · developer · active · spawned 5m ago · cyc 2 · pushed 1m ago [push]',
1432
- '- ✓ worker-b (mq2) · developer · done · spawned 10m ago · cyc 3 · pushed 2m ago [push]',
1433
- ].join('\n'),
1434
- },
1435
- ],
1436
- }, { expanded: false }, theme, {})
1437
- .render(140)
1438
- .join('\n');
1439
- assert.match(noDead, /1 working · 1 done/);
1440
- assert.doesNotMatch(noDead, /0 dead/);
1441
- assert.match(noDead, /<warning>3 unread<\/warning>/);
1442
- const noDeadExpanded = def
1443
- .renderResult({
1444
- content: [
1445
- {
1446
- type: 'text',
1447
- text: [
1448
- 'Safe to yield — 1 worker running async will wake you.',
1449
- '',
1450
- '2 nodes below you — 1 working, 1 done, 0 dead, 3 unread:',
1451
- '- ● worker-a (mq1) · developer · active · spawned 5m ago · cyc 2 · pushed 1m ago [push]',
1452
- '- ✓ worker-b (mq2) · developer · done · spawned 10m ago · cyc 3 · pushed 2m ago [push]',
1453
- ].join('\n'),
1454
- },
1455
- ],
1456
- }, { expanded: true }, theme, {})
1457
- .render(140)
1458
- .join('\n');
1459
- assert.match(noDeadExpanded, /Safe to yield — 1 worker running async will wake you\./);
1460
- });
1461
1277
  test('crtr push urgent/final cards collapse to a one-line outcome + report basename', () => {
1462
1278
  const urgentDef = createCrtrBashToolDefinition({ command: 'crtr push urgent' });
1463
1279
  const urgent = urgentDef