@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,97 @@
|
|
|
1
|
+
// The front door — bare `crtr` boots a resident root node.
|
|
2
|
+
//
|
|
3
|
+
// crtr → boot a root in this terminal (no prompt)
|
|
4
|
+
// crtr [dir] → root pinned to dir
|
|
5
|
+
// crtr [dir] ["prompt"] → root with a starter prompt
|
|
6
|
+
// crtr --name NAME ... → named root
|
|
7
|
+
// crtr <subcommand> ... → falls through to the normal dispatcher
|
|
8
|
+
// crtr -h | --help → root help (dispatcher)
|
|
9
|
+
//
|
|
10
|
+
// This is the only place that distinguishes "I want to live here" (root) from
|
|
11
|
+
// the subcommand surface. It runs before the dispatcher; if it boots, pi takes
|
|
12
|
+
// over the terminal and the process never returns.
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
14
|
+
import { resolve as resolvePath } from 'node:path';
|
|
15
|
+
import { bootRoot } from './spawn.js';
|
|
16
|
+
function isDir(p) {
|
|
17
|
+
try {
|
|
18
|
+
return existsSync(p) && statSync(p).isDirectory();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Parse `[dir] [prompt]` positionals + `--name`/`--kind` flags out of the
|
|
25
|
+
* leftover tokens after the bare `crtr`. */
|
|
26
|
+
function parseRootArgs(tokens) {
|
|
27
|
+
let cwd = process.cwd();
|
|
28
|
+
let name;
|
|
29
|
+
let kind;
|
|
30
|
+
const positionals = [];
|
|
31
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
32
|
+
const t = tokens[i];
|
|
33
|
+
if (t === '--name') {
|
|
34
|
+
name = tokens[++i];
|
|
35
|
+
}
|
|
36
|
+
else if (t === '--kind') {
|
|
37
|
+
kind = tokens[++i];
|
|
38
|
+
}
|
|
39
|
+
else if (t.startsWith('--')) {
|
|
40
|
+
// ignore unknown flags for the front door
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
positionals.push(t);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// First positional that is an existing dir → cwd; the rest → prompt.
|
|
47
|
+
if (positionals.length > 0 && isDir(resolvePath(positionals[0]))) {
|
|
48
|
+
cwd = resolvePath(positionals.shift());
|
|
49
|
+
}
|
|
50
|
+
const prompt = positionals.length > 0 ? positionals.join(' ') : undefined;
|
|
51
|
+
return { cwd, prompt, name, kind };
|
|
52
|
+
}
|
|
53
|
+
/** Env marker set on every pi the front door boots. Its presence means we are
|
|
54
|
+
* already inside a front-door-booted root, so a nested front-door launch must
|
|
55
|
+
* be refused — otherwise a removed/renamed subcommand that a child pi re-runs
|
|
56
|
+
* (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
|
|
57
|
+
export const FRONT_DOOR_ENV = 'CRTR_FRONT_DOOR';
|
|
58
|
+
/** If this invocation is a front-door (root) launch, boot it and never return.
|
|
59
|
+
* Returns false when it's a recognized subcommand / help / unknown token (let
|
|
60
|
+
* the dispatcher handle it — for unknown tokens it errors cleanly). */
|
|
61
|
+
export function maybeBootRoot(root, argv) {
|
|
62
|
+
const tokens = argv.slice(2);
|
|
63
|
+
const first = tokens[0];
|
|
64
|
+
// Recursion guard: never boot a root from inside a front-door-booted pi.
|
|
65
|
+
// This is the hard backstop against fork bombs — even a future footgun where
|
|
66
|
+
// a child re-invokes a removed subcommand cannot loop, because the second
|
|
67
|
+
// boot is refused and falls through to the dispatcher.
|
|
68
|
+
if (process.env[FRONT_DOOR_ENV])
|
|
69
|
+
return false;
|
|
70
|
+
// `crtr -h` / `crtr --help` / `crtr --version` → dispatcher (root help).
|
|
71
|
+
if (first === '-h' || first === '--help' || first === '--version' || first === '-v') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// A recognized subcommand → dispatcher.
|
|
75
|
+
const subtreeNames = new Set(root.subtrees.map((s) => s.name));
|
|
76
|
+
if (first !== undefined && subtreeNames.has(first))
|
|
77
|
+
return false;
|
|
78
|
+
// The front door boots pi ONLY on an unambiguous "live here" signal:
|
|
79
|
+
// • bare `crtr` (no tokens)
|
|
80
|
+
// • `crtr <dir> [prompt]` (first positional is an existing dir)
|
|
81
|
+
// • `crtr "multi word prompt"` (first token contains whitespace)
|
|
82
|
+
// Anything else — a bare word like `job`, or a leading flag — is treated as a
|
|
83
|
+
// mistyped/removed subcommand and handed to the dispatcher, which errors with
|
|
84
|
+
// "unknown subcommand: <token>". Booting pi for such tokens is what let the
|
|
85
|
+
// renamed `agent`/`job` subcommands fork-bomb the front door.
|
|
86
|
+
if (first !== undefined) {
|
|
87
|
+
const looksLikePrompt = /\s/.test(first);
|
|
88
|
+
const looksLikeDir = !first.startsWith('-') && isDir(resolvePath(first));
|
|
89
|
+
if (!looksLikePrompt && !looksLikeDir)
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// Unambiguous front-door launch → boot a resident root inline (exec pi in
|
|
93
|
+
// this terminal). Does not return.
|
|
94
|
+
const args = parseRootArgs(tokens);
|
|
95
|
+
bootRoot({ ...args, placement: 'inline' });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type NodeMeta } from '../canvas/index.js';
|
|
2
|
+
/** The goal file — the prompt/task a node was spawned with, persisted at birth
|
|
3
|
+
* so a fresh revive can re-read its mandate. */
|
|
4
|
+
export declare function goalPath(nodeId: string): string;
|
|
5
|
+
export declare function readGoal(nodeId: string): string | null;
|
|
6
|
+
/** Persist the spawning prompt as the node's goal. No-op for an empty prompt
|
|
7
|
+
* (e.g. a bare root). Idempotent enough — call once at spawn/boot. */
|
|
8
|
+
export declare function writeGoal(nodeId: string, text: string): void;
|
|
9
|
+
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
10
|
+
* revive ("on wake, do X"). Consumed (deleted) when the revive reads it. */
|
|
11
|
+
export declare function yieldMessagePath(nodeId: string): string;
|
|
12
|
+
export declare function writeYieldMessage(nodeId: string, text: string): void;
|
|
13
|
+
/** Read AND delete the yield message — it is a one-shot handoff to the next
|
|
14
|
+
* revive, so a later crash-revive never resurfaces a stale note. */
|
|
15
|
+
export declare function consumeYieldMessage(nodeId: string): string | null;
|
|
16
|
+
/** List the node's context/ dir (filenames, sorted). Empty when absent. */
|
|
17
|
+
export declare function listContextDir(nodeId: string): string[];
|
|
18
|
+
/** Build the auto-injected first message for a FRESH revive of `meta`. Reads
|
|
19
|
+
* the node's goal, roadmap, context dir, feed, and one-shot yield message off
|
|
20
|
+
* disk and frames them so the revived node can rebuild its bearings in one
|
|
21
|
+
* turn. Side effects: consumes the yield message and advances the feed cursor
|
|
22
|
+
* (both are "read" by surfacing them here). */
|
|
23
|
+
export declare function buildReviveKickoff(meta: NodeMeta): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The revive kickoff — the message auto-injected as a node's first turn when it
|
|
2
|
+
// comes back FRESH (a refresh-yield, or `canvas revive --fresh`). The node's
|
|
3
|
+
// in-memory context is gone, so this message IS its bearings: everything is
|
|
4
|
+
// read from disk and framed so the node can rebuild and continue without a
|
|
5
|
+
// round-trip. Resuming a saved conversation needs none of this (the
|
|
6
|
+
// conversation already holds the context).
|
|
7
|
+
//
|
|
8
|
+
// Layout (the framing a revived node sees):
|
|
9
|
+
// <goal file=…>…</goal> the mandate it was spawned with
|
|
10
|
+
// <roadmap file=…>…</roadmap> its evolving plan
|
|
11
|
+
// <context-dir path=…>…</context-dir> what artifacts exist on disk
|
|
12
|
+
// <feed>Awaiting N nodes … digest</feed> who it waits on + unread reports
|
|
13
|
+
// <yield-message>…</yield-message> the note its prior self left on yield
|
|
14
|
+
//
|
|
15
|
+
// The goal + yield-message are companion files in the node's context dir; the
|
|
16
|
+
// yield-message is one-shot (consumed on the next revive).
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { contextDir, getNode, subscriptionsOf, } from '../canvas/index.js';
|
|
20
|
+
import { readRoadmap, roadmapPath } from './roadmap.js';
|
|
21
|
+
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Companion context files: the goal (the spawning mandate) and the one-shot
|
|
24
|
+
// yield message (a note from the prior self to the revived self).
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** The goal file — the prompt/task a node was spawned with, persisted at birth
|
|
27
|
+
* so a fresh revive can re-read its mandate. */
|
|
28
|
+
export function goalPath(nodeId) {
|
|
29
|
+
return join(contextDir(nodeId), 'initial-prompt.md');
|
|
30
|
+
}
|
|
31
|
+
export function readGoal(nodeId) {
|
|
32
|
+
const p = goalPath(nodeId);
|
|
33
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
34
|
+
}
|
|
35
|
+
/** Persist the spawning prompt as the node's goal. No-op for an empty prompt
|
|
36
|
+
* (e.g. a bare root). Idempotent enough — call once at spawn/boot. */
|
|
37
|
+
export function writeGoal(nodeId, text) {
|
|
38
|
+
const body = text.trim();
|
|
39
|
+
if (body === '')
|
|
40
|
+
return;
|
|
41
|
+
mkdirSync(contextDir(nodeId), { recursive: true });
|
|
42
|
+
writeFileSync(goalPath(nodeId), body + '\n', 'utf8');
|
|
43
|
+
}
|
|
44
|
+
/** The yield-message file — a short note `crtr node yield` records for the next
|
|
45
|
+
* revive ("on wake, do X"). Consumed (deleted) when the revive reads it. */
|
|
46
|
+
export function yieldMessagePath(nodeId) {
|
|
47
|
+
return join(contextDir(nodeId), 'yield-message.md');
|
|
48
|
+
}
|
|
49
|
+
export function writeYieldMessage(nodeId, text) {
|
|
50
|
+
const body = text.trim();
|
|
51
|
+
if (body === '')
|
|
52
|
+
return;
|
|
53
|
+
mkdirSync(contextDir(nodeId), { recursive: true });
|
|
54
|
+
writeFileSync(yieldMessagePath(nodeId), body + '\n', 'utf8');
|
|
55
|
+
}
|
|
56
|
+
/** Read AND delete the yield message — it is a one-shot handoff to the next
|
|
57
|
+
* revive, so a later crash-revive never resurfaces a stale note. */
|
|
58
|
+
export function consumeYieldMessage(nodeId) {
|
|
59
|
+
const p = yieldMessagePath(nodeId);
|
|
60
|
+
if (!existsSync(p))
|
|
61
|
+
return null;
|
|
62
|
+
const body = readFileSync(p, 'utf8');
|
|
63
|
+
try {
|
|
64
|
+
rmSync(p);
|
|
65
|
+
}
|
|
66
|
+
catch { /* best-effort */ }
|
|
67
|
+
return body.trim() !== '' ? body : null;
|
|
68
|
+
}
|
|
69
|
+
/** List the node's context/ dir (filenames, sorted). Empty when absent. */
|
|
70
|
+
export function listContextDir(nodeId) {
|
|
71
|
+
const dir = contextDir(nodeId);
|
|
72
|
+
if (!existsSync(dir))
|
|
73
|
+
return [];
|
|
74
|
+
return readdirSync(dir).sort();
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Feed block — who the node is awaiting, plus a drained digest of unread
|
|
78
|
+
// reports. Draining here advances the cursor: the revived node has now "read"
|
|
79
|
+
// the feed, so a later `crtr feed read` shows only what arrives afterward.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function feedBlock(nodeId) {
|
|
82
|
+
// Awaiting = active subscriptions whose publisher is still live (active|idle).
|
|
83
|
+
const awaiting = subscriptionsOf(nodeId)
|
|
84
|
+
.filter((s) => s.active)
|
|
85
|
+
.map((s) => getNode(s.node_id))
|
|
86
|
+
.filter((m) => m !== null && (m.status === 'active' || m.status === 'idle'));
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push(`Awaiting ${awaiting.length} node${awaiting.length === 1 ? '' : 's'}.`);
|
|
89
|
+
for (const m of awaiting)
|
|
90
|
+
lines.push(` - ${m.name} (${m.node_id}) — ${m.status}`);
|
|
91
|
+
const cursor = readCursor(nodeId);
|
|
92
|
+
const entries = readInboxSince(nodeId, cursor);
|
|
93
|
+
if (entries.length > 0) {
|
|
94
|
+
writeCursor(nodeId, entries[entries.length - 1].ts);
|
|
95
|
+
lines.push('', coalesce(entries));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push('', '(no unread reports)');
|
|
99
|
+
}
|
|
100
|
+
return `<feed>\n${lines.join('\n')}\n</feed>`;
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// buildReviveKickoff — assemble the full fresh-revive first message.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/** Build the auto-injected first message for a FRESH revive of `meta`. Reads
|
|
106
|
+
* the node's goal, roadmap, context dir, feed, and one-shot yield message off
|
|
107
|
+
* disk and frames them so the revived node can rebuild its bearings in one
|
|
108
|
+
* turn. Side effects: consumes the yield message and advances the feed cursor
|
|
109
|
+
* (both are "read" by surfacing them here). */
|
|
110
|
+
export function buildReviveKickoff(meta) {
|
|
111
|
+
const nodeId = meta.node_id;
|
|
112
|
+
// Consume the one-shot yield note first so it never shows in the dir listing.
|
|
113
|
+
const yieldMsg = consumeYieldMessage(nodeId);
|
|
114
|
+
const parts = [
|
|
115
|
+
'You have been revived fresh after a context refresh — your previous in-memory ' +
|
|
116
|
+
'context is gone, by design. Everything below was just read from disk; it is your ' +
|
|
117
|
+
'full bearings. Rebuild from it and continue toward your goal.',
|
|
118
|
+
];
|
|
119
|
+
const goal = readGoal(nodeId);
|
|
120
|
+
if (goal !== null && goal.trim() !== '') {
|
|
121
|
+
parts.push(`<goal file="${goalPath(nodeId)}">\n${goal.trim()}\n</goal>`);
|
|
122
|
+
}
|
|
123
|
+
const roadmap = readRoadmap(nodeId);
|
|
124
|
+
parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${roadmap !== null && roadmap.trim() !== '' ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
|
|
125
|
+
const files = listContextDir(nodeId);
|
|
126
|
+
parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
|
|
127
|
+
parts.push(feedBlock(nodeId));
|
|
128
|
+
parts.push(yieldMsg !== null
|
|
129
|
+
? `<yield-message>\n${yieldMsg.trim()}\n</yield-message>`
|
|
130
|
+
: '<yield-message/>');
|
|
131
|
+
parts.push('If there is work to do, perform it. Otherwise stop — `crtr push final "<result>"` ' +
|
|
132
|
+
'if the goal is met, or end your turn to stay dormant awaiting your workers.');
|
|
133
|
+
return parts.join('\n\n');
|
|
134
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NodeMeta, LaunchSpec, Mode } from '../canvas/index.js';
|
|
2
|
+
export declare const CANVAS_STOPHOOK_PATH: string;
|
|
3
|
+
export declare const CANVAS_INBOX_WATCHER_PATH: string;
|
|
4
|
+
export declare const CANVAS_NAV_PATH: string;
|
|
5
|
+
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
6
|
+
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
7
|
+
* graph chrome). All self-gate on CRTR_NODE_ID. */
|
|
8
|
+
export declare const CANVAS_EXTENSIONS: string[];
|
|
9
|
+
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
10
|
+
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
11
|
+
export declare function normalizeModel(model: string): string;
|
|
12
|
+
/** Compose a node's full pi launch recipe from its persona. The two canvas
|
|
13
|
+
* extensions are always first; persona-declared extensions follow. */
|
|
14
|
+
export declare function buildLaunchSpec(kind: string, mode: Mode, opts?: {
|
|
15
|
+
extraEnv?: Record<string, string>;
|
|
16
|
+
}): {
|
|
17
|
+
launch: LaunchSpec;
|
|
18
|
+
lifecycle: 'terminal' | 'resident';
|
|
19
|
+
skills: string[];
|
|
20
|
+
};
|
|
21
|
+
export interface PiInvocation {
|
|
22
|
+
/** argv after the `pi` binary. */
|
|
23
|
+
argv: string[];
|
|
24
|
+
/** env to merge into the process. */
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
/** Construct the pi invocation for a node.
|
|
28
|
+
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
29
|
+
* - revive idle/done: pass `resumeSessionId` to `--resume` (keeps conversation).
|
|
30
|
+
* - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
|
|
31
|
+
export declare function buildPiArgv(meta: NodeMeta, opts?: {
|
|
32
|
+
prompt?: string;
|
|
33
|
+
resumeSessionId?: string;
|
|
34
|
+
}): PiInvocation;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The launch spec — how a node becomes (or comes back as) a running pi process.
|
|
2
|
+
//
|
|
3
|
+
// pi-only. No claude branch — we are a super-opinionated system. A node's
|
|
4
|
+
// LaunchSpec (persisted in meta.json) is the canonical recipe the daemon
|
|
5
|
+
// replays to revive it faithfully: `--resume` to wake a done/idle node (keeps
|
|
6
|
+
// its conversation), or fresh (against the context dir) for a refresh-yield.
|
|
7
|
+
// The spec is rewritten on every polymorph (base→orchestrator) so a node
|
|
8
|
+
// always comes back as its *current* self.
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { resolve as resolvePersona } from '../personas/index.js';
|
|
13
|
+
import { nodeEnv } from './nodes.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// The two canvas pi-extensions every node loads. They self-gate on the live
|
|
16
|
+
// {kind,mode} env, so the worker→orchestrator polymorph flips hook behavior
|
|
17
|
+
// with no respawn.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
function resolveExtension(name) {
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url)); // dist/core/runtime or src/core/runtime
|
|
21
|
+
const candidates = [
|
|
22
|
+
join(here, '..', '..', 'pi-extensions', `${name}.js`),
|
|
23
|
+
join(here, '..', '..', 'pi-extensions', `${name}.ts`),
|
|
24
|
+
];
|
|
25
|
+
return candidates.find((p) => existsSync(p)) ?? candidates[0];
|
|
26
|
+
}
|
|
27
|
+
export const CANVAS_STOPHOOK_PATH = resolveExtension('canvas-stophook');
|
|
28
|
+
export const CANVAS_INBOX_WATCHER_PATH = resolveExtension('canvas-inbox-watcher');
|
|
29
|
+
export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
|
|
30
|
+
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
31
|
+
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
32
|
+
* graph chrome). All self-gate on CRTR_NODE_ID. */
|
|
33
|
+
export const CANVAS_EXTENSIONS = [
|
|
34
|
+
CANVAS_STOPHOOK_PATH,
|
|
35
|
+
CANVAS_INBOX_WATCHER_PATH,
|
|
36
|
+
CANVAS_NAV_PATH,
|
|
37
|
+
];
|
|
38
|
+
/** Bare model aliases resolve to the anthropic provider under pi (avoids the
|
|
39
|
+
* bedrock default). Anything with a `/` or an unknown name passes through. */
|
|
40
|
+
export function normalizeModel(model) {
|
|
41
|
+
const bare = new Set(['sonnet', 'opus', 'haiku']);
|
|
42
|
+
if (bare.has(model))
|
|
43
|
+
return `anthropic/${model}`;
|
|
44
|
+
return model;
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Build the launch spec from {kind, mode}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/** Compose a node's full pi launch recipe from its persona. The two canvas
|
|
50
|
+
* extensions are always first; persona-declared extensions follow. */
|
|
51
|
+
export function buildLaunchSpec(kind, mode, opts = {}) {
|
|
52
|
+
const p = resolvePersona(kind, mode);
|
|
53
|
+
const launch = {
|
|
54
|
+
model: p.model !== undefined ? normalizeModel(p.model) : undefined,
|
|
55
|
+
tools: p.tools,
|
|
56
|
+
extensions: [...CANVAS_EXTENSIONS, ...p.extensions],
|
|
57
|
+
systemPrompt: p.systemPrompt,
|
|
58
|
+
env: { ...(opts.extraEnv ?? {}) },
|
|
59
|
+
};
|
|
60
|
+
return { launch, lifecycle: p.lifecycle, skills: p.skills };
|
|
61
|
+
}
|
|
62
|
+
/** Construct the pi invocation for a node.
|
|
63
|
+
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
64
|
+
* - revive idle/done: pass `resumeSessionId` to `--resume` (keeps conversation).
|
|
65
|
+
* - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
|
|
66
|
+
export function buildPiArgv(meta, opts = {}) {
|
|
67
|
+
const spec = meta.launch;
|
|
68
|
+
const argv = [];
|
|
69
|
+
for (const ext of spec?.extensions ?? CANVAS_EXTENSIONS) {
|
|
70
|
+
argv.push('-e', ext);
|
|
71
|
+
}
|
|
72
|
+
argv.push('-n', meta.name);
|
|
73
|
+
if (opts.resumeSessionId !== undefined)
|
|
74
|
+
argv.push('--resume', opts.resumeSessionId);
|
|
75
|
+
if (spec?.model !== undefined)
|
|
76
|
+
argv.push('--model', spec.model);
|
|
77
|
+
if (spec?.tools !== undefined && spec.tools.length > 0)
|
|
78
|
+
argv.push('--tools', spec.tools.join(','));
|
|
79
|
+
if (spec?.systemPrompt !== undefined && spec.systemPrompt !== '') {
|
|
80
|
+
argv.push('--append-system-prompt', spec.systemPrompt);
|
|
81
|
+
}
|
|
82
|
+
if (opts.prompt !== undefined && opts.prompt !== '')
|
|
83
|
+
argv.push(opts.prompt);
|
|
84
|
+
return { argv, env: nodeEnv(meta) };
|
|
85
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec } from '../canvas/index.js';
|
|
2
|
+
/** Generate a node id in the same shape as job ids (time-sortable + random). */
|
|
3
|
+
export declare function newNodeId(): string;
|
|
4
|
+
export interface NodeContext {
|
|
5
|
+
nodeId: string | null;
|
|
6
|
+
parentNodeId: string | null;
|
|
7
|
+
kind: string | null;
|
|
8
|
+
mode: Mode | null;
|
|
9
|
+
}
|
|
10
|
+
/** Read the current node's identity from the environment. A spawned pi process
|
|
11
|
+
* runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
|
|
12
|
+
* it by reading CRTR_NODE_ID as the parent. */
|
|
13
|
+
export declare function currentNodeContext(): NodeContext;
|
|
14
|
+
/** The env injected into a node's pi process. Self-gating extensions read
|
|
15
|
+
* CRTR_KIND/CRTR_MODE to flip behavior on polymorph without a respawn; the
|
|
16
|
+
* feed/inbox machinery reads CRTR_NODE_ID. */
|
|
17
|
+
export declare function nodeEnv(meta: NodeMeta): Record<string, string>;
|
|
18
|
+
export interface SpawnNodeOpts {
|
|
19
|
+
kind: string;
|
|
20
|
+
mode?: Mode;
|
|
21
|
+
lifecycle?: Lifecycle;
|
|
22
|
+
cwd: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Parent node id. Omit for a user-opened root. */
|
|
25
|
+
parent?: string | null;
|
|
26
|
+
/** New subscriptions this node opens default to passive when true. */
|
|
27
|
+
passiveDefault?: boolean;
|
|
28
|
+
/** Resolved pi launch recipe (from resolve(kind,mode)). */
|
|
29
|
+
launch?: LaunchSpec;
|
|
30
|
+
/** Override the generated id (e.g. when a caller pre-allocates one). */
|
|
31
|
+
nodeId?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Create a node on the canvas and wire its spawn-time edges.
|
|
34
|
+
*
|
|
35
|
+
* For a child (parent given): the parent auto-subscribes ACTIVE to the child
|
|
36
|
+
* (so it's woken when the child finishes), and a spawned_by audit edge is
|
|
37
|
+
* recorded. For a root (no parent): no edges, resident by default. */
|
|
38
|
+
export declare function spawnNode(opts: SpawnNodeOpts): NodeMeta;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Runtime node operations — the behavior layer above the canvas store.
|
|
2
|
+
//
|
|
3
|
+
// canvas/ is the pure data-access layer (nodes + edges). This is where the
|
|
4
|
+
// design's *rules* live: how a node comes into being, the env contract its pi
|
|
5
|
+
// process inherits, and the spawn-time wiring of the subscription spine.
|
|
6
|
+
//
|
|
7
|
+
// Two ways a node is born:
|
|
8
|
+
// • root — a user-opened entry point (bare `crtr`).
|
|
9
|
+
// No parent; resident by default (it's a conversation you live in).
|
|
10
|
+
// • child — spawned by another node. Terminal until it must persist. On
|
|
11
|
+
// spawn the PARENT auto-subscribes (active) to the child, so it
|
|
12
|
+
// learns when the work finishes — this seeds the subscription
|
|
13
|
+
// graph to mirror the spawn structure. A `spawned_by` audit edge
|
|
14
|
+
// is also recorded.
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
|
|
17
|
+
/** Generate a node id in the same shape as job ids (time-sortable + random). */
|
|
18
|
+
export function newNodeId() {
|
|
19
|
+
return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
20
|
+
}
|
|
21
|
+
/** Read the current node's identity from the environment. A spawned pi process
|
|
22
|
+
* runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
|
|
23
|
+
* it by reading CRTR_NODE_ID as the parent. */
|
|
24
|
+
export function currentNodeContext() {
|
|
25
|
+
const env = process.env;
|
|
26
|
+
return {
|
|
27
|
+
nodeId: env['CRTR_NODE_ID'] ?? null,
|
|
28
|
+
parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
|
|
29
|
+
kind: env['CRTR_KIND'] ?? null,
|
|
30
|
+
mode: env['CRTR_MODE'] ?? null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The env injected into a node's pi process. Self-gating extensions read
|
|
34
|
+
* CRTR_KIND/CRTR_MODE to flip behavior on polymorph without a respawn; the
|
|
35
|
+
* feed/inbox machinery reads CRTR_NODE_ID. */
|
|
36
|
+
export function nodeEnv(meta) {
|
|
37
|
+
const env = {
|
|
38
|
+
CRTR_NODE_ID: meta.node_id,
|
|
39
|
+
CRTR_KIND: meta.kind,
|
|
40
|
+
CRTR_MODE: meta.mode,
|
|
41
|
+
CRTR_LIFECYCLE: meta.lifecycle,
|
|
42
|
+
CRTR_NODE_CWD: meta.cwd,
|
|
43
|
+
};
|
|
44
|
+
if (meta.parent)
|
|
45
|
+
env['CRTR_PARENT_NODE_ID'] = meta.parent;
|
|
46
|
+
// Propagate an explicit canvas home so children share the same canvas.
|
|
47
|
+
const home = process.env['CRTR_HOME'];
|
|
48
|
+
if (home !== undefined && home !== '')
|
|
49
|
+
env['CRTR_HOME'] = home;
|
|
50
|
+
// Propagate the root's tmux session so every descendant spawns its windows
|
|
51
|
+
// into the same root session.
|
|
52
|
+
const rootSession = process.env['CRTR_ROOT_SESSION'];
|
|
53
|
+
if (rootSession !== undefined && rootSession !== '')
|
|
54
|
+
env['CRTR_ROOT_SESSION'] = rootSession;
|
|
55
|
+
// Merge any launch-spec env last (it may override / extend).
|
|
56
|
+
return { ...env, ...(meta.launch?.env ?? {}) };
|
|
57
|
+
}
|
|
58
|
+
/** Create a node on the canvas and wire its spawn-time edges.
|
|
59
|
+
*
|
|
60
|
+
* For a child (parent given): the parent auto-subscribes ACTIVE to the child
|
|
61
|
+
* (so it's woken when the child finishes), and a spawned_by audit edge is
|
|
62
|
+
* recorded. For a root (no parent): no edges, resident by default. */
|
|
63
|
+
export function spawnNode(opts) {
|
|
64
|
+
const parent = opts.parent ?? null;
|
|
65
|
+
const isRoot = parent === null;
|
|
66
|
+
const meta = {
|
|
67
|
+
node_id: opts.nodeId ?? newNodeId(),
|
|
68
|
+
name: opts.name ?? opts.kind,
|
|
69
|
+
created: new Date().toISOString(),
|
|
70
|
+
cwd: opts.cwd,
|
|
71
|
+
kind: opts.kind,
|
|
72
|
+
mode: opts.mode ?? 'base',
|
|
73
|
+
// A user-opened root is resident (a conversation you live in); a spawned
|
|
74
|
+
// node is terminal until it must persist (promotion handles that later).
|
|
75
|
+
lifecycle: opts.lifecycle ?? (isRoot ? 'resident' : 'terminal'),
|
|
76
|
+
status: 'active',
|
|
77
|
+
parent,
|
|
78
|
+
passive_default: opts.passiveDefault ?? false,
|
|
79
|
+
intent: null,
|
|
80
|
+
pi_session_id: null,
|
|
81
|
+
launch: opts.launch,
|
|
82
|
+
};
|
|
83
|
+
createNode(meta);
|
|
84
|
+
if (parent !== null) {
|
|
85
|
+
if (getNode(parent) === null) {
|
|
86
|
+
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
87
|
+
}
|
|
88
|
+
// The load-bearing seed: parent subscribes (active) to child so it learns
|
|
89
|
+
// when the work finishes. This mirrors spawn structure into the spine.
|
|
90
|
+
subscribe(parent, meta.node_id, true);
|
|
91
|
+
// Audit-only provenance.
|
|
92
|
+
recordSpawn(meta.node_id, parent);
|
|
93
|
+
}
|
|
94
|
+
return meta;
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NodeMeta } from '../canvas/index.js';
|
|
2
|
+
/** Persist `nodeId` as the currently focused node. Best-effort; never throws. */
|
|
3
|
+
export declare function setFocus(nodeId: string): void;
|
|
4
|
+
/** Read the currently focused node id, or null if the pointer is absent or
|
|
5
|
+
* empty (no active focus). Best-effort; never throws. */
|
|
6
|
+
export declare function getFocus(): string | null;
|
|
7
|
+
/** True when the node's tmux window is alive. A falsy tmux_session/window
|
|
8
|
+
* always returns false so callers don't need to null-guard. */
|
|
9
|
+
export declare function nodeLive(meta: NodeMeta): boolean;
|
|
10
|
+
/** Bring a node's tmux window to the foreground and record it as focused.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* - If the node has no live window (`nodeLive` is false), still write the
|
|
14
|
+
* focus pointer — the caller (e.g. revive logic) uses `focused:false` to
|
|
15
|
+
* know it needs to open a window first.
|
|
16
|
+
* - Otherwise call `switchClient` (lands us in the right session) then
|
|
17
|
+
* `selectWindow` (picks the right window within it). Both calls are
|
|
18
|
+
* best-effort; the focus pointer is always written regardless.
|
|
19
|
+
*
|
|
20
|
+
* Returns:
|
|
21
|
+
* focused — whether the tmux focus actually succeeded.
|
|
22
|
+
* session — the tmux session name if one was attempted, null otherwise. */
|
|
23
|
+
export declare function focusNode(nodeId: string): {
|
|
24
|
+
focused: boolean;
|
|
25
|
+
session: string | null;
|
|
26
|
+
};
|
|
27
|
+
/** Focus a node IN PLACE: bring its pane into the caller's current pane slot
|
|
28
|
+
* (swap-pane) instead of navigating the client to the node's own window. This
|
|
29
|
+
* is the default for `crtr node focus` and the nav-chrome spine jump — the
|
|
30
|
+
* agent appears where you are.
|
|
31
|
+
*
|
|
32
|
+
* Falls back to window focus when there is no caller pane (not inside tmux) or
|
|
33
|
+
* the target pane can't be resolved. `inPlace` reports which path ran. */
|
|
34
|
+
export declare function focusNodeInPlace(nodeId: string, callerPane?: string): {
|
|
35
|
+
focused: boolean;
|
|
36
|
+
session: string | null;
|
|
37
|
+
inPlace: boolean;
|
|
38
|
+
};
|