@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
@@ -36,11 +36,11 @@ function openBrowser(url) {
36
36
  }
37
37
  const webServeLeaf = defineLeaf({
38
38
  name: 'serve',
39
- description: 'start the crouter web UI server — the shell, the source/command bridge, and a browser broker relay',
40
- whenToUse: 'you want the crouter web UI: a browser shell to watch the canvas and drive headless nodes, instead of a terminal pane. It starts ONE long-running server (default 127.0.0.1) that serves the shell SPA, the source + command bridge (POST /__crtr/source — views\' reads AND `crtr` command writes), an SSE change-invalidation lane (GET /__crtr/events), and a VERBATIM browser ⇄ broker relay on /node/<id> (the browser becomes the same protocol peer as surface attach: one controller drives, extra viewers follow read-only). If you bind a non-loopback host, pass --token so the remote-capable bridge, SSE, and relay endpoints can authorize the page. The relay NEVER starts an engine each node must already have a running headless broker (focus or revive it first); all writes flow through `crtr` subprocesses the bridge runs. Pass --dev for Vite middleware (HMR) while iterating on the shell. The server runs until ctrl+c; the node is picked in the browser via the URL, not on the command line.',
39
+ description: 'start the crouter web UI server — the browser alternative to a tmux pane for watching the canvas and driving headless nodes',
40
+ whenToUse: 'you want the crouter web UI in a browser instead of a tmux pane — watching the canvas and driving headless nodes from a page rather than a terminal surface. Binds 127.0.0.1 by default; pass --host to bind elsewhere, in which case --token is required so the page can authorize. Pass --dev only if you are iterating on the shell itself (Vite HMR) rather than just running it. The node is picked in the browser via the URL, not on the command line; the server runs until ctrl+c.',
41
41
  help: {
42
42
  name: 'web serve',
43
- summary: 'start the long-running crouter web UI server: serves the shell SPA, the POST /__crtr/source bridge, the GET /__crtr/events SSE lane, and a VERBATIM browser ⇄ node-broker relay (browser picks the node via /node/<id>)',
43
+ summary: 'start the crouter web UI server the browser alternative to a tmux pane for watching the canvas and driving headless nodes',
44
44
  params: [
45
45
  {
46
46
  kind: 'flag',
@@ -125,14 +125,14 @@ export function registerWeb() {
125
125
  return defineBranch({
126
126
  name: 'web',
127
127
  rootEntry: {
128
- concept: 'the crouter web UI server — one host serving the browser shell, the source/command bridge, an SSE change lane, and a VERBATIM relay to nodes\' running brokers',
128
+ concept: 'the crouter web UI server — the browser alternative to a tmux pane for watching the canvas and driving headless nodes',
129
129
  desc: 'serve the crouter web UI for the canvas and headless nodes',
130
- useWhen: 'you want the crouter web UI in a browser instead of a tmux pane: watch the canvas and drive headless nodes. It runs one long-lived server that serves the shell SPA, runs view sources and crtr command writes over POST /__crtr/source, streams canvas-change invalidations over GET /__crtr/events, and relays a browser connection VERBATIM to a node\'s already-running broker on /node/<id>. The node is selected in the browser via the URL; if the server is bound off-loopback, the page must carry a token so the relay and bridge can authorize.',
130
+ useWhen: 'you want the crouter web UI in a browser instead of a tmux pane: watch the canvas and drive headless nodes from a page. Binds 127.0.0.1 by default; a non-loopback bind requires a token so the page can authorize. The node is selected in the browser via the URL, not on the command line.',
131
131
  },
132
132
  help: {
133
133
  name: 'web',
134
- summary: 'serve the crouter web UI: the shell SPA, the source/command bridge, the SSE change lane, and a VERBATIM relay to headless nodes\' brokers',
135
- model: 'The branch is a long-running browser surface. `serve` starts the unified server on the bound host and port, serves the shell SPA, exposes the source/command bridge and SSE change lane, and relays browser frames to a node\'s running broker. The node is chosen in the browser via the URL; the server itself only hosts the surface and never spawns an engine.',
134
+ summary: 'serve the crouter web UI the browser alternative to a tmux pane for the canvas and headless nodes',
135
+ model: 'The branch is a long-running browser surface, an alternative to a tmux pane. `serve` binds a host and port (loopback by default; --token required off-loopback) and runs until interrupted. The node is chosen in the browser via the URL, not on the command line; the server itself only hosts the surface and never spawns an engine.',
136
136
  },
137
137
  children: [webServeLeaf],
138
138
  });
@@ -100,7 +100,7 @@ describe('human show: params', () => {
100
100
  });
101
101
  });
102
102
  // ---------------------------------------------------------------------------
103
- // human ask — --context-file (context-file kind, key=deckFile) + --wait bool
103
+ // human ask — --context-file (context-file kind, key=deckFile)
104
104
  //
105
105
  // NOTE: The argv parser hardcodes the CLI token as `--context-file` regardless
106
106
  // of the param's `name` field. The `name` field ("deckFile") becomes the key
@@ -111,7 +111,6 @@ describe('human show: params', () => {
111
111
  describe('human ask: params', () => {
112
112
  const params = [
113
113
  { kind: 'context-file', name: 'deckFile', required: true, constraint: 'Humanloop deck JSON.' },
114
- { kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'Presence-only.' },
115
114
  ];
116
115
  test('--context-file parses JSON file and yields input.deckFile', async () => {
117
116
  const deck = { interactions: [{ id: 'q', title: 'Go?', options: [{ id: 'yes', label: 'Yes' }] }] };
@@ -132,18 +131,6 @@ describe('human ask: params', () => {
132
131
  writeFileSync(p, 'not json at all');
133
132
  await assert.rejects(() => parseArgv(params, ['--context-file', p]), (err) => { assert.match(err.message, /not valid JSON/); return true; });
134
133
  });
135
- test('--wait absent → false', async () => {
136
- const deck = { interactions: [] };
137
- const path = tmpJson(deck);
138
- const result = await parseArgv(params, ['--context-file', path]);
139
- assert.equal(result['wait'], false);
140
- });
141
- test('--wait present → true', async () => {
142
- const deck = { interactions: [] };
143
- const path = tmpJson(deck);
144
- const result = await parseArgv(params, ['--context-file', path, '--wait']);
145
- assert.equal(result['wait'], true);
146
- });
147
134
  });
148
135
  // ---------------------------------------------------------------------------
149
136
  // human ask — shorthand question + strict --context-file deck
@@ -159,7 +146,6 @@ describe('human ask: shorthand', () => {
159
146
  test('parses a positional question into input.question', async () => {
160
147
  const result = await parseArgv(humanAsk.help.params ?? [], ['Should we ship today?']);
161
148
  assert.equal(result['question'], 'Should we ship today?');
162
- assert.equal(result['wait'], false);
163
149
  });
164
150
  test('parses --context-file into input.deck while keeping the strict deck path', async () => {
165
151
  const deck = {
@@ -1,9 +1,11 @@
1
1
  // `crtr canvas attention` — aggregate pending human asks across the canvas.
2
2
  //
3
3
  // Pending asks are stored per-cwd by humanloop. This subtree surfaces two
4
- // views:
5
- // count — a single integer (stdout.count is parsed by nav chrome)
4
+ // public views:
5
+ // count — a single integer (stdout.count is machine-parseable)
6
6
  // list — itemised entries with cwd, node id, and per-cwd ask count
7
+ // `map` (hidden) is the batched per-node form the attach viewer's nav chrome
8
+ // polls; it is not an agent-facing decision.
7
9
  //
8
10
  // All three scope modes share the same underlying helpers in attention.ts:
9
11
  // --node <id> → countAsks(id) (one cwd)
@@ -21,7 +23,7 @@ import { countAsks, pendingAsksForView, asksAcrossCanvas, asksForNodes, } from '
21
23
  const attentionCount = defineLeaf({
22
24
  name: 'count',
23
25
  description: 'total pending ask count (machine-parseable stdout.count)',
24
- whenToUse: 'getting a single pending-ask count for a script or nav chrome to read; scope it to a node or a sub-DAG, or default to canvas-wide',
26
+ whenToUse: 'a lightweight summary when you just need the number, not which cwds are blocked — scope it to a node or a sub-DAG, or default to canvas-wide. Use `list` when you need to act on where to go answer.',
25
27
  help: {
26
28
  name: 'canvas attention count',
27
29
  // stdout.count is parsed directly by the nav chrome — keep the contract.
@@ -93,7 +95,7 @@ const attentionCount = defineLeaf({
93
95
  const attentionList = defineLeaf({
94
96
  name: 'list',
95
97
  description: 'itemised list of cwds with pending asks',
96
- whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending asks, oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when a script just needs the number, or `canvas attention map` for per-node counts to label a UI',
98
+ whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending asks, oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
97
99
  help: {
98
100
  name: 'canvas attention list',
99
101
  summary: 'list nodes with pending human asks, grouped by cwd, oldest first',
@@ -145,6 +147,7 @@ const attentionMap = defineLeaf({
145
147
  name: 'map',
146
148
  description: 'per-node ask counts for a visible set, batched in one pass',
147
149
  whenToUse: 'labelling every node in a UI with its pending-ask count in one batched pass — the form nav chrome polls (one process, one JSON blob) instead of N count shell-outs',
150
+ tier: 'hidden',
148
151
  help: {
149
152
  name: 'canvas attention map',
150
153
  summary: 'per-node pending-ask counts for a visible set of nodes in ONE pass — the batched form the nav chrome polls (one process, one JSON blob) instead of N count shell-outs',
@@ -211,11 +214,11 @@ const attentionMap = defineLeaf({
211
214
  export const attentionBranch = defineBranch({
212
215
  name: 'attention',
213
216
  description: 'count/list pending human asks across the graph',
214
- whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending asks, list the cwds that have them, or map per-node counts. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
217
+ whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending asks or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
215
218
  help: {
216
219
  name: 'canvas attention',
217
220
  summary: 'aggregate pending human asks across the canvas',
218
- model: 'Human asks are stored per-cwd by humanloop. `count` returns a single integer (stdout.count is parsed by nav chrome); `list` returns itemised entries. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide.',
221
+ model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised entries — the cwds with pending asks, oldest first. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide.',
219
222
  },
220
223
  children: [attentionCount, attentionList, attentionMap],
221
224
  });
@@ -1,7 +1,7 @@
1
1
  import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { buildCorpus, } from '../../core/canvas/index.js';
4
- const TYPES = ['report', 'doc', 'roadmap', 'meta'];
4
+ const TYPES = ['report', 'doc', 'roadmap', 'meta', 'inbox'];
5
5
  const NODE_KINDS = ['advisor', 'developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
6
6
  const STATUSES = ['active', 'idle', 'done', 'dead', 'canceled'];
7
7
  const SORTS = ['relevance', 'recency', 'oldest'];
@@ -86,7 +86,7 @@ export const searchLeaf = defineLeaf({
86
86
  { kind: 'flag', name: 'all-cwds', type: 'bool', required: false, constraint: 'Search every cwd on the canvas. Mutually exclusive with --cwd.' },
87
87
  { kind: 'flag', name: 'under', type: 'string', required: false, constraint: 'Restrict to a node and its subscription descendants (one initiative / sub-DAG).' },
88
88
  { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Restrict to specific node ids. Comma-separated for several.' },
89
- { kind: 'flag', name: 'type', type: 'string', required: false, constraint: `Corpus to search; comma-separated for several. One of: ${TYPES.join(', ')}. report = push history; doc = context artifacts; roadmap = roadmap.md specifically; meta = node name/description/kind. Default: all.` },
89
+ { kind: 'flag', name: 'type', type: 'string', required: false, constraint: `Corpus to search; comma-separated for several. One of: ${TYPES.join(', ')}. report = push history; doc = context artifacts; roadmap = roadmap.md specifically; meta = node name/description/kind; inbox = raw inbox.jsonl history, cursor-independent and including full un-clipped direct-message bodies (diagnostic, not curated work). Default: report, doc, roadmap, meta — inbox is opt-in only, pass --type inbox explicitly.` },
90
90
  { kind: 'flag', name: 'report-kind', type: 'enum', choices: ['final', 'update'], required: false, constraint: 'Narrow reports to outcome summaries (final) or progress (update). Implies --type report.' },
91
91
  { kind: 'flag', name: 'kind', type: 'string', required: false, constraint: `Node kind; comma-separated for several. One of: ${NODE_KINDS.join(', ')}.` },
92
92
  { kind: 'flag', name: 'status', type: 'string', required: false, constraint: `Node lifecycle status; comma-separated for several. One of: ${STATUSES.join(', ')} (e.g. done = completed work only).` },
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { notFound } from '../../core/errors.js';
3
3
  import { getNode, nodeArtifacts } from '../../core/canvas/index.js';
4
4
  import { statSync } from 'node:fs';
5
- const TYPES = ['report', 'doc', 'roadmap'];
5
+ const TYPES = ['report', 'doc', 'roadmap', 'inbox'];
6
6
  export const showLeaf = defineLeaf({
7
7
  name: 'show',
8
8
  description: 'enumerate one node\'s artifacts as refs',
@@ -12,12 +12,12 @@ export const showLeaf = defineLeaf({
12
12
  summary: 'list one node\'s artifacts (reports + context docs) as refs ready for `canvas history read`',
13
13
  params: [
14
14
  { kind: 'positional', name: 'node-id', required: true, constraint: 'The node whose artifacts to list.' },
15
- { kind: 'flag', name: 'type', type: 'enum', choices: [...TYPES], required: false, constraint: 'Narrow to one corpus: report | doc | roadmap. Default: all.' },
15
+ { kind: 'flag', name: 'type', type: 'enum', choices: [...TYPES], required: false, constraint: 'Narrow to one corpus: report | doc | roadmap | inbox. Default: report, doc, roadmap (inbox is opt-in only — the node\'s raw inbox.jsonl history, cursor-independent and including full un-clipped direct-message bodies; a diagnostic surface, not curated past work).' },
16
16
  { kind: 'flag', name: 'sort', type: 'enum', choices: ['recency', 'oldest'], required: false, constraint: 'Order by artifact timestamp. Default recency.' },
17
17
  ],
18
18
  output: [
19
19
  { name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
20
- { name: 'artifacts', type: 'object[]', required: true, constraint: 'Each: {ref, source, ts, detail}. source = report:<kind> | doc | roadmap. detail = report title / doc heading + size. ref passes verbatim to `canvas history read`.' },
20
+ { name: 'artifacts', type: 'object[]', required: true, constraint: 'Each: {ref, source, ts, detail}. source = report:<kind> | doc | roadmap | inbox. detail = report title / doc heading + size (inbox: `[kind] label`, no size). ref passes verbatim to `canvas history read`.' },
21
21
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
22
22
  ],
23
23
  outputKind: 'object',
@@ -23,7 +23,7 @@ export const historyBranch = defineBranch({
23
23
  help: {
24
24
  name: 'canvas history',
25
25
  summary: 'search and recall the canvas\'s record of past work in this cwd',
26
- model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries) and context docs (specs, designs, roadmaps, findings). `search` finds by content (ranked, filtered, sorted; omit the query to browse by recency); `read` pulls one hit\'s full body by its <node-id>:<relpath> ref; `show` lists everything one node left. This is the episodic record of what was DONE — distinct from `crtr memory`, which is curated semantic knowledge. To reopen a node you found, use `canvas revive`; for its place in the graph, `node inspect show`.',
26
+ model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries) and context docs (specs, designs, roadmaps, findings). `search` finds by content (ranked, filtered, sorted; omit the query to browse by recency); `read` pulls one hit\'s full body by its <node-id>:<relpath> ref; `show` lists everything one node left. This is the episodic record of what was DONE — distinct from `crtr memory`, which is curated semantic knowledge. A node\'s raw inbox history (`--type inbox`) is a separate, opt-in diagnostic corpus — the full cursor-independent inbox.jsonl for one node, including un-clipped direct-message bodies; reach for it only when the curated reports/docs above don\'t already answer the question. To reopen a node you found, use `canvas revive`; for its place in the graph, `node inspect show`.',
27
27
  dynamicState: corpusBlock,
28
28
  },
29
29
  children: [searchLeaf, readLeaf, showLeaf],
@@ -66,6 +66,7 @@ export const issueNewLeaf = defineLeaf({
66
66
  name: 'new',
67
67
  description: 'capture a todo/idea/bug note into a GitHub issue (or a .todos/ markdown note)',
68
68
  whenToUse: 'the alt+c \u2192 i chord runs this inside a tmux popup; rarely run by hand',
69
+ tier: 'hidden',
69
70
  help: {
70
71
  name: 'canvas issue new',
71
72
  summary: 'Capture a free-text note as a GitHub issue labeled with its type (idea|bug). Reads the note from the popup TTY, auto-generates the title via a headless pi call, and files it with `gh issue create` when the cwd is a GitHub repo, else writes `<git-root>/.todos/<slug>.md`. Bound to the alt+c \u2192 i prefix chord.',
@@ -5,6 +5,7 @@ export const canvasSnapshotLeaf = defineLeaf({
5
5
  name: 'snapshot',
6
6
  description: 'emit the browser canvas roster as structured data',
7
7
  whenToUse: 'you need a machine-readable roster of every canvas node for a browser or local tool; use dashboard for the human-readable tree',
8
+ tier: 'hidden',
8
9
  help: {
9
10
  name: 'canvas snapshot',
10
11
  summary: 'machine-readable snapshot of the whole canvas roster',
@@ -1,20 +1,18 @@
1
1
  // `crtr canvas` — observe and supervise the whole graph.
2
2
  //
3
- // Where `node` operates on one node and `push`/`feed` are a node's own spine
4
- // I/O, `canvas` is the bird's-eye / supervisor surface over the entire canvas:
3
+ // Where `node` operates on one node and `push` is a node's own spine write
4
+ // verb, `canvas` is the bird's-eye / supervisor surface over the entire canvas:
5
5
  // render the subscription forest (`dashboard`), see who is blocked on a human
6
- // (`attention`), bring a node back (`revive`), and manage the supervisor
7
- // process (`daemon`). It assembles leaves/branches the sibling command files
8
- // own, so each piece declares its own help one level down.
6
+ // (`attention`), and bring a node back (`revive`). It assembles leaves/branches
7
+ // the sibling command files own, so each piece declares its own help one level
8
+ // down.
9
9
  import { defineBranch } from '../core/command.js';
10
10
  import { dashboardLeaf } from './dashboard.js';
11
11
  import { canvasSnapshotLeaf } from './canvas-snapshot.js';
12
12
  import { browseLeaf } from './canvas-browse.js';
13
13
  import { reviveLeaf } from './revive.js';
14
14
  import { attentionBranch } from './attention.js';
15
- import { daemonBranch } from './daemon.js';
16
15
  import { chordLeaf } from './chord.js';
17
- import { tmuxSpreadLeaf } from './canvas-tmux-spread.js';
18
16
  import { canvasPruneLeaf } from './canvas-prune.js';
19
17
  import { canvasRebuildIndexLeaf } from './canvas-rebuild-index.js';
20
18
  import { historyBranch } from './canvas-history.js';
@@ -23,6 +21,7 @@ export const issueBranch = defineBranch({
23
21
  name: 'issue',
24
22
  description: 'capture a quick todo/idea/bug into a GitHub issue (or a .todos/ note)',
25
23
  whenToUse: 'the alt+c \u2192 i chord uses this; capture a passing thought without leaving your pane',
24
+ tier: 'hidden',
26
25
  help: {
27
26
  name: 'canvas issue',
28
27
  summary: 'capture a quick todo/idea/bug into a GitHub issue',
@@ -34,15 +33,15 @@ export function registerCanvas() {
34
33
  return defineBranch({
35
34
  name: 'canvas',
36
35
  rootEntry: {
37
- concept: 'the whole agent graph at a glance — render it, see who is blocked, revive a node, supervise the daemon',
36
+ concept: 'the whole agent graph at a glance — render it, see who is blocked, revive a node',
38
37
  desc: 'bird\'s-eye view and supervision of the entire canvas',
39
- useWhen: 'surveying the full graph, finding blocked agents, or managing revival/the daemon',
38
+ useWhen: 'surveying the full graph, finding blocked agents, or reviving a node',
40
39
  },
41
40
  help: {
42
41
  name: 'canvas',
43
42
  summary: 'observe and supervise the whole agent graph',
44
- model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
43
+ model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine write verb (`push`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `prune` bounds growth by deleting terminal nodes past a TTL.',
45
44
  },
46
- children: [dashboardLeaf, canvasSnapshotLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, daemonBranch, chordLeaf, tmuxSpreadLeaf, issueBranch, canvasPruneLeaf, canvasRebuildIndexLeaf],
45
+ children: [dashboardLeaf, canvasSnapshotLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, chordLeaf, issueBranch, canvasPruneLeaf, canvasRebuildIndexLeaf],
47
46
  });
48
47
  }
@@ -154,6 +154,7 @@ export const chordLeaf = defineLeaf({
154
154
  name: 'chord',
155
155
  description: 'tmux prefix-menu dispatcher (bound by alt+c; not run by hand)',
156
156
  whenToUse: 'never directly — the alt+c menu routes node-relative actions through it',
157
+ tier: 'hidden',
157
158
  help: {
158
159
  name: 'canvas chord',
159
160
  summary: 'tmux prefix-menu dispatcher — resolve the node in --pane, interpolate --run against it, and exec `crtr <argv>`. Bound by the alt+c menu; not meant to be run by hand.',
@@ -40,7 +40,6 @@ export const humanAsk = defineLeaf({
40
40
  params: [
41
41
  { kind: 'positional', name: 'question', type: 'string', required: false, constraint: 'Bare-question shorthand for a simple open-text ask. Mutually exclusive with `--context-file`.' },
42
42
  { kind: 'context-file', name: 'deck', required: false, constraint: 'Strict complex-ask deck JSON. Use this when you need multiple interactions, options, or other deck structure. Mutually exclusive with the question shorthand.', shape: DECK_SCHEMA_HINT },
43
- { kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'Accepted for symmetry with the job contract; the kickoff never blocks.' },
44
43
  ],
45
44
  output: [
46
45
  { name: 'job_id', type: 'string', required: true, constraint: 'Node id of this human interaction. Its answer is pushed to your inbox when the human responds.' },
@@ -520,6 +520,7 @@ export const humanResolve = defineLeaf({
520
520
  name: 'resolve',
521
521
  description: 'resolve one humanloop deck from stdin answers',
522
522
  whenToUse: 'a browser or local tool is submitting answers for a pending human interaction; this writes through the crouter/humanloop finalize path',
523
+ tier: 'hidden',
523
524
  help: {
524
525
  name: 'human resolve',
525
526
  summary: 'write deck answers atomically and deliver them back to the asking node',
@@ -63,6 +63,7 @@ export const nodeSnapshotLeaf = defineLeaf({
63
63
  name: 'snapshot',
64
64
  description: 'read a dormant node session snapshot without launching its broker',
65
65
  whenToUse: 'you need a read-only broker-compatible snapshot of a dormant node; it copies the session JSONL before parsing and never revives the node',
66
+ tier: 'hidden',
66
67
  help: {
67
68
  name: 'node inspect snapshot',
68
69
  summary: 'read-only dormant node snapshot in the broker welcome shape',
@@ -400,15 +400,15 @@ const nodeShow = defineLeaf({
400
400
  },
401
401
  });
402
402
  // ---------------------------------------------------------------------------
403
- // node inspect — read the graph (list, show, snapshot, context)
403
+ // node inspect — read the graph (list, show, context)
404
404
  // ---------------------------------------------------------------------------
405
405
  const nodeInspect = defineBranch({
406
406
  name: 'inspect',
407
- description: 'read the graph (list, show, snapshot, context)',
408
- whenToUse: 'reading the graph: enumerate nodes on the canvas (`list`), inspect one and its spine neighbors (`show`), or pull a dormant session snapshot with `node inspect snapshot` / browse on-disk context dirs with `node inspect context` while reading a node. Use `canvas dashboard` instead to render the tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
407
+ description: 'read the graph (list, show, context)',
408
+ whenToUse: 'reading the graph: enumerate nodes on the canvas (`list`), inspect one and its spine neighbors (`show`), or browse on-disk context dirs with `node inspect context` while reading a node. Use `canvas dashboard` instead to render the tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
409
409
  help: {
410
410
  name: 'node inspect',
411
- summary: 'read the canvas graph — enumerate nodes, inspect one, or pull snapshot/context views for a node (including dormant session snapshots and on-disk context dirs)',
411
+ summary: 'read the canvas graph — enumerate nodes, inspect one, or browse on-disk context dirs for a node',
412
412
  },
413
413
  children: [nodeList, nodeShow, nodeSnapshotLeaf, nodeContextLeaf],
414
414
  });
@@ -962,10 +962,10 @@ function resolveMsgTarget(input) {
962
962
  const nodeMsg = defineLeaf({
963
963
  name: 'msg',
964
964
  description: 'message any node now, or arm a trigger that messages/revives it later',
965
- whenToUse: 'you want to address a specific node directly — steer it mid-flight, hand it a correction, ping it, pass it new information, or schedule any of that for later — regardless of subscriptions. Default delivers immediately and revives a dormant target; `--at`/`--every` arm a clock trigger instead of acting now; `--fresh` revives with no inbox entry; `--self --until` puts a deadline backstop on your current inbox-wait; `--when` arms a daemon-evaluated predicate for an external condition (CI, a deploy) you cannot otherwise be pushed for. Use `node subscribe` instead to wire ONGOING push delivery rather than a one-off, `push` to report UP your own spine, and `node new` to schedule the BIRTH of a node rather than message an existing one',
965
+ whenToUse: 'you want to address a specific node directly — steer it mid-flight, hand it a correction, ping it, pass it new information, or schedule any of that for later — regardless of subscriptions. Default delivers immediately and revives a dormant target; `--at`/`--every` arm a clock trigger instead of acting now; `--fresh` revives with no inbox entry; `--self --until` binds a deadline that changes your task policy if no push arrives by then; `--when` arms a daemon-evaluated predicate for an external condition (CI, a deploy) you cannot otherwise be pushed for. Use `node subscribe` instead to wire ONGOING push delivery rather than a one-off, `push` to report UP your own spine, and `node new` to schedule the BIRTH of a node rather than message an existing one',
966
966
  help: {
967
967
  name: 'node msg',
968
- summary: 'message a node now, or arm a trigger (clock or predicate) that messages/revives it later. No trigger flag → immediate inbox delivery (or immediate fresh revive with --fresh). --at/--every → clock trigger. --self --until → self-only deadline backstop on the current inbox-wait. --when → daemon-evaluated predicate, fires once on exit 0',
968
+ summary: 'message a node now, or arm a trigger (clock or predicate) that messages/revives it later. No trigger flag → immediate inbox delivery (or immediate fresh revive with --fresh). --at/--every → clock trigger. --self --until → self-only deadline: fires whichever comes first, an inbox message or this time, and that outcome is what should change your next action. --when → daemon-evaluated predicate, fires once on exit 0',
969
969
  params: [
970
970
  { kind: 'stdin', name: 'body', required: false, constraint: 'Message body. Positional or stdin. Required for a plain or clock-triggered message and for --until. Forbidden with --fresh. Optional with --when — if omitted, the predicate\'s captured stdout becomes the body when it fires.' },
971
971
  { kind: 'flag', name: 'to', type: 'string', required: false, constraint: 'Target an existing node by id. Exactly one of --to/--self is required.' },
@@ -975,7 +975,7 @@ const nodeMsg = defineLeaf({
975
975
  { kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'Arm a one-shot clock trigger at this future time instead of acting now — a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", host-local or in --tz). Required unless --every is given (the cadence then sets the first fire). Mutually exclusive with --when.' },
976
976
  { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Arm a recurring clock trigger on this cadence — a duration ("6h","30m") or a 5-field cron / @alias ("0 9 * * *","@daily"). Combine with --at to anchor the first fire, then recur normally. Min cadence 60s. With --when, this instead sets the PREDICATE EVALUATION cadence (required), not a repeating action.' },
977
977
  { kind: 'flag', name: 'when', type: 'string', required: false, constraint: 'Arm a daemon-evaluated bash predicate instead of a clock trigger: exit 0 fires the message once, non-zero leaves it armed for the next evaluation. Requires --every (eval cadence) and --timeout (wall-clock bound). Mutually exclusive with --at, --fresh, and --until.' },
978
- { kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Self-only: bind a deadline to your current dormancy — you wake on the first inbox message, or at this time if none arrives (whichever fires first cancels the other). One-shot, urgent tier, ≤ 1 per node (a new --until replaces any prior). Mutually exclusive with --at, --every, --when, --fresh, --tier, and --timeout. Requires --self and a body.' },
978
+ { kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Self-only: bind a deadline that decides your next action — you wake on the first inbox message, or at this time if none arrives (whichever fires first cancels the other), and the body is the policy to follow in the no-message case. One-shot, urgent tier, ≤ 1 per node (a new --until replaces any prior). Mutually exclusive with --at, --every, --when, --fresh, --tier, and --timeout. Requires --self and a body.' },
979
979
  { kind: 'flag', name: 'timeout', type: 'string', required: false, constraint: 'Wall-clock bound for a --when predicate — a duration or absolute time after which the trigger is consumed and the owner gets one urgent notice. Required with --when; rejected otherwise.' },
980
980
  { kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone for a calendar --every, a bare-ISO --at, or a bare-ISO --until (default: host-local). Valid only alongside --at/--every/--until; rejected with --when, and rejected when acting immediately (no trigger flags) since none of those have anything to parse it against.' },
981
981
  ],
@@ -1225,8 +1225,9 @@ const nodeMsg = defineLeaf({
1225
1225
  }
1226
1226
  const tier = (hasTier ? tierRaw : 'normal');
1227
1227
  const from = process.env['CRTR_NODE_ID'] ?? 'human';
1228
- // A long body clips in the receiver's digest; appendInbox mints a short `id`
1229
- // for it so the receiver can read the full text back via `feed message <id>`.
1228
+ // A body over the preview bound spills to a ref file; appendInbox clips the
1229
+ // inline preview and the receiver dereferences the ref for the full text,
1230
+ // same as any other report.
1230
1231
  appendInbox(targetId, { from, tier, kind: 'message', label: body.split('\n')[0].slice(0, 120), data: { body: body } });
1231
1232
  // A direct message wakes any node: if the target has no live window
1232
1233
  // (done/dead/idle-released), revive it so its inbox-watcher delivers this.
@@ -1510,7 +1511,7 @@ const nodeYield = defineLeaf({
1510
1511
  const nodeTriggersList = defineLeaf({
1511
1512
  name: 'list',
1512
1513
  description: 'list pending triggers for a scope (default self)',
1513
- whenToUse: 'before re-arming or finishing, to see what you already have armed and reap stale ones. Defaults to your own triggers, with a --node or --canvas scope for anything wider',
1514
+ whenToUse: 'seeing what triggers (clock or predicate) are currently armed your own, another node\'s, or the whole canvas. Defaults to your own triggers, with a --node or --canvas scope for anything wider',
1514
1515
  help: {
1515
1516
  name: 'node triggers list',
1516
1517
  summary: 'list pending triggers (clock and predicate, incl. deferred spawns) for a scope — id, owner, target, action/mode, type, next fire/eval, cadence, timeout, label, predicate diagnostics. Fired one-shots are gone; a recurrence shows its NEXT fire/eval, not past slots',
@@ -1602,7 +1603,7 @@ const nodeTriggersList = defineLeaf({
1602
1603
  const nodeTriggersCancel = defineLeaf({
1603
1604
  name: 'cancel',
1604
1605
  description: 'cancel a pending trigger by id (idempotent)',
1605
- whenToUse: 'a wait you no longer need — a poll whose goal is met, a deferred spawn you reconsidered, a deadline you are replacing. Idempotent: canceling an already-fired or already-canceled id is a no-op. Closing/reaping a node already reaps its own triggers and the detached ones it armed; reach for this to drop a single trigger, or to reap a detached spawn/cron a finished or crashed node left running',
1606
+ whenToUse: 'a wait you no longer need — a poll whose goal is met, a deferred spawn you reconsidered, a deadline you are replacing. Idempotent: canceling an already-fired or already-canceled id is a no-op.',
1606
1607
  help: {
1607
1608
  name: 'node triggers cancel',
1608
1609
  summary: 'cancel a pending trigger by id — it never fires/evaluates again and leaves the list. Idempotent (canceling an already-gone id is a no-op, not an error)',
@@ -2,4 +2,3 @@ import type { BranchDef } from '../core/command.js';
2
2
  import { InputError } from '../core/io.js';
3
3
  export declare function mapFinalizeTransitionRaceError(nodeStatus: string | null | undefined, err: unknown): InputError | null;
4
4
  export declare function registerPush(): BranchDef;
5
- export declare function registerFeed(): BranchDef;