@crouton-kit/crouter 0.3.8 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +25 -27
- package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +15 -427
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +8 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +12 -681
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +9 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +63 -2
- package/dist/core/command.js +97 -24
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +69 -35
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +160 -2
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -80
- package/dist/core/spawn.js +15 -219
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +6 -5
- package/dist/commands/agent.js +0 -384
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -179
- package/dist/commands/job.js +0 -344
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -309
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -286
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -66
- package/dist/core/jobs.d.ts +0 -101
- package/dist/core/jobs.js +0 -462
- package/dist/prompts/agent.d.ts +0 -18
- package/dist/prompts/agent.js +0 -153
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
package/dist/core/resolver.js
CHANGED
|
@@ -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;
|