@crouton-kit/crouter 0.3.8 → 0.3.12

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 (184) hide show
  1. package/bin/crtrd +2 -0
  2. package/dist/builtin-personas/design/base.md +9 -0
  3. package/dist/builtin-personas/design/orchestrator.md +10 -0
  4. package/dist/builtin-personas/developer/base.md +9 -0
  5. package/dist/builtin-personas/developer/orchestrator.md +12 -0
  6. package/dist/builtin-personas/explore/base.md +9 -0
  7. package/dist/builtin-personas/explore/orchestrator.md +9 -0
  8. package/dist/builtin-personas/general/base.md +5 -0
  9. package/dist/builtin-personas/general/orchestrator.md +7 -0
  10. package/dist/builtin-personas/orchestration-kernel.md +71 -0
  11. package/dist/builtin-personas/plan/base.md +7 -0
  12. package/dist/builtin-personas/plan/orchestrator.md +12 -0
  13. package/dist/builtin-personas/review/base.md +7 -0
  14. package/dist/builtin-personas/review/orchestrator.md +9 -0
  15. package/dist/builtin-personas/runtime-base.md +39 -0
  16. package/dist/builtin-personas/spec/base.md +7 -0
  17. package/dist/builtin-personas/spec/orchestrator.md +10 -0
  18. package/dist/builtin-skills/skills/design/SKILL.md +51 -0
  19. package/dist/builtin-skills/skills/development/SKILL.md +109 -0
  20. package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
  21. package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
  22. package/dist/cli.js +25 -27
  23. package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
  24. package/dist/commands/attention.js +152 -0
  25. package/dist/commands/canvas.d.ts +2 -0
  26. package/dist/commands/canvas.js +35 -0
  27. package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
  28. package/dist/commands/daemon.js +111 -0
  29. package/dist/commands/dashboard.d.ts +2 -0
  30. package/dist/commands/dashboard.js +65 -0
  31. package/dist/commands/human/prompts.d.ts +5 -0
  32. package/dist/commands/human/prompts.js +269 -0
  33. package/dist/commands/human/queue.d.ts +3 -0
  34. package/dist/commands/human/queue.js +133 -0
  35. package/dist/commands/human/shared.d.ts +43 -0
  36. package/dist/commands/human/shared.js +107 -0
  37. package/dist/commands/human.js +15 -427
  38. package/dist/commands/node.d.ts +2 -0
  39. package/dist/commands/node.js +354 -0
  40. package/dist/commands/pkg/market-inspect.d.ts +1 -0
  41. package/dist/commands/pkg/market-inspect.js +157 -0
  42. package/dist/commands/pkg/market-manage.d.ts +1 -0
  43. package/dist/commands/pkg/market-manage.js +316 -0
  44. package/dist/commands/pkg/market.d.ts +1 -0
  45. package/dist/commands/pkg/market.js +16 -0
  46. package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
  47. package/dist/commands/pkg/plugin-inspect.js +142 -0
  48. package/dist/commands/pkg/plugin-manage.d.ts +1 -0
  49. package/dist/commands/pkg/plugin-manage.js +294 -0
  50. package/dist/commands/pkg/plugin.d.ts +1 -0
  51. package/dist/commands/pkg/plugin.js +16 -0
  52. package/dist/commands/pkg/shared.d.ts +5 -0
  53. package/dist/commands/pkg/shared.js +61 -0
  54. package/dist/commands/pkg.js +8 -1004
  55. package/dist/commands/push.d.ts +3 -0
  56. package/dist/commands/push.js +159 -0
  57. package/dist/commands/revive.d.ts +2 -0
  58. package/dist/commands/revive.js +64 -0
  59. package/dist/commands/skill/author.d.ts +3 -0
  60. package/dist/commands/skill/author.js +147 -0
  61. package/dist/commands/skill/find.d.ts +4 -0
  62. package/dist/commands/skill/find.js +254 -0
  63. package/dist/commands/skill/read.d.ts +1 -0
  64. package/dist/commands/skill/read.js +89 -0
  65. package/dist/commands/skill/shared.d.ts +19 -0
  66. package/dist/commands/skill/shared.js +207 -0
  67. package/dist/commands/skill/state.d.ts +3 -0
  68. package/dist/commands/skill/state.js +69 -0
  69. package/dist/commands/skill.js +12 -681
  70. package/dist/commands/sys/config.d.ts +1 -0
  71. package/dist/commands/sys/config.js +186 -0
  72. package/dist/commands/sys/doctor.d.ts +1 -0
  73. package/dist/commands/sys/doctor.js +369 -0
  74. package/dist/commands/sys/shared.d.ts +3 -0
  75. package/dist/commands/sys/shared.js +24 -0
  76. package/dist/commands/sys/update.d.ts +2 -0
  77. package/dist/commands/sys/update.js +114 -0
  78. package/dist/commands/sys.js +9 -694
  79. package/dist/core/__tests__/argv-parser.test.js +19 -1
  80. package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
  81. package/dist/core/__tests__/canvas.test.js +154 -0
  82. package/dist/core/__tests__/reset.test.js +105 -0
  83. package/dist/core/__tests__/resolver.test.js +69 -1
  84. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  85. package/dist/core/__tests__/unknown-path.test.js +52 -0
  86. package/dist/core/bootstrap.d.ts +2 -0
  87. package/dist/core/bootstrap.js +66 -0
  88. package/dist/core/canvas/attention.d.ts +24 -0
  89. package/dist/core/canvas/attention.js +94 -0
  90. package/dist/core/canvas/canvas.d.ts +40 -0
  91. package/dist/core/canvas/canvas.js +210 -0
  92. package/dist/core/canvas/db.d.ts +7 -0
  93. package/dist/core/canvas/db.js +61 -0
  94. package/dist/core/canvas/index.d.ts +4 -0
  95. package/dist/core/canvas/index.js +6 -0
  96. package/dist/core/canvas/paths.d.ts +16 -0
  97. package/dist/core/canvas/paths.js +62 -0
  98. package/dist/core/canvas/render.d.ts +30 -0
  99. package/dist/core/canvas/render.js +186 -0
  100. package/dist/core/canvas/types.d.ts +87 -0
  101. package/dist/core/canvas/types.js +8 -0
  102. package/dist/core/command.d.ts +63 -2
  103. package/dist/core/command.js +97 -24
  104. package/dist/core/feed/feed.d.ts +43 -0
  105. package/dist/core/feed/feed.js +116 -0
  106. package/dist/core/feed/inbox.d.ts +50 -0
  107. package/dist/core/feed/inbox.js +124 -0
  108. package/dist/core/frontmatter.d.ts +10 -0
  109. package/dist/core/frontmatter.js +24 -9
  110. package/dist/core/help.d.ts +39 -8
  111. package/dist/core/help.js +69 -35
  112. package/dist/core/io.d.ts +15 -1
  113. package/dist/core/io.js +56 -6
  114. package/dist/core/personas/index.d.ts +12 -0
  115. package/dist/core/personas/index.js +10 -0
  116. package/dist/core/personas/loader.d.ts +44 -0
  117. package/dist/core/personas/loader.js +157 -0
  118. package/dist/core/personas/resolve.d.ts +36 -0
  119. package/dist/core/personas/resolve.js +110 -0
  120. package/dist/core/render.d.ts +11 -0
  121. package/dist/core/render.js +126 -0
  122. package/dist/core/resolver.d.ts +10 -0
  123. package/dist/core/resolver.js +160 -2
  124. package/dist/core/runtime/front-door.d.ts +10 -0
  125. package/dist/core/runtime/front-door.js +97 -0
  126. package/dist/core/runtime/kickoff.d.ts +23 -0
  127. package/dist/core/runtime/kickoff.js +134 -0
  128. package/dist/core/runtime/launch.d.ts +34 -0
  129. package/dist/core/runtime/launch.js +85 -0
  130. package/dist/core/runtime/nodes.d.ts +38 -0
  131. package/dist/core/runtime/nodes.js +95 -0
  132. package/dist/core/runtime/presence.d.ts +38 -0
  133. package/dist/core/runtime/presence.js +152 -0
  134. package/dist/core/runtime/promote.d.ts +30 -0
  135. package/dist/core/runtime/promote.js +105 -0
  136. package/dist/core/runtime/reset.d.ts +13 -0
  137. package/dist/core/runtime/reset.js +97 -0
  138. package/dist/core/runtime/revive.d.ts +26 -0
  139. package/dist/core/runtime/revive.js +89 -0
  140. package/dist/core/runtime/roadmap.d.ts +12 -0
  141. package/dist/core/runtime/roadmap.js +52 -0
  142. package/dist/core/runtime/spawn.d.ts +33 -0
  143. package/dist/core/runtime/spawn.js +118 -0
  144. package/dist/core/runtime/stop-guard.d.ts +18 -0
  145. package/dist/core/runtime/stop-guard.js +33 -0
  146. package/dist/core/runtime/tmux.d.ts +88 -0
  147. package/dist/core/runtime/tmux.js +198 -0
  148. package/dist/core/spawn.d.ts +17 -80
  149. package/dist/core/spawn.js +15 -219
  150. package/dist/daemon/crtrd-cli.d.ts +1 -0
  151. package/dist/daemon/crtrd-cli.js +4 -0
  152. package/dist/daemon/crtrd.d.ts +20 -0
  153. package/dist/daemon/crtrd.js +200 -0
  154. package/dist/daemon/manage.d.ts +17 -0
  155. package/dist/daemon/manage.js +57 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
  157. package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
  158. package/dist/pi-extensions/canvas-nav.d.ts +32 -0
  159. package/dist/pi-extensions/canvas-nav.js +536 -0
  160. package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
  161. package/dist/pi-extensions/canvas-stophook.js +373 -0
  162. package/dist/types.d.ts +21 -0
  163. package/dist/types.js +3 -0
  164. package/package.json +6 -5
  165. package/dist/commands/agent.js +0 -384
  166. package/dist/commands/debug.d.ts +0 -3
  167. package/dist/commands/debug.js +0 -179
  168. package/dist/commands/job.js +0 -344
  169. package/dist/commands/plan.d.ts +0 -4
  170. package/dist/commands/plan.js +0 -309
  171. package/dist/commands/spec.d.ts +0 -3
  172. package/dist/commands/spec.js +0 -286
  173. package/dist/core/__tests__/flow-leaves.test.js +0 -248
  174. package/dist/core/__tests__/job.test.js +0 -310
  175. package/dist/core/__tests__/jobs.test.js +0 -66
  176. package/dist/core/jobs.d.ts +0 -101
  177. package/dist/core/jobs.js +0 -462
  178. package/dist/prompts/agent.d.ts +0 -18
  179. package/dist/prompts/agent.js +0 -153
  180. package/dist/prompts/debug.d.ts +0 -8
  181. package/dist/prompts/debug.js +0 -44
  182. /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
  183. /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
  184. /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
