@crouton-kit/crouter 0.3.14 → 0.3.16
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/dist/build-root.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +45 -67
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -99
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +129 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +196 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +266 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +178 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +334 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +105 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +205 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +81 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +24 -12
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +26 -12
- package/dist/core/runtime/launch.js +78 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +39 -1
- package/dist/core/runtime/nodes.js +69 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +299 -0
- package/dist/core/runtime/placement.js +688 -0
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux-chrome.d.ts +1 -0
- package/dist/core/runtime/tmux-chrome.js +4 -0
- package/dist/core/runtime/tmux.d.ts +113 -20
- package/dist/core/runtime/tmux.js +221 -39
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +594 -262
- package/dist/pi-extensions/canvas-resume.d.ts +22 -0
- package/dist/pi-extensions/canvas-resume.js +173 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +340 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +2 -2
- package/dist/core/runtime/presence.d.ts +0 -38
- package/dist/core/runtime/presence.js +0 -154
package/dist/commands/node.js
CHANGED
|
@@ -6,16 +6,40 @@
|
|
|
6
6
|
// the spine (`focus`/`msg`). The push/feed half lives under `crtr push`.
|
|
7
7
|
import { defineLeaf, defineBranch } from '../core/command.js';
|
|
8
8
|
import { InputError } from '../core/io.js';
|
|
9
|
-
import { spawnChild
|
|
9
|
+
import { spawnChild } from '../core/runtime/spawn.js';
|
|
10
10
|
import { promote, requestYield } from '../core/runtime/promote.js';
|
|
11
11
|
import { writeYieldMessage } from '../core/runtime/kickoff.js';
|
|
12
12
|
import { reviveNode } from '../core/runtime/revive.js';
|
|
13
|
-
import { focusNodeInPlace } from '../core/runtime/presence.js';
|
|
14
13
|
import { demoteNode } from '../core/runtime/demote.js';
|
|
15
|
-
import { windowAlive, windowOfPane, currentTmux } from '../core/runtime/
|
|
14
|
+
import { detachToBackground, focus as placementFocus, windowAlive, windowOfPane, currentTmux } from '../core/runtime/placement.js';
|
|
15
|
+
import { buildLaunchSpec } from '../core/runtime/launch.js';
|
|
16
|
+
import { closeNode } from '../core/runtime/close.js';
|
|
16
17
|
import { appendInbox } from '../core/feed/inbox.js';
|
|
17
18
|
import { availableKinds } from '../core/personas/index.js';
|
|
18
|
-
import { getNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, } from '../core/canvas/index.js';
|
|
19
|
+
import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, } from '../core/canvas/index.js';
|
|
20
|
+
// Past this much context, an ORCHESTRATOR that spawns a managed child is better
|
|
21
|
+
// off yielding than holding its fat window open for the child's result: the
|
|
22
|
+
// child's finish revives it fresh against its roadmap, so a clean window absorbs
|
|
23
|
+
// the result instead of this bloated one. Below the steering bands (130k+) on
|
|
24
|
+
// purpose — catch it at spawn, before the window is critically full.
|
|
25
|
+
const YIELD_NUDGE_THRESHOLD = 100_000;
|
|
26
|
+
const STD_CHILD_FOLLOW_UP = "Do not wait or poll on this child — there is no result to await and stopping will not strand you. You're auto-subscribed, so its finish wakes you on its own. Two moves only: continue other independent work right now, or stop and end your turn — the wake brings you back.";
|
|
27
|
+
/** Decision road sign for a managed (non-root) child. Normally STD_CHILD_FOLLOW_UP,
|
|
28
|
+
* but when the SPAWNER is an orchestrator whose context has already grown past
|
|
29
|
+
* YIELD_NUDGE_THRESHOLD, steer it to yield now and let its fresh revive handle
|
|
30
|
+
* the child's result. */
|
|
31
|
+
export function childFollowUp(spawnerId) {
|
|
32
|
+
if (spawnerId === undefined || spawnerId === '')
|
|
33
|
+
return STD_CHILD_FOLLOW_UP;
|
|
34
|
+
const spawner = getNode(spawnerId);
|
|
35
|
+
if (spawner === null || spawner.mode !== 'orchestrator')
|
|
36
|
+
return STD_CHILD_FOLLOW_UP;
|
|
37
|
+
const ctxTokens = readContextTokens(spawnerId);
|
|
38
|
+
if (ctxTokens === null || ctxTokens < YIELD_NUDGE_THRESHOLD)
|
|
39
|
+
return STD_CHILD_FOLLOW_UP;
|
|
40
|
+
const k = Math.round(ctxTokens / 1000);
|
|
41
|
+
return `Child spawned — you're auto-subscribed, so its finish wakes you on its own; never wait or poll. But you're an orchestrator already carrying ~${k}k of context: rather than hold this window open for the result, checkpoint context/roadmap.md and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap — let that clean revive absorb the result instead of this bloated context.`;
|
|
42
|
+
}
|
|
19
43
|
/** Validate a `--kind` against the installed personas; throws a listing InputError. */
|
|
20
44
|
function assertKind(kind) {
|
|
21
45
|
const kinds = availableKinds();
|
|
@@ -28,30 +52,35 @@ function assertKind(kind) {
|
|
|
28
52
|
// ---------------------------------------------------------------------------
|
|
29
53
|
const nodeNew = defineLeaf({
|
|
30
54
|
name: 'new',
|
|
55
|
+
description: 'spawn a node — a managed child (default), or an independent root with --root',
|
|
56
|
+
whenToUse: 'you have a self-contained unit of work — reach for this instead of doing it inline, so the reading and the tokens land in a fresh window and only the conclusion comes back: mapping an unfamiliar part of the codebase, writing a spec, designing an approach, breaking a job into a plan, implementing a change, or running a review. Match `--kind` to the work (explore/spec/design/plan/developer/review/general) and fan independent units out as concurrent children. Default is a managed child you auto-subscribe to, so its finish wakes you; pass `--mode orchestrator` when the unit is itself too big for one window (e.g. a large multi-area review) so it boots as a sub-orchestrator with its own roadmap instead of a base worker you hope promotes itself; pass `--root` to hand off an INDEPENDENT node you neither manage nor get woken by (e.g. one a human will sit and drive), not for ordinary delegation',
|
|
57
|
+
tier: 'important',
|
|
31
58
|
help: {
|
|
32
59
|
name: 'node new',
|
|
33
60
|
summary: 'spawn a terminal worker onto the canvas as a background window — returns its node id',
|
|
34
61
|
params: [
|
|
35
62
|
{ kind: 'stdin', name: 'prompt', required: true, constraint: 'First user message for the spawned node. Piped on stdin or passed as a positional.' },
|
|
36
63
|
{ kind: 'flag', name: 'kind', type: 'string', required: false, default: 'general', constraint: 'Persona kind — match the work: explore (map/investigate a codebase), spec (write a spec), design (architect a solution), plan (break work into steps), developer (implement a change), review (validate/critique), general (anything else).' },
|
|
37
|
-
{ kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode.
|
|
64
|
+
{ kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, default: 'base', constraint: 'Persona mode. base for a worker that finishes in one window; orchestrator to create the child directly as a sub-orchestrator (it boots with the orchestrator persona + a seeded roadmap and fans its scope out) — use it when the unit is too large for one window, e.g. a big review, instead of spawning a base worker and counting on it to promote itself.' },
|
|
38
65
|
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the node is pinned to. Defaults to the caller cwd.' },
|
|
39
66
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name (tmux window + resume picker). Defaults to the kind.' },
|
|
40
67
|
{ kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node (CRTR_NODE_ID).' },
|
|
68
|
+
{ kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with).' },
|
|
69
|
+
{ kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'FORK the new node from an existing pi conversation instead of starting it fresh: pass a node id (forks from that node\'s session), an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that whole history into a NEW session for the child (the source is untouched), then the prompt is delivered as the next message — i.e. the child wakes up as a continuation of that conversation. Use to branch exploratory work off a node that already built up the context you need, instead of re-deriving it. One-shot at birth: the fork resumes its own session thereafter.' },
|
|
41
70
|
],
|
|
42
71
|
output: [
|
|
43
72
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The new node id.' },
|
|
44
73
|
{ name: 'name', type: 'string', required: true, constraint: 'Display name.' },
|
|
45
74
|
{ name: 'window', type: 'string', required: false, constraint: 'tmux window id of the background window.' },
|
|
46
|
-
{ name: 'session', type: 'string', required: true, constraint: 'The
|
|
75
|
+
{ name: 'session', type: 'string', required: true, constraint: 'The tmux session the node was placed in — the shared crtr session for a child; your current session for an in-tmux --root.' },
|
|
47
76
|
{ name: 'status', type: 'string', required: true, constraint: 'Always "active" on spawn.' },
|
|
48
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller: the child runs independently and its finish wakes you on its own, so never wait or poll on it — either pick up other work now or end your turn. Read it, then act.' },
|
|
77
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller: the child runs independently and its finish wakes you on its own, so never wait or poll on it — either pick up other work now or end your turn. If you are an orchestrator already deep in context (>100k), it instead steers you to `crtr node yield` now so your fresh revive absorbs the child\'s result. Read it, then act.' },
|
|
49
78
|
],
|
|
50
79
|
outputKind: 'object',
|
|
51
80
|
effects: [
|
|
52
81
|
'Creates a node under ~/.crtr/nodes/<id>/ and indexes it in canvas.db.',
|
|
53
|
-
'
|
|
54
|
-
'Opens a background (non-focus-stealing)
|
|
82
|
+
'Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only.',
|
|
83
|
+
'Opens a tmux window running pi: a background (non-focus-stealing) window in the shared crtr session for a child; with --root, a new window in your current session (in-tmux) or the shared session (outside tmux), with the client switched to it.',
|
|
55
84
|
],
|
|
56
85
|
},
|
|
57
86
|
run: async (input) => {
|
|
@@ -64,14 +93,18 @@ const nodeNew = defineLeaf({
|
|
|
64
93
|
const cwd = input['cwd'] ?? process.cwd();
|
|
65
94
|
const name = input['name'];
|
|
66
95
|
const parent = input['parent'];
|
|
67
|
-
const
|
|
96
|
+
const root = input['root'] === true;
|
|
97
|
+
const forkFrom = input['forkFrom'];
|
|
98
|
+
const res = spawnChild({ kind, mode, cwd, name, prompt, parent, root, forkFrom });
|
|
68
99
|
return {
|
|
69
100
|
node_id: res.node.node_id,
|
|
70
101
|
name: res.node.name,
|
|
71
102
|
window: res.window ?? undefined,
|
|
72
103
|
session: res.session,
|
|
73
104
|
status: res.node.status,
|
|
74
|
-
follow_up:
|
|
105
|
+
follow_up: root
|
|
106
|
+
? "Independent root spawned — it is NOT under you. You are not subscribed, so its finish will NOT wake you and it does not hold you alive; it carries spawned_by=you for lineage only. It opened in its own window and the client was switched to it so it can be driven directly. Hand it off and move on — you will not be notified of its progress."
|
|
107
|
+
: childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
|
|
75
108
|
};
|
|
76
109
|
},
|
|
77
110
|
render: (r) => `<spawned name="${r['name']}" id="${r['node_id']}" status="${r['status']}">\n${r['follow_up']}\n</spawned>`,
|
|
@@ -81,11 +114,13 @@ const nodeNew = defineLeaf({
|
|
|
81
114
|
// ---------------------------------------------------------------------------
|
|
82
115
|
const nodeList = defineLeaf({
|
|
83
116
|
name: 'list',
|
|
117
|
+
description: 'list nodes on the canvas',
|
|
118
|
+
whenToUse: 'you want a flat roster of the nodes on the canvas, optionally sliced by status (active/idle/done/dead/canceled): a quick read of what exists and what is still running. Use `node inspect show` instead to drill into one node and its spine neighbors, `canvas dashboard` for the tree SHAPE, and `canvas attention` to find which nodes are blocked on a human',
|
|
84
119
|
help: {
|
|
85
120
|
name: 'node inspect list',
|
|
86
121
|
summary: 'list nodes on the canvas, optionally by status',
|
|
87
122
|
params: [
|
|
88
|
-
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter: active | idle | done | dead. Comma-separated for several.' },
|
|
123
|
+
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter: active | idle | done | dead | canceled. Comma-separated for several.' },
|
|
89
124
|
],
|
|
90
125
|
output: [
|
|
91
126
|
{ name: 'nodes', type: 'object[]', required: true, constraint: 'Rows: {node_id, name, kind, mode, lifecycle, status, cwd, parent, created}.' },
|
|
@@ -105,6 +140,8 @@ const nodeList = defineLeaf({
|
|
|
105
140
|
// ---------------------------------------------------------------------------
|
|
106
141
|
const nodeShow = defineLeaf({
|
|
107
142
|
name: 'show',
|
|
143
|
+
description: 'show a node + its spine neighbors',
|
|
144
|
+
whenToUse: 'you want one node in depth: its meta plus its spine neighbors — who it reports to (subscriptions) and who manages it (subscribers). Use `node inspect list` instead for the flat roster of every node, or `canvas dashboard` to see the whole-tree shape',
|
|
108
145
|
help: {
|
|
109
146
|
name: 'node inspect show',
|
|
110
147
|
summary: 'show a node\'s meta plus its subscriptions (reports) and subscribers (managers)',
|
|
@@ -133,13 +170,11 @@ const nodeShow = defineLeaf({
|
|
|
133
170
|
// ---------------------------------------------------------------------------
|
|
134
171
|
const nodeInspect = defineBranch({
|
|
135
172
|
name: 'inspect',
|
|
173
|
+
description: 'read the graph (list nodes / show one)',
|
|
174
|
+
whenToUse: 'reading the graph: enumerate the nodes on the canvas (`list`) or drill into one and its spine neighbors (`show`). Use `canvas dashboard` instead to render the tree SHAPE, or `canvas attention` to find which nodes are blocked on a human',
|
|
136
175
|
help: {
|
|
137
176
|
name: 'node inspect',
|
|
138
177
|
summary: 'read the canvas graph — enumerate nodes or inspect one with its spine neighbors',
|
|
139
|
-
children: [
|
|
140
|
-
{ name: 'list', desc: 'list nodes on the canvas', useWhen: 'surveying what exists' },
|
|
141
|
-
{ name: 'show', desc: 'show a node + its spine neighbors', useWhen: 'inspecting one node' },
|
|
142
|
-
],
|
|
143
178
|
},
|
|
144
179
|
children: [nodeList, nodeShow],
|
|
145
180
|
});
|
|
@@ -148,11 +183,14 @@ const nodeInspect = defineBranch({
|
|
|
148
183
|
// ---------------------------------------------------------------------------
|
|
149
184
|
const nodeFocus = defineLeaf({
|
|
150
185
|
name: 'focus',
|
|
186
|
+
description: 'bring a node window forefront',
|
|
187
|
+
whenToUse: 'you want to bring a specific node into view — swapped into your current pane — to watch or steer it directly, reviving it first if dormant. Use `node cycle` instead to walk neighbors one window at a time rather than jump to a named node, and `node msg` to steer a node without leaving where you are',
|
|
151
188
|
help: {
|
|
152
189
|
name: 'node focus',
|
|
153
190
|
summary: 'bring a node into your CURRENT pane in place (swap-pane) — the agent appears where you are instead of navigating you to its window',
|
|
154
191
|
params: [
|
|
155
192
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id to focus.' },
|
|
193
|
+
{ kind: 'flag', name: 'new-pane', type: 'bool', required: false, constraint: 'Open the node in a NEW viewport SIDE-BY-SIDE with your current pane (a second focus) instead of swapping it into your pane. Two agents on screen at once (F4).' },
|
|
156
194
|
],
|
|
157
195
|
output: [
|
|
158
196
|
{ name: 'focused', type: 'boolean', required: true, constraint: 'True when the node was brought into view.' },
|
|
@@ -161,25 +199,23 @@ const nodeFocus = defineLeaf({
|
|
|
161
199
|
{ name: 'in_place', type: 'boolean', required: true, constraint: 'True when the node was swapped into the caller pane; false when it fell back to window focus (no caller pane).' },
|
|
162
200
|
],
|
|
163
201
|
outputKind: 'object',
|
|
164
|
-
effects: ['Swaps the node\'s pane into the caller\'s current pane (tmux swap-pane -d) and
|
|
202
|
+
effects: ['Swaps the node\'s pane into the caller\'s current pane (tmux swap-pane -d) and retargets the caller\'s focus to it (focus pointer updated).', 'With --new-pane: splits a new viewport beside the caller (a second live focus) instead of swapping in place.', 'Revives a dormant node (resume) into the backstage if it has no live pane, then swaps it into the focus.'],
|
|
165
203
|
},
|
|
166
204
|
run: async (input) => {
|
|
167
205
|
const id = input['node'];
|
|
168
206
|
const node = getNode(id);
|
|
169
207
|
if (node === null)
|
|
170
208
|
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
const res = focusNodeInPlace(id);
|
|
182
|
-
return { focused: res.focused, session: res.session, revived, in_place: res.inPlace };
|
|
209
|
+
// Placement owns the whole act (§2.3): resolve the caller's focus (or open a
|
|
210
|
+
// new viewport with --new-pane), revive the target into the backstage if it
|
|
211
|
+
// is dormant, then hot-swap it onto the focus. The reviver is injected so
|
|
212
|
+
// placement need not import revive.ts.
|
|
213
|
+
const res = placementFocus(id, {
|
|
214
|
+
newPane: input['newPane'] === true,
|
|
215
|
+
callerNode: process.env['CRTR_NODE_ID'],
|
|
216
|
+
revive: (nid) => { reviveNode(nid, { resume: true }); },
|
|
217
|
+
});
|
|
218
|
+
return { focused: res.focused, session: res.session, revived: res.revived, in_place: res.inPlace };
|
|
183
219
|
},
|
|
184
220
|
});
|
|
185
221
|
// ---------------------------------------------------------------------------
|
|
@@ -197,14 +233,17 @@ function nodeByWindow(win) {
|
|
|
197
233
|
/** The live node occupying a tmux pane (pane → window → node), or undefined.
|
|
198
234
|
* Defaults to $TMUX_PANE / the caller's current pane when `pane` is omitted —
|
|
199
235
|
* shared by `node demote` and `node cycle`, both of which act on "the agent in
|
|
200
|
-
* front of you".
|
|
201
|
-
|
|
236
|
+
* front of you". Exported for the `canvas chord` / `canvas tmux-spread` leaves,
|
|
237
|
+
* which resolve the active pane's node the same way. */
|
|
238
|
+
export function nodeInPane(pane) {
|
|
202
239
|
const resolvePane = pane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
203
240
|
const win = resolvePane !== undefined && resolvePane !== '' ? windowOfPane(resolvePane) : null;
|
|
204
241
|
return win !== null ? nodeByWindow(win) : undefined;
|
|
205
242
|
}
|
|
206
243
|
const nodeDemote = defineLeaf({
|
|
207
244
|
name: 'demote',
|
|
245
|
+
description: 'finish the agent in your pane + recycle it into a fresh root',
|
|
246
|
+
whenToUse: 'you are at an agent\'s pane and done with it: finish it cleanly and recycle the pane in one move — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane to keep working. The human-driver way to end an agent and immediately start over in place. Use `node close` instead to tear a node and its subtree down WITHOUT finishing (no report, revivable), and `push final` when the agent should finish ITSELF from inside its own turn',
|
|
208
247
|
help: {
|
|
209
248
|
name: 'node demote',
|
|
210
249
|
summary: 'finish the agent in your current pane and recycle the pane — push its last message as a final report to everyone waiting on it, mark it done, then boot a fresh crtr root in the same pane',
|
|
@@ -243,6 +282,52 @@ const nodeDemote = defineLeaf({
|
|
|
243
282
|
: `<demote-failed id="${r['node_id'] ?? ''}">not in tmux, or no agent in this pane</demote-failed>`,
|
|
244
283
|
});
|
|
245
284
|
// ---------------------------------------------------------------------------
|
|
285
|
+
// node close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
const nodeClose = defineLeaf({
|
|
288
|
+
name: 'close',
|
|
289
|
+
description: 'close a node + cascade-cancel its exclusive subtree (revivable)',
|
|
290
|
+
whenToUse: 'you want to tear a node down WITHOUT finishing it, cascade-cancelling every descendant it exclusively owns: abandoning a line of work, killing a stuck or wrong-turn subtree, clearing a branch you no longer need. Windows die but nothing is deleted — each closed node keeps its pi session and can be revived later (`canvas revive`). Use `node demote` instead to FINISH the agent in your pane with a final report, and `push final` when a worker should end its own work normally (Alt+C → x)',
|
|
291
|
+
help: {
|
|
292
|
+
name: 'node close',
|
|
293
|
+
summary: 'close a node and cascade-cancel its subtree — kill its tmux window plus those of every descendant it EXCLUSIVELY owns (down the subscribes_to spine), mark them all canceled, and leave each a notice it reads on resume. A descendant still subscribed to by a manager outside the subtree is left running. Nothing is deleted: every closed node keeps its pi session and can be revived later (`crtr canvas revive`)',
|
|
294
|
+
params: [
|
|
295
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to close. Defaults to the node occupying --pane (or your current pane).' },
|
|
296
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to close. Defaults to $TMUX_PANE / your current pane. The Alt+C menu passes this for you.' },
|
|
297
|
+
],
|
|
298
|
+
output: [
|
|
299
|
+
{ name: 'closed', type: 'boolean', required: true, constraint: 'True when the node (and its exclusive subtree) was closed.' },
|
|
300
|
+
{ name: 'node_id', type: 'string', required: false, constraint: 'The node that was closed — the cascade root.' },
|
|
301
|
+
{ name: 'count', type: 'number', required: false, constraint: 'How many nodes were closed (root + cascaded descendants).' },
|
|
302
|
+
{ name: 'closed_ids', type: 'string[]', required: false, constraint: 'All closed node ids, kill order (leaves first, root last).' },
|
|
303
|
+
{ name: 'spared', type: 'string[]', required: false, constraint: 'Descendants left alive because a manager outside the subtree still subscribes to them.' },
|
|
304
|
+
],
|
|
305
|
+
outputKind: 'object',
|
|
306
|
+
effects: [
|
|
307
|
+
'Marks the node and its exclusive descendants `canceled` and clears intent (the daemon never revives a canceled node).',
|
|
308
|
+
'Kills each closed node\'s tmux window; their pi sessions and canvas edges persist for a later revive.',
|
|
309
|
+
'Appends a cancellation notice to each closed node\'s inbox, surfaced on its next resume.',
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
run: async (input) => {
|
|
313
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
314
|
+
let id = input['node'];
|
|
315
|
+
if (id === undefined || id === '')
|
|
316
|
+
id = nodeInPane(pane);
|
|
317
|
+
if (id === undefined || id === '') {
|
|
318
|
+
throw new InputError({ error: 'no_node', message: 'no node found in this pane to close', next: 'Pass --node <id>, or run from inside the agent\'s pane.' });
|
|
319
|
+
}
|
|
320
|
+
if (getNode(id) === null) {
|
|
321
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
322
|
+
}
|
|
323
|
+
const res = closeNode(id);
|
|
324
|
+
return { closed: true, node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
|
|
325
|
+
},
|
|
326
|
+
render: (r) => r['closed'] === true
|
|
327
|
+
? `<closed id="${r['node_id']}" count="${r['count']}" spared="${r['spared']?.length ?? 0}"/>`
|
|
328
|
+
: `<close-failed/>`,
|
|
329
|
+
});
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
246
331
|
// node cycle — DFS-walk the canvas one window at a time (Alt+] / Alt+[)
|
|
247
332
|
// ---------------------------------------------------------------------------
|
|
248
333
|
/** Every live node in DFS pre-order across the whole forest. The spawn tree is
|
|
@@ -282,6 +367,8 @@ function liveDfsOrder() {
|
|
|
282
367
|
}
|
|
283
368
|
const nodeCycle = defineLeaf({
|
|
284
369
|
name: 'cycle',
|
|
370
|
+
description: 'DFS-walk to the next/prev live node in place',
|
|
371
|
+
whenToUse: 'sweeping the canvas one window at a time, descending into children before siblings (bound to Alt+] forward / Alt+[ back). Use `node focus` instead to jump straight to a named node',
|
|
285
372
|
help: {
|
|
286
373
|
name: 'node cycle',
|
|
287
374
|
summary: 'focus the next/previous live node in DFS pre-order — the canvas walked one window at a time, descending into a node\'s children before its siblings (bound to Alt+] forward / Alt+[ back)',
|
|
@@ -313,15 +400,14 @@ const nodeCycle = defineLeaf({
|
|
|
313
400
|
const target = getNode(targetId);
|
|
314
401
|
if (target === null)
|
|
315
402
|
return { focused: false, from: fromId };
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const res = focusNodeInPlace(targetId, pane, fromId);
|
|
403
|
+
// Placement retargets the caller pane's focus to the neighbor (§2.3),
|
|
404
|
+
// reviving it into the backstage first if its pane was released. callerNode
|
|
405
|
+
// is the node we cycled AWAY from — the current occupant of the caller pane.
|
|
406
|
+
const res = placementFocus(targetId, {
|
|
407
|
+
pane,
|
|
408
|
+
callerNode: fromId,
|
|
409
|
+
revive: (nid) => { reviveNode(nid, { resume: true }); },
|
|
410
|
+
});
|
|
325
411
|
return { focused: res.focused, node_id: targetId, name: target.name, from: fromId };
|
|
326
412
|
},
|
|
327
413
|
render: (r) => r['focused'] === true
|
|
@@ -329,41 +415,12 @@ const nodeCycle = defineLeaf({
|
|
|
329
415
|
: `<cycle-noop>no other live node to focus</cycle-noop>`,
|
|
330
416
|
});
|
|
331
417
|
// ---------------------------------------------------------------------------
|
|
332
|
-
// node session — boot a NEW root in its own tmux session (the explicit form)
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
const nodeSession = defineLeaf({
|
|
335
|
-
name: 'session',
|
|
336
|
-
help: {
|
|
337
|
-
name: 'node session',
|
|
338
|
-
summary: 'start a fresh root node as its own window in the shared crtr session (use from inside a node to start a new root without taking your pane)',
|
|
339
|
-
params: [
|
|
340
|
-
{ kind: 'stdin', name: 'prompt', required: false, constraint: 'Optional starter prompt; a root needs none.' },
|
|
341
|
-
{ kind: 'flag', name: 'kind', type: 'string', required: false, default: 'general', constraint: 'Persona kind for the root.' },
|
|
342
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir to pin the root to. Defaults to the caller cwd.' },
|
|
343
|
-
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name.' },
|
|
344
|
-
],
|
|
345
|
-
output: [
|
|
346
|
-
{ name: 'node_id', type: 'string', required: true, constraint: 'The root node id.' },
|
|
347
|
-
{ name: 'session', type: 'string', required: true, constraint: 'The shared crtr tmux session this root\'s window was placed in.' },
|
|
348
|
-
{ name: 'window', type: 'string', required: false, constraint: 'The root node\'s window id.' },
|
|
349
|
-
],
|
|
350
|
-
outputKind: 'object',
|
|
351
|
-
effects: ['Opens a detached window in the shared crtr session and runs pi in it as a resident root node.'],
|
|
352
|
-
},
|
|
353
|
-
run: async (input) => {
|
|
354
|
-
const prompt = input['prompt'];
|
|
355
|
-
const kind = input['kind'] ?? 'general';
|
|
356
|
-
const cwd = input['cwd'] ?? process.cwd();
|
|
357
|
-
const name = input['name'];
|
|
358
|
-
const meta = bootRoot({ cwd, kind, name, prompt, placement: 'session' });
|
|
359
|
-
return { node_id: meta.node_id, session: meta.tmux_session ?? '', window: meta.window ?? undefined };
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
// ---------------------------------------------------------------------------
|
|
363
418
|
// node msg — direct-address any node at a wake tier (wakes a dormant target)
|
|
364
419
|
// ---------------------------------------------------------------------------
|
|
365
420
|
const nodeMsg = defineLeaf({
|
|
366
421
|
name: 'msg',
|
|
422
|
+
description: 'direct-message any node at a wake tier',
|
|
423
|
+
whenToUse: 'you want to address a specific node directly — steer it mid-flight, hand it a correction, ping it, or pass it new information — and have it land regardless of subscriptions, reviving a dormant target. Set `--tier` by urgency: critical interrupts with a new turn, urgent steers mid-turn, normal is a follow-up, deferred waits for its next cycle. Use `node subscribe` instead to wire ongoing push delivery rather than send a one-off, and `push` to report UP your own spine',
|
|
367
424
|
help: {
|
|
368
425
|
name: 'node msg',
|
|
369
426
|
summary: 'send a direct message to any node\'s inbox at a wake tier — a direct message wakes the node regardless of subscriptions (reviving it if dormant)',
|
|
@@ -417,6 +474,8 @@ function resolveSubscriber(input) {
|
|
|
417
474
|
}
|
|
418
475
|
const nodeSubscribe = defineLeaf({
|
|
419
476
|
name: 'subscribe',
|
|
477
|
+
description: 'wire a subscribes_to edge between any pair (active or --passive)',
|
|
478
|
+
whenToUse: 'you want to wire who-wakes-whom on the graph: make a node receive the pushes another node emits — yourself by default, or any node to any publisher via `--subscriber` (e.g. point a manager at a `--root` worker you spawned, or fan a reviewer to a second orchestrator). Active by default, so a push WAKES the subscriber; pass `--passive` to have pushes accumulate and auto-inject on its next message without waking it. You already auto-subscribe to any child you spawn, so reach for this for edges spawn did not create. Inverse is `node unsubscribe`',
|
|
420
479
|
help: {
|
|
421
480
|
name: 'node subscribe',
|
|
422
481
|
summary: 'wire a subscribes_to edge so one node receives another\'s pushes — the subscriber can be you (default) or, with --subscriber, ANY node, to ANY publisher. Re-running flips an existing edge\'s active/passive mode.',
|
|
@@ -452,6 +511,8 @@ const nodeSubscribe = defineLeaf({
|
|
|
452
511
|
});
|
|
453
512
|
const nodeUnsubscribe = defineLeaf({
|
|
454
513
|
name: 'unsubscribe',
|
|
514
|
+
description: 'drop a subscribes_to edge',
|
|
515
|
+
whenToUse: 'you want to stop a node receiving another\'s pushes: detach yourself (default) or any node via `--subscriber` from a publisher — quiet a feed you no longer track, or cut a manager loose from a finished worker. Idempotent. The inverse of `node subscribe`',
|
|
455
516
|
help: {
|
|
456
517
|
name: 'node unsubscribe',
|
|
457
518
|
summary: 'drop a subscribes_to edge — the subscriber (you by default, or any node via --subscriber) stops receiving the publisher\'s pushes.',
|
|
@@ -476,28 +537,35 @@ const nodeUnsubscribe = defineLeaf({
|
|
|
476
537
|
render: (r) => `<unsubscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}"/>`,
|
|
477
538
|
});
|
|
478
539
|
// ---------------------------------------------------------------------------
|
|
479
|
-
// node promote — become
|
|
540
|
+
// node promote — become an orchestrator (the worker→orchestrator polymorph)
|
|
480
541
|
// ---------------------------------------------------------------------------
|
|
481
542
|
const nodePromote = defineLeaf({
|
|
482
543
|
name: 'promote',
|
|
544
|
+
description: 'become an orchestrator of a chosen kind',
|
|
545
|
+
whenToUse: 'your task has outgrown a single context window — many phases to delegate and persist across refreshes — so become an orchestrator: a long-lived, roadmap-holding node that fans work out to children and survives context refreshes (`node yield`). Choose `--kind` to specialize (developer/review/spec/design/plan/explore/general). Pass `--resident` to ALSO make it interactable (stays dormant, woken by inbox/human, never forced to submit a final); without it you stay terminal/orchestrator — still reporting a final up the spine and reaping when done. Do NOT reach for this for work that fits one window, or merely because you spawned a child — a base worker that spawns a helper and ends with `push final` never needs to promote',
|
|
546
|
+
tier: 'important',
|
|
483
547
|
help: {
|
|
484
548
|
name: 'node promote',
|
|
485
|
-
summary: 'promote yourself to
|
|
549
|
+
summary: 'promote yourself to an orchestrator — do this when your task outgrows one context window (many phases to delegate and persist across refreshes); not for work that fits one window, and not merely because you spawned a child. Mode only — lifecycle stays as-is unless you pass --resident',
|
|
486
550
|
params: [
|
|
487
551
|
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Specialize as this kind of orchestrator: developer (own feature delivery), review, spec, design, plan, explore, general. Defaults to your current kind. Promoting from a generic kind? CHOOSE a concrete one — it sets the orchestrator persona you revive into.' },
|
|
552
|
+
{ kind: 'flag', name: 'resident', type: 'bool', required: false, constraint: 'ALSO flip lifecycle→resident: make the node interactable — it stays dormant, woken by inbox/human, and is never forced to submit a final. Omit to stay terminal/orchestrator (delegates + holds a roadmap, but still owes a final up the spine and reaps when done).' },
|
|
488
553
|
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to promote. Defaults to the caller (CRTR_NODE_ID).' },
|
|
489
554
|
],
|
|
490
555
|
output: [
|
|
491
556
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The promoted node.' },
|
|
492
557
|
{ name: 'kind', type: 'string', required: true, constraint: 'The kind it now orchestrates as.' },
|
|
493
558
|
{ name: 'mode', type: 'string', required: true, constraint: 'Now "orchestrator".' },
|
|
559
|
+
{ name: 'lifecycle', type: 'string', required: true, constraint: '"resident" if you passed --resident, else unchanged (typically "terminal").' },
|
|
494
560
|
{ name: 'roadmap_written', type: 'boolean', required: true, constraint: 'True if a roadmap scaffold was seeded by this call.' },
|
|
495
561
|
{ name: 'roadmap_path', type: 'string', required: true, constraint: 'Absolute path to your roadmap doc (context/roadmap.md) — edit it to author your plan.' },
|
|
496
562
|
{ name: 'goal_path', type: 'string', required: true, constraint: 'Absolute path to your goal doc (context/initial-prompt.md) — the mandate you were spawned with.' },
|
|
497
|
-
{ name: '
|
|
563
|
+
{ name: 'memory_path', type: 'string', required: true, constraint: 'Absolute path to your NODE-LOCAL memory index (context/memory/MEMORY.md) — facts specific to this goal; dies with this node.' },
|
|
564
|
+
{ name: 'user_memory_path', type: 'string', required: true, constraint: 'Absolute path to your USER-GLOBAL memory index (<crtrHome>/memory/MEMORY.md) — who the human is, how they like to work; loaded into every orchestrator everywhere.' },
|
|
565
|
+
{ name: 'project_memory_path', type: 'string', required: true, constraint: 'Absolute path to your PROJECT memory index (<crtrHome>/projects/<key>/memory/MEMORY.md) — facts bound to this repo; loaded into every orchestrator working here.' },
|
|
498
566
|
],
|
|
499
567
|
outputKind: 'object',
|
|
500
|
-
effects: ['Flips
|
|
568
|
+
effects: ['Flips mode→orchestrator + kind→chosen (lifecycle unchanged unless --resident, which also flips lifecycle→resident); rewrites the launch spec to that kind\'s orchestrator persona; seeds context/roadmap.md scaffold + all three scoped memory stores (user-global, project, node-local) if absent.', 'Your new-role guidance is injected automatically at the turn boundary by the persona injector — the command no longer returns it.'],
|
|
501
569
|
},
|
|
502
570
|
run: async (input) => {
|
|
503
571
|
const id = input['node'] ?? process.env['CRTR_NODE_ID'];
|
|
@@ -506,25 +574,90 @@ const nodePromote = defineLeaf({
|
|
|
506
574
|
const kind = input['kind'];
|
|
507
575
|
if (kind !== undefined)
|
|
508
576
|
assertKind(kind);
|
|
509
|
-
const
|
|
510
|
-
|
|
577
|
+
const resident = input['resident'] === true;
|
|
578
|
+
const res = promote(id, { ...(kind !== undefined ? { kind } : {}), ...(resident ? { resident: true } : {}) });
|
|
579
|
+
return { node_id: res.meta.node_id, kind: res.meta.kind, mode: res.meta.mode, lifecycle: res.meta.lifecycle, roadmap_written: res.roadmapWritten, roadmap_path: res.roadmapPath, goal_path: res.goalPath, memory_path: res.memoryPath, user_memory_path: res.userMemoryPath, project_memory_path: res.projectMemoryPath };
|
|
511
580
|
},
|
|
512
581
|
});
|
|
513
582
|
// ---------------------------------------------------------------------------
|
|
583
|
+
// node lifecycle — flip the lifecycle axis (terminal ↔ resident), independent
|
|
584
|
+
// of mode. The persona injector delivers the transition guidance.
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
const nodeLifecycle = defineLeaf({
|
|
587
|
+
name: 'lifecycle',
|
|
588
|
+
description: 'switch a node between terminal and resident',
|
|
589
|
+
whenToUse: 'you want to flip a node\'s LIFECYCLE independent of its mode: make a node RESIDENT so it becomes interactable — it stays dormant, wakes on inbox/human, and is never forced to submit a final; or make a node TERMINAL so it owes a final result up the spine and reaps when done. Orthogonal to `node promote`, which changes MODE (base↔orchestrator), not lifecycle. The new-state guidance is injected automatically at the next turn boundary. Pass `--detach` to ALSO send a still-running agent to the background crtr session, freeing your pane while it finishes — the human-driver demote (Alt+C → d demotes in place) and detach (Alt+C → D demotes + backgrounds)',
|
|
590
|
+
help: {
|
|
591
|
+
name: 'node lifecycle',
|
|
592
|
+
summary: 'set a node\'s lifecycle axis — terminal (owes a final up the spine, reaps when done) or resident (interactable, stays dormant, woken by inbox/human, never forced to submit). Orthogonal to mode; promotion does not touch it. `--detach` also relocates a live agent to the background crtr session',
|
|
593
|
+
params: [
|
|
594
|
+
{ kind: 'positional', name: 'lifecycle', required: true, constraint: 'terminal | resident.' },
|
|
595
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to change. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
596
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane id whose node to change, when --node is omitted. Defaults to $TMUX_PANE. The Alt+C menu passes this for you.' },
|
|
597
|
+
{ kind: 'flag', name: 'detach', type: 'bool', required: false, constraint: 'After flipping lifecycle, send the still-running agent to the background crtr session (break its pane out of the foreground). The pi keeps generating and — now terminal — pushes a final up the spine when done. The human-driver "I am done foregrounding this" move (Alt+C → D).' },
|
|
598
|
+
],
|
|
599
|
+
output: [
|
|
600
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The node.' },
|
|
601
|
+
{ name: 'lifecycle', type: 'string', required: true, constraint: 'Its new lifecycle (terminal | resident).' },
|
|
602
|
+
{ name: 'detached', type: 'boolean', required: false, constraint: 'True when --detach relocated the agent to the background crtr session.' },
|
|
603
|
+
],
|
|
604
|
+
outputKind: 'object',
|
|
605
|
+
effects: ['Sets lifecycle on the node meta and rebuilds its launch spec so a future revive boots with the new lifecycle\'s prompt baked in.', 'The persona injector delivers the transition guidance at the next turn boundary (or on the node\'s next revive if it is dormant).', 'With --detach: relocates the agent\'s live pane to the background crtr session (break-pane) WITHOUT killing the pi — it keeps generating in the background.'],
|
|
606
|
+
},
|
|
607
|
+
run: async (input) => {
|
|
608
|
+
const value = input['lifecycle']?.trim().toLowerCase();
|
|
609
|
+
if (value !== 'terminal' && value !== 'resident') {
|
|
610
|
+
throw new InputError({ error: 'bad_lifecycle', message: `invalid lifecycle: ${value ?? ''}`, field: 'lifecycle', next: 'Pass `terminal` or `resident`.' });
|
|
611
|
+
}
|
|
612
|
+
// Resolve the node: explicit --node, else the node occupying --pane (the
|
|
613
|
+
// Alt+C menu passes #{pane_id}), else the caller (CRTR_NODE_ID).
|
|
614
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
615
|
+
let id = input['node'];
|
|
616
|
+
if (id === undefined || id === '')
|
|
617
|
+
id = nodeInPane(pane);
|
|
618
|
+
if (id === undefined || id === '')
|
|
619
|
+
id = process.env['CRTR_NODE_ID'];
|
|
620
|
+
if (id === undefined || id === '')
|
|
621
|
+
throw new InputError({ error: 'no_node', message: 'no node (set CRTR_NODE_ID, pass --node, or run from the agent\'s pane)', next: 'Run from inside a node, pass --node <id>, or --pane <pane>.' });
|
|
622
|
+
if (getNode(id) === null)
|
|
623
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
624
|
+
// Rebuild the launch spec so a future revive comes back with the new
|
|
625
|
+
// lifecycle's prompt baked in (the live session is steered by the persona
|
|
626
|
+
// injector; this fixes the static prompt the daemon replays). Spine is fixed
|
|
627
|
+
// by parent-ness, so it carries through unchanged.
|
|
628
|
+
const target = getNode(id);
|
|
629
|
+
const { launch } = buildLaunchSpec(target.kind, target.mode, {
|
|
630
|
+
lifecycle: value,
|
|
631
|
+
hasManager: target.parent !== null,
|
|
632
|
+
});
|
|
633
|
+
const meta = updateNode(id, { lifecycle: value, launch });
|
|
634
|
+
// --detach: shove the still-running agent into the background crtr session,
|
|
635
|
+
// freeing the foreground pane. The pi is untouched (it keeps generating); now
|
|
636
|
+
// terminal, it pushes a final up the spine when it finishes.
|
|
637
|
+
let detached = false;
|
|
638
|
+
if (input['detach'] === true)
|
|
639
|
+
detached = detachToBackground(id, pane);
|
|
640
|
+
return { node_id: meta.node_id, lifecycle: meta.lifecycle, detached };
|
|
641
|
+
},
|
|
642
|
+
render: (r) => `<lifecycle node="${r['node_id']}" set="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
|
|
643
|
+
});
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
514
645
|
// node yield — refresh: discard context, revive fresh against the roadmap
|
|
515
646
|
// ---------------------------------------------------------------------------
|
|
516
647
|
const nodeYield = defineLeaf({
|
|
517
648
|
name: 'yield',
|
|
649
|
+
description: 'refresh your context against your roadmap',
|
|
650
|
+
whenToUse: 'your context window is filling up but the mandate is unfinished: request a refresh — end your turn and revive fresh against your roadmap, leaving a note to your future self for the moment you wake. A base node auto-promotes to orchestrator first (a yield needs a roadmap to refresh against). Use `node promote` instead when you need to BECOME an orchestrator with no refresh pending',
|
|
518
651
|
help: {
|
|
519
652
|
name: 'node yield',
|
|
520
|
-
summary: 'request a context refresh — you will be respawned fresh against your roadmap on your next stop (a
|
|
653
|
+
summary: 'request a context refresh — you will be respawned fresh against your roadmap on your next stop (a base node auto-promotes to orchestrator first)',
|
|
521
654
|
params: [
|
|
522
|
-
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'If this yield auto-promotes a
|
|
523
|
-
{ kind: 'stdin', name: 'message', required:
|
|
655
|
+
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'If this yield auto-promotes a base node, specialize it as this kind of orchestrator (developer, review, spec, design, plan, explore, general). Defaults to your current kind.' },
|
|
656
|
+
{ kind: 'stdin', name: 'message', required: true, constraint: 'A note to your future self — what to do the moment you wake fresh. Surfaced as <yield-message> in the next revive. Pass as a positional or pipe via heredoc.' },
|
|
524
657
|
],
|
|
525
658
|
output: [
|
|
526
659
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The yielding node.' },
|
|
527
|
-
{ name: 'promoted', type: 'boolean', required: true, constraint: 'True if this yield promoted a
|
|
660
|
+
{ name: 'promoted', type: 'boolean', required: true, constraint: 'True if this yield promoted a base node to orchestrator.' },
|
|
528
661
|
{ name: 'will_refresh', type: 'boolean', required: true, constraint: 'Always true: end your turn and you will revive fresh against your roadmap.' },
|
|
529
662
|
],
|
|
530
663
|
outputKind: 'object',
|
|
@@ -538,8 +671,10 @@ const nodeYield = defineLeaf({
|
|
|
538
671
|
if (kind !== undefined)
|
|
539
672
|
assertKind(kind);
|
|
540
673
|
const message = typeof input['message'] === 'string' ? input['message'].trim() : '';
|
|
541
|
-
if (message
|
|
542
|
-
|
|
674
|
+
if (message === '') {
|
|
675
|
+
throw new InputError({ error: 'empty_message', message: 'a yield message is required (stdin or positional)', next: 'Pass a note to your future self as an argument or pipe it on stdin.' });
|
|
676
|
+
}
|
|
677
|
+
writeYieldMessage(id, message);
|
|
543
678
|
const res = requestYield(id, kind !== undefined ? { kind } : {});
|
|
544
679
|
return { node_id: res.meta.node_id, promoted: res.promoted, will_refresh: res.willRefresh };
|
|
545
680
|
},
|
|
@@ -557,22 +692,9 @@ export function registerNode() {
|
|
|
557
692
|
summary: 'spawn and navigate canvas nodes',
|
|
558
693
|
model: 'Every agent is a node in one directed graph (the canvas); `subscribes_to` is the spine — spawn a child and you auto-subscribe (active) to it, so its finish wakes you.\n\n' +
|
|
559
694
|
'WHEN TO DELEGATE: hand any self-contained unit of work to a child instead of doing it inline — it keeps your own context window (your scarce resource) free for steering. Spawn independent units in PARALLEL; a wake with idle workers is wasted. Serialize only true dependencies, and never let two live children edit the same files.\n\n' +
|
|
560
|
-
'HOW: `crtr node new "<task>" --kind <kind>` returns a node id immediately and runs the worker in a background window. Match the kind to the work (see `node new -h`). You are woken when a child finishes
|
|
561
|
-
'FINISH: a worker ends its own work with `crtr push final "<result>"` (writes the canonical result, marks done, closes the window) — stopping without it is not finishing. For a job too big for one context window, `node promote` to
|
|
562
|
-
children: [
|
|
563
|
-
{ name: 'new', desc: 'spawn a terminal worker as a background window', useWhen: 'delegating a self-contained unit of work', tier: 'important' },
|
|
564
|
-
{ name: 'inspect', desc: 'read the graph (list nodes / show one)', useWhen: 'surveying the canvas or inspecting a node' },
|
|
565
|
-
{ name: 'focus', desc: 'bring a node window forefront', useWhen: 'jumping to a node to watch or steer it' },
|
|
566
|
-
{ name: 'cycle', desc: 'DFS-walk to the next/prev live node in place', useWhen: 'sweeping the canvas one window at a time (Alt+] forward / Alt+[ back)' },
|
|
567
|
-
{ name: 'demote', desc: 'finish the agent in your pane + recycle it into a fresh root', useWhen: 'wrapping up the agent in front of you and starting fresh (Alt+C → d)' },
|
|
568
|
-
{ name: 'session', desc: 'open a fresh root in its own tmux session', useWhen: 'starting a new top-level session from inside a node' },
|
|
569
|
-
{ name: 'msg', desc: 'direct-message any node at a wake tier', useWhen: 'steering or pinging a specific node (wakes it)' },
|
|
570
|
-
{ name: 'subscribe', desc: 'wire a subscribes_to edge between any pair (active or --passive)', useWhen: 'making a node (you or another) receive another node\'s pushes' },
|
|
571
|
-
{ name: 'unsubscribe', desc: 'drop a subscribes_to edge', useWhen: 'detaching a subscriber from a publisher' },
|
|
572
|
-
{ name: 'promote', desc: 'become a resident orchestrator of a chosen kind', useWhen: 'your task is bigger than one context window and you must delegate + persist', tier: 'important' },
|
|
573
|
-
{ name: 'yield', desc: 'refresh your context against your roadmap', useWhen: 'your context window is filling up' },
|
|
574
|
-
],
|
|
695
|
+
'HOW: `crtr node new "<task>" --kind <kind>` returns a node id immediately and runs the worker in a background window. Match the kind to the work (see `node new -h`). You are woken when a child finishes — the wake message ALREADY IS the coalesced digest (the watcher drains your inbox to wake you), so don\'t re-run `crtr feed read` to "open" it (it would read empty, the cursor already advanced); instead dereference the report paths in that digest that matter, don\'t act on a one-line label. (`crtr feed read` is for proactively polling before a wake, or inspecting a child\'s inbox via `--node`; `--all` re-reads history with full message bodies.) Integrate, then either delegate the next units or finish.\n\n' +
|
|
696
|
+
'FINISH: a worker ends its own work with `crtr push final "<result>"` (writes the canonical result, marks done, closes the window) — stopping without it is not finishing. For a job too big for one context window, `node promote` to an orchestrator (holds a roadmap, delegates phases); when context fills, `node yield` to refresh against that roadmap.',
|
|
575
697
|
},
|
|
576
|
-
children: [nodeNew, nodeInspect, nodeFocus, nodeCycle, nodeDemote,
|
|
698
|
+
children: [nodeNew, nodeInspect, nodeFocus, nodeCycle, nodeDemote, nodeClose, nodeMsg, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeLifecycle, nodeYield],
|
|
577
699
|
});
|
|
578
700
|
}
|
|
@@ -8,6 +8,8 @@ import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
const marketList = defineLeaf({
|
|
10
10
|
name: 'list',
|
|
11
|
+
description: 'list registered marketplaces',
|
|
12
|
+
whenToUse: 'listing which marketplaces are registered, with their git URL, ref, and scope',
|
|
11
13
|
help: {
|
|
12
14
|
name: 'pkg market inspect list',
|
|
13
15
|
summary: 'list registered marketplaces',
|
|
@@ -76,6 +78,8 @@ const marketList = defineLeaf({
|
|
|
76
78
|
// ---------------------------------------------------------------------------
|
|
77
79
|
const marketBrowse = defineLeaf({
|
|
78
80
|
name: 'browse',
|
|
81
|
+
description: 'list plugins available in a marketplace',
|
|
82
|
+
whenToUse: 'exploring what a marketplace offers so you can decide before installing — lists every plugin in a marketplace index with its description, keywords, version, and whether it is already installed. Reach for this to pick which plugin to pull, then install it by name with `pkg market manage install`',
|
|
79
83
|
help: {
|
|
80
84
|
name: 'pkg market inspect browse',
|
|
81
85
|
summary: 'list plugins available in a marketplace',
|
|
@@ -145,13 +149,11 @@ const marketBrowse = defineLeaf({
|
|
|
145
149
|
});
|
|
146
150
|
export const marketInspectBranch = defineBranch({
|
|
147
151
|
name: 'inspect',
|
|
152
|
+
description: 'list or browse marketplaces',
|
|
153
|
+
whenToUse: 'reading marketplace metadata to decide before you install — list registered marketplaces, or browse the plugins available in one marketplace. Read-only; switch to `pkg market manage` to add a marketplace or install a plugin from it',
|
|
148
154
|
help: {
|
|
149
155
|
name: 'pkg market inspect',
|
|
150
156
|
summary: 'read marketplace metadata without modifying state',
|
|
151
|
-
children: [
|
|
152
|
-
{ name: 'list', desc: 'list registered marketplaces', useWhen: 'seeing which marketplaces are configured' },
|
|
153
|
-
{ name: 'browse', desc: 'list plugins available in a marketplace', useWhen: 'exploring what a marketplace offers before installing' },
|
|
154
|
-
],
|
|
155
157
|
},
|
|
156
158
|
children: [marketList, marketBrowse],
|
|
157
159
|
});
|