@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.
Files changed (233) hide show
  1. package/dist/build-root.d.ts +8 -0
  2. package/dist/build-root.js +30 -0
  3. package/dist/builtin-personas/design/base.md +3 -7
  4. package/dist/builtin-personas/design/orchestrator.md +4 -3
  5. package/dist/builtin-personas/developer/base.md +3 -7
  6. package/dist/builtin-personas/developer/orchestrator.md +5 -4
  7. package/dist/builtin-personas/explore/base.md +3 -7
  8. package/dist/builtin-personas/explore/orchestrator.md +1 -5
  9. package/dist/builtin-personas/general/base.md +2 -4
  10. package/dist/builtin-personas/general/orchestrator.md +2 -4
  11. package/dist/builtin-personas/lifecycle/resident.md +2 -0
  12. package/dist/builtin-personas/lifecycle/terminal.md +6 -0
  13. package/dist/builtin-personas/orchestration-kernel.md +42 -3
  14. package/dist/builtin-personas/plan/base.md +3 -5
  15. package/dist/builtin-personas/plan/orchestrator.md +5 -4
  16. package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
  17. package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
  18. package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
  19. package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
  20. package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
  21. package/dist/builtin-personas/review/base.md +3 -5
  22. package/dist/builtin-personas/review/orchestrator.md +2 -6
  23. package/dist/builtin-personas/runtime-base.md +3 -19
  24. package/dist/builtin-personas/spec/base.md +3 -5
  25. package/dist/builtin-personas/spec/orchestrator.md +4 -3
  26. package/dist/builtin-personas/spine/has-manager.md +10 -0
  27. package/dist/builtin-personas/spine/no-manager.md +2 -0
  28. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
  29. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
  30. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
  31. package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
  32. package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
  33. package/dist/cli.js +6 -29
  34. package/dist/commands/__tests__/human.test.js +73 -2
  35. package/dist/commands/attention.js +76 -7
  36. package/dist/commands/canvas-prune.d.ts +2 -0
  37. package/dist/commands/canvas-prune.js +66 -0
  38. package/dist/commands/canvas.js +5 -8
  39. package/dist/commands/chord.d.ts +2 -0
  40. package/dist/commands/chord.js +143 -0
  41. package/dist/commands/daemon.js +8 -5
  42. package/dist/commands/dashboard.js +2 -0
  43. package/dist/commands/human/prompts.js +28 -27
  44. package/dist/commands/human/queue.d.ts +1 -0
  45. package/dist/commands/human/queue.js +105 -2
  46. package/dist/commands/human/shared.d.ts +28 -18
  47. package/dist/commands/human/shared.js +53 -60
  48. package/dist/commands/human.js +6 -14
  49. package/dist/commands/node.d.ts +11 -0
  50. package/dist/commands/node.js +381 -87
  51. package/dist/commands/pkg/market-inspect.js +6 -4
  52. package/dist/commands/pkg/market-manage.js +10 -6
  53. package/dist/commands/pkg/market.js +2 -4
  54. package/dist/commands/pkg/plugin-inspect.js +6 -4
  55. package/dist/commands/pkg/plugin-manage.js +12 -7
  56. package/dist/commands/pkg/plugin.js +2 -4
  57. package/dist/commands/pkg.js +0 -4
  58. package/dist/commands/push.js +178 -15
  59. package/dist/commands/revive.js +5 -3
  60. package/dist/commands/skill/author.js +6 -4
  61. package/dist/commands/skill/find.js +8 -5
  62. package/dist/commands/skill/read.js +2 -0
  63. package/dist/commands/skill/state.js +6 -4
  64. package/dist/commands/skill.js +0 -6
  65. package/dist/commands/sys/config.js +21 -7
  66. package/dist/commands/sys/doctor.js +2 -0
  67. package/dist/commands/sys/update.js +4 -0
  68. package/dist/commands/sys.js +0 -6
  69. package/dist/commands/tmux-spread.d.ts +2 -0
  70. package/dist/commands/tmux-spread.js +130 -0
  71. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  72. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  73. package/dist/core/__tests__/child-followup.test.js +83 -0
  74. package/dist/core/__tests__/close.test.d.ts +1 -0
  75. package/dist/core/__tests__/close.test.js +148 -0
  76. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  77. package/dist/core/__tests__/context-intro.test.js +196 -0
  78. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  80. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  81. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  82. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  83. package/dist/core/__tests__/focuses.test.js +259 -0
  84. package/dist/core/__tests__/fork.test.d.ts +1 -0
  85. package/dist/core/__tests__/fork.test.js +91 -0
  86. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  87. package/dist/core/__tests__/home-session.test.js +153 -0
  88. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  89. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  90. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  91. package/dist/core/__tests__/keystone.test.js +185 -0
  92. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  93. package/dist/core/__tests__/kickoff.test.js +89 -0
  94. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  95. package/dist/core/__tests__/lifecycle.test.js +178 -0
  96. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  97. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  98. package/dist/core/__tests__/memory.test.d.ts +1 -0
  99. package/dist/core/__tests__/memory.test.js +152 -0
  100. package/dist/core/__tests__/migration.test.d.ts +1 -0
  101. package/dist/core/__tests__/migration.test.js +238 -0
  102. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  103. package/dist/core/__tests__/pane-column.test.js +153 -0
  104. package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
  105. package/dist/core/__tests__/passive-subscription.test.js +164 -0
  106. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  107. package/dist/core/__tests__/persona-compose.test.js +53 -0
  108. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  109. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  110. package/dist/core/__tests__/persona.test.d.ts +1 -0
  111. package/dist/core/__tests__/persona.test.js +107 -0
  112. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  113. package/dist/core/__tests__/placement-focus.test.js +244 -0
  114. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  115. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  116. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  117. package/dist/core/__tests__/placement-revive.test.js +238 -0
  118. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  119. package/dist/core/__tests__/placement-teardown.test.js +183 -0
  120. package/dist/core/__tests__/prune.test.d.ts +1 -0
  121. package/dist/core/__tests__/prune.test.js +116 -0
  122. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  123. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  124. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  125. package/dist/core/__tests__/relaunch.test.js +328 -0
  126. package/dist/core/__tests__/reset.test.js +26 -7
  127. package/dist/core/__tests__/revive.test.d.ts +1 -0
  128. package/dist/core/__tests__/revive.test.js +217 -0
  129. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  130. package/dist/core/__tests__/spawn-root.test.js +73 -0
  131. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  132. package/dist/core/__tests__/steer-note.test.js +39 -0
  133. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  134. package/dist/core/__tests__/stop-guard.test.js +82 -0
  135. package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
  136. package/dist/core/__tests__/subcommand-tier.test.js +99 -0
  137. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  138. package/dist/core/__tests__/tmux-surface.test.js +106 -0
  139. package/dist/core/__tests__/unknown-path.test.js +8 -2
  140. package/dist/core/canvas/attention.d.ts +10 -0
  141. package/dist/core/canvas/attention.js +40 -0
  142. package/dist/core/canvas/canvas.d.ts +66 -7
  143. package/dist/core/canvas/canvas.js +209 -21
  144. package/dist/core/canvas/db.d.ts +8 -0
  145. package/dist/core/canvas/db.js +206 -4
  146. package/dist/core/canvas/focuses.d.ts +22 -0
  147. package/dist/core/canvas/focuses.js +80 -0
  148. package/dist/core/canvas/index.d.ts +3 -0
  149. package/dist/core/canvas/index.js +3 -0
  150. package/dist/core/canvas/labels.d.ts +27 -0
  151. package/dist/core/canvas/labels.js +36 -0
  152. package/dist/core/canvas/paths.d.ts +4 -0
  153. package/dist/core/canvas/paths.js +6 -0
  154. package/dist/core/canvas/render.js +25 -10
  155. package/dist/core/canvas/telemetry.d.ts +14 -0
  156. package/dist/core/canvas/telemetry.js +35 -0
  157. package/dist/core/canvas/types.d.ts +115 -12
  158. package/dist/core/command.d.ts +25 -1
  159. package/dist/core/command.js +48 -7
  160. package/dist/core/config.js +36 -2
  161. package/dist/core/feed/feed.js +14 -12
  162. package/dist/core/feed/inbox.d.ts +3 -1
  163. package/dist/core/feed/inbox.js +45 -5
  164. package/dist/core/feed/passive.d.ts +17 -0
  165. package/dist/core/feed/passive.js +92 -0
  166. package/dist/core/help.d.ts +59 -13
  167. package/dist/core/help.js +73 -28
  168. package/dist/core/personas/index.d.ts +1 -1
  169. package/dist/core/personas/index.js +1 -1
  170. package/dist/core/personas/loader.d.ts +40 -1
  171. package/dist/core/personas/loader.js +63 -1
  172. package/dist/core/personas/resolve.d.ts +13 -6
  173. package/dist/core/personas/resolve.js +46 -34
  174. package/dist/core/runtime/bearings.d.ts +20 -0
  175. package/dist/core/runtime/bearings.js +92 -0
  176. package/dist/core/runtime/close.d.ts +14 -0
  177. package/dist/core/runtime/close.js +151 -0
  178. package/dist/core/runtime/demote.d.ts +14 -0
  179. package/dist/core/runtime/demote.js +120 -0
  180. package/dist/core/runtime/front-door.js +1 -1
  181. package/dist/core/runtime/kickoff.d.ts +32 -6
  182. package/dist/core/runtime/kickoff.js +111 -37
  183. package/dist/core/runtime/launch.d.ts +29 -6
  184. package/dist/core/runtime/launch.js +85 -13
  185. package/dist/core/runtime/lifecycle.d.ts +13 -0
  186. package/dist/core/runtime/lifecycle.js +86 -0
  187. package/dist/core/runtime/memory.d.ts +43 -0
  188. package/dist/core/runtime/memory.js +165 -0
  189. package/dist/core/runtime/naming.d.ts +22 -0
  190. package/dist/core/runtime/naming.js +166 -0
  191. package/dist/core/runtime/nodes.d.ts +32 -1
  192. package/dist/core/runtime/nodes.js +60 -10
  193. package/dist/core/runtime/persona.d.ts +25 -0
  194. package/dist/core/runtime/persona.js +139 -0
  195. package/dist/core/runtime/placement.d.ts +287 -0
  196. package/dist/core/runtime/placement.js +663 -0
  197. package/dist/core/runtime/presence.d.ts +7 -32
  198. package/dist/core/runtime/presence.js +90 -110
  199. package/dist/core/runtime/promote.d.ts +18 -7
  200. package/dist/core/runtime/promote.js +70 -65
  201. package/dist/core/runtime/reset.d.ts +47 -4
  202. package/dist/core/runtime/reset.js +223 -52
  203. package/dist/core/runtime/revive.d.ts +26 -2
  204. package/dist/core/runtime/revive.js +166 -39
  205. package/dist/core/runtime/roadmap.d.ts +5 -4
  206. package/dist/core/runtime/roadmap.js +9 -16
  207. package/dist/core/runtime/spawn.d.ts +20 -5
  208. package/dist/core/runtime/spawn.js +169 -44
  209. package/dist/core/runtime/stop-guard.d.ts +1 -1
  210. package/dist/core/runtime/stop-guard.js +18 -8
  211. package/dist/core/runtime/tmux.d.ts +106 -21
  212. package/dist/core/runtime/tmux.js +249 -45
  213. package/dist/core/spawn.js +15 -0
  214. package/dist/daemon/crtrd.d.ts +12 -1
  215. package/dist/daemon/crtrd.js +152 -34
  216. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  217. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  218. package/dist/pi-extensions/canvas-commands.d.ts +34 -0
  219. package/dist/pi-extensions/canvas-commands.js +103 -0
  220. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  221. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  222. package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
  223. package/dist/pi-extensions/canvas-goal-capture.js +67 -0
  224. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  225. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  226. package/dist/pi-extensions/canvas-nav.js +586 -262
  227. package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
  228. package/dist/pi-extensions/canvas-passive-context.js +114 -0
  229. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  230. package/dist/pi-extensions/canvas-stophook.js +344 -228
  231. package/dist/types.d.ts +28 -0
  232. package/dist/types.js +16 -0
  233. 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 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 —
