@crouton-kit/crouter 0.3.8 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/bin/crtrd +2 -0
  2. package/dist/builtin-personas/design/base.md +9 -0
  3. package/dist/builtin-personas/design/orchestrator.md +10 -0
  4. package/dist/builtin-personas/developer/base.md +9 -0
  5. package/dist/builtin-personas/developer/orchestrator.md +12 -0
  6. package/dist/builtin-personas/explore/base.md +9 -0
  7. package/dist/builtin-personas/explore/orchestrator.md +9 -0
  8. package/dist/builtin-personas/general/base.md +5 -0
  9. package/dist/builtin-personas/general/orchestrator.md +7 -0
  10. package/dist/builtin-personas/orchestration-kernel.md +71 -0
  11. package/dist/builtin-personas/plan/base.md +7 -0
  12. package/dist/builtin-personas/plan/orchestrator.md +12 -0
  13. package/dist/builtin-personas/review/base.md +7 -0
  14. package/dist/builtin-personas/review/orchestrator.md +9 -0
  15. package/dist/builtin-personas/runtime-base.md +39 -0
  16. package/dist/builtin-personas/spec/base.md +7 -0
  17. package/dist/builtin-personas/spec/orchestrator.md +10 -0
  18. package/dist/builtin-skills/skills/design/SKILL.md +51 -0
  19. package/dist/builtin-skills/skills/development/SKILL.md +109 -0
  20. package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
  21. package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
  22. package/dist/cli.js +25 -27
  23. package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
  24. package/dist/commands/attention.js +152 -0
  25. package/dist/commands/canvas.d.ts +2 -0
  26. package/dist/commands/canvas.js +35 -0
  27. package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
  28. package/dist/commands/daemon.js +111 -0
  29. package/dist/commands/dashboard.d.ts +2 -0
  30. package/dist/commands/dashboard.js +65 -0
  31. package/dist/commands/human/prompts.d.ts +5 -0
  32. package/dist/commands/human/prompts.js +269 -0
  33. package/dist/commands/human/queue.d.ts +3 -0
  34. package/dist/commands/human/queue.js +133 -0
  35. package/dist/commands/human/shared.d.ts +43 -0
  36. package/dist/commands/human/shared.js +107 -0
  37. package/dist/commands/human.js +15 -427
  38. package/dist/commands/node.d.ts +2 -0
  39. package/dist/commands/node.js +354 -0
  40. package/dist/commands/pkg/market-inspect.d.ts +1 -0
  41. package/dist/commands/pkg/market-inspect.js +157 -0
  42. package/dist/commands/pkg/market-manage.d.ts +1 -0
  43. package/dist/commands/pkg/market-manage.js +316 -0
  44. package/dist/commands/pkg/market.d.ts +1 -0
  45. package/dist/commands/pkg/market.js +16 -0
  46. package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
  47. package/dist/commands/pkg/plugin-inspect.js +142 -0
  48. package/dist/commands/pkg/plugin-manage.d.ts +1 -0
  49. package/dist/commands/pkg/plugin-manage.js +294 -0
  50. package/dist/commands/pkg/plugin.d.ts +1 -0
  51. package/dist/commands/pkg/plugin.js +16 -0
  52. package/dist/commands/pkg/shared.d.ts +5 -0
  53. package/dist/commands/pkg/shared.js +61 -0
  54. package/dist/commands/pkg.js +8 -1004
  55. package/dist/commands/push.d.ts +3 -0
  56. package/dist/commands/push.js +159 -0
  57. package/dist/commands/revive.d.ts +2 -0
  58. package/dist/commands/revive.js +64 -0
  59. package/dist/commands/skill/author.d.ts +3 -0
  60. package/dist/commands/skill/author.js +147 -0
  61. package/dist/commands/skill/find.d.ts +4 -0
  62. package/dist/commands/skill/find.js +254 -0
  63. package/dist/commands/skill/read.d.ts +1 -0
  64. package/dist/commands/skill/read.js +89 -0
  65. package/dist/commands/skill/shared.d.ts +19 -0
  66. package/dist/commands/skill/shared.js +207 -0
  67. package/dist/commands/skill/state.d.ts +3 -0
  68. package/dist/commands/skill/state.js +69 -0
  69. package/dist/commands/skill.js +12 -681
  70. package/dist/commands/sys/config.d.ts +1 -0
  71. package/dist/commands/sys/config.js +186 -0
  72. package/dist/commands/sys/doctor.d.ts +1 -0
  73. package/dist/commands/sys/doctor.js +369 -0
  74. package/dist/commands/sys/shared.d.ts +3 -0
  75. package/dist/commands/sys/shared.js +24 -0
  76. package/dist/commands/sys/update.d.ts +2 -0
  77. package/dist/commands/sys/update.js +114 -0
  78. package/dist/commands/sys.js +9 -694
  79. package/dist/core/__tests__/argv-parser.test.js +19 -1
  80. package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
  81. package/dist/core/__tests__/canvas.test.js +154 -0
  82. package/dist/core/__tests__/reset.test.js +105 -0
  83. package/dist/core/__tests__/resolver.test.js +69 -1
  84. package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
  85. package/dist/core/__tests__/unknown-path.test.js +52 -0
  86. package/dist/core/bootstrap.d.ts +2 -0
  87. package/dist/core/bootstrap.js +66 -0
  88. package/dist/core/canvas/attention.d.ts +24 -0
  89. package/dist/core/canvas/attention.js +94 -0
  90. package/dist/core/canvas/canvas.d.ts +40 -0
  91. package/dist/core/canvas/canvas.js +210 -0
  92. package/dist/core/canvas/db.d.ts +7 -0
  93. package/dist/core/canvas/db.js +61 -0
  94. package/dist/core/canvas/index.d.ts +4 -0
  95. package/dist/core/canvas/index.js +6 -0
  96. package/dist/core/canvas/paths.d.ts +16 -0
  97. package/dist/core/canvas/paths.js +62 -0
  98. package/dist/core/canvas/render.d.ts +30 -0
  99. package/dist/core/canvas/render.js +186 -0
  100. package/dist/core/canvas/types.d.ts +87 -0
  101. package/dist/core/canvas/types.js +8 -0
  102. package/dist/core/command.d.ts +63 -2
  103. package/dist/core/command.js +97 -24
  104. package/dist/core/feed/feed.d.ts +43 -0
  105. package/dist/core/feed/feed.js +116 -0
  106. package/dist/core/feed/inbox.d.ts +50 -0
  107. package/dist/core/feed/inbox.js +124 -0
  108. package/dist/core/frontmatter.d.ts +10 -0
  109. package/dist/core/frontmatter.js +24 -9
  110. package/dist/core/help.d.ts +39 -8
  111. package/dist/core/help.js +69 -35
  112. package/dist/core/io.d.ts +15 -1
  113. package/dist/core/io.js +56 -6
  114. package/dist/core/personas/index.d.ts +12 -0
  115. package/dist/core/personas/index.js +10 -0
  116. package/dist/core/personas/loader.d.ts +44 -0
  117. package/dist/core/personas/loader.js +157 -0
  118. package/dist/core/personas/resolve.d.ts +36 -0
  119. package/dist/core/personas/resolve.js +110 -0
  120. package/dist/core/render.d.ts +11 -0
  121. package/dist/core/render.js +126 -0
  122. package/dist/core/resolver.d.ts +10 -0
  123. package/dist/core/resolver.js +160 -2
  124. package/dist/core/runtime/front-door.d.ts +10 -0
  125. package/dist/core/runtime/front-door.js +97 -0
  126. package/dist/core/runtime/kickoff.d.ts +23 -0
  127. package/dist/core/runtime/kickoff.js +134 -0
  128. package/dist/core/runtime/launch.d.ts +34 -0
  129. package/dist/core/runtime/launch.js +85 -0
  130. package/dist/core/runtime/nodes.d.ts +38 -0
  131. package/dist/core/runtime/nodes.js +95 -0
  132. package/dist/core/runtime/presence.d.ts +38 -0
  133. package/dist/core/runtime/presence.js +152 -0
  134. package/dist/core/runtime/promote.d.ts +30 -0
  135. package/dist/core/runtime/promote.js +105 -0
  136. package/dist/core/runtime/reset.d.ts +13 -0
  137. package/dist/core/runtime/reset.js +97 -0
  138. package/dist/core/runtime/revive.d.ts +26 -0
  139. package/dist/core/runtime/revive.js +89 -0
  140. package/dist/core/runtime/roadmap.d.ts +12 -0
  141. package/dist/core/runtime/roadmap.js +52 -0
  142. package/dist/core/runtime/spawn.d.ts +33 -0
  143. package/dist/core/runtime/spawn.js +118 -0
  144. package/dist/core/runtime/stop-guard.d.ts +18 -0
  145. package/dist/core/runtime/stop-guard.js +33 -0
  146. package/dist/core/runtime/tmux.d.ts +88 -0
  147. package/dist/core/runtime/tmux.js +198 -0
  148. package/dist/core/spawn.d.ts +17 -80
  149. package/dist/core/spawn.js +15 -219
  150. package/dist/daemon/crtrd-cli.d.ts +1 -0
  151. package/dist/daemon/crtrd-cli.js +4 -0
  152. package/dist/daemon/crtrd.d.ts +20 -0
  153. package/dist/daemon/crtrd.js +200 -0
  154. package/dist/daemon/manage.d.ts +17 -0
  155. package/dist/daemon/manage.js +57 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
  157. package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
  158. package/dist/pi-extensions/canvas-nav.d.ts +32 -0
  159. package/dist/pi-extensions/canvas-nav.js +536 -0
  160. package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
  161. package/dist/pi-extensions/canvas-stophook.js +373 -0
  162. package/dist/types.d.ts +21 -0
  163. package/dist/types.js +3 -0
  164. package/package.json +6 -5
  165. package/dist/commands/agent.js +0 -384
  166. package/dist/commands/debug.d.ts +0 -3
  167. package/dist/commands/debug.js +0 -179
  168. package/dist/commands/job.js +0 -344
  169. package/dist/commands/plan.d.ts +0 -4
  170. package/dist/commands/plan.js +0 -309
  171. package/dist/commands/spec.d.ts +0 -3
  172. package/dist/commands/spec.js +0 -286
  173. package/dist/core/__tests__/flow-leaves.test.js +0 -248
  174. package/dist/core/__tests__/job.test.js +0 -310
  175. package/dist/core/__tests__/jobs.test.js +0 -66
  176. package/dist/core/jobs.d.ts +0 -101
  177. package/dist/core/jobs.js +0 -462
  178. package/dist/prompts/agent.d.ts +0 -18
  179. package/dist/prompts/agent.js +0 -153
  180. package/dist/prompts/debug.d.ts +0 -8
  181. package/dist/prompts/debug.js +0 -44
  182. /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
  183. /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
  184. /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { join, relative, sep, dirname } from 'node:path';