@@ -0,0 +1,186 @@
1
+ // render.ts — ASCII tree rendering of the canvas subscription sub-DAG.
2
+ //
3
+ // `subscriptionsOf(nodeId)` returns the nodes a node subscribes to, which in
4
+ // the crtr model are its *reports* / *children*: a parent auto-subscribes to
5
+ // each child it spawns so it wakes on the child's output. Walking subscriptionsOf
6
+ // recursively therefore walks DOWN the org chart.
7
+ //
8
+ // Telemetry is read directly from <crtrHome>/nodes/<id>/job/telemetry.json
9
+ // (the node-local job dir written by canvas-stophook on every turn_end).
10
+ // Missing or corrupt telemetry → ctx 0k (best-effort, never throws).
11
+ //
12
+ // Cycle guard: the subscription graph is declared acyclic (a node cannot
13
+ // subscribe to its own ancestor), but we track visited ids defensively because
14
+ // the db is mutable and bugs happen.
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
18
+ import { jobDir } from './paths.js';
19
+ import { countAsks } from './attention.js';
20
+ // ---------------------------------------------------------------------------
21
+ // Glyphs
22
+ // ---------------------------------------------------------------------------
23
+ const STATUS_GLYPH = {
24
+ active: '●',
25
+ idle: '○',
26
+ done: '✓',
27
+ dead: '✗',
28
+ };
29
+ function readNodeTelemetry(nodeId) {
30
+ const path = join(jobDir(nodeId), 'telemetry.json');
31
+ if (!existsSync(path))
32
+ return {};
33
+ try {
34
+ return JSON.parse(readFileSync(path, 'utf8'));
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ /** Format a token count as `Nk` (rounded down to nearest 1 k). */
41
+ function fmtCtx(tokensIn) {
42
+ if (tokensIn === undefined || tokensIn === 0)
43
+ return '0k';
44
+ return `${Math.floor(tokensIn / 1000)}k`;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Tree builder
48
+ // ---------------------------------------------------------------------------
49
+ /** Build one line of the ASCII tree. */
50
+ function nodeLine(nodeId, indent, connector) {
51
+ const node = getNode(nodeId);
52
+ if (node === null) {
53
+ // Node id is in the db but meta.json is gone — paranoid guard.
54
+ return `${indent}${connector}? <missing meta: ${nodeId}>`;
55
+ }
56
+ const glyph = STATUS_GLYPH[node.status] ?? '?';
57
+ const tel = readNodeTelemetry(nodeId);
58
+ const ctx = fmtCtx(tel.tokens_in);
59
+ const asks = countAsks(nodeId);
60
+ const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
61
+ return `${indent}${connector}${glyph} ${node.name} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`;
62
+ }
63
+ /**
64
+ * Recursively walk the subscription sub-DAG rooted at `nodeId`, appending
65
+ * rendered lines to `out`. Cycle-safe via `visited`.
66
+ */
67
+ function walkTree(nodeId, indent, isLast, visited, out) {
68
+ // Guard: if we have already rendered this node in this traversal, emit a
69
+ // back-ref marker instead of recursing (prevents infinite loops in graphs
70
+ // with cycles introduced by manual edge manipulation).
71
+ if (visited.has(nodeId)) {
72
+ // The line for this node was already emitted by the caller; just return.
73
+ return;
74
+ }
75
+ visited.add(nodeId);
76
+ const connector = isLast ? '└─ ' : '├─ ';
77
+ out.push(nodeLine(nodeId, indent, connector));
78
+ const children = subscriptionsOf(nodeId);
79
+ const childIndent = indent + (isLast ? ' ' : '│ ');
80
+ for (let i = 0; i < children.length; i++) {
81
+ const child = children[i];
82
+ const childIsLast = i === children.length - 1;
83
+ if (visited.has(child.node_id)) {
84
+ // Cycle reference — show the back-edge without recursing.
85
+ const cycleConnector = childIsLast ? '└─ ' : '├─ ';
86
+ out.push(`${childIndent}${cycleConnector}↺ <cycle: ${child.node_id}>`);
87
+ continue;
88
+ }
89
+ walkTree(child.node_id, childIndent, childIsLast, visited, out);
90
+ }
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Public API
94
+ // ---------------------------------------------------------------------------
95
+ /**
96
+ * Render the subscription sub-DAG rooted at `rootId` as an ASCII tree.
97
+ * The root is the first line (no connector prefix); children are indented.
98
+ *
99
+ * Each line: `<glyph> <name> [<kind>/<mode>] ctx <Nk>[ ⚑<asks>]`
100
+ *
101
+ * Returns a multi-line string (no trailing newline).
102
+ */
103
+ export function renderTree(rootId) {
104
+ const node = getNode(rootId);
105
+ if (node === null)
106
+ return `? <missing node: ${rootId}>`;
107
+ const tel = readNodeTelemetry(rootId);
108
+ const ctx = fmtCtx(tel.tokens_in);
109
+ const asks = countAsks(rootId);
110
+ const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
111
+ const glyph = STATUS_GLYPH[node.status] ?? '?';
112
+ const out = [];
113
+ out.push(`${glyph} ${node.name} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
114
+ // visited starts with root already rendered (walkTree doesn't re-emit root).
115
+ const visited = new Set([rootId]);
116
+ const children = subscriptionsOf(rootId);
117
+ for (let i = 0; i < children.length; i++) {
118
+ const child = children[i];
119
+ const isLast = i === children.length - 1;
120
+ walkTree(child.node_id, '', isLast, visited, out);
121
+ }
122
+ return out.join('\n');
123
+ }
124
+ /**
125
+ * Render all canvas roots as a forest. A root is a node with no subscribers
126
+ * (no one subscribes to it = it has no managers in the org chart).
127
+ *
128
+ * If there are no roots on the canvas, returns a placeholder string.
129
+ */
130
+ export function renderForest() {
131
+ const all = listNodes();
132
+ if (all.length === 0)
133
+ return '(canvas is empty)';
134
+ // A root has no subscribers (nobody is watching it). We discover this by
135
+ // looking for nodes whose node_id never appears as a "to" side of a
136
+ // subscribes_to edge — equivalently, nodes with parent === null are the
137
+ // authoritative roots per the spawn contract (spawn sets parent and records
138
+ // a spawned_by edge + subscribe). Fall back to parent===null because querying
139
+ // the full edge table would require opening the db here.
140
+ //
141
+ // Fine to use parent===null: roots are created by `node session` / `node new`
142
+ // without a parent; non-roots always have a parent.
143
+ const roots = all.filter((n) => n.parent === null);
144
+ // If for some reason we have no parent===null nodes (unusual: e.g., all nodes
145
+ // were created by hand with a parent), fall back to all nodes.
146
+ const renderRoots = roots.length > 0 ? roots : all;
147
+ const parts = [];
148
+ for (const r of renderRoots) {
149
+ parts.push(renderTree(r.node_id));
150
+ }
151
+ return parts.join('\n\n');
152
+ }
153
+ /** One row per node visible in the sub-DAG of `rootId` (including root). */
154
+ export function dashboardRows(rootId) {
155
+ const ids = [rootId, ...view(rootId)];
156
+ return ids.flatMap((id) => {
157
+ const node = getNode(id);
158
+ if (node === null)
159
+ return [];
160
+ const tel = readNodeTelemetry(id);
161
+ return [{
162
+ node_id: id,
163
+ name: node.name,
164
+ status: node.status,
165
+ kind: node.kind,
166
+ mode: node.mode,
167
+ ctx_tokens: tel.tokens_in ?? 0,
168
+ asks: countAsks(id),
169
+ }];
170
+ });
171
+ }
172
+ /** One row per node across the entire canvas. */
173
+ export function dashboardRowsAll() {
174
+ return listNodes().flatMap((row) => {
175
+ const tel = readNodeTelemetry(row.node_id);
176
+ return [{
177
+ node_id: row.node_id,
178
+ name: row.name,
179
+ status: row.status,
180
+ kind: row.kind,
181
+ mode: row.mode,
182
+ ctx_tokens: tel.tokens_in ?? 0,
183
+ asks: countAsks(row.node_id),
184
+ }];
185
+ });
186
+ }
@@ -0,0 +1,87 @@
1
+ /** What a node is doing right now. UI shows active+idle; `done` is hidden but
2
+ * revivable; only `dead` is a fault. */
3
+ export type NodeStatus = 'active' | 'idle' | 'done' | 'dead';
4
+ /** Does stopping finalize the node? terminal = worker (finalizes on push --final);
5
+ * resident = manager/orchestrator (stays dormant, woken by inbox). */
6
+ export type Lifecycle = 'terminal' | 'resident';
7
+ /** base = hands-on worker; orchestrator = delegating manager. Bespoke per kind. */
8
+ export type Mode = 'base' | 'orchestrator';
9
+ /** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
10
+ export type ExitIntent = 'done' | 'refresh' | 'idle-release' | null;
11
+ /** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
12
+ * org chart, views, completion routing). `spawned_by` is audit only. */
13
+ export type EdgeType = 'subscribes_to' | 'spawned_by';
14
+ /** The pi launch recipe, persisted so the daemon can faithfully revive a node
15
+ * as its *current* self. Rewritten on every polymorph (base→orchestrator). */
16
+ export interface LaunchSpec {
17
+ /** Model id/pattern passed to pi `--model`. */
18
+ model?: string;
19
+ /** pi `--tools` allow-list. */
20
+ tools?: string[];
21
+ /** pi `-e` extension paths, loaded once; they self-gate on live {kind,mode}. */
22
+ extensions: string[];
23
+ /** Resolved system prompt text (passed via --append-system-prompt / --system-prompt). */
24
+ systemPrompt?: string;
25
+ /** Extra env injected into the pi process. */
26
+ env: Record<string, string>;
27
+ }
28
+ /** A node's `meta.json` — source of truth for its canvas row. Files for flesh,
29
+ * sqlite for skeleton: the db indexes the queryable subset of these fields. */
30
+ export interface NodeMeta {
31
+ node_id: string;
32
+ name: string;
33
+ created: string;
34
+ /** The dir this node is pinned to — its cwd (where pi runs, bash executes). */
35
+ cwd: string;
36
+ /** Role the node was born as: explore | developer | plan | review | general… */
37
+ kind: string;
38
+ mode: Mode;
39
+ lifecycle: Lifecycle;
40
+ status: NodeStatus;
41
+ /** spawned_by target — who created me. Audit only; null for user-opened roots. */
42
+ parent?: string | null;
43
+ /** New subscriptions this node opens default to passive when true. */
44
+ passive_default?: boolean;
45
+ /** Why the node last stopped (done | refresh). Drives reap-vs-revive. */
46
+ intent?: ExitIntent;
47
+ /** The pi session id for `--resume`. */
48
+ pi_session_id?: string | null;
49
+ /** Full pi launch recipe; rewritten on every polymorph. */
50
+ launch?: LaunchSpec;
51
+ /** Presence: the tmux session (its root's home) and window this node renders
52
+ * in while active. Cleared when the node goes done/dead and its window closes.
53
+ * (Phase 5 promotes this to a dedicated presence registry.) */
54
+ tmux_session?: string | null;
55
+ window?: string | null;
56
+ }
57
+ /** The queryable projection of a NodeMeta stored as a canvas.db row. */
58
+ export interface NodeRow {
59
+ node_id: string;
60
+ name: string;
61
+ kind: string;
62
+ mode: Mode;
63
+ lifecycle: Lifecycle;
64
+ status: NodeStatus;
65
+ cwd: string;
66
+ parent: string | null;
67
+ created: string;
68
+ }
69
+ /** An edge as stored. For `subscribes_to`, `from` is the subscriber and `to`
70
+ * is the publisher (A subscribes_to B ⇒ A receives B's output). For
71
+ * `spawned_by`, `from` is the child and `to` is the parent. */
72
+ export interface Edge {
73
+ type: EdgeType;
74
+ from: string;
75
+ to: string;
76
+ /** Only meaningful for subscribes_to: active = wake the subscriber on emit;
77
+ * passive = accumulate pointers, no wake. */
78
+ active: boolean;
79
+ created: string;
80
+ }
81
+ /** A subscription as seen from one endpoint. */
82
+ export interface SubscriptionRef {
83
+ /** The node id at the other end of the edge. */
84
+ node_id: string;
85
+ active: boolean;
86
+ created: string;
87
+ }
@@ -0,0 +1,8 @@
1
+ // The canvas vocabulary — the node + edge model the whole runtime hangs on.
2
+ //
3
+ // One global canvas (`~/.crtr/canvas.db`) holds the topology (nodes + edges);
4
+ // each node's flesh lives on disk under `~/.crtr/nodes/<id>/`. A node's
5
+ // `meta.json` is the source of truth for its own row; the db is a queryable
6
+ // index over those metas, plus the authoritative store for the mutable
7
+ // `subscribes_to` edges (which no single meta owns).
8
+ export {};
@@ -1,14 +1,43 @@
1
- import type { RootHelp, BranchHelp, LeafHelp, InputParam } from './help.js';
1
+ import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam } from './help.js';
2
+ import { CrtrError } from './errors.js';
3
+ /** Opt-in flag that surfaces a node as an editor slash command (a pi prompt
4
+ * template / Claude Code command). When set, the bootstrap auto-writes a
5
+ * markdown template named `<name>.md` to the host's command dirs on each crtr
6
+ * run, so `/name` becomes available. The body is thin — it points the agent at
7
+ * the live `crtr` workflow so the CLI stays the source of truth. */
8
+ export interface SlashSpec {
9
+ /** Command name → `/<name>` and the template filename. */
10
+ name: string;
11
+ /** Frontmatter description shown in the autocomplete dropdown. */
12
+ description: string;
13
+ /** Optional autocomplete hint, e.g. `<url>` or `[topic]`. */
14
+ argumentHint?: string;
15
+ /** Markdown body (no frontmatter). Bootstrap wraps it with frontmatter + a
16
+ * version marker. Use `$ARGUMENTS` for the invocation's free text. */
17
+ body: string;
18
+ }
2
19
  export interface LeafDef {
3
20
  kind: 'leaf';
4
21
  name: string;
5
22
  help: LeafHelp;
23
+ /** Opt into editor slash-command exposure (see SlashSpec). */
24
+ slash?: SlashSpec;
6
25
  run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
26
+ /** Optional bespoke renderer: turn the result into instruction-shaped
27
+ * XML+markdown the agent acts on. Omit to fall back to the schema-driven
28
+ * generic renderer. Ignored when `--json` is set. */
29
+ render?: (result: Record<string, unknown>) => string;
7
30
  }
8
31
  export interface BranchDef {
9
32
  kind: 'branch';
10
33
  name: string;
11
34
  help: BranchHelp;
35
+ /** How this subtree represents itself one level up. Present on top-level
36
+ * subtrees (assembled into root -h by defineRoot); omitted on nested
37
+ * branches, whose parent representation is the branch's own children list. */
38
+ rootEntry?: RootEntry;
39
+ /** Opt into editor slash-command exposure (see SlashSpec). */
40
+ slash?: SlashSpec;
12
41
  children: (LeafDef | BranchDef)[];
13
42
  }
14
43
  export interface RootDef {
@@ -19,18 +48,50 @@ export interface RootDef {
19
48
  export declare function defineLeaf(opts: {
20
49
  name: string;
21
50
  help: LeafHelp;
51
+ slash?: SlashSpec;
22
52
  run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
53
+ render?: (result: Record<string, unknown>) => string;
23
54
  }): LeafDef;
24
55
  export declare function defineBranch(opts: {
25
56
  name: string;
26
57
  help: BranchHelp;
58
+ rootEntry?: RootEntry;
59
+ slash?: SlashSpec;
27
60
  children: (LeafDef | BranchDef)[];
28
61
  }): BranchDef;
62
+ /** Walk the whole tree and collect every node's SlashSpec (depth-first). Used
63
+ * by the bootstrap to discover which commands opted into slash exposure. */
64
+ export declare function collectSlashSpecs(root: RootDef): SlashSpec[];
65
+ /** Assemble root -h from the subtrees themselves. Root owns only the tagline
66
+ * and globals; every subtree's concept line, selection rubric, and dynamic
67
+ * block come from its own RootEntry. A subtree without a rootEntry does not
68
+ * appear in root -h — declaring the parent-level representation is how a
69
+ * subtree opts into being listed. */
29
70
  export declare function defineRoot(opts: {
30
- help: RootHelp;
71
+ tagline: string;
72
+ globals: {
73
+ name: string;
74
+ desc: string;
75
+ }[];
31
76
  subtrees: BranchDef[];
32
77
  }): RootDef;
78
+ type AnyNode = RootDef | BranchDef | LeafDef;
79
+ /** Walk argv tokens to the deepest matched node.
80
+ * Returns { node, path, remaining } where path is the sequence of matched node
81
+ * names from root (excluding root itself) and remaining are unconsumed tokens.
82
+ * -h / --help tokens are NOT consumed here — the caller checks for them. */
83
+ export declare function walk(root: RootDef, tokens: string[]): {
84
+ node: AnyNode;
85
+ path: string[];
86
+ remaining: string[];
87
+ };
88
+ /** Build a structured unknown-path error. Names valid children of the deepest
89
+ * matched node and names the entry command per the spec. The entry command is
90
+ * the full path to the matched node (not just its local name), so the recovery
91
+ * hint is a command that actually exists. No fuzzy matching. */
92
+ export declare function unknownPathError(node: AnyNode, path: string[], bad: string): CrtrError;
33
93
  /** Parse remaining argv tokens against the leaf's InputParam schema.
34
94
  * Returns a plain object whose keys are camelCase parameter names. */
35
95
  export declare function parseArgv(params: InputParam[], tokens: string[]): Promise<Record<string, unknown>>;
36
96
  export declare function runCli(root: RootDef, argv: string[]): Promise<void>;
97
+ export {};
@@ -5,7 +5,8 @@
5
5
  // A plain array walk + a small flag parser is ~120 lines and has no surprising
6
6
  // edge cases.
7
7
  import { renderRoot, renderBranch, renderLeafArgv } from './help.js';
8
- import { readStdinRaw, emit, handle } from './io.js';
8
+ import { readStdinRaw, emit, handle, setJsonOutput, isJsonOutput } from './io.js';
9
+ import { renderResult } from './render.js';
9
10
  import { CrtrError } from './errors.js';
10
11
  import { ExitCode } from '../types.js';
11
12
  import { readFileSync } from 'node:fs';
@@ -17,14 +18,61 @@ export function defineLeaf(opts) {
17
18
  kind: 'leaf',
18
19
  name: opts.name,
19
20
  help: opts.help,
21
+ slash: opts.slash,
20
22
  run: opts.run,
23
+ render: opts.render,
21
24
  };
22
25
  }
23
26
  export function defineBranch(opts) {
24
- return { kind: 'branch', name: opts.name, help: opts.help, children: opts.children };
27
+ return {
28
+ kind: 'branch',
29
+ name: opts.name,
30
+ help: opts.help,
31
+ rootEntry: opts.rootEntry,
32
+ slash: opts.slash,
33
+ children: opts.children,
34
+ };
25
35
  }
36
+ /** Walk the whole tree and collect every node's SlashSpec (depth-first). Used
37
+ * by the bootstrap to discover which commands opted into slash exposure. */
38
+ export function collectSlashSpecs(root) {
39
+ const out = [];
40
+ const visit = (node) => {
41
+ if (node.slash !== undefined)
42
+ out.push(node.slash);
43
+ if (node.kind === 'branch')
44
+ for (const c of node.children)
45
+ visit(c);
46
+ };
47
+ for (const s of root.subtrees)
48
+ visit(s);
49
+ return out;
50
+ }
51
+ /** Assemble root -h from the subtrees themselves. Root owns only the tagline
52
+ * and globals; every subtree's concept line, selection rubric, and dynamic
53
+ * block come from its own RootEntry. A subtree without a rootEntry does not
54
+ * appear in root -h — declaring the parent-level representation is how a
55
+ * subtree opts into being listed. */
26
56
  export function defineRoot(opts) {
27
- return { kind: 'root', help: opts.help, subtrees: opts.subtrees };
57
+ // Each listed subtree becomes one <name> block at root, assembled straight
58
+ // from its RootEntry. Root composes nothing and hardcodes nothing: add a
59
+ // subtree with a rootEntry and it surfaces; its concept, rubric, state tag,
60
+ // and dynamic block all travel with it.
61
+ const commands = opts.subtrees
62
+ .filter((s) => s.rootEntry !== undefined)
63
+ .map((s) => ({
64
+ name: s.name,
65
+ concept: s.rootEntry.concept,
66
+ desc: s.rootEntry.desc,
67
+ useWhen: s.rootEntry.useWhen,
68
+ dynamicState: s.rootEntry.dynamicState,
69
+ }));
70
+ const help = {
71
+ tagline: opts.tagline,
72
+ commands,
73
+ globals: opts.globals,
74
+ };
75
+ return { kind: 'root', help, subtrees: opts.subtrees };
28
76
  }
29
77
  /** Validate and return child names for an unknown-path error. */
30
78
  function childNames(node) {
@@ -35,10 +83,12 @@ function childNames(node) {
35
83
  return [];
36
84
  }
37
85
  /** Walk argv tokens to the deepest matched node.
38
- * Returns { node, remaining } where remaining are unconsumed tokens.
86
+ * Returns { node, path, remaining } where path is the sequence of matched node
87
+ * names from root (excluding root itself) and remaining are unconsumed tokens.
39
88
  * -h / --help tokens are NOT consumed here — the caller checks for them. */
40
- function walk(root, tokens) {
89
+ export function walk(root, tokens) {
41
90
  let current = root;
91
+ const path = [];
42
92
  let i = 0;
43
93
  while (i < tokens.length) {
44
94
  const token = tokens[i];
@@ -50,6 +100,7 @@ function walk(root, tokens) {
50
100
  if (nextNode === undefined)
51
101
  break;
52
102
  current = nextNode;
103
+ path.push(nextNode.name);
53
104
  i++;
54
105
  }
55
106
  else if (current.kind === 'branch') {
@@ -57,6 +108,7 @@ function walk(root, tokens) {
57
108
  if (nextNode === undefined)
58
109
  break;
59
110
  current = nextNode;
111
+ path.push(nextNode.name);
60
112
  i++;
61
113
  }
62
114
  else {
@@ -64,7 +116,7 @@ function walk(root, tokens) {
64
116
  break;
65
117
  }
66
118
  }
67
- return { node: current, remaining: tokens.slice(i) };
119
+ return { node: current, path, remaining: tokens.slice(i) };
68
120
  }
69
121
  function renderNode(node) {
70
122
  if (node.kind === 'root')
@@ -77,15 +129,13 @@ function helpRequested(remaining) {
77
129
  return remaining.some((t) => t === '-h' || t === '--help');
78
130
  }
79
131
  /** Build a structured unknown-path error. Names valid children of the deepest
80
- * matched node and names the entry command per the spec. No fuzzy matching. */
81
- function unknownPathError(node, bad) {
132
+ * matched node and names the entry command per the spec. The entry command is
133
+ * the full path to the matched node (not just its local name), so the recovery
134
+ * hint is a command that actually exists. No fuzzy matching. */
135
+ export function unknownPathError(node, path, bad) {
82
136
  const valid = childNames(node);
83
137
  const validStr = valid.length > 0 ? valid.join(', ') : '(none)';
84
- const entryCmd = node.kind === 'root'
85
- ? 'crtr -h'
86
- : node.kind === 'branch'
87
- ? `crtr ${node.name} -h`
88
- : 'crtr -h';
138
+ const entryCmd = path.length > 0 ? `crtr ${path.join(' ')} -h` : 'crtr -h';
89
139
  return new CrtrError('unknown_path', `unknown subcommand: ${bad}`, ExitCode.USAGE, {
90
140
  received: bad,
91
141
  next: `Valid children: ${validStr}. Run \`${entryCmd}\` for the full list.`,
@@ -215,7 +265,10 @@ export async function parseArgv(params, tokens) {
215
265
  if (positionalValue !== undefined) {
216
266
  throw parseArgvError('bad_invocation', `unexpected extra positional argument: ${token}`, tokens.join(' '), undefined, 'Use --flag for parameters; only one positional allowed.');
217
267
  }
218
- if (positionalParam === undefined) {
268
+ // A bare positional is accepted when the leaf declares a positional param,
269
+ // OR when it declares a stdin param (the positional supplies the stdin
270
+ // value as an ergonomic alternative to piping). Otherwise it's an error.
271
+ if (positionalParam === undefined && stdinParam === undefined) {
219
272
  throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
220
273
  }
221
274
  positionalValue = token;
@@ -225,13 +278,20 @@ export async function parseArgv(params, tokens) {
225
278
  if (positionalValue !== undefined && positionalParam !== undefined) {
226
279
  result[flagNameToKey(positionalParam.name)] = positionalValue;
227
280
  }
228
- // Read stdin if declared
281
+ // Resolve stdin if declared. A positional token (when there's no dedicated
282
+ // positional param to claim it) satisfies the stdin param directly, so
283
+ // `crtr node new "Say hi"` works as well as piping on stdin.
229
284
  if (stdinParam !== undefined) {
230
- const raw = await readStdinRaw();
231
- if (raw.trim() === '' && stdinParam.required) {
232
- throw parseArgvError('missing_parameter', `stdin is required for this leaf`, '', stdinParam.name, 'Pipe the required content on stdin.');
285
+ if (positionalValue !== undefined && positionalParam === undefined) {
286
+ result[flagNameToKey(stdinParam.name)] = positionalValue;
287
+ }
288
+ else {
289
+ const raw = await readStdinRaw();
290
+ if (raw.trim() === '' && stdinParam.required) {
291
+ throw parseArgvError('missing_parameter', `stdin is required for this leaf`, '', stdinParam.name, 'Pipe the required content on stdin, or pass it as a positional argument.');
292
+ }
293
+ result[flagNameToKey(stdinParam.name)] = raw;
233
294
  }
234
- result[flagNameToKey(stdinParam.name)] = raw;
235
295
  }
236
296
  // Validate required params
237
297
  for (const p of params) {
@@ -250,14 +310,21 @@ export async function parseArgv(params, tokens) {
250
310
  return result;
251
311
  }
252
312
  export async function runCli(root, argv) {
253
- // argv is process.argv — strip node binary + script path
254
- const tokens = argv.slice(2);
313
+ // argv is process.argv — strip node binary + script path. `--json` is a
314
+ // global: pull it out anywhere it appears so the rest of argv parses against
315
+ // the leaf schema unchanged, and switch stdout from rendered prose to raw
316
+ // JSON. It is intentionally undocumented — the default prose output is the
317
+ // agent contract; --json exists only for programmatic/tooling consumers.
318
+ const rawTokens = argv.slice(2);
319
+ const tokens = rawTokens.filter((t) => t !== '--json');
320
+ if (tokens.length !== rawTokens.length)
321
+ setJsonOutput(true);
255
322
  // Bare root invocation or -h at root
256
323
  if (tokens.length === 0 || (tokens.length === 1 && (tokens[0] === '-h' || tokens[0] === '--help'))) {
257
324
  process.stdout.write(renderRoot(root.help) + '\n');
258
325
  process.exit(ExitCode.SUCCESS);
259
326
  }
260
- const { node, remaining } = walk(root, tokens);
327
+ const { node, path, remaining } = walk(root, tokens);
261
328
  try {
262
329
  // Help anywhere in remaining tokens → print node help and exit
263
330
  if (helpRequested(remaining)) {
@@ -267,7 +334,7 @@ export async function runCli(root, argv) {
267
334
  // Bare branch or bare root (no -h, but no leaf selected) → help surface
268
335
  if (node.kind === 'root' || node.kind === 'branch') {
269
336
  if (remaining.length > 0) {
270
- throw unknownPathError(node, remaining[0]);
337
+ throw unknownPathError(node, path, remaining[0]);
271
338
  }
272
339
  process.stdout.write(renderNode(node) + '\n');
273
340
  process.exit(ExitCode.SUCCESS);
@@ -277,7 +344,13 @@ export async function runCli(root, argv) {
277
344
  const input = await parseArgv(params, remaining);
278
345
  const result = await node.run(input);
279
346
  if (result !== undefined && result !== null) {
280
- emit(result);
347
+ if (isJsonOutput()) {
348
+ emit(result);
349
+ }
350
+ else {
351
+ const text = node.render !== undefined ? node.render(result) : renderResult(result, node.help);
352
+ process.stdout.write(text + '\n');
353
+ }
281
354
  }
282
355
  // JSONL leaves call emitLine themselves and return void
283
356
  }
@@ -0,0 +1,43 @@
1
+ export type PushKind = 'update' | 'urgent' | 'final';
2
+ export interface PushOpts {
3
+ /** Semantic kind of this push. `final` also finalises the node. */
4
+ kind: PushKind;
5
+ /** Report body (markdown). Written verbatim after the YAML frontmatter. */
6
+ body: string;
7
+ /**
8
+ * Node id of the sender — recorded as `from` on each inbox entry.
9
+ * Defaults to `nodeId` (the publisher) when omitted.
10
+ */
11
+ from?: string;
12
+ }
13
+ export interface PushResult {
14
+ /** Absolute path of the written report file. */
15
+ reportPath: string;
16
+ /** Node ids that received an inbox pointer. */
17
+ deliveredTo: string[];
18
+ }
19
+ /**
20
+ * Push a report from `nodeId` and fan it out as inbox pointers to all
21
+ * current subscribers.
22
+ *
23
+ * Steps:
24
+ * (a) Write nodes/<nodeId>/reports/<ts>-<kind>.md (YAML front + body).
25
+ * (b) For each active/passive subscriber, append a pointer to their inbox.
26
+ * (c) If kind === 'final', mark the node done.
27
+ */
28
+ export declare function push(nodeId: string, opts: PushOpts): Promise<PushResult>;
29
+ /** Emit a routine progress update from `nodeId`. */
30
+ export declare function pushUpdate(nodeId: string, body: string, opts?: {
31
+ from?: string;
32
+ }): Promise<PushResult>;
33
+ /** Emit an urgent alert from `nodeId` (inbox tier: urgent). */
34
+ export declare function pushUrgent(nodeId: string, body: string, opts?: {
35
+ from?: string;
36
+ }): Promise<PushResult>;
37
+ /**
38
+ * Emit the final report from `nodeId` (inbox tier: normal, kind: final).
39
+ * Also transitions the node to status=done / intent=done.
40
+ */
41
+ export declare function pushFinal(nodeId: string, body: string, opts?: {
42
+ from?: string;
43
+ }): Promise<PushResult>;