@crouton-kit/crouter 0.3.11 → 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.
- package/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +14 -6
- package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +10 -454
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +3 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +6 -691
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +4 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +5 -0
- package/dist/core/command.js +35 -10
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/help.js +5 -3
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +109 -1
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -197
- package/dist/core/spawn.js +16 -539
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/package.json +6 -5
- package/dist/commands/agent.d.ts +0 -6
- package/dist/commands/agent.js +0 -585
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -192
- package/dist/commands/job.d.ts +0 -11
- package/dist/commands/job.js +0 -384
- package/dist/commands/mode.js +0 -231
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -322
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -299
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -98
- package/dist/core/__tests__/spawn.test.js +0 -138
- package/dist/core/__tests__/subagents.test.d.ts +0 -1
- package/dist/core/__tests__/subagents.test.js +0 -75
- package/dist/core/jobs.d.ts +0 -107
- package/dist/core/jobs.js +0 -565
- package/dist/core/subagents.d.ts +0 -18
- package/dist/core/subagents.js +0 -163
- package/dist/prompts/agent.d.ts +0 -27
- package/dist/prompts/agent.js +0 -184
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
- /package/dist/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// The canvas data-access layer. The one place that reads/writes the node+edge
|
|
2
|
+
// model. Later phases (spawn, push, lifecycle, daemon) call only this.
|
|
3
|
+
//
|
|
4
|
+
// Source-of-truth split: a node's meta.json is canonical for its own fields;
|
|
5
|
+
// the db row is a queryable index re-derivable from it. The subscribes_to edges
|
|
6
|
+
// are db-authoritative (mutable, many-writers — what WAL is for).
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { openDb } from './db.js';
|
|
10
|
+
import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, } from './paths.js';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// meta.json (source of truth)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function readMeta(nodeId) {
|
|
15
|
+
const p = nodeMetaPath(nodeId);
|
|
16
|
+
if (!existsSync(p))
|
|
17
|
+
return null;
|
|
18
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
function writeMeta(meta) {
|
|
21
|
+
const p = nodeMetaPath(meta.node_id);
|
|
22
|
+
const tmp = `${p}.tmp`;
|
|
23
|
+
writeFileSync(tmp, JSON.stringify(meta, null, 2));
|
|
24
|
+
renameSync(tmp, p);
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// row index (derived from meta)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
function upsertRow(meta) {
|
|
30
|
+
openDb()
|
|
31
|
+
.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, status, cwd, parent, created)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
33
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
34
|
+
name=excluded.name, kind=excluded.kind, mode=excluded.mode,
|
|
35
|
+
lifecycle=excluded.lifecycle, status=excluded.status, cwd=excluded.cwd,
|
|
36
|
+
parent=excluded.parent`)
|
|
37
|
+
.run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.status, meta.cwd, meta.parent ?? null, meta.created);
|
|
38
|
+
}
|
|
39
|
+
function rowFrom(r) {
|
|
40
|
+
return {
|
|
41
|
+
node_id: r['node_id'],
|
|
42
|
+
name: r['name'],
|
|
43
|
+
kind: r['kind'],
|
|
44
|
+
mode: r['mode'],
|
|
45
|
+
lifecycle: r['lifecycle'],
|
|
46
|
+
status: r['status'],
|
|
47
|
+
cwd: r['cwd'],
|
|
48
|
+
parent: r['parent'] ?? null,
|
|
49
|
+
created: r['created'],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Nodes
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/** Create a node: scaffold its dirs, write meta.json, index the row. */
|
|
56
|
+
export function createNode(meta) {
|
|
57
|
+
ensureHome();
|
|
58
|
+
ensureNodeDirs(meta.node_id);
|
|
59
|
+
writeMeta(meta);
|
|
60
|
+
upsertRow(meta);
|
|
61
|
+
return meta;
|
|
62
|
+
}
|
|
63
|
+
/** The canonical node record (from meta.json), or null if unknown. */
|
|
64
|
+
export function getNode(nodeId) {
|
|
65
|
+
return readMeta(nodeId);
|
|
66
|
+
}
|
|
67
|
+
/** The indexed row (from the db) — cheap for queries that don't need full meta. */
|
|
68
|
+
export function getRow(nodeId) {
|
|
69
|
+
const r = openDb()
|
|
70
|
+
.prepare('SELECT * FROM nodes WHERE node_id = ?')
|
|
71
|
+
.get(nodeId);
|
|
72
|
+
return r ? rowFrom(r) : null;
|
|
73
|
+
}
|
|
74
|
+
/** Merge a patch into a node's meta.json and re-index its row. */
|
|
75
|
+
export function updateNode(nodeId, patch) {
|
|
76
|
+
const cur = readMeta(nodeId);
|
|
77
|
+
if (!cur)
|
|
78
|
+
throw new Error(`unknown node: ${nodeId}`);
|
|
79
|
+
const next = { ...cur, ...patch, node_id: cur.node_id };
|
|
80
|
+
writeMeta(next);
|
|
81
|
+
upsertRow(next);
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
/** Convenience for the most common mutation. */
|
|
85
|
+
export function setStatus(nodeId, status) {
|
|
86
|
+
updateNode(nodeId, { status });
|
|
87
|
+
}
|
|
88
|
+
/** All rows, optionally filtered by status. */
|
|
89
|
+
export function listNodes(filter) {
|
|
90
|
+
const db = openDb();
|
|
91
|
+
let rows;
|
|
92
|
+
if (filter?.status !== undefined) {
|
|
93
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
94
|
+
const placeholders = statuses.map(() => '?').join(',');
|
|
95
|
+
rows = db
|
|
96
|
+
.prepare(`SELECT * FROM nodes WHERE status IN (${placeholders}) ORDER BY created`)
|
|
97
|
+
.all(...statuses);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
rows = db.prepare('SELECT * FROM nodes ORDER BY created').all();
|
|
101
|
+
}
|
|
102
|
+
return rows.map(rowFrom);
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Edges
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
function addEdge(type, from, to, active) {
|
|
108
|
+
openDb()
|
|
109
|
+
.prepare(`INSERT INTO edges (type, from_id, to_id, active, created)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?)
|
|
111
|
+
ON CONFLICT(type, from_id, to_id) DO UPDATE SET active=excluded.active`)
|
|
112
|
+
.run(type, from, to, active ? 1 : 0, new Date().toISOString());
|
|
113
|
+
}
|
|
114
|
+
/** Record `A subscribes_to B` — A receives B's output. active=true wakes A on
|
|
115
|
+
* emit; passive accumulates pointers without a wake. Mutable; callable by anyone. */
|
|
116
|
+
export function subscribe(subscriber, publisher, active = true) {
|
|
117
|
+
addEdge('subscribes_to', subscriber, publisher, active);
|
|
118
|
+
}
|
|
119
|
+
/** Drop a subscription edge. */
|
|
120
|
+
export function unsubscribe(subscriber, publisher) {
|
|
121
|
+
openDb()
|
|
122
|
+
.prepare('DELETE FROM edges WHERE type = ? AND from_id = ? AND to_id = ?')
|
|
123
|
+
.run('subscribes_to', subscriber, publisher);
|
|
124
|
+
}
|
|
125
|
+
/** Flip an existing subscription's wake behavior. */
|
|
126
|
+
export function setSubscriptionActive(subscriber, publisher, active) {
|
|
127
|
+
openDb()
|
|
128
|
+
.prepare('UPDATE edges SET active = ? WHERE type = ? AND from_id = ? AND to_id = ?')
|
|
129
|
+
.run(active ? 1 : 0, 'subscribes_to', subscriber, publisher);
|
|
130
|
+
}
|
|
131
|
+
/** Record the audit-only `child spawned_by parent` edge. */
|
|
132
|
+
export function recordSpawn(child, parent) {
|
|
133
|
+
addEdge('spawned_by', child, parent, true);
|
|
134
|
+
}
|
|
135
|
+
/** Who subscribes to `publisher` — the targets a push fans out to. */
|
|
136
|
+
export function subscribersOf(publisher) {
|
|
137
|
+
return openDb()
|
|
138
|
+
.prepare(`SELECT from_id AS node_id, active, created FROM edges
|
|
139
|
+
WHERE type = 'subscribes_to' AND to_id = ? ORDER BY created`)
|
|
140
|
+
.all(publisher).map((r) => ({
|
|
141
|
+
node_id: r['node_id'],
|
|
142
|
+
active: r['active'] === 1,
|
|
143
|
+
created: r['created'],
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
/** Who `subscriber` subscribes to — its reports / the nodes feeding it. */
|
|
147
|
+
export function subscriptionsOf(subscriber) {
|
|
148
|
+
return openDb()
|
|
149
|
+
.prepare(`SELECT to_id AS node_id, active, created FROM edges
|
|
150
|
+
WHERE type = 'subscribes_to' AND from_id = ? ORDER BY created`)
|
|
151
|
+
.all(subscriber).map((r) => ({
|
|
152
|
+
node_id: r['node_id'],
|
|
153
|
+
active: r['active'] === 1,
|
|
154
|
+
created: r['created'],
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Graph queries
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/** A "view": every node whose output cascades up to `root` via subscriptions —
|
|
161
|
+
* the subscription sub-DAG reachable downward (root → its reports → theirs …).
|
|
162
|
+
* Returns ids excluding root, in BFS order. Cycle-safe. */
|
|
163
|
+
export function view(root) {
|
|
164
|
+
const seen = new Set([root]);
|
|
165
|
+
const out = [];
|
|
166
|
+
const queue = subscriptionsOf(root).map((s) => s.node_id);
|
|
167
|
+
while (queue.length > 0) {
|
|
168
|
+
const id = queue.shift();
|
|
169
|
+
if (seen.has(id))
|
|
170
|
+
continue;
|
|
171
|
+
seen.add(id);
|
|
172
|
+
out.push(id);
|
|
173
|
+
for (const s of subscriptionsOf(id)) {
|
|
174
|
+
if (!seen.has(s.node_id))
|
|
175
|
+
queue.push(s.node_id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
/** Stop-guard primitive: does this node hold an *active* subscription to a node
|
|
181
|
+
* that's still live (active|idle) — i.e. something that can actually wake it?
|
|
182
|
+
* If so, stopping is a legitimate await; if not, it must finish or escalate. */
|
|
183
|
+
export function hasActiveLiveSubscription(nodeId) {
|
|
184
|
+
const r = openDb()
|
|
185
|
+
.prepare(`SELECT 1 FROM edges e JOIN nodes n ON n.node_id = e.to_id
|
|
186
|
+
WHERE e.type = 'subscribes_to' AND e.from_id = ? AND e.active = 1
|
|
187
|
+
AND n.status IN ('active', 'idle') LIMIT 1`)
|
|
188
|
+
.get(nodeId);
|
|
189
|
+
return r !== undefined;
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Index rebuild
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/** Rebuild node rows from on-disk metas (the db node table is a derived index).
|
|
195
|
+
* Edges are left intact — subscribes_to is db-authoritative; spawned_by is
|
|
196
|
+
* re-derived from each meta's `parent`. */
|
|
197
|
+
export function rebuildIndex() {
|
|
198
|
+
if (!existsSync(nodesRoot()))
|
|
199
|
+
return;
|
|
200
|
+
for (const id of readdirSync(nodesRoot())) {
|
|
201
|
+
if (!existsSync(join(nodeDir(id), 'meta.json')))
|
|
202
|
+
continue;
|
|
203
|
+
const meta = readMeta(id);
|
|
204
|
+
if (!meta)
|
|
205
|
+
continue;
|
|
206
|
+
upsertRow(meta);
|
|
207
|
+
if (meta.parent)
|
|
208
|
+
recordSpawn(meta.node_id, meta.parent);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/** Open (or reuse) the canvas db at the current `CRTR_HOME`, initializing the
|
|
3
|
+
* schema and WAL on first open. Keyed by path so tests with distinct homes get
|
|
4
|
+
* independent handles. */
|
|
5
|
+
export declare function openDb(): DatabaseSync;
|
|
6
|
+
/** Close and forget the handle for the current home. Mainly for tests. */
|
|
7
|
+
export declare function closeDb(): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// canvas.db — the topology skeleton. sqlite in WAL mode so the many concurrent
|
|
2
|
+
// writers a sisyphus-grade swarm produces don't contend on a single locked JSON.
|
|
3
|
+
//
|
|
4
|
+
// Node rows are a rebuildable index over each node's meta.json (the source of
|
|
5
|
+
// truth). The `subscribes_to` edges are the one genuinely-mutable part no meta
|
|
6
|
+
// owns, so the db is authoritative for them.
|
|
7
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
8
|
+
import { canvasDbPath, ensureHome } from './paths.js';
|
|
9
|
+
const SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
11
|
+
node_id TEXT PRIMARY KEY,
|
|
12
|
+
name TEXT NOT NULL,
|
|
13
|
+
kind TEXT NOT NULL,
|
|
14
|
+
mode TEXT NOT NULL DEFAULT 'base',
|
|
15
|
+
lifecycle TEXT NOT NULL DEFAULT 'terminal',
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
17
|
+
cwd TEXT NOT NULL,
|
|
18
|
+
parent TEXT,
|
|
19
|
+
created TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
23
|
+
type TEXT NOT NULL, -- 'subscribes_to' | 'spawned_by'
|
|
24
|
+
from_id TEXT NOT NULL,
|
|
25
|
+
to_id TEXT NOT NULL,
|
|
26
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
27
|
+
created TEXT NOT NULL,
|
|
28
|
+
PRIMARY KEY (type, from_id, to_id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
|
34
|
+
`;
|
|
35
|
+
const handles = new Map();
|
|
36
|
+
/** Open (or reuse) the canvas db at the current `CRTR_HOME`, initializing the
|
|
37
|
+
* schema and WAL on first open. Keyed by path so tests with distinct homes get
|
|
38
|
+
* independent handles. */
|
|
39
|
+
export function openDb() {
|
|
40
|
+
const path = canvasDbPath();
|
|
41
|
+
const existing = handles.get(path);
|
|
42
|
+
if (existing)
|
|
43
|
+
return existing;
|
|
44
|
+
ensureHome();
|
|
45
|
+
const db = new DatabaseSync(path);
|
|
46
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
47
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
48
|
+
db.exec('PRAGMA busy_timeout = 5000;');
|
|
49
|
+
db.exec(SCHEMA);
|
|
50
|
+
handles.set(path, db);
|
|
51
|
+
return db;
|
|
52
|
+
}
|
|
53
|
+
/** Close and forget the handle for the current home. Mainly for tests. */
|
|
54
|
+
export function closeDb() {
|
|
55
|
+
const path = canvasDbPath();
|
|
56
|
+
const db = handles.get(path);
|
|
57
|
+
if (db) {
|
|
58
|
+
db.close();
|
|
59
|
+
handles.delete(path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The canvas: one global graph of nodes + edges. Phase 0 of the pi-native
|
|
2
|
+
// agent runtime. Topology in sqlite (WAL), node flesh on disk.
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export * from './paths.js';
|
|
5
|
+
export * from './canvas.js';
|
|
6
|
+
export { openDb, closeDb } from './db.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Root of the global canvas home (`~/.crtr` unless `CRTR_HOME` is set). */
|
|
2
|
+
export declare function crtrHome(): string;
|
|
3
|
+
export declare function canvasDbPath(): string;
|
|
4
|
+
export declare function nodesRoot(): string;
|
|
5
|
+
export declare function nodeDir(nodeId: string): string;
|
|
6
|
+
export declare function contextDir(nodeId: string): string;
|
|
7
|
+
export declare function jobDir(nodeId: string): string;
|
|
8
|
+
export declare function reportsDir(nodeId: string): string;
|
|
9
|
+
export declare function nodeMetaPath(nodeId: string): string;
|
|
10
|
+
export declare function inboxPath(nodeId: string): string;
|
|
11
|
+
export declare function transcriptPath(nodeId: string): string;
|
|
12
|
+
export declare function sessionPtrPath(nodeId: string): string;
|
|
13
|
+
/** Create the full directory skeleton for a node. Idempotent. */
|
|
14
|
+
export declare function ensureNodeDirs(nodeId: string): void;
|
|
15
|
+
/** Ensure the canvas home exists. Idempotent. */
|
|
16
|
+
export declare function ensureHome(): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The `~/.crtr/` layout. One global, cwd-agnostic home for the whole canvas.
|
|
2
|
+
//
|
|
3
|
+
// ~/.crtr/
|
|
4
|
+
// canvas.db sqlite (WAL) — topology only (nodes + edges)
|
|
5
|
+
// nodes/<node_id>/
|
|
6
|
+
// meta.json source of truth for the node's row
|
|
7
|
+
// context/ roadmap.md, initial-prompt.md, explore-*.md, artifacts
|
|
8
|
+
// job/ log.jsonl, telemetry.json
|
|
9
|
+
// reports/ append-only push history (<ts>-<kind>.md)
|
|
10
|
+
// inbox.jsonl messages + coalesced subscription feed
|
|
11
|
+
// transcript.jsonl mirror/pointer of the pi session
|
|
12
|
+
// session.ptr pi session id/path
|
|
13
|
+
//
|
|
14
|
+
// `CRTR_HOME` overrides the root (used by tests and isolated runs).
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { mkdirSync } from 'node:fs';
|
|
18
|
+
/** Root of the global canvas home (`~/.crtr` unless `CRTR_HOME` is set). */
|
|
19
|
+
export function crtrHome() {
|
|
20
|
+
const override = process.env['CRTR_HOME'];
|
|
21
|
+
return override !== undefined && override !== '' ? override : join(homedir(), '.crtr');
|
|
22
|
+
}
|
|
23
|
+
export function canvasDbPath() {
|
|
24
|
+
return join(crtrHome(), 'canvas.db');
|
|
25
|
+
}
|
|
26
|
+
export function nodesRoot() {
|
|
27
|
+
return join(crtrHome(), 'nodes');
|
|
28
|
+
}
|
|
29
|
+
export function nodeDir(nodeId) {
|
|
30
|
+
return join(nodesRoot(), nodeId);
|
|
31
|
+
}
|
|
32
|
+
export function contextDir(nodeId) {
|
|
33
|
+
return join(nodeDir(nodeId), 'context');
|
|
34
|
+
}
|
|
35
|
+
export function jobDir(nodeId) {
|
|
36
|
+
return join(nodeDir(nodeId), 'job');
|
|
37
|
+
}
|
|
38
|
+
export function reportsDir(nodeId) {
|
|
39
|
+
return join(nodeDir(nodeId), 'reports');
|
|
40
|
+
}
|
|
41
|
+
export function nodeMetaPath(nodeId) {
|
|
42
|
+
return join(nodeDir(nodeId), 'meta.json');
|
|
43
|
+
}
|
|
44
|
+
export function inboxPath(nodeId) {
|
|
45
|
+
return join(nodeDir(nodeId), 'inbox.jsonl');
|
|
46
|
+
}
|
|
47
|
+
export function transcriptPath(nodeId) {
|
|
48
|
+
return join(nodeDir(nodeId), 'transcript.jsonl');
|
|
49
|
+
}
|
|
50
|
+
export function sessionPtrPath(nodeId) {
|
|
51
|
+
return join(nodeDir(nodeId), 'session.ptr');
|
|
52
|
+
}
|
|
53
|
+
/** Create the full directory skeleton for a node. Idempotent. */
|
|
54
|
+
export function ensureNodeDirs(nodeId) {
|
|
55
|
+
for (const d of [contextDir(nodeId), jobDir(nodeId), reportsDir(nodeId)]) {
|
|
56
|
+
mkdirSync(d, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Ensure the canvas home exists. Idempotent. */
|
|
60
|
+
export function ensureHome() {
|
|
61
|
+
mkdirSync(nodesRoot(), { recursive: true });
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NodeStatus } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render the subscription sub-DAG rooted at `rootId` as an ASCII tree.
|
|
4
|
+
* The root is the first line (no connector prefix); children are indented.
|
|
5
|
+
*
|
|
6
|
+
* Each line: `<glyph> <name> [<kind>/<mode>] ctx <Nk>[ ⚑<asks>]`
|
|
7
|
+
*
|
|
8
|
+
* Returns a multi-line string (no trailing newline).
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderTree(rootId: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Render all canvas roots as a forest. A root is a node with no subscribers
|
|
13
|
+
* (no one subscribes to it = it has no managers in the org chart).
|
|
14
|
+
*
|
|
15
|
+
* If there are no roots on the canvas, returns a placeholder string.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderForest(): string;
|
|
18
|
+
export interface DashboardRow {
|
|
19
|
+
node_id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
status: NodeStatus;
|
|
22
|
+
kind: string;
|
|
23
|
+
mode: string;
|
|
24
|
+
ctx_tokens: number;
|
|
25
|
+
asks: number;
|
|
26
|
+
}
|
|
27
|
+
/** One row per node visible in the sub-DAG of `rootId` (including root). */
|
|
28
|
+
export declare function dashboardRows(rootId: string): DashboardRow[];
|
|
29
|
+
/** One row per node across the entire canvas. */
|
|
30
|
+
export declare function dashboardRowsAll(): DashboardRow[];
|
|
@@ -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
|
+
}
|