@crouton-kit/crouter 0.3.8 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +25 -27
- package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +15 -427
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +8 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +12 -681
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +9 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +63 -2
- package/dist/core/command.js +97 -24
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +69 -35
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +160 -2
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -80
- package/dist/core/spawn.js +15 -219
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +6 -5
- package/dist/commands/agent.js +0 -384
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -179
- package/dist/commands/job.js +0 -344
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -309
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -286
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -66
- package/dist/core/jobs.d.ts +0 -101
- package/dist/core/jobs.js +0 -462
- package/dist/prompts/agent.d.ts +0 -18
- package/dist/prompts/agent.js +0 -153
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Runtime node operations — the behavior layer above the canvas store.
|
|
2
|
+
//
|
|
3
|
+
// canvas/ is the pure data-access layer (nodes + edges). This is where the
|
|
4
|
+
// design's *rules* live: how a node comes into being, the env contract its pi
|
|
5
|
+
// process inherits, and the spawn-time wiring of the subscription spine.
|
|
6
|
+
//
|
|
7
|
+
// Two ways a node is born:
|
|
8
|
+
// • root — a user-opened entry point (bare `crtr`).
|
|
9
|
+
// No parent; resident by default (it's a conversation you live in).
|
|
10
|
+
// • child — spawned by another node. Terminal until it must persist. On
|
|
11
|
+
// spawn the PARENT auto-subscribes (active) to the child, so it
|
|
12
|
+
// learns when the work finishes — this seeds the subscription
|
|
13
|
+
// graph to mirror the spawn structure. A `spawned_by` audit edge
|
|
14
|
+
// is also recorded.
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
|
|
17
|
+
/** Generate a node id in the same shape as job ids (time-sortable + random). */
|
|
18
|
+
export function newNodeId() {
|
|
19
|
+
return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
20
|
+
}
|
|
21
|
+
/** Read the current node's identity from the environment. A spawned pi process
|
|
22
|
+
* runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
|
|
23
|
+
* it by reading CRTR_NODE_ID as the parent. */
|
|
24
|
+
export function currentNodeContext() {
|
|
25
|
+
const env = process.env;
|
|
26
|
+
return {
|
|
27
|
+
nodeId: env['CRTR_NODE_ID'] ?? null,
|
|
28
|
+
parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
|
|
29
|
+
kind: env['CRTR_KIND'] ?? null,
|
|
30
|
+
mode: env['CRTR_MODE'] ?? null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The env injected into a node's pi process. Self-gating extensions read
|
|
34
|
+
* CRTR_KIND/CRTR_MODE to flip behavior on polymorph without a respawn; the
|
|
35
|
+
* feed/inbox machinery reads CRTR_NODE_ID. */
|
|
36
|
+
export function nodeEnv(meta) {
|
|
37
|
+
const env = {
|
|
38
|
+
CRTR_NODE_ID: meta.node_id,
|
|
39
|
+
CRTR_KIND: meta.kind,
|
|
40
|
+
CRTR_MODE: meta.mode,
|
|
41
|
+
CRTR_LIFECYCLE: meta.lifecycle,
|
|
42
|
+
CRTR_NODE_CWD: meta.cwd,
|
|
43
|
+
};
|
|
44
|
+
if (meta.parent)
|
|
45
|
+
env['CRTR_PARENT_NODE_ID'] = meta.parent;
|
|
46
|
+
// Propagate an explicit canvas home so children share the same canvas.
|
|
47
|
+
const home = process.env['CRTR_HOME'];
|
|
48
|
+
if (home !== undefined && home !== '')
|
|
49
|
+
env['CRTR_HOME'] = home;
|
|
50
|
+
// Propagate the root's tmux session so every descendant spawns its windows
|
|
51
|
+
// into the same root session.
|
|
52
|
+
const rootSession = process.env['CRTR_ROOT_SESSION'];
|
|
53
|
+
if (rootSession !== undefined && rootSession !== '')
|
|
54
|
+
env['CRTR_ROOT_SESSION'] = rootSession;
|
|
55
|
+
// Merge any launch-spec env last (it may override / extend).
|
|
56
|
+
return { ...env, ...(meta.launch?.env ?? {}) };
|
|
57
|
+
}
|
|
58
|
+
/** Create a node on the canvas and wire its spawn-time edges.
|
|
59
|
+
*
|
|
60
|
+
* For a child (parent given): the parent auto-subscribes ACTIVE to the child
|
|
61
|
+
* (so it's woken when the child finishes), and a spawned_by audit edge is
|
|
62
|
+
* recorded. For a root (no parent): no edges, resident by default. */
|
|
63
|
+
export function spawnNode(opts) {
|
|
64
|
+
const parent = opts.parent ?? null;
|
|
65
|
+
const isRoot = parent === null;
|
|
66
|
+
const meta = {
|
|
67
|
+
node_id: opts.nodeId ?? newNodeId(),
|
|
68
|
+
name: opts.name ?? opts.kind,
|
|
69
|
+
created: new Date().toISOString(),
|
|
70
|
+
cwd: opts.cwd,
|
|
71
|
+
kind: opts.kind,
|
|
72
|
+
mode: opts.mode ?? 'base',
|
|
73
|
+
// A user-opened root is resident (a conversation you live in); a spawned
|
|
74
|
+
// node is terminal until it must persist (promotion handles that later).
|
|
75
|
+
lifecycle: opts.lifecycle ?? (isRoot ? 'resident' : 'terminal'),
|
|
76
|
+
status: 'active',
|
|
77
|
+
parent,
|
|
78
|
+
passive_default: opts.passiveDefault ?? false,
|
|
79
|
+
intent: null,
|
|
80
|
+
pi_session_id: null,
|
|
81
|
+
launch: opts.launch,
|
|
82
|
+
};
|
|
83
|
+
createNode(meta);
|
|
84
|
+
if (parent !== null) {
|
|
85
|
+
if (getNode(parent) === null) {
|
|
86
|
+
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
87
|
+
}
|
|
88
|
+
// The load-bearing seed: parent subscribes (active) to child so it learns
|
|
89
|
+
// when the work finishes. This mirrors spawn structure into the spine.
|
|
90
|
+
subscribe(parent, meta.node_id, true);
|
|
91
|
+
// Audit-only provenance.
|
|
92
|
+
recordSpawn(meta.node_id, parent);
|
|
93
|
+
}
|
|
94
|
+
return meta;
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NodeMeta } from '../canvas/index.js';
|
|
2
|
+
/** Persist `nodeId` as the currently focused node. Best-effort; never throws. */
|
|
3
|
+
export declare function setFocus(nodeId: string): void;
|
|
4
|
+
/** Read the currently focused node id, or null if the pointer is absent or
|
|
5
|
+
* empty (no active focus). Best-effort; never throws. */
|
|
6
|
+
export declare function getFocus(): string | null;
|
|
7
|
+
/** True when the node's tmux window is alive. A falsy tmux_session/window
|
|
8
|
+
* always returns false so callers don't need to null-guard. */
|
|
9
|
+
export declare function nodeLive(meta: NodeMeta): boolean;
|
|
10
|
+
/** Bring a node's tmux window to the foreground and record it as focused.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* - If the node has no live window (`nodeLive` is false), still write the
|
|
14
|
+
* focus pointer — the caller (e.g. revive logic) uses `focused:false` to
|
|
15
|
+
* know it needs to open a window first.
|
|
16
|
+
* - Otherwise call `switchClient` (lands us in the right session) then
|
|
17
|
+
* `selectWindow` (picks the right window within it). Both calls are
|
|
18
|
+
* best-effort; the focus pointer is always written regardless.
|
|
19
|
+
*
|
|
20
|
+
* Returns:
|
|
21
|
+
* focused — whether the tmux focus actually succeeded.
|
|
22
|
+
* session — the tmux session name if one was attempted, null otherwise. */
|
|
23
|
+
export declare function focusNode(nodeId: string): {
|
|
24
|
+
focused: boolean;
|
|
25
|
+
session: string | null;
|
|
26
|
+
};
|
|
27
|
+
/** Focus a node IN PLACE: bring its pane into the caller's current pane slot
|
|
28
|
+
* (swap-pane) instead of navigating the client to the node's own window. This
|
|
29
|
+
* is the default for `crtr node focus` and the nav-chrome spine jump — the
|
|
30
|
+
* agent appears where you are.
|
|
31
|
+
*
|
|
32
|
+
* Falls back to window focus when there is no caller pane (not inside tmux) or
|
|
33
|
+
* the target pane can't be resolved. `inPlace` reports which path ran. */
|
|
34
|
+
export declare function focusNodeInPlace(nodeId: string, callerPane?: string): {
|
|
35
|
+
focused: boolean;
|
|
36
|
+
session: string | null;
|
|
37
|
+
inPlace: boolean;
|
|
38
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// presence.ts — focus pointer + per-node liveness helpers.
|
|
2
|
+
//
|
|
3
|
+
// The focus pointer (`<crtrHome>/focus.ptr`) is a plain-text file holding the
|
|
4
|
+
// node id that currently "has focus" — meaning the user's terminal is showing
|
|
5
|
+
// that node's tmux window. It is written on every explicit `focusNode()` call
|
|
6
|
+
// and read by the dashboard / status-line to highlight the active node.
|
|
7
|
+
//
|
|
8
|
+
// This is intentionally a simple file-based pointer rather than a database
|
|
9
|
+
// column: focus is transient UI state, not durable business data. A crash
|
|
10
|
+
// leaves a stale pointer that the next focusNode() clobbers — harmless.
|
|
11
|
+
//
|
|
12
|
+
// focusNode() does two things:
|
|
13
|
+
// 1. Ensures the user's terminal lands on the right tmux window by calling
|
|
14
|
+
// switchClient (cross-session) then selectWindow (in-session). Both are
|
|
15
|
+
// best-effort; we set the pointer regardless so the dashboard stays in sync.
|
|
16
|
+
// 2. Persists the node id to focus.ptr so any process can quickly read "what
|
|
17
|
+
// is the user looking at?".
|
|
18
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { crtrHome, getNode, updateNode } from '../canvas/index.js';
|
|
22
|
+
import { selectWindow, switchClient, windowAlive, currentTmux, paneOfWindow, swapPaneInPlace, windowOfPane } from './tmux.js';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Focus pointer
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Absolute path to the focus pointer file. */
|
|
27
|
+
function focusPtrPath() {
|
|
28
|
+
return join(crtrHome(), 'focus.ptr');
|
|
29
|
+
}
|
|
30
|
+
/** Persist `nodeId` as the currently focused node. Best-effort; never throws. */
|
|
31
|
+
export function setFocus(nodeId) {
|
|
32
|
+
try {
|
|
33
|
+
const p = focusPtrPath();
|
|
34
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
35
|
+
writeFileSync(p, nodeId, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* focus pointer is best-effort; never surface */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Read the currently focused node id, or null if the pointer is absent or
|
|
42
|
+
* empty (no active focus). Best-effort; never throws. */
|
|
43
|
+
export function getFocus() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(focusPtrPath(), 'utf8').trim();
|
|
46
|
+
return raw !== '' ? raw : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Liveness
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/** True when the node's tmux window is alive. A falsy tmux_session/window
|
|
56
|
+
* always returns false so callers don't need to null-guard. */
|
|
57
|
+
export function nodeLive(meta) {
|
|
58
|
+
return windowAlive(meta.tmux_session, meta.window);
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Focus
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/** Bring a node's tmux window to the foreground and record it as focused.
|
|
64
|
+
*
|
|
65
|
+
* Strategy:
|
|
66
|
+
* - If the node has no live window (`nodeLive` is false), still write the
|
|
67
|
+
* focus pointer — the caller (e.g. revive logic) uses `focused:false` to
|
|
68
|
+
* know it needs to open a window first.
|
|
69
|
+
* - Otherwise call `switchClient` (lands us in the right session) then
|
|
70
|
+
* `selectWindow` (picks the right window within it). Both calls are
|
|
71
|
+
* best-effort; the focus pointer is always written regardless.
|
|
72
|
+
*
|
|
73
|
+
* Returns:
|
|
74
|
+
* focused — whether the tmux focus actually succeeded.
|
|
75
|
+
* session — the tmux session name if one was attempted, null otherwise. */
|
|
76
|
+
export function focusNode(nodeId) {
|
|
77
|
+
const meta = getNode(nodeId);
|
|
78
|
+
// Always write the pointer so the dashboard reflects intent even when focus
|
|
79
|
+
// fails (e.g. we're not currently inside tmux).
|
|
80
|
+
setFocus(nodeId);
|
|
81
|
+
if (meta === null || !nodeLive(meta)) {
|
|
82
|
+
// Node not found or window is gone — caller may need to revive.
|
|
83
|
+
return { focused: false, session: meta?.tmux_session ?? null };
|
|
84
|
+
}
|
|
85
|
+
// Both fields are non-null thanks to nodeLive() returning true.
|
|
86
|
+
const session = meta.tmux_session;
|
|
87
|
+
const window = meta.window;
|
|
88
|
+
// Cross-session hop first, then window selection within the session.
|
|
89
|
+
// switchClient may be a no-op when already in the same session but is
|
|
90
|
+
// always safe to call — tmux handles it gracefully.
|
|
91
|
+
const clientOk = switchClient(session);
|
|
92
|
+
const windowOk = selectWindow(session, window);
|
|
93
|
+
return { focused: clientOk && windowOk, session };
|
|
94
|
+
}
|
|
95
|
+
/** Focus a node IN PLACE: bring its pane into the caller's current pane slot
|
|
96
|
+
* (swap-pane) instead of navigating the client to the node's own window. This
|
|
97
|
+
* is the default for `crtr node focus` and the nav-chrome spine jump — the
|
|
98
|
+
* agent appears where you are.
|
|
99
|
+
*
|
|
100
|
+
* Falls back to window focus when there is no caller pane (not inside tmux) or
|
|
101
|
+
* the target pane can't be resolved. `inPlace` reports which path ran. */
|
|
102
|
+
export function focusNodeInPlace(nodeId, callerPane) {
|
|
103
|
+
const meta = getNode(nodeId);
|
|
104
|
+
// Always write the pointer so the dashboard reflects intent even on failure.
|
|
105
|
+
setFocus(nodeId);
|
|
106
|
+
if (meta === null || !nodeLive(meta)) {
|
|
107
|
+
return { focused: false, session: meta?.tmux_session ?? null, inPlace: false };
|
|
108
|
+
}
|
|
109
|
+
const session = meta.tmux_session;
|
|
110
|
+
const window = meta.window;
|
|
111
|
+
const pane = callerPane ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
112
|
+
// No caller pane (not in tmux) — best we can do is bring the window forefront.
|
|
113
|
+
if (pane === undefined || pane === '') {
|
|
114
|
+
const ok = switchClient(session) && selectWindow(session, window);
|
|
115
|
+
return { focused: ok, session, inPlace: false };
|
|
116
|
+
}
|
|
117
|
+
const targetPane = paneOfWindow(session, window);
|
|
118
|
+
if (targetPane === null) {
|
|
119
|
+
const ok = switchClient(session) && selectWindow(session, window);
|
|
120
|
+
return { focused: ok, session, inPlace: false };
|
|
121
|
+
}
|
|
122
|
+
if (targetPane === pane)
|
|
123
|
+
return { focused: true, session, inPlace: true }; // already here
|
|
124
|
+
// The window the caller's pane currently sits in — the slot the target's pane
|
|
125
|
+
// is about to be swapped INTO.
|
|
126
|
+
const callerWindow = windowOfPane(pane);
|
|
127
|
+
const ok = swapPaneInPlace(targetPane, pane);
|
|
128
|
+
// Keep the canvas window mapping in sync with the physical swap. swap-pane
|
|
129
|
+
// exchanges the two PANES between their windows (pane ids are stable, windows
|
|
130
|
+
// are slots): after the swap the target's pane occupies the caller's window
|
|
131
|
+
// and the caller's pane occupies the target's old window. Without this update
|
|
132
|
+
// meta.window goes stale, and a later paneOfWindow(session, meta.window)
|
|
133
|
+
// resolves the WRONG pane — the bug that made focusing back to a manager a
|
|
134
|
+
// no-op (it kept resolving the pane already in view) and made a focused node's
|
|
135
|
+
// exit collapse the visible window instead of its background one.
|
|
136
|
+
if (ok && callerWindow !== null && callerWindow !== window) {
|
|
137
|
+
try {
|
|
138
|
+
updateNode(nodeId, { window: callerWindow });
|
|
139
|
+
}
|
|
140
|
+
catch { /* best-effort */ }
|
|
141
|
+
// The caller is the node running this focus (its pi process owns callerPane).
|
|
142
|
+
// Its pane moved to the target's old window, so re-point its window there.
|
|
143
|
+
const callerNodeId = process.env['CRTR_NODE_ID'];
|
|
144
|
+
if (callerNodeId !== undefined && callerNodeId.trim() !== '' && callerNodeId !== nodeId) {
|
|
145
|
+
try {
|
|
146
|
+
updateNode(callerNodeId, { window });
|
|
147
|
+
}
|
|
148
|
+
catch { /* best-effort */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { focused: ok, session, inPlace: true };
|
|
152
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type NodeMeta } from '../canvas/index.js';
|
|
2
|
+
export interface PromoteResult {
|
|
3
|
+
meta: NodeMeta;
|
|
4
|
+
/** Orchestration guidance to surface into the node's current context now. */
|
|
5
|
+
guidance: string;
|
|
6
|
+
roadmapWritten: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Promote a node to resident orchestrator, optionally specializing its kind
|
|
9
|
+
* (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
|
|
10
|
+
* re-promoting just rewrites the spec + returns fresh guidance. Seeds a
|
|
11
|
+
* roadmap SCAFFOLD if absent (a boss with no map is a failure mode) — no goal
|
|
12
|
+
* is forced here; authoring the goal + roadmap is the node's next act. */
|
|
13
|
+
export declare function promote(nodeId: string, opts?: {
|
|
14
|
+
kind?: string;
|
|
15
|
+
}): PromoteResult;
|
|
16
|
+
export interface YieldResult {
|
|
17
|
+
meta: NodeMeta;
|
|
18
|
+
promoted: boolean;
|
|
19
|
+
/** Always true on success — the node will refresh-revive on its next stop. */
|
|
20
|
+
willRefresh: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Request a refresh-yield: discard in-memory context and revive fresh against
|
|
23
|
+
* the roadmap. A *terminal* node that yields is choosing to persist — it
|
|
24
|
+
* promotes first (refresh-with-open-work is the canonical promotion trigger),
|
|
25
|
+
* so it comes back as an orchestrator, optionally specializing its kind. Sets
|
|
26
|
+
* intent='refresh'; the stophook shuts the process down on the next stop and
|
|
27
|
+
* the daemon revives it fresh. */
|
|
28
|
+
export declare function requestYield(nodeId: string, opts?: {
|
|
29
|
+
kind?: string;
|
|
30
|
+
}): YieldResult;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Promotion — terminal → resident, and the worker→orchestrator polymorph.
|
|
2
|
+
//
|
|
3
|
+
// Two stages (the pi-mode-switch pattern):
|
|
4
|
+
// 1. Promotion → guidance dump (mid-turn, ephemeral). This call flips the
|
|
5
|
+
// node's mode/lifecycle and (optionally) its KIND, REWRITES its launch
|
|
6
|
+
// spec to that kind's orchestrator persona (so the next revive comes back
|
|
7
|
+
// as that orchestrator), seeds a roadmap scaffold, and RETURNS kind-
|
|
8
|
+
// specific orchestration + roadmap-shaping guidance — which enters the
|
|
9
|
+
// current context so the node can author its roadmap before any refresh.
|
|
10
|
+
// 2. Refresh → persona swap (permanent). On the next fresh revive the node
|
|
11
|
+
// starts with the orchestrator system prompt baked in (because the launch
|
|
12
|
+
// spec now says orchestrator). The guidance dump bridges until then.
|
|
13
|
+
//
|
|
14
|
+
// Trigger is persistence-need (deliberate, or a refresh-yield with open work),
|
|
15
|
+
// never the mere act of spawning a child.
|
|
16
|
+
import { getNode, updateNode, hasActiveLiveSubscription } from '../canvas/index.js';
|
|
17
|
+
import { buildLaunchSpec } from './launch.js';
|
|
18
|
+
import { loadKernel, loadPersona } from '../personas/index.js';
|
|
19
|
+
import { resolveSkill } from '../resolver.js';
|
|
20
|
+
import { readText } from '../fs-utils.js';
|
|
21
|
+
import { parseFrontmatter } from '../frontmatter.js';
|
|
22
|
+
import { hasRoadmap, seedRoadmap, readRoadmap } from './roadmap.js';
|
|
23
|
+
/** Load a skill's body text by name, or null if it can't be resolved. Used to
|
|
24
|
+
* inline a kind's roadmap-shaping skill into the promotion guidance dump. */
|
|
25
|
+
function loadSkillBody(name) {
|
|
26
|
+
try {
|
|
27
|
+
const skill = resolveSkill(name, {});
|
|
28
|
+
return parseFrontmatter(readText(skill.path)).body.trim();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Build the mid-turn guidance dump, specialized to the node's (possibly
|
|
35
|
+
* just-chosen) kind: the shared kernel + that kind's roadmap-shaping skill
|
|
36
|
+
* (auto-loaded now, before the persona swap bakes in on revive) + the roadmap
|
|
37
|
+
* scaffold the node must author. No goal is assumed — writing it is step one. */
|
|
38
|
+
function orchestrationGuidance(nodeId, kind) {
|
|
39
|
+
const kernel = loadKernel();
|
|
40
|
+
const orch = loadPersona(kind, 'orchestrator');
|
|
41
|
+
const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
|
|
42
|
+
? orch.frontmatter['roadmapSkill']
|
|
43
|
+
: undefined;
|
|
44
|
+
const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
|
|
45
|
+
const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
|
|
46
|
+
const parts = [
|
|
47
|
+
`You are now a RESIDENT ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
|
|
48
|
+
'Your job is to manage context and delegate — not to do the goal yourself.',
|
|
49
|
+
'',
|
|
50
|
+
kernel,
|
|
51
|
+
];
|
|
52
|
+
if (skillBody) {
|
|
53
|
+
parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
|
|
54
|
+
}
|
|
55
|
+
parts.push('', 'Your roadmap scaffold (`context/roadmap.md`) — author it now: state the goal, exit criteria, scope assumptions, and the phase skeleton, using the approach above:', '', roadmap, '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
|
|
56
|
+
return parts.join('\n');
|
|
57
|
+
}
|
|
58
|
+
/** Promote a node to resident orchestrator, optionally specializing its kind
|
|
59
|
+
* (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
|
|
60
|
+
* re-promoting just rewrites the spec + returns fresh guidance. Seeds a
|
|
61
|
+
* roadmap SCAFFOLD if absent (a boss with no map is a failure mode) — no goal
|
|
62
|
+
* is forced here; authoring the goal + roadmap is the node's next act. */
|
|
63
|
+
export function promote(nodeId, opts = {}) {
|
|
64
|
+
const node = getNode(nodeId);
|
|
65
|
+
if (node === null)
|
|
66
|
+
throw new Error(`unknown node: ${nodeId}`);
|
|
67
|
+
// The node may specialize as it promotes; default to its current kind.
|
|
68
|
+
const targetKind = opts.kind ?? node.kind;
|
|
69
|
+
// Rewrite the launch spec to the target kind's orchestrator persona so the
|
|
70
|
+
// *next* revive comes back orchestrating in that kind (polymorph stage 2).
|
|
71
|
+
// nodeEnv reads meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for
|
|
72
|
+
// the live process's children too.
|
|
73
|
+
const { launch } = buildLaunchSpec(targetKind, 'orchestrator');
|
|
74
|
+
// Seed a roadmap scaffold if absent so the file exists for a refresh. The
|
|
75
|
+
// node fills in the goal/body next, guided by the kind skill dumped below.
|
|
76
|
+
let roadmapWritten = false;
|
|
77
|
+
if (!hasRoadmap(nodeId)) {
|
|
78
|
+
seedRoadmap(nodeId);
|
|
79
|
+
roadmapWritten = true;
|
|
80
|
+
}
|
|
81
|
+
const meta = updateNode(nodeId, { kind: targetKind, lifecycle: 'resident', mode: 'orchestrator', launch });
|
|
82
|
+
return { meta, guidance: orchestrationGuidance(nodeId, targetKind), roadmapWritten };
|
|
83
|
+
}
|
|
84
|
+
/** Request a refresh-yield: discard in-memory context and revive fresh against
|
|
85
|
+
* the roadmap. A *terminal* node that yields is choosing to persist — it
|
|
86
|
+
* promotes first (refresh-with-open-work is the canonical promotion trigger),
|
|
87
|
+
* so it comes back as an orchestrator, optionally specializing its kind. Sets
|
|
88
|
+
* intent='refresh'; the stophook shuts the process down on the next stop and
|
|
89
|
+
* the daemon revives it fresh. */
|
|
90
|
+
export function requestYield(nodeId, opts = {}) {
|
|
91
|
+
const node = getNode(nodeId);
|
|
92
|
+
if (node === null)
|
|
93
|
+
throw new Error(`unknown node: ${nodeId}`);
|
|
94
|
+
let promoted = false;
|
|
95
|
+
if (node.lifecycle === 'terminal') {
|
|
96
|
+
// Yielding with open work ⇒ must survive a context reset ⇒ promote
|
|
97
|
+
// (optionally specializing the kind).
|
|
98
|
+
promote(nodeId, opts.kind !== undefined ? { kind: opts.kind } : {});
|
|
99
|
+
promoted = true;
|
|
100
|
+
}
|
|
101
|
+
// Mark the intent; the stophook enacts the shutdown, the daemon the revive.
|
|
102
|
+
const meta = updateNode(nodeId, { intent: 'refresh' });
|
|
103
|
+
void hasActiveLiveSubscription; // (open-work signal, reserved for future gating)
|
|
104
|
+
return { meta, promoted, willRefresh: true };
|
|
105
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ResetRootResult {
|
|
2
|
+
/** Descendant node ids torn down (window killed + marked dead). */
|
|
3
|
+
reaped: string[];
|
|
4
|
+
/** Direct subscriptions dropped off the root. */
|
|
5
|
+
detached: string[];
|
|
6
|
+
/** True when the node was a root and a full reset ran. */
|
|
7
|
+
reset: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Reset a root node to a pristine, empty graph (the `/new` semantics).
|
|
10
|
+
*
|
|
11
|
+
* For a non-root (spawned child), a `/new` is not a graph reset — we only
|
|
12
|
+
* refresh its session id so a later `--resume` wakes the right conversation. */
|
|
13
|
+
export declare function resetRoot(nodeId: string, newSessionId?: string): ResetRootResult;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Root reset — the `/new` equivalent.
|
|
2
|
+
//
|
|
3
|
+
// A live pi process is bound to one node via CRTR_NODE_ID (set at launch, not
|
|
4
|
+
// rebindable mid-process). When the user runs `/new`, the conversation is reset
|
|
5
|
+
// but the process — and thus the node id — stays the same. To make `/new`
|
|
6
|
+
// behave like re-running `crtr` (a brand-new graph on the canvas) we reset the
|
|
7
|
+
// root in place: reap its entire descendant sub-DAG, detach its subscriptions,
|
|
8
|
+
// and wipe its working state, then re-point it at a fresh base persona and the
|
|
9
|
+
// new pi session id. The node keeps its id; from the dashboard/nav it is a
|
|
10
|
+
// pristine root with an empty graph.
|
|
11
|
+
//
|
|
12
|
+
// Best-effort throughout: a tmux/fs failure on one node never aborts the reset.
|
|
13
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
14
|
+
import { getNode, updateNode, setStatus, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, } from '../canvas/index.js';
|
|
15
|
+
import { closeWindow, windowAlive } from './tmux.js';
|
|
16
|
+
import { buildLaunchSpec } from './launch.js';
|
|
17
|
+
import { roadmapPath } from './roadmap.js';
|
|
18
|
+
/** Reset a root node to a pristine, empty graph (the `/new` semantics).
|
|
19
|
+
*
|
|
20
|
+
* For a non-root (spawned child), a `/new` is not a graph reset — we only
|
|
21
|
+
* refresh its session id so a later `--resume` wakes the right conversation. */
|
|
22
|
+
export function resetRoot(nodeId, newSessionId) {
|
|
23
|
+
const meta = getNode(nodeId);
|
|
24
|
+
if (meta === null)
|
|
25
|
+
return { reaped: [], detached: [], reset: false };
|
|
26
|
+
// Only roots own a graph in the "ran crtr again" sense.
|
|
27
|
+
if (meta.parent != null) {
|
|
28
|
+
if (newSessionId !== undefined) {
|
|
29
|
+
try {
|
|
30
|
+
updateNode(nodeId, { pi_session_id: newSessionId });
|
|
31
|
+
}
|
|
32
|
+
catch { /* */ }
|
|
33
|
+
}
|
|
34
|
+
return { reaped: [], detached: [], reset: false };
|
|
35
|
+
}
|
|
36
|
+
// 1) Reap the descendant sub-DAG. Mark dead + clear intent FIRST, then kill
|
|
37
|
+
// the window: the daemon revives on a window-gone + intent==='refresh'
|
|
38
|
+
// (or 'idle-release'), so flipping to dead before the window dies closes
|
|
39
|
+
// the race where a descendant mid-yield gets revived as we tear it down.
|
|
40
|
+
const reaped = [];
|
|
41
|
+
for (const id of view(nodeId)) {
|
|
42
|
+
try {
|
|
43
|
+
const dmeta = getNode(id);
|
|
44
|
+
setStatus(id, 'dead');
|
|
45
|
+
updateNode(id, { intent: null });
|
|
46
|
+
if (dmeta !== null && windowAlive(dmeta.tmux_session, dmeta.window)) {
|
|
47
|
+
closeWindow(dmeta.window);
|
|
48
|
+
}
|
|
49
|
+
reaped.push(id);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* one bad node never aborts the reset */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2) Detach the root's own subscriptions so its view is empty.
|
|
56
|
+
const detached = [];
|
|
57
|
+
for (const sub of subscriptionsOf(nodeId)) {
|
|
58
|
+
try {
|
|
59
|
+
unsubscribe(nodeId, sub.node_id);
|
|
60
|
+
detached.push(sub.node_id);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 3) Wipe the root's working state (reports / inbox / roadmap).
|
|
67
|
+
for (const p of [
|
|
68
|
+
reportsDir(nodeId),
|
|
69
|
+
inboxPath(nodeId),
|
|
70
|
+
`${inboxPath(nodeId)}.cursor`,
|
|
71
|
+
roadmapPath(nodeId),
|
|
72
|
+
]) {
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(p))
|
|
75
|
+
rmSync(p, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 4) Re-point the root at a fresh base persona + the new pi session id.
|
|
82
|
+
try {
|
|
83
|
+
const { launch } = buildLaunchSpec(meta.kind, 'base');
|
|
84
|
+
updateNode(nodeId, {
|
|
85
|
+
mode: 'base',
|
|
86
|
+
lifecycle: 'resident',
|
|
87
|
+
intent: null,
|
|
88
|
+
status: 'active',
|
|
89
|
+
launch,
|
|
90
|
+
...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* */
|
|
95
|
+
}
|
|
96
|
+
return { reaped, detached, reset: true };
|
|
97
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ReviveResult {
|
|
2
|
+
/** The new tmux window id, or null if openNodeWindow failed. */
|
|
3
|
+
window: string | null;
|
|
4
|
+
/** The tmux session the node was placed in. */
|
|
5
|
+
session: string;
|
|
6
|
+
/** True when pi was instructed to resume its saved conversation (`--resume`). */
|
|
7
|
+
resumed: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Open a fresh background tmux window for `nodeId` and update canvas meta.
|
|
10
|
+
*
|
|
11
|
+
* Throws if the node does not exist. All other failures (e.g. tmux not
|
|
12
|
+
* available) propagate as-is — callers (daemon, command) decide how to handle.
|
|
13
|
+
*/
|
|
14
|
+
export declare function reviveNode(nodeId: string, opts: {
|
|
15
|
+
resume: boolean;
|
|
16
|
+
}): ReviveResult;
|
|
17
|
+
/** Re-exec a node's pi FRESH in its EXISTING tmux pane (the refresh-yield
|
|
18
|
+
* path). Unlike `reviveNode`, this opens no new window: the pane's current pi
|
|
19
|
+
* is replaced in place via `respawn-pane -k`, so a foreground/interactive
|
|
20
|
+
* session keeps its terminal and a background node keeps its window. Always
|
|
21
|
+
* fresh (no resume) — the node re-reads its roadmap/context dir.
|
|
22
|
+
*
|
|
23
|
+
* `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
|
|
24
|
+
* Throws on unknown node or when the respawn could not be dispatched, so the
|
|
25
|
+
* caller can fall back to a plain shutdown (daemon revives in a new window). */
|
|
26
|
+
export declare function reviveInPlace(nodeId: string, pane: string): ReviveResult;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The revive primitive — restores a node to active status under a fresh tmux
|
|
2
|
+
// window. Used by both the supervisor daemon (on crash/refresh detection) and
|
|
3
|
+
// the explicit `crtr canvas revive` command.
|
|
4
|
+
//
|
|
5
|
+
// A revive always opens a NEW window: the old one is gone (crashed, or the
|
|
6
|
+
// node exited with intent=refresh). The node's persisted LaunchSpec and cwd
|
|
7
|
+
// are the canonical recipe; reviveNode replays them faithfully.
|
|
8
|
+
//
|
|
9
|
+
// resume=true → `pi --resume <pi_session_id>` — wakes the saved conversation.
|
|
10
|
+
// resume=false → fresh pi invocation — the node re-reads its roadmap/context dir.
|
|
11
|
+
import { getNode, updateNode, } from '../canvas/index.js';
|
|
12
|
+
import { buildPiArgv } from './launch.js';
|
|
13
|
+
import { buildReviveKickoff } from './kickoff.js';
|
|
14
|
+
import { ensureSession, openNodeWindow, piCommand, respawnPane, } from './tmux.js';
|
|
15
|
+
import { rootSessionName } from './spawn.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// reviveNode
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/** Open a fresh background tmux window for `nodeId` and update canvas meta.
|
|
20
|
+
*
|
|
21
|
+
* Throws if the node does not exist. All other failures (e.g. tmux not
|
|
22
|
+
* available) propagate as-is — callers (daemon, command) decide how to handle.
|
|
23
|
+
*/
|
|
24
|
+
export function reviveNode(nodeId, opts) {
|
|
25
|
+
const meta = getNode(nodeId);
|
|
26
|
+
if (meta === null) {
|
|
27
|
+
throw new Error(`reviveNode: unknown node ${nodeId}`);
|
|
28
|
+
}
|
|
29
|
+
// The node lives in its root's tmux session. Prefer the stored session name;
|
|
30
|
+
// fall back to deriving it from the parent (or the node itself for roots).
|
|
31
|
+
const session = meta.tmux_session ??
|
|
32
|
+
rootSessionName((meta.parent ?? meta.node_id));
|
|
33
|
+
ensureSession(session, meta.cwd);
|
|
34
|
+
// Decide whether to wake the saved pi conversation or start fresh.
|
|
35
|
+
const resumeId = opts.resume && meta.pi_session_id != null
|
|
36
|
+
? meta.pi_session_id
|
|
37
|
+
: undefined;
|
|
38
|
+
// A fresh revive (no resume) gets a kickoff prompt so it re-reads its roadmap
|
|
39
|
+
// and continues; resuming a saved conversation needs none.
|
|
40
|
+
const inv = resumeId !== undefined
|
|
41
|
+
? buildPiArgv(meta, { resumeSessionId: resumeId })
|
|
42
|
+
: buildPiArgv(meta, { prompt: buildReviveKickoff(meta) });
|
|
43
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: session };
|
|
44
|
+
const window = openNodeWindow({
|
|
45
|
+
session,
|
|
46
|
+
name: meta.name,
|
|
47
|
+
cwd: meta.cwd,
|
|
48
|
+
env,
|
|
49
|
+
command: piCommand(inv.argv),
|
|
50
|
+
});
|
|
51
|
+
updateNode(nodeId, {
|
|
52
|
+
status: 'active',
|
|
53
|
+
intent: null,
|
|
54
|
+
window,
|
|
55
|
+
tmux_session: session,
|
|
56
|
+
});
|
|
57
|
+
return { window, session, resumed: resumeId !== undefined };
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// reviveInPlace — refresh-yield without churning the window
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/** Re-exec a node's pi FRESH in its EXISTING tmux pane (the refresh-yield
|
|
63
|
+
* path). Unlike `reviveNode`, this opens no new window: the pane's current pi
|
|
64
|
+
* is replaced in place via `respawn-pane -k`, so a foreground/interactive
|
|
65
|
+
* session keeps its terminal and a background node keeps its window. Always
|
|
66
|
+
* fresh (no resume) — the node re-reads its roadmap/context dir.
|
|
67
|
+
*
|
|
68
|
+
* `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
|
|
69
|
+
* Throws on unknown node or when the respawn could not be dispatched, so the
|
|
70
|
+
* caller can fall back to a plain shutdown (daemon revives in a new window). */
|
|
71
|
+
export function reviveInPlace(nodeId, pane) {
|
|
72
|
+
const meta = getNode(nodeId);
|
|
73
|
+
if (meta === null) {
|
|
74
|
+
throw new Error(`reviveInPlace: unknown node ${nodeId}`);
|
|
75
|
+
}
|
|
76
|
+
const session = meta.tmux_session ??
|
|
77
|
+
rootSessionName((meta.parent ?? meta.node_id));
|
|
78
|
+
// Fresh re-exec: same recipe as a no-resume reviveNode, with the kickoff so
|
|
79
|
+
// the node rebuilds its bearings from disk.
|
|
80
|
+
const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta) });
|
|
81
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: session };
|
|
82
|
+
const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
|
|
83
|
+
if (!ok) {
|
|
84
|
+
throw new Error(`reviveInPlace: respawn-pane dispatch failed for ${nodeId}`);
|
|
85
|
+
}
|
|
86
|
+
updateNode(nodeId, { status: 'active', intent: null, tmux_session: session });
|
|
87
|
+
// Window is unchanged (we re-execed in place); report the existing one.
|
|
88
|
+
return { window: meta.window ?? null, session, resumed: false };
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function roadmapPath(nodeId: string): string;
|
|
2
|
+
export declare function hasRoadmap(nodeId: string): boolean;
|
|
3
|
+
export declare function readRoadmap(nodeId: string): string | null;
|
|
4
|
+
/** Seed a fresh roadmap SCAFFOLD. No goal is required — promotion lays this
|
|
5
|
+
* down so the file exists for a refresh, and the owner authors the goal +
|
|
6
|
+
* body as its next act (guided by its kind's roadmap skill). `goal`/
|
|
7
|
+
* `exitCriteria` are optional overrides. Idempotent only if you intend it —
|
|
8
|
+
* call sites guard on hasRoadmap to avoid clobbering an evolved map. */
|
|
9
|
+
export declare function seedRoadmap(nodeId: string, opts?: {
|
|
10
|
+
goal?: string;
|
|
11
|
+
exitCriteria?: string;
|
|
12
|
+
}): string;
|