@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.
Files changed (182) hide show
  1. package/bin/crtrd +2 -0
  2. package/dist/builtin-personas/design/base.md +9 -0
  3. package/dist/builtin-personas/design/orchestrator.md +10 -0
  4. package/dist/builtin-personas/developer/base.md +9 -0
  5. package/dist/builtin-personas/developer/orchestrator.md +12 -0
  6. package/dist/builtin-personas/explore/base.md +9 -0
  7. package/dist/builtin-personas/explore/orchestrator.md +9 -0
  8. package/dist/builtin-personas/general/base.md +5 -0
  9. package/dist/builtin-personas/general/orchestrator.md +7 -0
  10. package/dist/builtin-personas/orchestration-kernel.md +71 -0
  11. package/dist/builtin-personas/plan/base.md +7 -0
  12. package/dist/builtin-personas/plan/orchestrator.md +12 -0
  13. package/dist/builtin-personas/review/base.md +7 -0
  14. package/dist/builtin-personas/review/orchestrator.md +9 -0
  15. package/dist/builtin-personas/runtime-base.md +39 -0
  16. package/dist/builtin-personas/spec/base.md +7 -0
  17. package/dist/builtin-personas/spec/orchestrator.md +10 -0
  18. package/dist/builtin-skills/skills/design/SKILL.md +51 -0
  19. package/dist/builtin-skills/skills/development/SKILL.md +109 -0
  20. package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
  21. package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
  22. package/dist/cli.js +14 -6
  23. package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
  24. package/dist/commands/attention.js +152 -0
  25. package/dist/commands/canvas.d.ts +2 -0
  26. package/dist/commands/canvas.js +35 -0
  27. package/dist/commands/daemon.d.ts +2 -0
  28. package/dist/commands/daemon.js +111 -0
  29. package/dist/commands/dashboard.d.ts +2 -0
  30. package/dist/commands/dashboard.js +65 -0
  31. package/dist/commands/human/prompts.d.ts +5 -0
  32. package/dist/commands/human/prompts.js +269 -0
  33. package/dist/commands/human/queue.d.ts +3 -0
  34. package/dist/commands/human/queue.js +133 -0
  35. package/dist/commands/human/shared.d.ts +43 -0
  36. package/dist/commands/human/shared.js +107 -0
  37. package/dist/commands/human.js +10 -454
  38. package/dist/commands/node.d.ts +2 -0
  39. package/dist/commands/node.js +354 -0
  40. package/dist/commands/pkg/market-inspect.d.ts +1 -0
  41. package/dist/commands/pkg/market-inspect.js +157 -0
  42. package/dist/commands/pkg/market-manage.d.ts +1 -0
  43. package/dist/commands/pkg/market-manage.js +316 -0
  44. package/dist/commands/pkg/market.d.ts +1 -0
  45. package/dist/commands/pkg/market.js +16 -0
  46. package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
  47. package/dist/commands/pkg/plugin-inspect.js +142 -0
  48. package/dist/commands/pkg/plugin-manage.d.ts +1 -0
  49. package/dist/commands/pkg/plugin-manage.js +294 -0
  50. package/dist/commands/pkg/plugin.d.ts +1 -0
  51. package/dist/commands/pkg/plugin.js +16 -0
  52. package/dist/commands/pkg/shared.d.ts +5 -0
  53. package/dist/commands/pkg/shared.js +61 -0
  54. package/dist/commands/pkg.js +3 -1004
  55. package/dist/commands/push.d.ts +3 -0
  56. package/dist/commands/push.js +159 -0
  57. package/dist/commands/revive.d.ts +2 -0
  58. package/dist/commands/revive.js +64 -0
  59. package/dist/commands/skill/author.d.ts +3 -0
  60. package/dist/commands/skill/author.js +147 -0
  61. package/dist/commands/skill/find.d.ts +4 -0
  62. package/dist/commands/skill/find.js +254 -0
  63. package/dist/commands/skill/read.d.ts +1 -0
  64. package/dist/commands/skill/read.js +89 -0
  65. package/dist/commands/skill/shared.d.ts +19 -0
  66. package/dist/commands/skill/shared.js +207 -0
  67. package/dist/commands/skill/state.d.ts +3 -0
  68. package/dist/commands/skill/state.js +69 -0
  69. package/dist/commands/skill.js +6 -691
  70. package/dist/commands/sys/config.d.ts +1 -0
  71. package/dist/commands/sys/config.js +186 -0
  72. package/dist/commands/sys/doctor.d.ts +1 -0
  73. package/dist/commands/sys/doctor.js +369 -0
  74. package/dist/commands/sys/shared.d.ts +3 -0
  75. package/dist/commands/sys/shared.js +24 -0
  76. package/dist/commands/sys/update.d.ts +2 -0
  77. package/dist/commands/sys/update.js +114 -0
  78. package/dist/commands/sys.js +4 -694
  79. package/dist/core/__tests__/argv-parser.test.js +19 -1
  80. package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
  81. package/dist/core/__tests__/canvas.test.js +154 -0
  82. package/dist/core/__tests__/reset.test.js +105 -0
  83. package/dist/core/canvas/attention.d.ts +24 -0
  84. package/dist/core/canvas/attention.js +94 -0
  85. package/dist/core/canvas/canvas.d.ts +40 -0
  86. package/dist/core/canvas/canvas.js +210 -0
  87. package/dist/core/canvas/db.d.ts +7 -0
  88. package/dist/core/canvas/db.js +61 -0
  89. package/dist/core/canvas/index.d.ts +4 -0
  90. package/dist/core/canvas/index.js +6 -0
  91. package/dist/core/canvas/paths.d.ts +16 -0
  92. package/dist/core/canvas/paths.js +62 -0
  93. package/dist/core/canvas/render.d.ts +30 -0
  94. package/dist/core/canvas/render.js +186 -0
  95. package/dist/core/canvas/types.d.ts +87 -0
  96. package/dist/core/canvas/types.js +8 -0
  97. package/dist/core/command.d.ts +5 -0
  98. package/dist/core/command.js +35 -10
  99. package/dist/core/feed/feed.d.ts +43 -0
  100. package/dist/core/feed/feed.js +116 -0
  101. package/dist/core/feed/inbox.d.ts +50 -0
  102. package/dist/core/feed/inbox.js +124 -0
  103. package/dist/core/help.js +5 -3
  104. package/dist/core/io.d.ts +15 -1
  105. package/dist/core/io.js +56 -6
  106. package/dist/core/personas/index.d.ts +12 -0
  107. package/dist/core/personas/index.js +10 -0
  108. package/dist/core/personas/loader.d.ts +44 -0
  109. package/dist/core/personas/loader.js +157 -0
  110. package/dist/core/personas/resolve.d.ts +36 -0
  111. package/dist/core/personas/resolve.js +110 -0
  112. package/dist/core/render.d.ts +11 -0
  113. package/dist/core/render.js +126 -0
  114. package/dist/core/resolver.d.ts +10 -0
  115. package/dist/core/resolver.js +109 -1
  116. package/dist/core/runtime/front-door.d.ts +10 -0
  117. package/dist/core/runtime/front-door.js +97 -0
  118. package/dist/core/runtime/kickoff.d.ts +23 -0
  119. package/dist/core/runtime/kickoff.js +134 -0
  120. package/dist/core/runtime/launch.d.ts +34 -0
  121. package/dist/core/runtime/launch.js +85 -0
  122. package/dist/core/runtime/nodes.d.ts +38 -0
  123. package/dist/core/runtime/nodes.js +95 -0
  124. package/dist/core/runtime/presence.d.ts +38 -0
  125. package/dist/core/runtime/presence.js +152 -0
  126. package/dist/core/runtime/promote.d.ts +30 -0
  127. package/dist/core/runtime/promote.js +105 -0
  128. package/dist/core/runtime/reset.d.ts +13 -0
  129. package/dist/core/runtime/reset.js +97 -0
  130. package/dist/core/runtime/revive.d.ts +26 -0
  131. package/dist/core/runtime/revive.js +89 -0
  132. package/dist/core/runtime/roadmap.d.ts +12 -0
  133. package/dist/core/runtime/roadmap.js +52 -0
  134. package/dist/core/runtime/spawn.d.ts +33 -0
  135. package/dist/core/runtime/spawn.js +118 -0
  136. package/dist/core/runtime/stop-guard.d.ts +18 -0
  137. package/dist/core/runtime/stop-guard.js +33 -0
  138. package/dist/core/runtime/tmux.d.ts +88 -0
  139. package/dist/core/runtime/tmux.js +198 -0
  140. package/dist/core/spawn.d.ts +17 -197
  141. package/dist/core/spawn.js +16 -539
  142. package/dist/daemon/crtrd-cli.js +4 -0
  143. package/dist/daemon/crtrd.d.ts +20 -0
  144. package/dist/daemon/crtrd.js +200 -0
  145. package/dist/daemon/manage.d.ts +17 -0
  146. package/dist/daemon/manage.js +57 -0
  147. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
  148. package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
  149. package/dist/pi-extensions/canvas-nav.d.ts +32 -0
  150. package/dist/pi-extensions/canvas-nav.js +536 -0
  151. package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
  152. package/dist/pi-extensions/canvas-stophook.js +373 -0
  153. package/package.json +6 -5
  154. package/dist/commands/agent.d.ts +0 -6
  155. package/dist/commands/agent.js +0 -585
  156. package/dist/commands/debug.d.ts +0 -3
  157. package/dist/commands/debug.js +0 -192
  158. package/dist/commands/job.d.ts +0 -11
  159. package/dist/commands/job.js +0 -384
  160. package/dist/commands/mode.js +0 -231
  161. package/dist/commands/plan.d.ts +0 -4
  162. package/dist/commands/plan.js +0 -322
  163. package/dist/commands/spec.d.ts +0 -3
  164. package/dist/commands/spec.js +0 -299
  165. package/dist/core/__tests__/flow-leaves.test.js +0 -248
  166. package/dist/core/__tests__/job.test.js +0 -310
  167. package/dist/core/__tests__/jobs.test.js +0 -98
  168. package/dist/core/__tests__/spawn.test.js +0 -138
  169. package/dist/core/__tests__/subagents.test.d.ts +0 -1
  170. package/dist/core/__tests__/subagents.test.js +0 -75
  171. package/dist/core/jobs.d.ts +0 -107
  172. package/dist/core/jobs.js +0 -565
  173. package/dist/core/subagents.d.ts +0 -18
  174. package/dist/core/subagents.js +0 -163
  175. package/dist/prompts/agent.d.ts +0 -27
  176. package/dist/prompts/agent.js +0 -184
  177. package/dist/prompts/debug.d.ts +0 -8
  178. package/dist/prompts/debug.js +0 -44
  179. /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
  180. /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
  181. /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
  182. /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
+ };