2
2
  import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, skillConfigKey, } from '../types.js';
3
3
  import { readConfig } from './config.js';
4
- import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
4
+ import { listDirs, pathExists, readText, readTextIfExists, walkFiles, } from './fs-utils.js';
5
5
  import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
6
6
  import { parseFrontmatter } from './frontmatter.js';
7
7
  import { ambiguous, notFound, usage } from './errors.js';
@@ -240,12 +240,62 @@ export function resolveSkill(rawName, opts = {}) {
240
240
  const direct = findSkillMatches(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
241
241
  if (direct.length > 0)
242
242
  return pickMatch(direct, skillName, pluginQualifier);
243
+ // Leaf-name fallback: the caller supplied only the final path segment
244
+ // (e.g. "cli-design" for "ai/interface/cli-design"). A direct path lookup
245
+ // missed because the skill lives under a nested path. Match by last segment.
246
+ const byLeaf = findSkillsByLeaf(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
247
+ if (byLeaf.length === 1)
248
+ return byLeaf[0];
249
+ if (byLeaf.length > 1) {
250
+ throw ambiguous(formatLeafAmbiguousMessage(skillName, byLeaf), {
251
+ skill: skillName,
252
+ candidates: byLeaf.map((m) => ({
253
+ id: formatSkillId(m),
254
+ plugin: m.plugin,
255
+ scope: m.scope,
256
+ path: m.path,
257
+ })),
258
+ next: 'Multiple skills share this leaf name. Re-run with one of the full paths in candidates.',
259
+ });
260
+ }
243
261
  throw notFound(formatNotFoundMessage(rawName, skillName, pluginQualifier), {
244
262
  skill: skillName,
245
263
  plugin: pluginQualifier,
246
264
  scope: parsed.scope,
247
265
  });
248
266
  }
267
+ /** Canonical, unambiguous identifier for a skill. Scope-root skills are
268
+ * qualified by scope; plugin skills by plugin name. */
269
+ function formatSkillId(s) {
270
+ return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}/${s.name}` : `${s.plugin}/${s.name}`;
271
+ }
272
+ /** Match skills whose final path segment equals `leaf`. Only meaningful when
273
+ * `leaf` is a bare segment (no slash) — a slashed query can never equal a
274
+ * single segment, so this returns empty and the caller falls through. */
275
+ function findSkillsByLeaf(leaf, pluginQualifier, scope, pluginFilter) {
276
+ if (leaf.includes('/'))
277
+ return [];
278
+ let all;
279
+ try {
280
+ all = scope ? listAllSkills(scope) : listAllSkills();
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ return all.filter((s) => {
286
+ if ((s.name.split('/').pop() ?? s.name) !== leaf)
287
+ return false;
288
+ if (pluginQualifier && s.plugin !== pluginQualifier)
289
+ return false;
290
+ if (pluginFilter && s.plugin !== pluginFilter)
291
+ return false;
292
+ return true;
293
+ });
294
+ }
295
+ function formatLeafAmbiguousMessage(leaf, matches) {
296
+ const ids = matches.map(formatSkillId).join(', ');
297
+ return `ambiguous skill: ${leaf} matches multiple skills: ${ids}`;
298
+ }
249
299
  function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
250
300
  const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
251
301
  const enabledPlugins = plugins.filter((p) => p.enabled);
@@ -332,7 +382,7 @@ function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
332
382
  lines.push(` did you mean: ${formatted.join(', ')}`);
333
383
  }
334
384
  else {
335
- lines.push(' run `crtr skill list` or `crtr skill search <query>` to discover skills');
385
+ lines.push(' run `crtr skill find list` or `crtr skill find search <query>` to discover skills');
336
386
  }
337
387
  return lines.join('\n');
338
388
  }
@@ -484,6 +534,114 @@ export function findMarketplaceByName(name, scope) {
484
534
  }
485
535
  return null;
486
536
  }
537
+ export function resolveCategory(name, opts = {}) {
538
+ const parsed = parseSkillQualifier(name);
539
+ if (parsed.segments.length === 0)
540
+ return null;
541
+ const effectiveScope = opts.scope ?? parsed.scope;
542
+ let pluginQualifier;
543
+ let subpath;
544
+ if (opts.pluginFilter !== undefined) {
545
+ pluginQualifier = opts.pluginFilter;
546
+ const sub = parsed.segments.join('/');
547
+ subpath = sub || undefined;
548
+ }
549
+ else if (parsed.segments.length > 1) {
550
+ const maybePlugin = parsed.segments[0];
551
+ const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
552
+ (effectiveScope === undefined ? null : findPluginByName(maybePlugin));
553
+ if (pluginMatch !== null) {
554
+ pluginQualifier = maybePlugin;
555
+ subpath = parsed.segments.slice(1).join('/');
556
+ }
557
+ else {
558
+ pluginQualifier = undefined;
559
+ subpath = parsed.segments.join('/');
560
+ }
561
+ }
562
+ else {
563
+ const maybePlugin = parsed.segments[0];
564
+ const pluginMatch = findPluginByName(maybePlugin, effectiveScope) ??
565
+ (effectiveScope === undefined ? null : findPluginByName(maybePlugin));
566
+ if (pluginMatch !== null) {
567
+ pluginQualifier = maybePlugin;
568
+ subpath = undefined;
569
+ }
570
+ else {
571
+ pluginQualifier = undefined;
572
+ subpath = maybePlugin;
573
+ }
574
+ }
575
+ let skills;
576
+ let dir;
577
+ let id;
578
+ let resolvedScope;
579
+ if (pluginQualifier !== undefined) {
580
+ const plugin = findPluginByName(pluginQualifier, effectiveScope) ??
581
+ (effectiveScope === undefined ? null : findPluginByName(pluginQualifier));
582
+ if (!plugin)
583
+ return null;
584
+ resolvedScope = plugin.scope;
585
+ const allPluginSkills = listSkillsInPlugin(plugin);
586
+ if (subpath === undefined) {
587
+ skills = allPluginSkills;
588
+ dir = join(plugin.root, SKILLS_DIR);
589
+ id = pluginQualifier;
590
+ }
591
+ else {
592
+ skills = allPluginSkills.filter((s) => s.name.startsWith(subpath + '/'));
593
+ dir = join(plugin.root, SKILLS_DIR, ...subpath.split('/'));
594
+ id = `${pluginQualifier}/${subpath}`;
595
+ }
596
+ }
597
+ else if (subpath !== undefined) {
598
+ const scope = effectiveScope ?? 'user';
599
+ resolvedScope = scope;
600
+ const skillsRoot = scopeSkillsDir(scope);
601
+ if (!skillsRoot)
602
+ return null;
603
+ const allScopeSkills = listScopeRootSkills(scope);
604
+ skills = allScopeSkills.filter((s) => s.name.startsWith(subpath + '/'));
605
+ dir = join(skillsRoot, ...subpath.split('/'));
606
+ id = `${scope}/${subpath}`;
607
+ }
608
+ else {
609
+ return null;
610
+ }
611
+ if (skills.length === 0)
612
+ return null;
613
+ const indexMd = join(dir, 'index.md');
614
+ const indexPath = pathExists(indexMd) ? indexMd : undefined;
615
+ return { id, plugin: pluginQualifier, scope: resolvedScope, dir, indexPath, skills };
616
+ }
617
+ export function buildCategoryIndex(cat) {
618
+ const lines = [];
619
+ lines.push(`# ${cat.id} — ${cat.skills.length} skills`);
620
+ lines.push('');
621
+ if (cat.indexPath !== undefined) {
622
+ const authored = readTextIfExists(cat.indexPath);
623
+ if (authored !== null) {
624
+ const { body } = parseFrontmatter(authored);
625
+ const trimmed = body.trim();
626
+ if (trimmed.length > 0) {
627
+ lines.push(trimmed);
628
+ lines.push('');
629
+ }
630
+ }
631
+ }
632
+ lines.push('## Skills');
633
+ const sorted = [...cat.skills].sort((a, b) => a.name.localeCompare(b.name));
634
+ for (const skill of sorted) {
635
+ const fullId = skill.plugin === SCOPE_SKILL_PLUGIN
636
+ ? `${skill.scope}/${skill.name}`
637
+ : `${skill.plugin}/${skill.name}`;
638
+ const desc = skill.frontmatter.description ?? '(no description)';
639
+ lines.push(`- \`${fullId}\` — ${desc}`);
640
+ }
641
+ lines.push('');
642
+ lines.push(`Read one with \`crtr skill read <full-id>\`. Narrow with \`crtr skill find list --plugin ${cat.plugin ?? cat.id}\`.`);
643
+ return lines.join('\n');
644
+ }
487
645
  export function scopeRootsLabel() {
488
646
  const proj = projectScopeRoot();
489
647
  return proj ? `project=${proj}, user=${userScopeRoot()}` : `user=${userScopeRoot()}`;
@@ -0,0 +1,10 @@
1
+ import type { RootDef } from './../command.js';
2
+ /** Env marker set on every pi the front door boots. Its presence means we are
3
+ * already inside a front-door-booted root, so a nested front-door launch must
4
+ * be refused — otherwise a removed/renamed subcommand that a child pi re-runs
5
+ * (e.g. `crtr node -h`) fork-bombs pi until the machine must be rebooted. */
6
+ export declare const FRONT_DOOR_ENV = "CRTR_FRONT_DOOR";
7
+ /** If this invocation is a front-door (root) launch, boot it and never return.
8
+ * Returns false when it's a recognized subcommand / help / unknown token (let
9
+ * the dispatcher handle it — for unknown tokens it errors cleanly). */
10
+ export declare function maybeBootRoot(root: RootDef, argv: string[]): boolean;
@@ -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;