23
+ /** Seed a fresh, EXTREMELY BAREBONES roadmap scaffoldjust 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() ?? '- (state the high-level goal you now own — write this as your first act)'}
35
+ ${opts.goal?.trim() ?? '(the goal you now own)'}
36
36
 
37
37
  ## Exit criteria
38
- ${opts.exitCriteria?.trim() ?? '- (define what "done" looks like)'}
38
+ ${opts.exitCriteria?.trim() ?? '(what "done" looks like)'}
39
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)
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 terminal worker as a background window under the root session.
30
- * The parent auto-subscribes (active) to it via spawnNode. */
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 — 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.
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 { ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, nodeSession, installMenuBinding, } from './tmux.js';
15
- import { updateNode, getNode } from '../canvas/index.js';
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 (idempotent, in-tmux only).
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
- if (opts.placement === 'session') {
54
- updateNode(meta.node_id, { tmux_session: session });
55
- const withSession = getNode(meta.node_id);
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?.session ?? session;
74
- updateNode(meta.node_id, { tmux_session: adopted, window: here?.window ?? null });
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
- /** Spawn a terminal worker as a background window under the root session.
82
- * The parent auto-subscribes (active) to it via spawnNode. */
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 parent = opts.parent ?? ctx.nodeId;
90
- if (parent === null || parent === undefined) {
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
- const { launch } = buildLaunchSpec(opts.kind, mode);
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: 'terminal',
154
+ lifecycle,
99
155
  cwd: opts.cwd,
100
156
  name: opts.name ?? opts.kind,
101
- parent,
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
- // Children always land in the shared global session: inherited from the
107
- // parent's CRTR_ROOT_SESSION, else the default node session.
108
- let session = process.env['CRTR_ROOT_SESSION'];
109
- if (session === undefined || session === '')
110
- session = nodeSession();
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
- const inv = buildPiArgv(meta, { prompt: opts.prompt });
113
- const env = { ...inv.env, CRTR_ROOT_SESSION: session };
114
- const window = openNodeWindow({
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.name,
215
+ name: fullName(meta),
117
216
  cwd: opts.cwd,
118
217
  env,
119
- command: piCommand(inv.argv),
218
+ command,
120
219
  });
121
- const saved = updateNode(meta.node_id, { tmux_session: session, window });
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
  }
@@ -6,7 +6,7 @@ export interface StopSignals {
6
6
  }
7
7
  export type StopAction = {
8
8
  action: 'allow';
9
- reason: 'awaiting' | 'finished' | 'escalated' | 'attended';
9
+ reason: 'awaiting' | 'finished' | 'escalated' | 'dormant';
10
10
  } | {
11
11
  action: 'reprompt';
12
12
  reason: 'stalled';
@@ -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
- // • waiting stopping is correct; it's a dormant orchestrator awaiting
9
- // its workers. Let it sleep; a child's push wakes it.
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 → it has nothing live to wait for and hasn't resolved.
13
- // Re-prompt it to finish or escalate. Stalls are impossible.
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 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.
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 && (node.parent === null || node.parent === undefined)) {
28
- return { action: 'allow', reason: 'attended' };
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
- export declare function openNodeWindow(opts: OpenWindowOpts): string | null;
42
- /** Open a background window running a plain login shell (no pi) and return its
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. `-k` kills the pane's
81
- * current process (e.g. a yielding pi) and starts `command` in the same pane
82
- * the window/pane survives, so an interactive session is never dropped to a
83
- * shell and no window churns. Used by refresh-yield.
84
- *
85
- * Spawned DETACHED (own process group, unref'd) so the request reaches the
86
- * tmux server even though killing the pane tears down the caller's own pi.
87
- * Returns true once the request was dispatched. */
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
- /** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails. */
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;