@crouton-kit/crouter 0.3.14 → 0.3.15
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 +44 -66
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -98
- 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 +130 -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 +259 -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 +244 -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 +183 -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 +328 -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 +106 -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 +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -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 +27 -10
- 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 +24 -12
- package/dist/core/runtime/launch.js +75 -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 +32 -1
- package/dist/core/runtime/nodes.js +60 -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 +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -15
- package/dist/core/runtime/presence.js +90 -66
- 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.d.ts +100 -14
- package/dist/core/runtime/tmux.js +201 -28
- 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 +586 -262
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
package/dist/commands/node.js
CHANGED
|
@@ -6,16 +6,41 @@
|
|
|
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';
|
|
14
|
+
import { detachToBackground, focus as placementFocus } from '../core/runtime/placement.js';
|
|
15
|
+
import { buildLaunchSpec } from '../core/runtime/launch.js';
|
|
16
|
+
import { closeNode } from '../core/runtime/close.js';
|
|
15
17
|
import { windowAlive, windowOfPane, currentTmux } from '../core/runtime/tmux.js';
|
|
16
18
|
import { appendInbox } from '../core/feed/inbox.js';
|
|
17
19
|
import { availableKinds } from '../core/personas/index.js';
|
|
18
|
-
import { getNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, } from '../core/canvas/index.js';
|
|
20
|
+
import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, } from '../core/canvas/index.js';
|
|
21
|
+
// Past this much context, an ORCHESTRATOR that spawns a managed child is better
|
|
22
|
+
// off yielding than holding its fat window open for the child's result: the
|
|
23
|
+
// child's finish revives it fresh against its roadmap, so a clean window absorbs
|
|
24
|
+
// the result instead of this bloated one. Below the steering bands (130k+) on
|
|
25
|
+
// purpose — catch it at spawn, before the window is critically full.
|
|
26
|
+
const YIELD_NUDGE_THRESHOLD = 100_000;
|
|
27
|
+
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.";
|
|
28
|
+
/** Decision road sign for a managed (non-root) child. Normally STD_CHILD_FOLLOW_UP,
|
|
29
|
+
* but when the SPAWNER is an orchestrator whose context has already grown past
|
|
30
|
+
* YIELD_NUDGE_THRESHOLD, steer it to yield now and let its fresh revive handle
|
|
31
|
+
* the child's result. */
|
|
32
|
+
export function childFollowUp(spawnerId) {
|
|
33
|
+
if (spawnerId === undefined || spawnerId === '')
|
|
34
|
+
return STD_CHILD_FOLLOW_UP;
|
|
35
|
+
const spawner = getNode(spawnerId);
|
|
36
|
+
if (spawner === null || spawner.mode !== 'orchestrator')
|
|
37
|
+
return STD_CHILD_FOLLOW_UP;
|
|
38
|
+
const ctxTokens = readContextTokens(spawnerId);
|
|
39
|
+
if (ctxTokens === null || ctxTokens < YIELD_NUDGE_THRESHOLD)
|
|
40
|
+
return STD_CHILD_FOLLOW_UP;
|
|
41
|
+
const k = Math.round(ctxTokens / 1000);
|
|
42
|
+
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.`;
|
|
43
|
+
}
|
|
19
44
|
/** Validate a `--kind` against the installed personas; throws a listing InputError. */
|
|
20
45
|
function assertKind(kind) {
|
|
21
46
|
const kinds = availableKinds();
|
|
@@ -28,30 +53,35 @@ function assertKind(kind) {
|
|
|
28
53
|
// ---------------------------------------------------------------------------
|
|
29
54
|
const nodeNew = defineLeaf({
|
|
30
55
|
name: 'new',
|
|
56
|
+
description: 'spawn a node — a managed child (default), or an independent root with --root',
|
|
57
|
+
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',
|
|
58
|
+
tier: 'important',
|
|
31
59
|
help: {
|
|
32
60
|
name: 'node new',
|
|
33
61
|
summary: 'spawn a terminal worker onto the canvas as a background window — returns its node id',
|
|
34
62
|
params: [
|
|
35
63
|
{ kind: 'stdin', name: 'prompt', required: true, constraint: 'First user message for the spawned node. Piped on stdin or passed as a positional.' },
|
|
36
64
|
{ 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.
|
|
65
|
+
{ 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
66
|
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the node is pinned to. Defaults to the caller cwd.' },
|
|
39
67
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name (tmux window + resume picker). Defaults to the kind.' },
|
|
40
68
|
{ kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node (CRTR_NODE_ID).' },
|
|
69
|
+
{ 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).' },
|
|
70
|
+
{ 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
71
|
],
|
|
42
72
|
output: [
|
|
43
73
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The new node id.' },
|
|
44
74
|
{ name: 'name', type: 'string', required: true, constraint: 'Display name.' },
|
|
45
75
|
{ name: 'window', type: 'string', required: false, constraint: 'tmux window id of the background window.' },
|
|
46
|
-
{ name: 'session', type: 'string', required: true, constraint: 'The
|
|
76
|
+
{ 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
77
|
{ 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.' },
|
|
78
|
+
{ 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
79
|
],
|
|
50
80
|
outputKind: 'object',
|
|
51
81
|
effects: [
|
|
52
82
|
'Creates a node under ~/.crtr/nodes/<id>/ and indexes it in canvas.db.',
|
|
53
|
-
'
|
|
54
|
-
'Opens a background (non-focus-stealing)
|
|
83
|
+
'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.',
|
|
84
|
+
'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
85
|
],
|
|
56
86
|
},
|
|
57
87
|
run: async (input) => {
|
|
@@ -64,14 +94,18 @@ const nodeNew = defineLeaf({
|
|
|
64
94
|
const cwd = input['cwd'] ?? process.cwd();
|
|
65
95
|
const name = input['name'];
|
|
66
96
|
const parent = input['parent'];
|
|
67
|
-
const
|
|
97
|
+
const root = input['root'] === true;
|
|
98
|
+
const forkFrom = input['forkFrom'];
|
|
99
|
+
const res = spawnChild({ kind, mode, cwd, name, prompt, parent, root, forkFrom });
|
|
68
100
|
return {
|
|
69
101
|
node_id: res.node.node_id,
|
|
70
102
|
name: res.node.name,
|
|
71
103
|
window: res.window ?? undefined,
|
|
72
104
|
session: res.session,
|
|
73
105
|
status: res.node.status,
|
|
74
|
-
follow_up:
|
|
106
|
+
follow_up: root
|
|
107
|
+
? "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."
|
|
108
|
+
: childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
|
|
75
109
|
};
|
|
76
110
|
},
|
|
77
111
|
render: (r) => `<spawned name="${r['name']}" id="${r['node_id']}" status="${r['status']}">\n${r['follow_up']}\n</spawned>`,
|
|
@@ -81,11 +115,13 @@ const nodeNew = defineLeaf({
|
|
|
81
115
|
// ---------------------------------------------------------------------------
|
|
82
116
|
const nodeList = defineLeaf({
|
|
83
117
|
name: 'list',
|
|
118
|
+
description: 'list nodes on the canvas',
|
|
119
|
+
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
120
|
help: {
|
|
85
121
|
name: 'node inspect list',
|
|
86
122
|
summary: 'list nodes on the canvas, optionally by status',
|
|
87
123
|
params: [
|
|
88
|
-
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter: active | idle | done | dead. Comma-separated for several.' },
|
|
124
|
+
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: 'Filter: active | idle | done | dead | canceled. Comma-separated for several.' },
|
|
89
125
|
],
|
|
90
126
|
output: [
|
|
91
127
|
{ name: 'nodes', type: 'object[]', required: true, constraint: 'Rows: {node_id, name, kind, mode, lifecycle, status, cwd, parent, created}.' },
|
|
@@ -105,6 +141,8 @@ const nodeList = defineLeaf({
|
|
|
105
141
|
// ---------------------------------------------------------------------------
|
|
106
142
|
const nodeShow = defineLeaf({
|
|
107
143
|
name: 'show',
|
|
144
|
+
description: 'show a node + its spine neighbors',
|
|
145
|
+
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
146
|
help: {
|
|
109
147
|
name: 'node inspect show',
|
|
110
148
|
summary: 'show a node\'s meta plus its subscriptions (reports) and subscribers (managers)',
|
|
@@ -133,13 +171,11 @@ const nodeShow = defineLeaf({
|
|
|
133
171
|
// ---------------------------------------------------------------------------
|
|
134
172
|
const nodeInspect = defineBranch({
|
|
135
173
|
name: 'inspect',
|
|
174
|
+
description: 'read the graph (list nodes / show one)',
|
|
175
|
+
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
176
|
help: {
|
|
137
177
|
name: 'node inspect',
|
|
138
178
|
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
179
|
},
|
|
144
180
|
children: [nodeList, nodeShow],
|
|
145
181
|
});
|
|
@@ -148,11 +184,14 @@ const nodeInspect = defineBranch({
|
|
|
148
184
|
// ---------------------------------------------------------------------------
|
|
149
185
|
const nodeFocus = defineLeaf({
|
|
150
186
|
name: 'focus',
|
|
187
|
+
description: 'bring a node window forefront',
|
|
188
|
+
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
189
|
help: {
|
|
152
190
|
name: 'node focus',
|
|
153
191
|
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
192
|
params: [
|
|
155
193
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id to focus.' },
|
|
194
|
+
{ 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
195
|
],
|
|
157
196
|
output: [
|
|
158
197
|
{ name: 'focused', type: 'boolean', required: true, constraint: 'True when the node was brought into view.' },
|
|
@@ -161,25 +200,23 @@ const nodeFocus = defineLeaf({
|
|
|
161
200
|
{ 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
201
|
],
|
|
163
202
|
outputKind: 'object',
|
|
164
|
-
effects: ['Swaps the node\'s pane into the caller\'s current pane (tmux swap-pane -d) and
|
|
203
|
+
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
204
|
},
|
|
166
205
|
run: async (input) => {
|
|
167
206
|
const id = input['node'];
|
|
168
207
|
const node = getNode(id);
|
|
169
208
|
if (node === null)
|
|
170
209
|
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 };
|
|
210
|
+
// Placement owns the whole act (§2.3): resolve the caller's focus (or open a
|
|
211
|
+
// new viewport with --new-pane), revive the target into the backstage if it
|
|
212
|
+
// is dormant, then hot-swap it onto the focus. The reviver is injected so
|
|
213
|
+
// placement need not import revive.ts.
|
|
214
|
+
const res = placementFocus(id, {
|
|
215
|
+
newPane: input['newPane'] === true,
|
|
216
|
+
callerNode: process.env['CRTR_NODE_ID'],
|
|
217
|
+
revive: (nid) => { reviveNode(nid, { resume: true }); },
|
|
218
|
+
});
|
|
219
|
+
return { focused: res.focused, session: res.session, revived: res.revived, in_place: res.inPlace };
|
|
183
220
|
},
|
|
184
221
|
});
|
|
185
222
|
// ---------------------------------------------------------------------------
|
|
@@ -197,14 +234,17 @@ function nodeByWindow(win) {
|
|
|
197
234
|
/** The live node occupying a tmux pane (pane → window → node), or undefined.
|
|
198
235
|
* Defaults to $TMUX_PANE / the caller's current pane when `pane` is omitted —
|
|
199
236
|
* shared by `node demote` and `node cycle`, both of which act on "the agent in
|
|
200
|
-
* front of you".
|
|
201
|
-
|
|
237
|
+
* front of you". Exported for the `canvas chord` / `canvas tmux-spread` leaves,
|
|
238
|
+
* which resolve the active pane's node the same way. */
|
|
239
|
+
export function nodeInPane(pane) {
|
|
202
240
|
const resolvePane = pane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
203
241
|
const win = resolvePane !== undefined && resolvePane !== '' ? windowOfPane(resolvePane) : null;
|
|
204
242
|
return win !== null ? nodeByWindow(win) : undefined;
|
|
205
243
|
}
|
|
206
244
|
const nodeDemote = defineLeaf({
|
|
207
245
|
name: 'demote',
|
|
246
|
+
description: 'finish the agent in your pane + recycle it into a fresh root',
|
|
247
|
+
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
248
|
help: {
|
|
209
249
|
name: 'node demote',
|
|
210
250
|
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 +283,52 @@ const nodeDemote = defineLeaf({
|
|
|
243
283
|
: `<demote-failed id="${r['node_id'] ?? ''}">not in tmux, or no agent in this pane</demote-failed>`,
|
|
244
284
|
});
|
|
245
285
|
// ---------------------------------------------------------------------------
|
|
286
|
+
// node close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
const nodeClose = defineLeaf({
|
|
289
|
+
name: 'close',
|
|
290
|
+
description: 'close a node + cascade-cancel its exclusive subtree (revivable)',
|
|
291
|
+
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)',
|
|
292
|
+
help: {
|
|
293
|
+
name: 'node close',
|
|
294
|
+
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`)',
|
|
295
|
+
params: [
|
|
296
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to close. Defaults to the node occupying --pane (or your current pane).' },
|
|
297
|
+
{ 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.' },
|
|
298
|
+
],
|
|
299
|
+
output: [
|
|
300
|
+
{ name: 'closed', type: 'boolean', required: true, constraint: 'True when the node (and its exclusive subtree) was closed.' },
|
|
301
|
+
{ name: 'node_id', type: 'string', required: false, constraint: 'The node that was closed — the cascade root.' },
|
|
302
|
+
{ name: 'count', type: 'number', required: false, constraint: 'How many nodes were closed (root + cascaded descendants).' },
|
|
303
|
+
{ name: 'closed_ids', type: 'string[]', required: false, constraint: 'All closed node ids, kill order (leaves first, root last).' },
|
|
304
|
+
{ name: 'spared', type: 'string[]', required: false, constraint: 'Descendants left alive because a manager outside the subtree still subscribes to them.' },
|
|
305
|
+
],
|
|
306
|
+
outputKind: 'object',
|
|
307
|
+
effects: [
|
|
308
|
+
'Marks the node and its exclusive descendants `canceled` and clears intent (the daemon never revives a canceled node).',
|
|
309
|
+
'Kills each closed node\'s tmux window; their pi sessions and canvas edges persist for a later revive.',
|
|
310
|
+
'Appends a cancellation notice to each closed node\'s inbox, surfaced on its next resume.',
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
run: async (input) => {
|
|
314
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
315
|
+
let id = input['node'];
|
|
316
|
+
if (id === undefined || id === '')
|
|
317
|
+
id = nodeInPane(pane);
|
|
318
|
+
if (id === undefined || id === '') {
|
|
319
|
+
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.' });
|
|
320
|
+
}
|
|
321
|
+
if (getNode(id) === null) {
|
|
322
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
323
|
+
}
|
|
324
|
+
const res = closeNode(id);
|
|
325
|
+
return { closed: true, node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
|
|
326
|
+
},
|
|
327
|
+
render: (r) => r['closed'] === true
|
|
328
|
+
? `<closed id="${r['node_id']}" count="${r['count']}" spared="${r['spared']?.length ?? 0}"/>`
|
|
329
|
+
: `<close-failed/>`,
|
|
330
|
+
});
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
246
332
|
// node cycle — DFS-walk the canvas one window at a time (Alt+] / Alt+[)
|
|
247
333
|
// ---------------------------------------------------------------------------
|
|
248
334
|
/** Every live node in DFS pre-order across the whole forest. The spawn tree is
|
|
@@ -282,6 +368,8 @@ function liveDfsOrder() {
|
|
|
282
368
|
}
|
|
283
369
|
const nodeCycle = defineLeaf({
|
|
284
370
|
name: 'cycle',
|
|
371
|
+
description: 'DFS-walk to the next/prev live node in place',
|
|
372
|
+
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
373
|
help: {
|
|
286
374
|
name: 'node cycle',
|
|
287
375
|
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 +401,14 @@ const nodeCycle = defineLeaf({
|
|
|
313
401
|
const target = getNode(targetId);
|
|
314
402
|
if (target === null)
|
|
315
403
|
return { focused: false, from: fromId };
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const res = focusNodeInPlace(targetId, pane, fromId);
|
|
404
|
+
// Placement retargets the caller pane's focus to the neighbor (§2.3),
|
|
405
|
+
// reviving it into the backstage first if its pane was released. callerNode
|
|
406
|
+
// is the node we cycled AWAY from — the current occupant of the caller pane.
|
|
407
|
+
const res = placementFocus(targetId, {
|
|
408
|
+
pane,
|
|
409
|
+
callerNode: fromId,
|
|
410
|
+
revive: (nid) => { reviveNode(nid, { resume: true }); },
|
|
411
|
+
});
|
|
325
412
|
return { focused: res.focused, node_id: targetId, name: target.name, from: fromId };
|
|
326
413
|
},
|
|
327
414
|
render: (r) => r['focused'] === true
|
|
@@ -329,41 +416,12 @@ const nodeCycle = defineLeaf({
|
|
|
329
416
|
: `<cycle-noop>no other live node to focus</cycle-noop>`,
|
|
330
417
|
});
|
|
331
418
|
// ---------------------------------------------------------------------------
|
|
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
419
|
// node msg — direct-address any node at a wake tier (wakes a dormant target)
|
|
364
420
|
// ---------------------------------------------------------------------------
|
|
365
421
|
const nodeMsg = defineLeaf({
|
|
366
422
|
name: 'msg',
|
|
423
|
+
description: 'direct-message any node at a wake tier',
|
|
424
|
+
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
425
|
help: {
|
|
368
426
|
name: 'node msg',
|
|
369
427
|
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 +475,8 @@ function resolveSubscriber(input) {
|
|
|
417
475
|
}
|
|
418
476
|
const nodeSubscribe = defineLeaf({
|
|
419
477
|
name: 'subscribe',
|
|
478
|
+
description: 'wire a subscribes_to edge between any pair (active or --passive)',
|
|
479
|
+
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
480
|
help: {
|
|
421
481
|
name: 'node subscribe',
|
|
422
482
|
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 +512,8 @@ const nodeSubscribe = defineLeaf({
|
|
|
452
512
|
});
|
|
453
513
|
const nodeUnsubscribe = defineLeaf({
|
|
454
514
|
name: 'unsubscribe',
|
|
515
|
+
description: 'drop a subscribes_to edge',
|
|
516
|
+
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
517
|
help: {
|
|
456
518
|
name: 'node unsubscribe',
|
|
457
519
|
summary: 'drop a subscribes_to edge — the subscriber (you by default, or any node via --subscriber) stops receiving the publisher\'s pushes.',
|
|
@@ -476,28 +538,35 @@ const nodeUnsubscribe = defineLeaf({
|
|
|
476
538
|
render: (r) => `<unsubscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}"/>`,
|
|
477
539
|
});
|
|
478
540
|
// ---------------------------------------------------------------------------
|
|
479
|
-
// node promote — become
|
|
541
|
+
// node promote — become an orchestrator (the worker→orchestrator polymorph)
|
|
480
542
|
// ---------------------------------------------------------------------------
|
|
481
543
|
const nodePromote = defineLeaf({
|
|
482
544
|
name: 'promote',
|
|
545
|
+
description: 'become an orchestrator of a chosen kind',
|
|
546
|
+
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',
|
|
547
|
+
tier: 'important',
|
|
483
548
|
help: {
|
|
484
549
|
name: 'node promote',
|
|
485
|
-
summary: 'promote yourself to
|
|
550
|
+
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
551
|
params: [
|
|
487
552
|
{ 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.' },
|
|
553
|
+
{ 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
554
|
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to promote. Defaults to the caller (CRTR_NODE_ID).' },
|
|
489
555
|
],
|
|
490
556
|
output: [
|
|
491
557
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The promoted node.' },
|
|
492
558
|
{ name: 'kind', type: 'string', required: true, constraint: 'The kind it now orchestrates as.' },
|
|
493
559
|
{ name: 'mode', type: 'string', required: true, constraint: 'Now "orchestrator".' },
|
|
560
|
+
{ name: 'lifecycle', type: 'string', required: true, constraint: '"resident" if you passed --resident, else unchanged (typically "terminal").' },
|
|
494
561
|
{ name: 'roadmap_written', type: 'boolean', required: true, constraint: 'True if a roadmap scaffold was seeded by this call.' },
|
|
495
562
|
{ name: 'roadmap_path', type: 'string', required: true, constraint: 'Absolute path to your roadmap doc (context/roadmap.md) — edit it to author your plan.' },
|
|
496
563
|
{ 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: '
|
|
564
|
+
{ 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.' },
|
|
565
|
+
{ 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.' },
|
|
566
|
+
{ 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
567
|
],
|
|
499
568
|
outputKind: 'object',
|
|
500
|
-
effects: ['Flips
|
|
569
|
+
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
570
|
},
|
|
502
571
|
run: async (input) => {
|
|
503
572
|
const id = input['node'] ?? process.env['CRTR_NODE_ID'];
|
|
@@ -506,25 +575,90 @@ const nodePromote = defineLeaf({
|
|
|
506
575
|
const kind = input['kind'];
|
|
507
576
|
if (kind !== undefined)
|
|
508
577
|
assertKind(kind);
|
|
509
|
-
const
|
|
510
|
-
|
|
578
|
+
const resident = input['resident'] === true;
|
|
579
|
+
const res = promote(id, { ...(kind !== undefined ? { kind } : {}), ...(resident ? { resident: true } : {}) });
|
|
580
|
+
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
581
|
},
|
|
512
582
|
});
|
|
513
583
|
// ---------------------------------------------------------------------------
|
|
584
|
+
// node lifecycle — flip the lifecycle axis (terminal ↔ resident), independent
|
|
585
|
+
// of mode. The persona injector delivers the transition guidance.
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
const nodeLifecycle = defineLeaf({
|
|
588
|
+
name: 'lifecycle',
|
|
589
|
+
description: 'switch a node between terminal and resident',
|
|
590
|
+
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)',
|
|
591
|
+
help: {
|
|
592
|
+
name: 'node lifecycle',
|
|
593
|
+
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',
|
|
594
|
+
params: [
|
|
595
|
+
{ kind: 'positional', name: 'lifecycle', required: true, constraint: 'terminal | resident.' },
|
|
596
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to change. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
597
|
+
{ 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.' },
|
|
598
|
+
{ 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).' },
|
|
599
|
+
],
|
|
600
|
+
output: [
|
|
601
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The node.' },
|
|
602
|
+
{ name: 'lifecycle', type: 'string', required: true, constraint: 'Its new lifecycle (terminal | resident).' },
|
|
603
|
+
{ name: 'detached', type: 'boolean', required: false, constraint: 'True when --detach relocated the agent to the background crtr session.' },
|
|
604
|
+
],
|
|
605
|
+
outputKind: 'object',
|
|
606
|
+
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.'],
|
|
607
|
+
},
|
|
608
|
+
run: async (input) => {
|
|
609
|
+
const value = input['lifecycle']?.trim().toLowerCase();
|
|
610
|
+
if (value !== 'terminal' && value !== 'resident') {
|
|
611
|
+
throw new InputError({ error: 'bad_lifecycle', message: `invalid lifecycle: ${value ?? ''}`, field: 'lifecycle', next: 'Pass `terminal` or `resident`.' });
|
|
612
|
+
}
|
|
613
|
+
// Resolve the node: explicit --node, else the node occupying --pane (the
|
|
614
|
+
// Alt+C menu passes #{pane_id}), else the caller (CRTR_NODE_ID).
|
|
615
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
616
|
+
let id = input['node'];
|
|
617
|
+
if (id === undefined || id === '')
|
|
618
|
+
id = nodeInPane(pane);
|
|
619
|
+
if (id === undefined || id === '')
|
|
620
|
+
id = process.env['CRTR_NODE_ID'];
|
|
621
|
+
if (id === undefined || id === '')
|
|
622
|
+
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>.' });
|
|
623
|
+
if (getNode(id) === null)
|
|
624
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
625
|
+
// Rebuild the launch spec so a future revive comes back with the new
|
|
626
|
+
// lifecycle's prompt baked in (the live session is steered by the persona
|
|
627
|
+
// injector; this fixes the static prompt the daemon replays). Spine is fixed
|
|
628
|
+
// by parent-ness, so it carries through unchanged.
|
|
629
|
+
const target = getNode(id);
|
|
630
|
+
const { launch } = buildLaunchSpec(target.kind, target.mode, {
|
|
631
|
+
lifecycle: value,
|
|
632
|
+
hasManager: target.parent !== null,
|
|
633
|
+
});
|
|
634
|
+
const meta = updateNode(id, { lifecycle: value, launch });
|
|
635
|
+
// --detach: shove the still-running agent into the background crtr session,
|
|
636
|
+
// freeing the foreground pane. The pi is untouched (it keeps generating); now
|
|
637
|
+
// terminal, it pushes a final up the spine when it finishes.
|
|
638
|
+
let detached = false;
|
|
639
|
+
if (input['detach'] === true)
|
|
640
|
+
detached = detachToBackground(id, pane);
|
|
641
|
+
return { node_id: meta.node_id, lifecycle: meta.lifecycle, detached };
|
|
642
|
+
},
|
|
643
|
+
render: (r) => `<lifecycle node="${r['node_id']}" set="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
|
|
644
|
+
});
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
514
646
|
// node yield — refresh: discard context, revive fresh against the roadmap
|
|
515
647
|
// ---------------------------------------------------------------------------
|
|
516
648
|
const nodeYield = defineLeaf({
|
|
517
649
|
name: 'yield',
|
|
650
|
+
description: 'refresh your context against your roadmap',
|
|
651
|
+
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
652
|
help: {
|
|
519
653
|
name: 'node yield',
|
|
520
|
-
summary: 'request a context refresh — you will be respawned fresh against your roadmap on your next stop (a
|
|
654
|
+
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
655
|
params: [
|
|
522
|
-
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'If this yield auto-promotes a
|
|
523
|
-
{ kind: 'stdin', name: 'message', required:
|
|
656
|
+
{ 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.' },
|
|
657
|
+
{ 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
658
|
],
|
|
525
659
|
output: [
|
|
526
660
|
{ 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
|
|
661
|
+
{ name: 'promoted', type: 'boolean', required: true, constraint: 'True if this yield promoted a base node to orchestrator.' },
|
|
528
662
|
{ name: 'will_refresh', type: 'boolean', required: true, constraint: 'Always true: end your turn and you will revive fresh against your roadmap.' },
|
|
529
663
|
],
|
|
530
664
|
outputKind: 'object',
|
|
@@ -538,8 +672,10 @@ const nodeYield = defineLeaf({
|
|
|
538
672
|
if (kind !== undefined)
|
|
539
673
|
assertKind(kind);
|
|
540
674
|
const message = typeof input['message'] === 'string' ? input['message'].trim() : '';
|
|
541
|
-
if (message
|
|
542
|
-
|
|
675
|
+
if (message === '') {
|
|
676
|
+
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.' });
|
|
677
|
+
}
|
|
678
|
+
writeYieldMessage(id, message);
|
|
543
679
|
const res = requestYield(id, kind !== undefined ? { kind } : {});
|
|
544
680
|
return { node_id: res.meta.node_id, promoted: res.promoted, will_refresh: res.willRefresh };
|
|
545
681
|
},
|
|
@@ -557,22 +693,9 @@ export function registerNode() {
|
|
|
557
693
|
summary: 'spawn and navigate canvas nodes',
|
|
558
694
|
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
695
|
'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
|
-
],
|
|
696
|
+
'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' +
|
|
697
|
+
'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
698
|
},
|
|
576
|
-
children: [nodeNew, nodeInspect, nodeFocus, nodeCycle, nodeDemote,
|
|
699
|
+
children: [nodeNew, nodeInspect, nodeFocus, nodeCycle, nodeDemote, nodeClose, nodeMsg, nodeSubscribe, nodeUnsubscribe, nodePromote, nodeLifecycle, nodeYield],
|
|
577
700
|
});
|
|
578
701
|
}
|
|
@@ -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
|
});
|