@crouton-kit/crouter 0.3.13 → 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/__tests__/human.test.js +73 -2
- 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.d.ts +1 -0
- package/dist/commands/human/queue.js +105 -2
- package/dist/commands/human/shared.d.ts +28 -18
- package/dist/commands/human/shared.js +53 -60
- package/dist/commands/human.js +6 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +381 -87
- 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.d.ts +1 -0
- package/dist/core/__tests__/passive-subscription.test.js +164 -0
- 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.d.ts +1 -0
- package/dist/core/__tests__/subcommand-tier.test.js +99 -0
- 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/paths.d.ts +4 -0
- package/dist/core/canvas/paths.js +6 -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 +48 -7
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +14 -12
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.d.ts +17 -0
- package/dist/core/feed/passive.js +92 -0
- package/dist/core/help.d.ts +59 -13
- package/dist/core/help.js +73 -28
- 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.d.ts +14 -0
- package/dist/core/runtime/demote.js +120 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +32 -6
- package/dist/core/runtime/kickoff.js +111 -37
- package/dist/core/runtime/launch.d.ts +29 -6
- package/dist/core/runtime/launch.js +85 -13
- 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 -32
- package/dist/core/runtime/presence.js +90 -110
- package/dist/core/runtime/promote.d.ts +18 -7
- package/dist/core/runtime/promote.js +70 -65
- 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/roadmap.d.ts +5 -4
- package/dist/core/runtime/roadmap.js +9 -16
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +169 -44
- 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 +106 -21
- package/dist/core/runtime/tmux.js +249 -45
- 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.d.ts +34 -0
- package/dist/pi-extensions/canvas-commands.js +103 -0
- 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 +21 -0
- package/dist/pi-extensions/canvas-goal-capture.js +67 -0
- 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-passive-context.d.ts +32 -0
- package/dist/pi-extensions/canvas-passive-context.js +114 -0
- 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
|
@@ -20,32 +20,25 @@ export function readRoadmap(nodeId) {
|
|
|
20
20
|
const p = roadmapPath(nodeId);
|
|
21
21
|
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
22
22
|
}
|
|
23
|
-
/** Seed a fresh
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
/** Seed a fresh, EXTREMELY BAREBONES roadmap scaffold — just the section
|
|
24
|
+
* skeleton with one-line prompts. Promotion lays this down so the file exists
|
|
25
|
+
* for a refresh; the owner fleshes it out as its next act (guided by its
|
|
26
|
+
* kind's roadmap skill). `goal`/`exitCriteria` pre-fill those sections when
|
|
27
|
+
* known (e.g. from the node's goal doc). Idempotent only if you intend it —
|
|
27
28
|
* call sites guard on hasRoadmap to avoid clobbering an evolved map. */
|
|
28
29
|
export function seedRoadmap(nodeId, opts = {}) {
|
|
29
30
|
const dir = contextDir(nodeId);
|
|
30
31
|
mkdirSync(dir, { recursive: true });
|
|
31
32
|
const body = `# Roadmap
|
|
32
33
|
|
|
33
|
-
<!-- frozen core: set once, rarely changes -->
|
|
34
34
|
## Goal
|
|
35
|
-
${opts.goal?.trim() ?? '
|
|
35
|
+
${opts.goal?.trim() ?? '(the goal you now own)'}
|
|
36
36
|
|
|
37
37
|
## Exit criteria
|
|
38
|
-
${opts.exitCriteria?.trim() ?? '
|
|
38
|
+
${opts.exitCriteria?.trim() ?? '(what "done" looks like)'}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- (record what's out of scope and what's settled — e.g. "reuse existing auth", "security isn't a concern here" — so children inherit the framing)
|
|
43
|
-
|
|
44
|
-
## Strategy / phases
|
|
45
|
-
- (your high-level shape of how you reach the goal; the ordered phases from here to done, the current one carrying a one-line status. Each phase can become a child whose own roadmap is that phase)
|
|
46
|
-
|
|
47
|
-
## Active context
|
|
48
|
-
- (the context/ files currently relevant to the work, by path; none yet)
|
|
40
|
+
## Phases
|
|
41
|
+
(ordered phases from here to done; the current one carries a one-line status)
|
|
49
42
|
`;
|
|
50
43
|
writeFileSync(roadmapPath(nodeId), body);
|
|
51
44
|
return body;
|
|
@@ -5,9 +5,6 @@ export interface BootRootOpts {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
/** Optional starter prompt (bare `crtr` requires none). */
|
|
7
7
|
prompt?: string;
|
|
8
|
-
/** 'inline' — exec pi in the current terminal (bare `crtr`).
|
|
9
|
-
* 'session' — create a dedicated tmux session and run pi there (`session new`). */
|
|
10
|
-
placement: 'inline' | 'session';
|
|
11
8
|
}
|
|
12
9
|
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
13
10
|
* only returns after pi exits (it took over the terminal). */
|
|
@@ -20,12 +17,30 @@ export interface SpawnChildOpts {
|
|
|
20
17
|
prompt: string;
|
|
21
18
|
/** Override the parent (defaults to the calling node from env). */
|
|
22
19
|
parent?: string;
|
|
20
|
+
/** Spawn an INDEPENDENT root instead of a managed child: parent=null, no
|
|
21
|
+
* subscription back to the spawner, resident lifecycle, spawned_by=spawner.
|
|
22
|
+
* Brought forefront on spawn so a human can drive it directly. */
|
|
23
|
+
root?: boolean;
|
|
24
|
+
/** Fork the new node from an existing pi conversation instead of starting it
|
|
25
|
+
* fresh: a node id (resolved to that node's session file), an absolute
|
|
26
|
+
* `.jsonl` path, or a partial pi session uuid. pi COPIES that history into a
|
|
27
|
+
* new session for the child — the source is untouched — then `prompt` is the
|
|
28
|
+
* next message. A one-shot at birth; the child resumes its own session after. */
|
|
29
|
+
forkFrom?: string;
|
|
23
30
|
}
|
|
31
|
+
/** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
|
|
32
|
+
* A live node id resolves to its captured session FILE (absolute, cwd-immune),
|
|
33
|
+
* falling back to its bare session id; a path or partial uuid passes straight
|
|
34
|
+
* through to pi. Throws when a known node has no session to fork yet. */
|
|
35
|
+
export declare function resolveForkSource(value: string): string;
|
|
24
36
|
export interface SpawnChildResult {
|
|
25
37
|
node: NodeMeta;
|
|
26
38
|
window: string | null;
|
|
27
39
|
session: string;
|
|
28
40
|
}
|
|
29
|
-
/** Spawn a
|
|
30
|
-
*
|
|
41
|
+
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
42
|
+
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
43
|
+
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
44
|
+
* to the spawner (it carries spawned_by=spawner for provenance only), brought
|
|
45
|
+
* forefront so a human can pick up the conversation directly. */
|
|
31
46
|
export declare function spawnChild(opts: SpawnChildOpts): SpawnChildResult;
|
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
// a running pi process on the canvas. Composes canvas (birth + spine), persona
|
|
3
3
|
// (resolve), launch (pi argv), and tmux (placement).
|
|
4
4
|
//
|
|
5
|
-
// bootRoot —
|
|
6
|
-
//
|
|
7
|
-
// spawnChild — a
|
|
8
|
-
//
|
|
5
|
+
// bootRoot — the user-opened front door (bare `crtr`). Resident; runs pi
|
|
6
|
+
// inline, taking over the current terminal.
|
|
7
|
+
// spawnChild — a node spawned by a live node (`crtr node new`). A managed,
|
|
8
|
+
// terminal background worker by default; with `root`, an
|
|
9
|
+
// INDEPENDENT resident root (no subscription back to the spawner,
|
|
10
|
+
// provenance via spawned_by) brought forefront for direct driving.
|
|
9
11
|
import { spawnSync } from 'node:child_process';
|
|
10
12
|
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
11
|
-
import { spawnNode, currentNodeContext } from './nodes.js';
|
|
13
|
+
import { spawnNode, currentNodeContext, resolveBirthSession } from './nodes.js';
|
|
12
14
|
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
13
15
|
import { writeGoal } from './kickoff.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
17
|
+
import { seedMemory, seedUserMemory, seedProjectMemory } from './memory.js';
|
|
18
|
+
import { generateSessionName } from './naming.js';
|
|
19
|
+
import { ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, nodeSession, focusWindow, installMenuBinding, installNavBindings, } from './tmux.js';
|
|
20
|
+
import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
|
|
21
|
+
import { registerRootFocus } from './placement.js';
|
|
22
|
+
import { transition } from './lifecycle.js';
|
|
16
23
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
17
24
|
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
18
25
|
* only returns after pi exits (it took over the terminal). */
|
|
@@ -26,13 +33,20 @@ export function bootRoot(opts) {
|
|
|
26
33
|
const kind = opts.kind ?? 'general';
|
|
27
34
|
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
28
35
|
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
29
|
-
const { launch } = buildLaunchSpec(kind, 'base');
|
|
36
|
+
const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
37
|
+
// A root opened WITH a prompt gets its editor name now (so the first pi
|
|
38
|
+
// session already carries it). A bare root has no prompt yet — the
|
|
39
|
+
// goal-capture extension names it from the first message (async, next cycle).
|
|
40
|
+
const description = opts.prompt !== undefined && opts.prompt.trim() !== ''
|
|
41
|
+
? generateSessionName(opts.prompt)
|
|
42
|
+
: undefined;
|
|
30
43
|
const meta = spawnNode({
|
|
31
44
|
kind,
|
|
32
45
|
mode: 'base',
|
|
33
46
|
lifecycle: 'resident',
|
|
34
47
|
cwd: opts.cwd,
|
|
35
48
|
name: opts.name ?? kind,
|
|
49
|
+
description,
|
|
36
50
|
parent: null,
|
|
37
51
|
launch,
|
|
38
52
|
});
|
|
@@ -43,81 +57,192 @@ export function bootRoot(opts) {
|
|
|
43
57
|
// Every node window — root or child — lives in the one shared session.
|
|
44
58
|
const session = nodeSession();
|
|
45
59
|
ensureSession(session, opts.cwd);
|
|
46
|
-
// Make the Alt+C action menu live on this server
|
|
60
|
+
// Make the Alt+C action menu + Alt+] / Alt+[ nav keys live on this server
|
|
61
|
+
// (idempotent, in-tmux only).
|
|
47
62
|
if (inTmux()) {
|
|
48
63
|
try {
|
|
49
64
|
installMenuBinding();
|
|
50
65
|
}
|
|
51
66
|
catch { /* best-effort */ }
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
57
|
-
const env = { ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
58
|
-
const win = openNodeWindow({
|
|
59
|
-
session,
|
|
60
|
-
name: meta.name,
|
|
61
|
-
cwd: opts.cwd,
|
|
62
|
-
env,
|
|
63
|
-
command: piCommand(inv.argv),
|
|
64
|
-
});
|
|
65
|
-
updateNode(meta.node_id, { window: win });
|
|
66
|
-
return getNode(meta.node_id);
|
|
67
|
+
try {
|
|
68
|
+
installNavBindings();
|
|
69
|
+
}
|
|
70
|
+
catch { /* best-effort */ }
|
|
67
71
|
}
|
|
68
72
|
// inline: the root's pi takes over THIS terminal, so its own window stays
|
|
69
73
|
// where the user is (its tmux_session tracks that real pane so supervision
|
|
70
74
|
// sees it alive). But its children spawn into the shared global session via
|
|
71
75
|
// CRTR_ROOT_SESSION — they never clutter the user's working session.
|
|
72
76
|
const here = currentTmux();
|
|
73
|
-
const adopted = here
|
|
74
|
-
|
|
77
|
+
const adopted = resolveBirthSession({ adoptCaller: true, here, rootSession: undefined });
|
|
78
|
+
setPresence(meta.node_id, { tmux_session: adopted, window: here?.window ?? null, pane: here?.pane ?? null });
|
|
79
|
+
// REVIVE-HOME: the inline root's durable revive target is the session it
|
|
80
|
+
// adopts (the caller's when inside tmux, else the shared backstage). Set once
|
|
81
|
+
// at birth, alongside the live LOCATION above.
|
|
82
|
+
updateNode(meta.node_id, { home_session: adopted });
|
|
83
|
+
// Root boot registers focus #1 (§2.6): the FOREGROUND inline root owns the
|
|
84
|
+
// user's viewport, so its OWN pane becomes a durable focus (remain-on-exit so
|
|
85
|
+
// a clean exit freezes rather than detaching the terminal). A background
|
|
86
|
+
// `--root` (spawnChild) does NOT — it stays a plain window until the user
|
|
87
|
+
// focuses it (§6). Only possible inside tmux (a pane to anchor on).
|
|
88
|
+
if (here) {
|
|
89
|
+
try {
|
|
90
|
+
registerRootFocus(meta.node_id, here.pane, adopted, here.window);
|
|
91
|
+
}
|
|
92
|
+
catch { /* best-effort */ }
|
|
93
|
+
}
|
|
75
94
|
const withSession = getNode(meta.node_id);
|
|
76
95
|
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
77
96
|
const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
78
97
|
const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
|
|
79
98
|
process.exit(r.status ?? 0);
|
|
80
99
|
}
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
100
|
+
/** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
|
|
101
|
+
* A live node id resolves to its captured session FILE (absolute, cwd-immune),
|
|
102
|
+
* falling back to its bare session id; a path or partial uuid passes straight
|
|
103
|
+
* through to pi. Throws when a known node has no session to fork yet. */
|
|
104
|
+
export function resolveForkSource(value) {
|
|
105
|
+
const v = value.trim();
|
|
106
|
+
if (v === '')
|
|
107
|
+
throw new Error('--fork-from requires a node id, session file, or session uuid.');
|
|
108
|
+
// A path (contains `/` or ends `.jsonl`) is a session file — hand it to pi as-is.
|
|
109
|
+
if (v.includes('/') || v.endsWith('.jsonl'))
|
|
110
|
+
return v;
|
|
111
|
+
// A live node id — fork from the conversation it has accumulated.
|
|
112
|
+
const n = getNode(v);
|
|
113
|
+
if (n !== null) {
|
|
114
|
+
const src = n.pi_session_file ?? n.pi_session_id;
|
|
115
|
+
if (src === undefined || src === null || src === '') {
|
|
116
|
+
throw new Error(`node ${v} has no pi session yet — it has not started a conversation to fork from.`);
|
|
117
|
+
}
|
|
118
|
+
return src;
|
|
119
|
+
}
|
|
120
|
+
// Not a known node — treat as a bare/partial pi session id for pi to resolve.
|
|
121
|
+
return v;
|
|
122
|
+
}
|
|
123
|
+
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
124
|
+
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
125
|
+
* With `root`: an independent resident root — parent=null, NO subscription back
|
|
126
|
+
* to the spawner (it carries spawned_by=spawner for provenance only), brought
|
|
127
|
+
* forefront so a human can pick up the conversation directly. */
|
|
83
128
|
export function spawnChild(opts) {
|
|
84
129
|
try {
|
|
85
130
|
ensureDaemon();
|
|
86
131
|
}
|
|
87
132
|
catch { /* daemon is best-effort */ }
|
|
88
133
|
const ctx = currentNodeContext();
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
134
|
+
const spawner = opts.parent ?? ctx.nodeId;
|
|
135
|
+
if (spawner === null || spawner === undefined) {
|
|
91
136
|
throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
|
|
92
137
|
}
|
|
138
|
+
const root = opts.root === true;
|
|
93
139
|
const mode = opts.mode ?? 'base';
|
|
94
|
-
|
|
140
|
+
// Lifecycle keys on ROOT-ness only, independent of mode: an independent root
|
|
141
|
+
// (or `--root`) is resident (a conversation that persists, woken by inbox/
|
|
142
|
+
// human); every spawned child is terminal — it owes a final up the spine and
|
|
143
|
+
// reaps when done. A child born as an orchestrator is terminal/orchestrator
|
|
144
|
+
// (delegates + holds a roadmap, but still reports up), NOT resident.
|
|
145
|
+
const lifecycle = root ? 'resident' : 'terminal';
|
|
146
|
+
// Spine: a managed child reports up to its spawner (has a manager); an
|
|
147
|
+
// independent root sits top-of-spine with nobody to push to. Mirrors the
|
|
148
|
+
// `parent` set below (root ? null : spawner), so hasManager === parent!==null.
|
|
149
|
+
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root });
|
|
150
|
+
// Name the worker from its task now, so its first editor label carries it.
|
|
95
151
|
const meta = spawnNode({
|
|
96
152
|
kind: opts.kind,
|
|
97
153
|
mode,
|
|
98
|
-
lifecycle
|
|
154
|
+
lifecycle,
|
|
99
155
|
cwd: opts.cwd,
|
|
100
156
|
name: opts.name ?? opts.kind,
|
|
101
|
-
|
|
157
|
+
description: generateSessionName(opts.prompt),
|
|
158
|
+
// A root has no spine parent (top-level, nobody subscribes); it still
|
|
159
|
+
// records spawned_by=spawner. A child's parent IS its manager.
|
|
160
|
+
parent: root ? null : spawner,
|
|
161
|
+
spawnedBy: root ? spawner : undefined,
|
|
102
162
|
launch,
|
|
103
163
|
});
|
|
104
164
|
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
105
165
|
writeGoal(meta.node_id, opts.prompt);
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
166
|
+
// A fork copies an existing conversation into this child's first session
|
|
167
|
+
// (resolved to an absolute file path when forking from a node). Resolved here
|
|
168
|
+
// — not in buildPiArgv — so a bad reference fails the spawn loudly before any
|
|
169
|
+
// window opens, rather than after pi is already booting.
|
|
170
|
+
const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
|
|
171
|
+
// A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
|
|
172
|
+
// with the orchestrator persona but bypasses promote(), which is where a
|
|
173
|
+
// roadmap scaffold would normally be seeded. Lay one down here (goal
|
|
174
|
+
// pre-filled from the task) so the orchestrator has its memory artifact from
|
|
175
|
+
// birth, instead of waking memory-less. Guarded so it never clobbers.
|
|
176
|
+
if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
|
|
177
|
+
seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
|
|
178
|
+
}
|
|
179
|
+
// Born an orchestrator ⇒ also lay down its three scoped long-term memory
|
|
180
|
+
// stores, the companions to the roadmap: user-global (key-less), project
|
|
181
|
+
// (keyed off the child's cwd), and node-local. Each guarded against clobber.
|
|
182
|
+
if (mode === 'orchestrator') {
|
|
183
|
+
seedUserMemory();
|
|
184
|
+
seedProjectMemory(opts.cwd);
|
|
185
|
+
seedMemory(meta.node_id);
|
|
186
|
+
}
|
|
187
|
+
// A managed CHILD lands in the shared global session: inherited from the
|
|
188
|
+
// parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
|
|
189
|
+
// from inside tmux instead opens its window in the CALLER'S CURRENT session,
|
|
190
|
+
// so it appears where the spawner is working rather than exiled to a separate
|
|
191
|
+
// crtr session. Either way the root's OWN descendants still flow to the shared
|
|
192
|
+
// session (childSession) via CRTR_ROOT_SESSION, to keep the subtree from
|
|
193
|
+
// cluttering the user's session.
|
|
194
|
+
const rootSessionEnv = process.env['CRTR_ROOT_SESSION'];
|
|
195
|
+
const here = root ? currentTmux() : null;
|
|
196
|
+
// The shared backstage the whole subtree flows into (this child's own
|
|
197
|
+
// CRTR_ROOT_SESSION): the inherited root session, else the default `crtr`.
|
|
198
|
+
const childSession = resolveBirthSession({ adoptCaller: false, here, rootSession: rootSessionEnv });
|
|
199
|
+
// Where THIS node's window opens — and its durable REVIVE-HOME. A managed
|
|
200
|
+
// child lands in the backstage; a --root adopts the caller's current session
|
|
201
|
+
// when inside tmux, so it appears where the spawner is working.
|
|
202
|
+
const session = resolveBirthSession({ adoptCaller: root, here, rootSession: rootSessionEnv });
|
|
111
203
|
ensureSession(session, opts.cwd);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
204
|
+
// REVIVE-HOME set once at birth: a managed child's revive target is the
|
|
205
|
+
// backstage, never a user session — this is what keeps a background revive
|
|
206
|
+
// off the user's screen (the focus taint cannot reach it).
|
|
207
|
+
updateNode(meta.node_id, { home_session: session });
|
|
208
|
+
const inv = buildPiArgv(meta, { prompt: opts.prompt, forkFrom });
|
|
209
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: childSession, [FRONT_DOOR_ENV]: '1' };
|
|
210
|
+
const command = piCommand(inv.argv);
|
|
211
|
+
// openNodeWindow now returns {window, pane}; pane is unused until the
|
|
212
|
+
// placement layer lands, so destructure the window and proceed unchanged.
|
|
213
|
+
const opened = openNodeWindow({
|
|
115
214
|
session,
|
|
116
|
-
name: meta
|
|
215
|
+
name: fullName(meta),
|
|
117
216
|
cwd: opts.cwd,
|
|
118
217
|
env,
|
|
119
|
-
command
|
|
218
|
+
command,
|
|
120
219
|
});
|
|
121
|
-
const
|
|
220
|
+
const window = opened?.window ?? null;
|
|
221
|
+
// Two-stage failure model. Opening the window is instant and definitive, so a
|
|
222
|
+
// failure here is reported SYNCHRONOUSLY: crash the node (so it isn't a zombie
|
|
223
|
+
// 'active' the daemon can't reap — it has no window to watch) and throw so
|
|
224
|
+
// `crtr node new` exits non-zero with a clear message for the caller. The node
|
|
225
|
+
// is still 'active' from spawnNode, so transition('crash') is a legal from-LIVE
|
|
226
|
+
// move — the last scattered node-status write, now through the lifecycle machine.
|
|
227
|
+
//
|
|
228
|
+
// pi BOOTING inside the window, by contrast, is inherently slow (and slower
|
|
229
|
+
// under load), so we stay optimistic and return status='active' the instant
|
|
230
|
+
// the window exists. A vehicle that then dies before its first session_start
|
|
231
|
+
// is caught by the daemon — it surfaces the boot failure up the spine rather
|
|
232
|
+
// than letting the node die silently (see crtrd.ts surfaceBootFailure).
|
|
233
|
+
if (window === null) {
|
|
234
|
+
transition(meta.node_id, 'crash');
|
|
235
|
+
throw new Error(`failed to open a tmux window for ${meta.node_id} (${meta.name}) in session '${session}' — the node was not started.`);
|
|
236
|
+
}
|
|
237
|
+
setPresence(meta.node_id, { tmux_session: session, window });
|
|
238
|
+
const saved = getNode(meta.node_id);
|
|
239
|
+
// A root is spawned to be driven directly — bring it forefront so whoever
|
|
240
|
+
// asked for it picks up the conversation. A child stays a background window.
|
|
241
|
+
if (root) {
|
|
242
|
+
try {
|
|
243
|
+
focusWindow(session, window);
|
|
244
|
+
}
|
|
245
|
+
catch { /* best-effort */ }
|
|
246
|
+
}
|
|
122
247
|
return { node: saved, window, session };
|
|
123
248
|
}
|
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
// subscription to a node that's still live (active|idle) — something that can
|
|
6
6
|
// actually wake it. (A passive sub won't wake you, so it doesn't count.)
|
|
7
7
|
//
|
|
8
|
-
// •
|
|
9
|
-
//
|
|
8
|
+
// • resident → an interactable / human-driven node is NEVER forced to
|
|
9
|
+
// submit a final: stopping to go dormant is always
|
|
10
|
+
// legitimate (woken by inbox/human). Keyed on the LIFECYCLE
|
|
11
|
+
// value, not on parent/mode — what matters is residency.
|
|
12
|
+
// • waiting → a TERMINAL node holding an active live subscription is a
|
|
13
|
+
// dormant orchestrator awaiting its workers. Let it sleep;
|
|
14
|
+
// a child's push wakes it (and idle-releases its window).
|
|
10
15
|
// • finished/asked → it pushed --final (done) or called `crtr ask` this turn.
|
|
11
16
|
// Also fine.
|
|
12
|
-
// • otherwise →
|
|
13
|
-
// Re-prompt it to finish or escalate.
|
|
17
|
+
// • otherwise → a TERMINAL node with nothing live to wait for and no
|
|
18
|
+
// final pushed. Re-prompt it to finish or escalate.
|
|
14
19
|
import { hasActiveLiveSubscription, getNode } from '../canvas/index.js';
|
|
15
20
|
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
16
21
|
'Run `crtr push final "<result>"` if the work is done, or `crtr human ask` if you are blocked or need the user.';
|
|
@@ -21,13 +26,18 @@ export function evaluateStop(nodeId, signals) {
|
|
|
21
26
|
return { action: 'allow', reason: 'finished' };
|
|
22
27
|
if (signals.askedHuman)
|
|
23
28
|
return { action: 'allow', reason: 'escalated' };
|
|
24
|
-
// A
|
|
25
|
-
//
|
|
29
|
+
// A RESIDENT node is interactable / human-driven and is never forced to submit
|
|
30
|
+
// a final: stopping to go dormant is always legitimate (the inbox or the human
|
|
31
|
+
// wakes it). Keyed on lifecycle, not parent — whether it has a parent doesn't
|
|
32
|
+
// matter, only whether it's resident. Roots are resident by birth default, so
|
|
33
|
+
// this still covers "don't nag the human's root" while generalizing it.
|
|
26
34
|
const node = getNode(nodeId);
|
|
27
|
-
if (node !== null &&
|
|
28
|
-
return { action: 'allow', reason: '
|
|
35
|
+
if (node !== null && node.lifecycle === 'resident') {
|
|
36
|
+
return { action: 'allow', reason: 'dormant' };
|
|
29
37
|
}
|
|
38
|
+
// A terminal node holding something live to wake it is legitimately awaiting.
|
|
30
39
|
if (hasActiveLiveSubscription(nodeId))
|
|
31
40
|
return { action: 'allow', reason: 'awaiting' };
|
|
41
|
+
// A terminal node with nothing live and no final pushed has stalled.
|
|
32
42
|
return { action: 'reprompt', reason: 'stalled', message: STALL_REPROMPT };
|
|
33
43
|
}
|
|
@@ -28,7 +28,8 @@ export interface OpenWindowOpts {
|
|
|
28
28
|
}
|
|
29
29
|
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
30
30
|
* detached so it doesn't steal focus or become the current window. Returns the
|
|
31
|
-
* new window id
|
|
31
|
+
* new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
|
|
32
|
+
* anchor) — callers that only need the window destructure `.window`.
|
|
32
33
|
*
|
|
33
34
|
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
34
35
|
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
@@ -37,24 +38,41 @@ export interface OpenWindowOpts {
|
|
|
37
38
|
* "create window failed: index N in use" whenever the active window is not the
|
|
38
39
|
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
39
40
|
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
40
|
-
* dashboard.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* window + pane ids. Used by demote: the agent's pi is swapped OUT into this
|
|
44
|
-
* window's slot and the shell is swapped INTO the caller's pane. `-a` keeps it
|
|
45
|
-
* off index 0 (reserved for a dashboard), `-d` keeps it from stealing focus. */
|
|
46
|
-
export declare function openShellWindow(opts: {
|
|
47
|
-
session: string;
|
|
48
|
-
name: string;
|
|
49
|
-
cwd: string;
|
|
50
|
-
}): {
|
|
41
|
+
* dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
|
|
42
|
+
* INVARIANT — never let new-window fall back to tmux's global current session. */
|
|
43
|
+
export declare function openNodeWindow(opts: OpenWindowOpts): {
|
|
51
44
|
window: string;
|
|
52
45
|
pane: string;
|
|
53
46
|
} | null;
|
|
47
|
+
export interface SplitWindowOpts {
|
|
48
|
+
cwd: string;
|
|
49
|
+
env: Record<string, string>;
|
|
50
|
+
/** The full command to run in the new pane (already a shell string). */
|
|
51
|
+
command: string;
|
|
52
|
+
/** Stack the new pane below instead of beside (default: beside, `-h`). */
|
|
53
|
+
vertical?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
|
|
56
|
+
* and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
|
|
57
|
+
* (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
|
|
58
|
+
* makes the split side-by-side (left/right), the default for a focus viewport.
|
|
59
|
+
*
|
|
60
|
+
* §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
|
|
61
|
+
* would split tmux's global current pane, which can leak a pane into an
|
|
62
|
+
* unrelated user session (the exact bug this design kills). The explicit
|
|
63
|
+
* `-t <targetPane>` makes the destination structurally un-leakable. Returns
|
|
64
|
+
* null if tmux fails. */
|
|
65
|
+
export declare function splitWindow(targetPane: string, opts: SplitWindowOpts): string | null;
|
|
54
66
|
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
55
67
|
export declare function focusWindow(session: string, window: string): boolean;
|
|
56
68
|
/** Close a node's window (drop it from the UI). */
|
|
57
69
|
export declare function closeWindow(window: string): boolean;
|
|
70
|
+
/** Close a single PANE. Its window closes automatically once this was the last
|
|
71
|
+
* pane, but sibling panes survive — so co-located nodes (several agents sharing
|
|
72
|
+
* one window via swap-pane focus) are torn down one at a time instead of all
|
|
73
|
+
* at once by a window kill. Pane ids are the stable vehicle handle; windows
|
|
74
|
+
* shift under swap-pane focus, so pane-granular teardown is the correct unit. */
|
|
75
|
+
export declare function closePane(pane: string): boolean;
|
|
58
76
|
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
59
77
|
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
60
78
|
export declare function paneOfWindow(session: string, window: string): string | null;
|
|
@@ -63,6 +81,35 @@ export declare function paneOfWindow(session: string, window: string): string |
|
|
|
63
81
|
* are not, so the node→window mapping must be re-derived from the pane. Returns
|
|
64
82
|
* null if the pane is gone or tmux fails. */
|
|
65
83
|
export declare function windowOfPane(pane: string): string | null;
|
|
84
|
+
/** The session + window a pane currently lives in (`display-message -p -t %id`).
|
|
85
|
+
* The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
|
|
86
|
+
* window/session from its durable pane id before any act, so crtr follows a
|
|
87
|
+
* manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
|
|
88
|
+
* the pane is gone or tmux fails. */
|
|
89
|
+
export declare function paneLocation(pane: string): {
|
|
90
|
+
session: string;
|
|
91
|
+
window: string;
|
|
92
|
+
} | null;
|
|
93
|
+
/** Does this pane id still exist? A `display-message` probe on the `%id` — the
|
|
94
|
+
* v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
|
|
95
|
+
* moving a pane to another window/session never reads as "gone". True iff tmux
|
|
96
|
+
* knows the pane.
|
|
97
|
+
*
|
|
98
|
+
* NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
|
|
99
|
+
* not error on an unresolvable pane target) — so an `.ok` check alone would
|
|
100
|
+
* report a dead pane as alive, defeating the whole point of pane-existence
|
|
101
|
+
* liveness. We therefore require the echoed `#{pane_id}` to equal the requested
|
|
102
|
+
* pane: a live pane echoes its own id, a gone/bogus one yields empty. */
|
|
103
|
+
export declare function paneExists(pane: string): boolean;
|
|
104
|
+
/** Relocate a pane into another session as its own window WITHOUT killing the
|
|
105
|
+
* process in it — `break-pane -d` moves the pane out of its current window (the
|
|
106
|
+
* pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
|
|
107
|
+
* client where it is rather than following the pane to the background, and `-a`
|
|
108
|
+
* allocates the next free window index (same dodge as openNodeWindow). The
|
|
109
|
+
* "detach to background" driver behind `node lifecycle --detach`. Best-effort;
|
|
110
|
+
* false if tmux refuses (e.g. the pane is gone). The caller reconciles presence
|
|
111
|
+
* so the canvas follows the move. */
|
|
112
|
+
export declare function breakPaneToSession(pane: string, session: string): boolean;
|
|
66
113
|
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
67
114
|
* caller's window active, so the target's pane appears where the caller is
|
|
68
115
|
* rather than navigating the client off to the target's window. The caller's
|
|
@@ -77,14 +124,20 @@ export interface RespawnPaneOpts {
|
|
|
77
124
|
/** The full command to run in the pane (already a shell string). */
|
|
78
125
|
command: string;
|
|
79
126
|
}
|
|
80
|
-
/** Re-exec a command in an EXISTING pane, in place.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
127
|
+
/** Re-exec a command in an EXISTING pane, in place — DETACHED. Spawned in its own
|
|
128
|
+
* process group (unref'd) so the request reaches the tmux server even though
|
|
129
|
+
* `-k` tears down the caller's own pi mid-flight. Used when a node respawns ITS
|
|
130
|
+
* OWN pane (refresh-yield): the dispatch can't be awaited because it kills the
|
|
131
|
+
* awaiter. Returns true once the request was dispatched. */
|
|
132
|
+
export declare function respawnPaneDetached(opts: RespawnPaneOpts): boolean;
|
|
133
|
+
/** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
|
|
134
|
+
* `respawn-pane` to completion and reports the real exit status. Used when the
|
|
135
|
+
* caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
|
|
136
|
+
* focus pane), so it can confirm the respawn landed. Returns true on success. */
|
|
137
|
+
export declare function respawnPaneSync(opts: RespawnPaneOpts): boolean;
|
|
138
|
+
/** @deprecated Use respawnPaneDetached. Retained so existing refresh-yield
|
|
139
|
+
* callers stay green while the placement layer migrates onto the explicit
|
|
140
|
+
* sync/detached split. */
|
|
88
141
|
export declare function respawnPane(opts: RespawnPaneOpts): boolean;
|
|
89
142
|
/** Turn a pi argv array into a single shell command string. */
|
|
90
143
|
export declare function piCommand(argv: string[], binary?: string): string;
|
|
@@ -103,5 +156,37 @@ export declare function selectWindow(session: string, window: string): boolean;
|
|
|
103
156
|
* `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
|
|
104
157
|
* responsible for following up with selectWindow to land on the right window. */
|
|
105
158
|
export declare function switchClient(session: string): boolean;
|
|
106
|
-
/**
|
|
159
|
+
/** Move a source pane into a destination window (`tmux join-pane`). The source
|
|
160
|
+
* pane's running process (e.g. a child's live pi) is preserved; its now-empty
|
|
161
|
+
* source window auto-closes. Best-effort; false if tmux fails. */
|
|
162
|
+
export declare function joinPane(srcPane: string, dstWindow: string): boolean;
|
|
163
|
+
/** Apply a named tmux layout to a window (`tmux select-layout`). Use
|
|
164
|
+
* `main-vertical` for one wide pane on the left + the rest stacked right.
|
|
165
|
+
* Best-effort; never throws. */
|
|
166
|
+
export declare function selectLayout(window: string, layout: string): boolean;
|
|
167
|
+
/** Set a tmux window option (`tmux set-window-option`). Used to size the main
|
|
168
|
+
* pane (`main-pane-width`) before a main-vertical layout. Best-effort. */
|
|
169
|
+
export declare function setWindowOption(window: string, name: string, value: string): boolean;
|
|
170
|
+
/** Toggle `remain-on-exit` on a window (§1.5 F3). `on` keeps a focus pane on
|
|
171
|
+
* screen after its pi exits — the viewport survives (F1), the final transcript
|
|
172
|
+
* is preserved, and `respawn-pane -k` can resurrect the node into the SAME pane
|
|
173
|
+
* id. NOTE (§1.5/§2.5, spike-confirmed): a dead/frozen pane is reaped only by
|
|
174
|
+
* `kill-pane`/`respawn-pane`, NEVER by toggling this off — the toggle does not
|
|
175
|
+
* reap an already-dead pane. Best-effort; never throws. */
|
|
176
|
+
export declare function setRemainOnExit(window: string, on: boolean): boolean;
|
|
177
|
+
/** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
|
|
178
|
+
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
179
|
+
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
180
|
+
export declare function sendKeysEnter(pane: string, text: string): boolean;
|
|
181
|
+
/** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
|
|
182
|
+
* The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
|
|
183
|
+
* chords (graph/manager/expand/report-N + any custom prefixBind) are appended
|
|
184
|
+
* from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
|
|
185
|
+
* the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
|
|
186
|
+
* while behaviour is config-driven. */
|
|
107
187
|
export declare function installMenuBinding(): boolean;
|
|
188
|
+
/** Bind Alt+] (forward) and Alt+[ (back) to the DFS canvas walk. Best-effort;
|
|
189
|
+
* false if either bind fails. NOTE: Alt+[ is only delivered cleanly when the
|
|
190
|
+
* terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
|
|
191
|
+
*/
|
|
192
|
+
export declare function installNavBindings(): boolean;
|