@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.
- 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 +25 -27
- package/dist/commands/{job.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/{agent.d.ts → daemon.d.ts} +1 -1
- 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 +15 -427
- 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 +8 -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 +12 -681
- 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 +9 -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/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -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 +63 -2
- package/dist/core/command.js +97 -24
- 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/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +69 -35
- 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 +160 -2
- 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 -80
- package/dist/core/spawn.js +15 -219
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- 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/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +6 -5
- package/dist/commands/agent.js +0 -384
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -179
- package/dist/commands/job.js +0 -344
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -309
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -286
- 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 -66
- package/dist/core/jobs.d.ts +0 -101
- package/dist/core/jobs.js +0 -462
- package/dist/prompts/agent.d.ts +0 -18
- package/dist/prompts/agent.js +0 -153
- 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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// attention.ts — pending human-ask counters across the canvas.
|
|
2
|
+
//
|
|
3
|
+
// Human asks are stored per-cwd, not per-node (interactionsRoot is keyed by
|
|
4
|
+
// the cwd the agent ran in, same pattern as humanloop's human list command).
|
|
5
|
+
// A cwd can be shared by multiple nodes, so we de-dup on cwd before summing to
|
|
6
|
+
// avoid counting the same pending ask N times.
|
|
7
|
+
//
|
|
8
|
+
// All public functions are best-effort: scanInbox failures return 0 / empty.
|
|
9
|
+
// Callers are display code (dashboard, attention queue) that must not blow up
|
|
10
|
+
// on a cold canvas or missing humanloop state.
|
|
11
|
+
import { scanInbox } from '@crouton-kit/humanloop';
|
|
12
|
+
import { interactionsRoot } from '../artifact.js';
|
|
13
|
+
import { getNode, listNodes, view } from './canvas.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Count pending asks for a single cwd root. Never throws.
|
|
19
|
+
*
|
|
20
|
+
* When `nodeId` is given, count only asks raised by THAT node — humanloop
|
|
21
|
+
* stamps `deck.source.nodeId` with the originating CRTR_NODE_ID, so two nodes
|
|
22
|
+
* sharing a cwd no longer pollute each other's count. Asks with no stamp
|
|
23
|
+
* (legacy, or raised outside a canvas node) are not attributable to any node
|
|
24
|
+
* and are excluded from the per-node count. Read via a cast so this doesn't
|
|
25
|
+
* hard-depend on a humanloop type bump.
|
|
26
|
+
*/
|
|
27
|
+
function countForCwd(cwd, nodeId) {
|
|
28
|
+
try {
|
|
29
|
+
const items = scanInbox([interactionsRoot(cwd)]);
|
|
30
|
+
if (nodeId === undefined)
|
|
31
|
+
return items.length;
|
|
32
|
+
return items.filter((i) => i.source?.nodeId === nodeId).length;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// humanloop not installed, or interactions dir doesn't exist — both fine.
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Public API
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Count pending asks for the cwd of a single node.
|
|
44
|
+
* Returns 0 when the node is unknown or humanloop is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
export function countAsks(nodeId) {
|
|
47
|
+
const node = getNode(nodeId);
|
|
48
|
+
if (node === null)
|
|
49
|
+
return 0;
|
|
50
|
+
return countForCwd(node.cwd, nodeId);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pending asks for all nodes reachable in the subscription sub-DAG from
|
|
54
|
+
* `rootId` (including root itself). De-duped by cwd: when multiple nodes
|
|
55
|
+
* share a cwd the first one encountered claims the entry.
|
|
56
|
+
*
|
|
57
|
+
* Returns only entries with count > 0.
|
|
58
|
+
*/
|
|
59
|
+
export function pendingAsksForView(rootId) {
|
|
60
|
+
// view() returns children only (excludes root), so prepend root.
|
|
61
|
+
const ids = [rootId, ...view(rootId)];
|
|
62
|
+
const seen = new Map(); // cwd → entry
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
const node = getNode(id);
|
|
65
|
+
if (node === null)
|
|
66
|
+
continue;
|
|
67
|
+
if (seen.has(node.cwd))
|
|
68
|
+
continue; // already counted this cwd
|
|
69
|
+
const count = countForCwd(node.cwd);
|
|
70
|
+
if (count === 0) {
|
|
71
|
+
// Still mark the cwd seen so later nodes with the same cwd are skipped.
|
|
72
|
+
seen.set(node.cwd, { node_id: id, name: node.name, cwd: node.cwd, count: 0 });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
seen.set(node.cwd, { node_id: id, name: node.name, cwd: node.cwd, count });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return Array.from(seen.values()).filter((e) => e.count > 0);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Pending asks across the entire canvas — every distinct cwd among all known
|
|
82
|
+
* nodes. Returns only entries with count > 0.
|
|
83
|
+
*/
|
|
84
|
+
export function asksAcrossCanvas() {
|
|
85
|
+
const rows = listNodes();
|
|
86
|
+
const seen = new Map(); // cwd → entry
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
if (seen.has(row.cwd))
|
|
89
|
+
continue;
|
|
90
|
+
const count = countForCwd(row.cwd);
|
|
91
|
+
seen.set(row.cwd, { node_id: row.node_id, name: row.name, cwd: row.cwd, count });
|
|
92
|
+
}
|
|
93
|
+
return Array.from(seen.values()).filter((e) => e.count > 0);
|
|
94
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { NodeMeta, NodeRow, NodeStatus, SubscriptionRef } from './types.js';
|
|
2
|
+
/** Create a node: scaffold its dirs, write meta.json, index the row. */
|
|
3
|
+
export declare function createNode(meta: NodeMeta): NodeMeta;
|
|
4
|
+
/** The canonical node record (from meta.json), or null if unknown. */
|
|
5
|
+
export declare function getNode(nodeId: string): NodeMeta | null;
|
|
6
|
+
/** The indexed row (from the db) — cheap for queries that don't need full meta. */
|
|
7
|
+
export declare function getRow(nodeId: string): NodeRow | null;
|
|
8
|
+
/** Merge a patch into a node's meta.json and re-index its row. */
|
|
9
|
+
export declare function updateNode(nodeId: string, patch: Partial<NodeMeta>): NodeMeta;
|
|
10
|
+
/** Convenience for the most common mutation. */
|
|
11
|
+
export declare function setStatus(nodeId: string, status: NodeStatus): void;
|
|
12
|
+
/** All rows, optionally filtered by status. */
|
|
13
|
+
export declare function listNodes(filter?: {
|
|
14
|
+
status?: NodeStatus | NodeStatus[];
|
|
15
|
+
}): NodeRow[];
|
|
16
|
+
/** Record `A subscribes_to B` — A receives B's output. active=true wakes A on
|
|
17
|
+
* emit; passive accumulates pointers without a wake. Mutable; callable by anyone. */
|
|
18
|
+
export declare function subscribe(subscriber: string, publisher: string, active?: boolean): void;
|
|
19
|
+
/** Drop a subscription edge. */
|
|
20
|
+
export declare function unsubscribe(subscriber: string, publisher: string): void;
|
|
21
|
+
/** Flip an existing subscription's wake behavior. */
|
|
22
|
+
export declare function setSubscriptionActive(subscriber: string, publisher: string, active: boolean): void;
|
|
23
|
+
/** Record the audit-only `child spawned_by parent` edge. */
|
|
24
|
+
export declare function recordSpawn(child: string, parent: string): void;
|
|
25
|
+
/** Who subscribes to `publisher` — the targets a push fans out to. */
|
|
26
|
+
export declare function subscribersOf(publisher: string): SubscriptionRef[];
|
|
27
|
+
/** Who `subscriber` subscribes to — its reports / the nodes feeding it. */
|
|
28
|
+
export declare function subscriptionsOf(subscriber: string): SubscriptionRef[];
|
|
29
|
+
/** A "view": every node whose output cascades up to `root` via subscriptions —
|
|
30
|
+
* the subscription sub-DAG reachable downward (root → its reports → theirs …).
|
|
31
|
+
* Returns ids excluding root, in BFS order. Cycle-safe. */
|
|
32
|
+
export declare function view(root: string): string[];
|
|
33
|
+
/** Stop-guard primitive: does this node hold an *active* subscription to a node
|
|
34
|
+
* that's still live (active|idle) — i.e. something that can actually wake it?
|
|
35
|
+
* If so, stopping is a legitimate await; if not, it must finish or escalate. */
|
|
36
|
+
export declare function hasActiveLiveSubscription(nodeId: string): boolean;
|
|
37
|
+
/** Rebuild node rows from on-disk metas (the db node table is a derived index).
|
|
38
|
+
* Edges are left intact — subscribes_to is db-authoritative; spawned_by is
|
|
39
|
+
* re-derived from each meta's `parent`. */
|
|
40
|
+
export declare function rebuildIndex(): void;
|
|
@@ -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[];
|