@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,52 @@
|
|
|
1
|
+
// The roadmap — one document, two temperatures. A small frozen core (goal +
|
|
2
|
+
// exit criteria) and an evolving body (scope, strategy, active context) the
|
|
3
|
+
// owner keeps current. It holds how you intend to reach the goal and where you
|
|
4
|
+
// are right now — not a journal of what you did or a queue of what's next. It
|
|
5
|
+
// is what lets a resident node survive a refresh-yield: revive with no memory,
|
|
6
|
+
// re-read the map, continue.
|
|
7
|
+
//
|
|
8
|
+
// Written at resident-promotion (a born-resident root, or a spawned node's
|
|
9
|
+
// first refresh-with-open-work). Leaf/terminal workers write nothing.
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { contextDir } from '../canvas/index.js';
|
|
13
|
+
export function roadmapPath(nodeId) {
|
|
14
|
+
return join(contextDir(nodeId), 'roadmap.md');
|
|
15
|
+
}
|
|
16
|
+
export function hasRoadmap(nodeId) {
|
|
17
|
+
return existsSync(roadmapPath(nodeId));
|
|
18
|
+
}
|
|
19
|
+
export function readRoadmap(nodeId) {
|
|
20
|
+
const p = roadmapPath(nodeId);
|
|
21
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
22
|
+
}
|
|
23
|
+
/** Seed a fresh roadmap SCAFFOLD. No goal is required — promotion lays this
|
|
24
|
+
* down so the file exists for a refresh, and the owner authors the goal +
|
|
25
|
+
* body as its next act (guided by its kind's roadmap skill). `goal`/
|
|
26
|
+
* `exitCriteria` are optional overrides. Idempotent only if you intend it —
|
|
27
|
+
* call sites guard on hasRoadmap to avoid clobbering an evolved map. */
|
|
28
|
+
export function seedRoadmap(nodeId, opts = {}) {
|
|
29
|
+
const dir = contextDir(nodeId);
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
const body = `# Roadmap
|
|
32
|
+
|
|
33
|
+
<!-- frozen core: set once, rarely changes -->
|
|
34
|
+
## Goal
|
|
35
|
+
${opts.goal?.trim() ?? '- (state the high-level goal you now own — write this as your first act)'}
|
|
36
|
+
|
|
37
|
+
## Exit criteria
|
|
38
|
+
${opts.exitCriteria?.trim() ?? '- (define what "done" looks like)'}
|
|
39
|
+
|
|
40
|
+
<!-- evolving body: keep this current as you learn scope + intent -->
|
|
41
|
+
## Scope assumptions / non-goals
|
|
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)
|
|
49
|
+
`;
|
|
50
|
+
writeFileSync(roadmapPath(nodeId), body);
|
|
51
|
+
return body;
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type NodeMeta, type Mode } from '../canvas/index.js';
|
|
2
|
+
/** A root's tmux session name — its home; every descendant is a window in it. */
|
|
3
|
+
export declare function rootSessionName(rootId: string): string;
|
|
4
|
+
export interface BootRootOpts {
|
|
5
|
+
cwd: string;
|
|
6
|
+
kind?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
/** Optional starter prompt (bare `crtr` requires none). */
|
|
9
|
+
prompt?: string;
|
|
10
|
+
/** 'inline' — exec pi in the current terminal (bare `crtr`).
|
|
11
|
+
* 'session' — create a dedicated tmux session and run pi there (`session new`). */
|
|
12
|
+
placement: 'inline' | 'session';
|
|
13
|
+
}
|
|
14
|
+
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
15
|
+
* only returns after pi exits (it took over the terminal). */
|
|
16
|
+
export declare function bootRoot(opts: BootRootOpts): NodeMeta;
|
|
17
|
+
export interface SpawnChildOpts {
|
|
18
|
+
kind: string;
|
|
19
|
+
mode?: Mode;
|
|
20
|
+
cwd: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
prompt: string;
|
|
23
|
+
/** Override the parent (defaults to the calling node from env). */
|
|
24
|
+
parent?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SpawnChildResult {
|
|
27
|
+
node: NodeMeta;
|
|
28
|
+
window: string | null;
|
|
29
|
+
session: string;
|
|
30
|
+
}
|
|
31
|
+
/** Spawn a terminal worker as a background window under the root session.
|
|
32
|
+
* The parent auto-subscribes (active) to it via spawnNode. */
|
|
33
|
+
export declare function spawnChild(opts: SpawnChildOpts): SpawnChildResult;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The spawn orchestration — the one place that turns "I want a node here" into
|
|
2
|
+
// a running pi process on the canvas. Composes canvas (birth + spine), persona
|
|
3
|
+
// (resolve), launch (pi argv), and tmux (placement).
|
|
4
|
+
//
|
|
5
|
+
// bootRoot — a user-opened entry point (bare `crtr`).
|
|
6
|
+
// Resident. Runs pi in the foreground (inline) or its own session.
|
|
7
|
+
// spawnChild — a background worker spawned by a live node (`crtr node new`).
|
|
8
|
+
// Terminal. Opens a non-focus-stealing window under the root.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
11
|
+
import { spawnNode, currentNodeContext } from './nodes.js';
|
|
12
|
+
import { buildLaunchSpec, buildPiArgv } from './launch.js';
|
|
13
|
+
import { writeGoal } from './kickoff.js';
|
|
14
|
+
import { ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, } from './tmux.js';
|
|
15
|
+
import { updateNode, getNode } from '../canvas/index.js';
|
|
16
|
+
import { ensureDaemon } from '../../daemon/manage.js';
|
|
17
|
+
/** A root's tmux session name — its home; every descendant is a window in it. */
|
|
18
|
+
export function rootSessionName(rootId) {
|
|
19
|
+
return `crtr-${rootId.replace(/[^a-zA-Z0-9]/g, '').slice(0, 12)}`;
|
|
20
|
+
}
|
|
21
|
+
/** Create a root node and bring up its pi. Returns the node; for 'inline' this
|
|
22
|
+
* only returns after pi exits (it took over the terminal). */
|
|
23
|
+
export function bootRoot(opts) {
|
|
24
|
+
// The thin supervisor must be up before any node exists, so a refresh-yield
|
|
25
|
+
// or crash can be reaped/revived. Idempotent.
|
|
26
|
+
try {
|
|
27
|
+
ensureDaemon();
|
|
28
|
+
}
|
|
29
|
+
catch { /* daemon is best-effort */ }
|
|
30
|
+
const kind = opts.kind ?? 'general';
|
|
31
|
+
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
32
|
+
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
33
|
+
const { launch } = buildLaunchSpec(kind, 'base');
|
|
34
|
+
const meta = spawnNode({
|
|
35
|
+
kind,
|
|
36
|
+
mode: 'base',
|
|
37
|
+
lifecycle: 'resident',
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
name: opts.name ?? kind,
|
|
40
|
+
parent: null,
|
|
41
|
+
launch,
|
|
42
|
+
});
|
|
43
|
+
// Persist the spawning prompt as the goal so a fresh revive can re-read its
|
|
44
|
+
// mandate (bare `crtr` has none — writeGoal no-ops on empty).
|
|
45
|
+
if (opts.prompt !== undefined)
|
|
46
|
+
writeGoal(meta.node_id, opts.prompt);
|
|
47
|
+
const session = rootSessionName(meta.node_id);
|
|
48
|
+
if (opts.placement === 'session') {
|
|
49
|
+
ensureSession(session, opts.cwd);
|
|
50
|
+
updateNode(meta.node_id, { tmux_session: session });
|
|
51
|
+
const withSession = getNode(meta.node_id);
|
|
52
|
+
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
53
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
|
|
54
|
+
const win = openNodeWindow({
|
|
55
|
+
session,
|
|
56
|
+
name: meta.name,
|
|
57
|
+
cwd: opts.cwd,
|
|
58
|
+
env,
|
|
59
|
+
command: piCommand(inv.argv),
|
|
60
|
+
});
|
|
61
|
+
updateNode(meta.node_id, { window: win });
|
|
62
|
+
return getNode(meta.node_id);
|
|
63
|
+
}
|
|
64
|
+
// inline: the root adopts the current tmux session (if any) as its home, so
|
|
65
|
+
// children spawn as windows alongside it. Then exec pi in this terminal.
|
|
66
|
+
const here = currentTmux();
|
|
67
|
+
const adopted = here?.session ?? session;
|
|
68
|
+
updateNode(meta.node_id, { tmux_session: adopted, window: here?.window ?? null });
|
|
69
|
+
const withSession = getNode(meta.node_id);
|
|
70
|
+
const inv = buildPiArgv(withSession, { prompt: opts.prompt });
|
|
71
|
+
const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: adopted, [FRONT_DOOR_ENV]: '1' };
|
|
72
|
+
const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
|
|
73
|
+
process.exit(r.status ?? 0);
|
|
74
|
+
}
|
|
75
|
+
/** Spawn a terminal worker as a background window under the root session.
|
|
76
|
+
* The parent auto-subscribes (active) to it via spawnNode. */
|
|
77
|
+
export function spawnChild(opts) {
|
|
78
|
+
try {
|
|
79
|
+
ensureDaemon();
|
|
80
|
+
}
|
|
81
|
+
catch { /* daemon is best-effort */ }
|
|
82
|
+
const ctx = currentNodeContext();
|
|
83
|
+
const parent = opts.parent ?? ctx.nodeId;
|
|
84
|
+
if (parent === null || parent === undefined) {
|
|
85
|
+
throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
|
|
86
|
+
}
|
|
87
|
+
const mode = opts.mode ?? 'base';
|
|
88
|
+
const { launch } = buildLaunchSpec(opts.kind, mode);
|
|
89
|
+
const meta = spawnNode({
|
|
90
|
+
kind: opts.kind,
|
|
91
|
+
mode,
|
|
92
|
+
lifecycle: 'terminal',
|
|
93
|
+
cwd: opts.cwd,
|
|
94
|
+
name: opts.name ?? opts.kind,
|
|
95
|
+
parent,
|
|
96
|
+
launch,
|
|
97
|
+
});
|
|
98
|
+
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
99
|
+
writeGoal(meta.node_id, opts.prompt);
|
|
100
|
+
// Resolve the root session: inherited from env, else derive + create one.
|
|
101
|
+
let session = process.env['CRTR_ROOT_SESSION'];
|
|
102
|
+
if (session === undefined || session === '') {
|
|
103
|
+
const here = inTmux() ? currentTmux() : null;
|
|
104
|
+
session = here?.session ?? rootSessionName(parent);
|
|
105
|
+
ensureSession(session, opts.cwd);
|
|
106
|
+
}
|
|
107
|
+
const inv = buildPiArgv(meta, { prompt: opts.prompt });
|
|
108
|
+
const env = { ...inv.env, CRTR_ROOT_SESSION: session };
|
|
109
|
+
const window = openNodeWindow({
|
|
110
|
+
session,
|
|
111
|
+
name: meta.name,
|
|
112
|
+
cwd: opts.cwd,
|
|
113
|
+
env,
|
|
114
|
+
command: piCommand(inv.argv),
|
|
115
|
+
});
|
|
116
|
+
const saved = updateNode(meta.node_id, { tmux_session: session, window });
|
|
117
|
+
return { node: saved, window, session };
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StopSignals {
|
|
2
|
+
/** Did the node call `push --final` (finish) this turn? */
|
|
3
|
+
pushedFinal: boolean;
|
|
4
|
+
/** Did the node call `crtr ask` (escalate to the human) this turn? */
|
|
5
|
+
askedHuman: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type StopAction = {
|
|
8
|
+
action: 'allow';
|
|
9
|
+
reason: 'awaiting' | 'finished' | 'escalated' | 'attended';
|
|
10
|
+
} | {
|
|
11
|
+
action: 'reprompt';
|
|
12
|
+
reason: 'stalled';
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const STALL_REPROMPT: string;
|
|
16
|
+
/** Decide what to do when a node stops. Pure given the canvas + this turn's
|
|
17
|
+
* signals — the stophook supplies the signals and enacts the action. */
|
|
18
|
+
export declare function evaluateStop(nodeId: string, signals: StopSignals): StopAction;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The stop-guard — no stalled agents.
|
|
2
|
+
//
|
|
3
|
+
// Every time a node's pi process stops, we ask one question: is this node
|
|
4
|
+
// *legitimately waiting*? A node is legitimately waiting iff it holds an ACTIVE
|
|
5
|
+
// subscription to a node that's still live (active|idle) — something that can
|
|
6
|
+
// actually wake it. (A passive sub won't wake you, so it doesn't count.)
|
|
7
|
+
//
|
|
8
|
+
// • waiting → stopping is correct; it's a dormant orchestrator awaiting
|
|
9
|
+
// its workers. Let it sleep; a child's push wakes it.
|
|
10
|
+
// • finished/asked → it pushed --final (done) or called `crtr ask` this turn.
|
|
11
|
+
// Also fine.
|
|
12
|
+
// • otherwise → it has nothing live to wait for and hasn't resolved.
|
|
13
|
+
// Re-prompt it to finish or escalate. Stalls are impossible.
|
|
14
|
+
import { hasActiveLiveSubscription, getNode } from '../canvas/index.js';
|
|
15
|
+
export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
|
|
16
|
+
'Run `crtr push final "<result>"` if the work is done, or `crtr human ask` if you are blocked or need the user.';
|
|
17
|
+
/** Decide what to do when a node stops. Pure given the canvas + this turn's
|
|
18
|
+
* signals — the stophook supplies the signals and enacts the action. */
|
|
19
|
+
export function evaluateStop(nodeId, signals) {
|
|
20
|
+
if (signals.pushedFinal)
|
|
21
|
+
return { action: 'allow', reason: 'finished' };
|
|
22
|
+
if (signals.askedHuman)
|
|
23
|
+
return { action: 'allow', reason: 'escalated' };
|
|
24
|
+
// A user-opened root (no parent) is human-attended: the human is its wake
|
|
25
|
+
// source, so stopping to await input is always legitimate — never nag it.
|
|
26
|
+
const node = getNode(nodeId);
|
|
27
|
+
if (node !== null && (node.parent === null || node.parent === undefined)) {
|
|
28
|
+
return { action: 'allow', reason: 'attended' };
|
|
29
|
+
}
|
|
30
|
+
if (hasActiveLiveSubscription(nodeId))
|
|
31
|
+
return { action: 'allow', reason: 'awaiting' };
|
|
32
|
+
return { action: 'reprompt', reason: 'stalled', message: STALL_REPROMPT };
|
|
33
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** POSIX single-quote escaping for one shell word. */
|
|
2
|
+
export declare function shellQuote(s: string): string;
|
|
3
|
+
export declare function inTmux(): boolean;
|
|
4
|
+
export interface TmuxLocation {
|
|
5
|
+
session: string;
|
|
6
|
+
window: string;
|
|
7
|
+
pane: string;
|
|
8
|
+
}
|
|
9
|
+
/** Where the caller currently is, or null if not inside tmux. */
|
|
10
|
+
export declare function currentTmux(): TmuxLocation | null;
|
|
11
|
+
export declare function sessionExists(name: string): boolean;
|
|
12
|
+
/** Create a detached session rooted at `cwd` if it doesn't exist. The session
|
|
13
|
+
* name is a root's tmux home; every node under that root is a window in it. */
|
|
14
|
+
export declare function ensureSession(name: string, cwd: string): void;
|
|
15
|
+
export interface OpenWindowOpts {
|
|
16
|
+
session: string;
|
|
17
|
+
/** Window name (the node's display name). */
|
|
18
|
+
name: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
env: Record<string, string>;
|
|
21
|
+
/** The full command to run in the window (already a shell string). */
|
|
22
|
+
command: string;
|
|
23
|
+
}
|
|
24
|
+
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
25
|
+
* detached so it doesn't steal focus or become the current window. Returns the
|
|
26
|
+
* new window id.
|
|
27
|
+
*
|
|
28
|
+
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
29
|
+
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
30
|
+
* Passing a bare session name resolves to the session's *active window*, which
|
|
31
|
+
* makes new-window try to create AT that occupied index and fail with
|
|
32
|
+
* "create window failed: index N in use" whenever the active window is not the
|
|
33
|
+
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
34
|
+
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
35
|
+
* dashboard. */
|
|
36
|
+
export declare function openNodeWindow(opts: OpenWindowOpts): string | null;
|
|
37
|
+
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
38
|
+
export declare function focusWindow(session: string, window: string): boolean;
|
|
39
|
+
/** Close a node's window (drop it from the UI). */
|
|
40
|
+
export declare function closeWindow(window: string): boolean;
|
|
41
|
+
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
42
|
+
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
43
|
+
export declare function paneOfWindow(session: string, window: string): string | null;
|
|
44
|
+
/** The window a pane currently lives in. Used after a swap-pane to learn which
|
|
45
|
+
* slot the caller's pane occupied — pane ids are stable across swaps, windows
|
|
46
|
+
* are not, so the node→window mapping must be re-derived from the pane. Returns
|
|
47
|
+
* null if the pane is gone or tmux fails. */
|
|
48
|
+
export declare function windowOfPane(pane: string): string | null;
|
|
49
|
+
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
50
|
+
* caller's window active, so the target's pane appears where the caller is
|
|
51
|
+
* rather than navigating the client off to the target's window. The caller's
|
|
52
|
+
* old pane lives on in the target's former window — the move is reversible
|
|
53
|
+
* (focusing back swaps it in again). Best-effort; never throws. */
|
|
54
|
+
export declare function swapPaneInPlace(targetPane: string, callerPane: string): boolean;
|
|
55
|
+
export interface RespawnPaneOpts {
|
|
56
|
+
/** Target pane id (e.g. `%3`) — the pane to re-exec in place. */
|
|
57
|
+
pane: string;
|
|
58
|
+
cwd: string;
|
|
59
|
+
env: Record<string, string>;
|
|
60
|
+
/** The full command to run in the pane (already a shell string). */
|
|
61
|
+
command: string;
|
|
62
|
+
}
|
|
63
|
+
/** Re-exec a command in an EXISTING pane, in place. `-k` kills the pane's
|
|
64
|
+
* current process (e.g. a yielding pi) and starts `command` in the same pane
|
|
65
|
+
* — the window/pane survives, so an interactive session is never dropped to a
|
|
66
|
+
* shell and no window churns. Used by refresh-yield.
|
|
67
|
+
*
|
|
68
|
+
* Spawned DETACHED (own process group, unref'd) so the request reaches the
|
|
69
|
+
* tmux server even though killing the pane tears down the caller's own pi.
|
|
70
|
+
* Returns true once the request was dispatched. */
|
|
71
|
+
export declare function respawnPane(opts: RespawnPaneOpts): boolean;
|
|
72
|
+
/** Turn a pi argv array into a single shell command string. */
|
|
73
|
+
export declare function piCommand(argv: string[], binary?: string): string;
|
|
74
|
+
/** List all window ids present in `session`. Returns [] if the session does
|
|
75
|
+
* not exist or tmux fails for any reason. Each entry is the raw window id
|
|
76
|
+
* string reported by tmux (e.g. `@1`, `@2`, …). */
|
|
77
|
+
export declare function listWindowIds(session: string): string[];
|
|
78
|
+
/** True when both `session` and `window` are present (non-null/undefined) and
|
|
79
|
+
* the window currently exists inside the session. False whenever either arg
|
|
80
|
+
* is absent, the session is gone, or tmux does not know the window. */
|
|
81
|
+
export declare function windowAlive(session: string | null | undefined, window: string | null | undefined): boolean;
|
|
82
|
+
/** Activate a window within its session (same-session navigation). Equivalent
|
|
83
|
+
* to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
|
|
84
|
+
export declare function selectWindow(session: string, window: string): boolean;
|
|
85
|
+
/** Switch the tmux client to a different session (cross-session focus). Runs
|
|
86
|
+
* `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
|
|
87
|
+
* responsible for following up with selectWindow to land on the right window. */
|
|
88
|
+
export declare function switchClient(session: string): boolean;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// tmux placement — one window per active node.
|
|
2
|
+
//
|
|
3
|
+
// session = a root window = a node window 0 = optional dashboard
|
|
4
|
+
//
|
|
5
|
+
// Background windows run but don't render — only the current window draws. That
|
|
6
|
+
// is the "detached but switchable" model: nothing tiles, you never see a node's
|
|
7
|
+
// UI unless you switch to it. Bring one forefront with select-window (within a
|
|
8
|
+
// root) or switch-client + select-window (across roots). done/dead nodes close
|
|
9
|
+
// their window; reviving opens a fresh one.
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Shell quoting + tmux invocation
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** POSIX single-quote escaping for one shell word. */
|
|
15
|
+
export function shellQuote(s) {
|
|
16
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
17
|
+
}
|
|
18
|
+
function tmux(args) {
|
|
19
|
+
const r = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
20
|
+
return {
|
|
21
|
+
ok: r.status === 0,
|
|
22
|
+
stdout: (r.stdout ?? '').trim(),
|
|
23
|
+
stderr: (r.stderr ?? '').trim(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function inTmux() {
|
|
27
|
+
return process.env['TMUX'] !== undefined && process.env['TMUX'] !== '';
|
|
28
|
+
}
|
|
29
|
+
/** Where the caller currently is, or null if not inside tmux. */
|
|
30
|
+
export function currentTmux() {
|
|
31
|
+
if (!inTmux())
|
|
32
|
+
return null;
|
|
33
|
+
const r = tmux([
|
|
34
|
+
'display-message',
|
|
35
|
+
'-p',
|
|
36
|
+
'#{session_name}\t#{window_id}\t#{pane_id}',
|
|
37
|
+
]);
|
|
38
|
+
if (!r.ok)
|
|
39
|
+
return null;
|
|
40
|
+
const [session, window, pane] = r.stdout.split('\t');
|
|
41
|
+
return { session, window, pane };
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Sessions + windows
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export function sessionExists(name) {
|
|
47
|
+
return tmux(['has-session', '-t', name]).ok;
|
|
48
|
+
}
|
|
49
|
+
/** Create a detached session rooted at `cwd` if it doesn't exist. The session
|
|
50
|
+
* name is a root's tmux home; every node under that root is a window in it. */
|
|
51
|
+
export function ensureSession(name, cwd) {
|
|
52
|
+
if (sessionExists(name))
|
|
53
|
+
return;
|
|
54
|
+
tmux(['new-session', '-d', '-s', name, '-c', cwd]);
|
|
55
|
+
}
|
|
56
|
+
function envFlags(env) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const [k, v] of Object.entries(env))
|
|
59
|
+
out.push('-e', `${k}=${v}`);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** Open a background window for a node and run `command` in it. `-d` keeps it
|
|
63
|
+
* detached so it doesn't steal focus or become the current window. Returns the
|
|
64
|
+
* new window id.
|
|
65
|
+
*
|
|
66
|
+
* Target is `${session}:` (trailing colon = the session, no window index) plus
|
|
67
|
+
* `-a` (insert after the current window) so tmux allocates the next free index.
|
|
68
|
+
* Passing a bare session name resolves to the session's *active window*, which
|
|
69
|
+
* makes new-window try to create AT that occupied index and fail with
|
|
70
|
+
* "create window failed: index N in use" whenever the active window is not the
|
|
71
|
+
* last one (common when base-index is 0 but the live window sits at index 1).
|
|
72
|
+
* `-a` also keeps node windows off index 0, which is reserved for the optional
|
|
73
|
+
* dashboard. */
|
|
74
|
+
export function openNodeWindow(opts) {
|
|
75
|
+
const r = tmux([
|
|
76
|
+
'new-window',
|
|
77
|
+
'-d',
|
|
78
|
+
'-a',
|
|
79
|
+
'-P',
|
|
80
|
+
'-F',
|
|
81
|
+
'#{window_id}',
|
|
82
|
+
'-t',
|
|
83
|
+
`${opts.session}:`,
|
|
84
|
+
'-n',
|
|
85
|
+
opts.name,
|
|
86
|
+
'-c',
|
|
87
|
+
opts.cwd,
|
|
88
|
+
...envFlags(opts.env),
|
|
89
|
+
opts.command,
|
|
90
|
+
]);
|
|
91
|
+
return r.ok ? r.stdout : null;
|
|
92
|
+
}
|
|
93
|
+
/** Bring a node's window forefront. Switches client across roots when needed. */
|
|
94
|
+
export function focusWindow(session, window) {
|
|
95
|
+
const here = currentTmux();
|
|
96
|
+
const sameRoot = here?.session === session;
|
|
97
|
+
if (!sameRoot) {
|
|
98
|
+
if (!tmux(['switch-client', '-t', session]).ok)
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return tmux(['select-window', '-t', window]).ok;
|
|
102
|
+
}
|
|
103
|
+
/** Close a node's window (drop it from the UI). */
|
|
104
|
+
export function closeWindow(window) {
|
|
105
|
+
return tmux(['kill-window', '-t', window]).ok;
|
|
106
|
+
}
|
|
107
|
+
/** The active pane id of a window. Node windows are single-pane, so this is the
|
|
108
|
+
* node's pane. Returns null if the window is gone or tmux fails. */
|
|
109
|
+
export function paneOfWindow(session, window) {
|
|
110
|
+
const r = tmux(['display-message', '-p', '-t', `${session}:${window}`, '#{pane_id}']);
|
|
111
|
+
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
112
|
+
}
|
|
113
|
+
/** The window a pane currently lives in. Used after a swap-pane to learn which
|
|
114
|
+
* slot the caller's pane occupied — pane ids are stable across swaps, windows
|
|
115
|
+
* are not, so the node→window mapping must be re-derived from the pane. Returns
|
|
116
|
+
* null if the pane is gone or tmux fails. */
|
|
117
|
+
export function windowOfPane(pane) {
|
|
118
|
+
const r = tmux(['display-message', '-p', '-t', pane, '#{window_id}']);
|
|
119
|
+
return r.ok && r.stdout !== '' ? r.stdout : null;
|
|
120
|
+
}
|
|
121
|
+
/** Swap `targetPane` into `callerPane`'s layout slot, IN PLACE. `-d` keeps the
|
|
122
|
+
* caller's window active, so the target's pane appears where the caller is
|
|
123
|
+
* rather than navigating the client off to the target's window. The caller's
|
|
124
|
+
* old pane lives on in the target's former window — the move is reversible
|
|
125
|
+
* (focusing back swaps it in again). Best-effort; never throws. */
|
|
126
|
+
export function swapPaneInPlace(targetPane, callerPane) {
|
|
127
|
+
if (targetPane === callerPane)
|
|
128
|
+
return true;
|
|
129
|
+
return tmux(['swap-pane', '-d', '-s', targetPane, '-t', callerPane]).ok;
|
|
130
|
+
}
|
|
131
|
+
/** Re-exec a command in an EXISTING pane, in place. `-k` kills the pane's
|
|
132
|
+
* current process (e.g. a yielding pi) and starts `command` in the same pane
|
|
133
|
+
* — the window/pane survives, so an interactive session is never dropped to a
|
|
134
|
+
* shell and no window churns. Used by refresh-yield.
|
|
135
|
+
*
|
|
136
|
+
* Spawned DETACHED (own process group, unref'd) so the request reaches the
|
|
137
|
+
* tmux server even though killing the pane tears down the caller's own pi.
|
|
138
|
+
* Returns true once the request was dispatched. */
|
|
139
|
+
export function respawnPane(opts) {
|
|
140
|
+
try {
|
|
141
|
+
const child = spawn('tmux', [
|
|
142
|
+
'respawn-pane',
|
|
143
|
+
'-k',
|
|
144
|
+
'-c',
|
|
145
|
+
opts.cwd,
|
|
146
|
+
...envFlags(opts.env),
|
|
147
|
+
'-t',
|
|
148
|
+
opts.pane,
|
|
149
|
+
opts.command,
|
|
150
|
+
], { detached: true, stdio: 'ignore' });
|
|
151
|
+
child.unref();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// pi command assembly
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/** Turn a pi argv array into a single shell command string. */
|
|
162
|
+
export function piCommand(argv, binary = 'pi') {
|
|
163
|
+
return [binary, ...argv.map(shellQuote)].join(' ');
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Window liveness helpers (used by the supervisor daemon)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
/** List all window ids present in `session`. Returns [] if the session does
|
|
169
|
+
* not exist or tmux fails for any reason. Each entry is the raw window id
|
|
170
|
+
* string reported by tmux (e.g. `@1`, `@2`, …). */
|
|
171
|
+
export function listWindowIds(session) {
|
|
172
|
+
const r = tmux(['list-windows', '-t', session, '-F', '#{window_id}']);
|
|
173
|
+
if (!r.ok || r.stdout === '')
|
|
174
|
+
return [];
|
|
175
|
+
return r.stdout.split('\n').filter((s) => s !== '');
|
|
176
|
+
}
|
|
177
|
+
/** True when both `session` and `window` are present (non-null/undefined) and
|
|
178
|
+
* the window currently exists inside the session. False whenever either arg
|
|
179
|
+
* is absent, the session is gone, or tmux does not know the window. */
|
|
180
|
+
export function windowAlive(session, window) {
|
|
181
|
+
if (session == null || window == null)
|
|
182
|
+
return false;
|
|
183
|
+
return listWindowIds(session).includes(window);
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Focus helpers (used by the presence layer)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/** Activate a window within its session (same-session navigation). Equivalent
|
|
189
|
+
* to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
|
|
190
|
+
export function selectWindow(session, window) {
|
|
191
|
+
return tmux(['select-window', '-t', `${session}:${window}`]).ok;
|
|
192
|
+
}
|
|
193
|
+
/** Switch the tmux client to a different session (cross-session focus). Runs
|
|
194
|
+
* `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
|
|
195
|
+
* responsible for following up with selectWindow to land on the right window. */
|
|
196
|
+
export function switchClient(session) {
|
|
197
|
+
return tmux(['switch-client', '-t', session]).ok;
|
|
198
|
+
}
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,100 +1,37 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Max panes per tmux window before overflowing to a new window. */
|
|
12
|
-
maxPanesPerWindow: number;
|
|
13
|
-
/** Display name passed to `claude -n`; surfaces in pane title and /resume picker. */
|
|
14
|
-
name?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface SpawnAgentResult {
|
|
17
|
-
status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
|
|
18
|
-
/** tmux pane id of the spawned pane. */
|
|
19
|
-
paneId?: string;
|
|
20
|
-
/** How the pane was placed. */
|
|
21
|
-
placement?: 'split-window' | 'new-window';
|
|
22
|
-
message: string;
|
|
23
|
-
}
|
|
1
|
+
export declare function isInTmux(): boolean;
|
|
2
|
+
export declare function shellQuote(s: string): string;
|
|
3
|
+
/** Count panes in the current tmux window (0 outside tmux / on error). */
|
|
4
|
+
export declare function countPanesInCurrentWindow(): number;
|
|
5
|
+
/**
|
|
6
|
+
* Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
|
|
7
|
+
* so the caller can return normally before the pane dies. No-op outside tmux,
|
|
8
|
+
* when TMUX_PANE is unset, or when delaySeconds <= 0.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
|
|
24
11
|
export interface DetachOptions {
|
|
25
|
-
/** Inner command to run in the pane.
|
|
26
|
-
command
|
|
27
|
-
/** Full first user message for the new claude session (claude mode only;
|
|
28
|
-
* ignored when `command` is set). No custom system prompt. */
|
|
29
|
-
prompt?: string;
|
|
12
|
+
/** Inner command to run in the new pane. */
|
|
13
|
+
command: string;
|
|
30
14
|
cwd: string;
|
|
31
|
-
/**
|
|
32
|
-
* `_fail` guard. Optional only when `failGuard` is false. */
|
|
15
|
+
/** Optional id injected as the CRTR_JOB_ID env var in the pane. */
|
|
33
16
|
jobId?: string;
|
|
34
17
|
/** Where to open the new pane. */
|
|
35
18
|
placement: 'split-h' | 'split-v' | 'new-window';
|
|
36
|
-
/** Seconds
|
|
19
|
+
/** Seconds before killing the originating pane so the caller can finish. */
|
|
37
20
|
killAfterSeconds: number;
|
|
38
|
-
/** Append `; crtr job _fail <jobId>` and inject CRTR_JOB_ID. Default true. */
|
|
39
|
-
failGuard?: boolean;
|
|
40
21
|
/** Pin the new pane to this tmux pane: split-window splits it; new-window is
|
|
41
22
|
* inserted immediately after its window (-a -t <pane>). Without this, tmux
|
|
42
23
|
* uses the attached client's currently-focused pane — which drifts if the
|
|
43
24
|
* user switches windows between kickoff and spawn. */
|
|
44
25
|
targetPane?: string;
|
|
45
|
-
/** Display name passed to `claude -n`; ignored when `command` is set
|
|
46
|
-
* (caller controls the full argv in that mode). */
|
|
47
|
-
name?: string;
|
|
48
26
|
}
|
|
49
27
|
export interface DetachResult {
|
|
50
28
|
status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
|
|
51
29
|
paneId?: string;
|
|
52
30
|
message: string;
|
|
53
31
|
}
|
|
54
|
-
export declare function isInTmux(): boolean;
|
|
55
|
-
export declare function shellQuote(s: string): string;
|
|
56
|
-
export declare function countPanesInCurrentWindow(): number;
|
|
57
|
-
/**
|
|
58
|
-
* Find a window in the current tmux session with fewer than `maxPanesPerWindow`
|
|
59
|
-
* panes AND where every existing pane has `claude` as a foreground process.
|
|
60
|
-
* Prefers the active window so the spawned pane is visible to the user;
|
|
61
|
-
* otherwise falls back to the first other eligible window. Returns the tmux
|
|
62
|
-
* window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
|
|
63
|
-
*
|
|
64
|
-
* Windows holding non-agent panes (dashboards, log tails, idle shells, editors,
|
|
65
|
-
* REPLs, etc.) are skipped so spawning never disrupts those workflows. A pane
|
|
66
|
-
* qualifies as long as `claude` is among its foreground commands — co-resident
|
|
67
|
-
* helpers like `caffeinate` don't disqualify it.
|
|
68
|
-
*/
|
|
69
|
-
export declare function findWindowWithSpace(maxPanesPerWindow: number): string | null;
|
|
70
32
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* Used by `crtr job submit` (kill_pane=true) so a reviewer agent can self-close
|
|
76
|
-
* its pane after delivering its verdict, and by `spawnAndDetach` for handoff
|
|
77
|
-
* self-kill.
|
|
78
|
-
*/
|
|
79
|
-
export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
|
|
80
|
-
/**
|
|
81
|
-
* Fire-and-forget: launch an interactive `claude` in a new pane (or window),
|
|
82
|
-
* then schedule the originating pane to be killed after `killAfterSeconds`.
|
|
83
|
-
*
|
|
84
|
-
* No custom system prompt — the task is delivered as the first user message.
|
|
85
|
-
* Returns as soon as the new pane is up; does NOT wait for claude to finish.
|
|
33
|
+
* Fire-and-forget: launch `opts.command` in a new pane (or window), then
|
|
34
|
+
* schedule the originating pane to be killed after `killAfterSeconds`. Returns
|
|
35
|
+
* as soon as the new pane is up; does NOT wait for the command to finish.
|
|
86
36
|
*/
|
|
87
37
|
export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
|
|
88
|
-
/**
|
|
89
|
-
* Async sibling spawn. Launches a claude session in a tmux pane, progressively
|
|
90
|
-
* filling existing windows up to `maxPanesPerWindow` before creating a new
|
|
91
|
-
* window. Returns immediately with the pane id; the parent stays alive.
|
|
92
|
-
*
|
|
93
|
-
* Placement order:
|
|
94
|
-
* 1. Current window, if it has space.
|
|
95
|
-
* 2. Any other window in the session with space.
|
|
96
|
-
* 3. New window (every existing window at capacity).
|
|
97
|
-
*
|
|
98
|
-
* If `fork` is set, uses `claude --resume <id> --fork-session`.
|
|
99
|
-
*/
|
|
100
|
-
export declare function spawnAgent(opts: SpawnAgentOptions): SpawnAgentResult;
|