@crouton-kit/crouter 0.3.14 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) 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/attention.js +76 -7
  35. package/dist/commands/canvas-prune.d.ts +2 -0
  36. package/dist/commands/canvas-prune.js +66 -0
  37. package/dist/commands/canvas.js +5 -8
  38. package/dist/commands/chord.d.ts +2 -0
  39. package/dist/commands/chord.js +143 -0
  40. package/dist/commands/daemon.js +8 -5
  41. package/dist/commands/dashboard.js +2 -0
  42. package/dist/commands/human/prompts.js +28 -27
  43. package/dist/commands/human/queue.js +30 -14
  44. package/dist/commands/human/shared.d.ts +26 -21
  45. package/dist/commands/human/shared.js +44 -66
  46. package/dist/commands/human.js +4 -14
  47. package/dist/commands/node.d.ts +11 -0
  48. package/dist/commands/node.js +221 -98
  49. package/dist/commands/pkg/market-inspect.js +6 -4
  50. package/dist/commands/pkg/market-manage.js +10 -6
  51. package/dist/commands/pkg/market.js +2 -4
  52. package/dist/commands/pkg/plugin-inspect.js +6 -4
  53. package/dist/commands/pkg/plugin-manage.js +12 -7
  54. package/dist/commands/pkg/plugin.js +2 -4
  55. package/dist/commands/pkg.js +0 -4
  56. package/dist/commands/push.js +178 -15
  57. package/dist/commands/revive.js +5 -3
  58. package/dist/commands/skill/author.js +6 -4
  59. package/dist/commands/skill/find.js +8 -5
  60. package/dist/commands/skill/read.js +2 -0
  61. package/dist/commands/skill/state.js +6 -4
  62. package/dist/commands/skill.js +0 -6
  63. package/dist/commands/sys/config.js +21 -7
  64. package/dist/commands/sys/doctor.js +2 -0
  65. package/dist/commands/sys/update.js +4 -0
  66. package/dist/commands/sys.js +0 -6
  67. package/dist/commands/tmux-spread.d.ts +2 -0
  68. package/dist/commands/tmux-spread.js +130 -0
  69. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  70. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  71. package/dist/core/__tests__/child-followup.test.js +83 -0
  72. package/dist/core/__tests__/close.test.d.ts +1 -0
  73. package/dist/core/__tests__/close.test.js +148 -0
  74. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  75. package/dist/core/__tests__/context-intro.test.js +196 -0
  76. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  77. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  78. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  80. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  81. package/dist/core/__tests__/focuses.test.js +259 -0
  82. package/dist/core/__tests__/fork.test.d.ts +1 -0
  83. package/dist/core/__tests__/fork.test.js +91 -0
  84. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  85. package/dist/core/__tests__/home-session.test.js +153 -0
  86. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  87. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  88. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  89. package/dist/core/__tests__/keystone.test.js +185 -0
  90. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  91. package/dist/core/__tests__/kickoff.test.js +89 -0
  92. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  93. package/dist/core/__tests__/lifecycle.test.js +178 -0
  94. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  95. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  96. package/dist/core/__tests__/memory.test.d.ts +1 -0
  97. package/dist/core/__tests__/memory.test.js +152 -0
  98. package/dist/core/__tests__/migration.test.d.ts +1 -0
  99. package/dist/core/__tests__/migration.test.js +238 -0
  100. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  101. package/dist/core/__tests__/pane-column.test.js +153 -0
  102. package/dist/core/__tests__/passive-subscription.test.js +24 -1
  103. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  104. package/dist/core/__tests__/persona-compose.test.js +53 -0
  105. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  106. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  107. package/dist/core/__tests__/persona.test.d.ts +1 -0
  108. package/dist/core/__tests__/persona.test.js +107 -0
  109. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  110. package/dist/core/__tests__/placement-focus.test.js +244 -0
  111. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  112. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  113. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  114. package/dist/core/__tests__/placement-revive.test.js +238 -0
  115. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  116. package/dist/core/__tests__/placement-teardown.test.js +183 -0
  117. package/dist/core/__tests__/prune.test.d.ts +1 -0
  118. package/dist/core/__tests__/prune.test.js +116 -0
  119. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  120. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  121. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  122. package/dist/core/__tests__/relaunch.test.js +328 -0
  123. package/dist/core/__tests__/reset.test.js +26 -7
  124. package/dist/core/__tests__/revive.test.d.ts +1 -0
  125. package/dist/core/__tests__/revive.test.js +217 -0
  126. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  127. package/dist/core/__tests__/spawn-root.test.js +73 -0
  128. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  129. package/dist/core/__tests__/steer-note.test.js +39 -0
  130. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  131. package/dist/core/__tests__/stop-guard.test.js +82 -0
  132. package/dist/core/__tests__/subcommand-tier.test.js +35 -33
  133. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  134. package/dist/core/__tests__/tmux-surface.test.js +106 -0
  135. package/dist/core/__tests__/unknown-path.test.js +8 -2
  136. package/dist/core/canvas/attention.d.ts +10 -0
  137. package/dist/core/canvas/attention.js +40 -0
  138. package/dist/core/canvas/canvas.d.ts +66 -7
  139. package/dist/core/canvas/canvas.js +209 -21
  140. package/dist/core/canvas/db.d.ts +8 -0
  141. package/dist/core/canvas/db.js +206 -4
  142. package/dist/core/canvas/focuses.d.ts +22 -0
  143. package/dist/core/canvas/focuses.js +80 -0
  144. package/dist/core/canvas/index.d.ts +3 -0
  145. package/dist/core/canvas/index.js +3 -0
  146. package/dist/core/canvas/labels.d.ts +27 -0
  147. package/dist/core/canvas/labels.js +36 -0
  148. package/dist/core/canvas/render.js +25 -10
  149. package/dist/core/canvas/telemetry.d.ts +14 -0
  150. package/dist/core/canvas/telemetry.js +35 -0
  151. package/dist/core/canvas/types.d.ts +115 -12
  152. package/dist/core/command.d.ts +25 -1
  153. package/dist/core/command.js +23 -15
  154. package/dist/core/config.js +36 -2
  155. package/dist/core/feed/feed.js +3 -3
  156. package/dist/core/feed/inbox.d.ts +3 -1
  157. package/dist/core/feed/inbox.js +45 -5
  158. package/dist/core/feed/passive.js +24 -11
  159. package/dist/core/help.d.ts +26 -13
  160. package/dist/core/help.js +44 -37
  161. package/dist/core/personas/index.d.ts +1 -1
  162. package/dist/core/personas/index.js +1 -1
  163. package/dist/core/personas/loader.d.ts +40 -1
  164. package/dist/core/personas/loader.js +63 -1
  165. package/dist/core/personas/resolve.d.ts +13 -6
  166. package/dist/core/personas/resolve.js +46 -34
  167. package/dist/core/runtime/bearings.d.ts +20 -0
  168. package/dist/core/runtime/bearings.js +92 -0
  169. package/dist/core/runtime/close.d.ts +14 -0
  170. package/dist/core/runtime/close.js +151 -0
  171. package/dist/core/runtime/demote.js +27 -10
  172. package/dist/core/runtime/front-door.js +1 -1
  173. package/dist/core/runtime/kickoff.d.ts +23 -6
  174. package/dist/core/runtime/kickoff.js +92 -36
  175. package/dist/core/runtime/launch.d.ts +24 -12
  176. package/dist/core/runtime/launch.js +75 -19
  177. package/dist/core/runtime/lifecycle.d.ts +13 -0
  178. package/dist/core/runtime/lifecycle.js +86 -0
  179. package/dist/core/runtime/memory.d.ts +43 -0
  180. package/dist/core/runtime/memory.js +165 -0
  181. package/dist/core/runtime/naming.d.ts +22 -0
  182. package/dist/core/runtime/naming.js +166 -0
  183. package/dist/core/runtime/nodes.d.ts +32 -1
  184. package/dist/core/runtime/nodes.js +60 -10
  185. package/dist/core/runtime/persona.d.ts +25 -0
  186. package/dist/core/runtime/persona.js +139 -0
  187. package/dist/core/runtime/placement.d.ts +287 -0
  188. package/dist/core/runtime/placement.js +663 -0
  189. package/dist/core/runtime/presence.d.ts +7 -15
  190. package/dist/core/runtime/presence.js +90 -66
  191. package/dist/core/runtime/promote.d.ts +14 -7
  192. package/dist/core/runtime/promote.js +57 -67
  193. package/dist/core/runtime/reset.d.ts +47 -4
  194. package/dist/core/runtime/reset.js +223 -52
  195. package/dist/core/runtime/revive.d.ts +26 -2
  196. package/dist/core/runtime/revive.js +166 -39
  197. package/dist/core/runtime/spawn.d.ts +20 -5
  198. package/dist/core/runtime/spawn.js +163 -43
  199. package/dist/core/runtime/stop-guard.d.ts +1 -1
  200. package/dist/core/runtime/stop-guard.js +18 -8
  201. package/dist/core/runtime/tmux.d.ts +100 -14
  202. package/dist/core/runtime/tmux.js +201 -28
  203. package/dist/core/spawn.js +15 -0
  204. package/dist/daemon/crtrd.d.ts +12 -1
  205. package/dist/daemon/crtrd.js +152 -34
  206. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  207. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  208. package/dist/pi-extensions/canvas-commands.js +16 -13
  209. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  210. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  211. package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
  212. package/dist/pi-extensions/canvas-goal-capture.js +15 -1
  213. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  214. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  215. package/dist/pi-extensions/canvas-nav.js +586 -262
  216. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  217. package/dist/pi-extensions/canvas-stophook.js +344 -228
  218. package/dist/types.d.ts +28 -0
  219. package/dist/types.js +16 -0
  220. package/package.json +1 -1
@@ -1,9 +1,34 @@
1
1
  import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec } from '../canvas/index.js';
2
2
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
3
3
  export declare function newNodeId(): string;
4
+ /** Resolve the tmux session a freshly-born node's window/pane opens into — and
5
+ * thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
6
+ * unit-testable without a live tmux:
7
+ * - managed background child (`adoptCaller=false`) → the shared backstage:
8
+ * the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
9
+ * - independent `--root` / inline front door (`adoptCaller=true`) → the
10
+ * caller's CURRENT session when inside tmux (`here`), else the backstage.
11
+ * This is exactly the session each birth site already places the node into;
12
+ * centralizing it keeps `home_session` and the actual placement in lockstep. */
13
+ export declare function resolveBirthSession(opts: {
14
+ /** True for an independent root or the inline front door (both adopt the
15
+ * caller's session); false for a managed background child. */
16
+ adoptCaller: boolean;
17
+ /** The caller's current tmux location, or null when not inside tmux. */
18
+ here: {
19
+ session: string;
20
+ } | null;
21
+ /** The inherited CRTR_ROOT_SESSION (the backstage the subtree flows into). */
22
+ rootSession?: string | null;
23
+ }): string;
24
+ /** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
25
+ * before `home_session` existed have no such field in meta — they fall back to
26
+ * their last live LOCATION (`tmux_session`), then to the shared backstage
27
+ * (`nodeSession()`). The defaulted read for the placement layer; a present
28
+ * `home_session` is always returned verbatim. */
29
+ export declare function homeSessionOf(nodeId: string): string;
4
30
  export interface NodeContext {
5
31
  nodeId: string | null;
6
- parentNodeId: string | null;
7
32
  kind: string | null;
8
33
  mode: Mode | null;
9
34
  }
@@ -21,8 +46,14 @@ export interface SpawnNodeOpts {
21
46
  lifecycle?: Lifecycle;
22
47
  cwd: string;
23
48
  name?: string;
49
+ /** Editor-label handle (2-4 word kebab-case) for the node's first prompt. */
50
+ description?: string;
24
51
  /** Parent node id. Omit for a user-opened root. */
25
52
  parent?: string | null;
53
+ /** Who spawned me (the `spawned_by` provenance edge), when it differs from
54
+ * `parent` — e.g. an independent root (parent=null) still records its
55
+ * spawner. Defaults to `parent`. */
56
+ spawnedBy?: string | null;
26
57
  /** New subscriptions this node opens default to passive when true. */
27
58
  passiveDefault?: boolean;
28
59
  /** Resolved pi launch recipe (from resolve(kind,mode)). */
@@ -14,10 +14,42 @@
14
14
  // is also recorded.
15
15
  import { randomBytes } from 'node:crypto';
16
16
  import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
17
+ import { nodeSession } from './tmux.js';
17
18
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
18
19
  export function newNodeId() {
19
20
  return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
20
21
  }
22
+ // ---------------------------------------------------------------------------
23
+ // REVIVE-HOME (home_session) — the durable session a node is (re)opened into
24
+ // ---------------------------------------------------------------------------
25
+ /** Resolve the tmux session a freshly-born node's window/pane opens into — and
26
+ * thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
27
+ * unit-testable without a live tmux:
28
+ * - managed background child (`adoptCaller=false`) → the shared backstage:
29
+ * the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
30
+ * - independent `--root` / inline front door (`adoptCaller=true`) → the
31
+ * caller's CURRENT session when inside tmux (`here`), else the backstage.
32
+ * This is exactly the session each birth site already places the node into;
33
+ * centralizing it keeps `home_session` and the actual placement in lockstep. */
34
+ export function resolveBirthSession(opts) {
35
+ const backstage = opts.rootSession !== undefined && opts.rootSession !== null && opts.rootSession !== ''
36
+ ? opts.rootSession
37
+ : nodeSession();
38
+ if (opts.adoptCaller && opts.here !== null)
39
+ return opts.here.session;
40
+ return backstage;
41
+ }
42
+ /** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
43
+ * before `home_session` existed have no such field in meta — they fall back to
44
+ * their last live LOCATION (`tmux_session`), then to the shared backstage
45
+ * (`nodeSession()`). The defaulted read for the placement layer; a present
46
+ * `home_session` is always returned verbatim. */
47
+ export function homeSessionOf(nodeId) {
48
+ const meta = getNode(nodeId);
49
+ if (meta === null)
50
+ return nodeSession();
51
+ return meta.home_session ?? meta.tmux_session ?? nodeSession();
52
+ }
21
53
  /** Read the current node's identity from the environment. A spawned pi process
22
54
  * runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
23
55
  * it by reading CRTR_NODE_ID as the parent. */
@@ -25,7 +57,6 @@ export function currentNodeContext() {
25
57
  const env = process.env;
26
58
  return {
27
59
  nodeId: env['CRTR_NODE_ID'] ?? null,
28
- parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
29
60
  kind: env['CRTR_KIND'] ?? null,
30
61
  mode: env['CRTR_MODE'] ?? null,
31
62
  };
@@ -63,33 +94,52 @@ export function nodeEnv(meta) {
63
94
  export function spawnNode(opts) {
64
95
  const parent = opts.parent ?? null;
65
96
  const isRoot = parent === null;
97
+ // Provenance is independent of the spine: a root has no parent but still
98
+ // records who spawned it. A child's spawner is its parent unless overridden.
99
+ const spawnedBy = opts.spawnedBy ?? parent;
100
+ const mode = opts.mode ?? 'base';
101
+ // A user-opened root is resident (a conversation you live in); a spawned node
102
+ // is terminal until it must persist (promotion handles that later).
103
+ const lifecycle = opts.lifecycle ?? (isRoot ? 'resident' : 'terminal');
66
104
  const meta = {
67
105
  node_id: opts.nodeId ?? newNodeId(),
68
106
  name: opts.name ?? opts.kind,
107
+ description: opts.description,
108
+ cycles: 0,
69
109
  created: new Date().toISOString(),
70
110
  cwd: opts.cwd,
71
111
  kind: opts.kind,
72
- mode: opts.mode ?? 'base',
73
- // A user-opened root is resident (a conversation you live in); a spawned
74
- // node is terminal until it must persist (promotion handles that later).
75
- lifecycle: opts.lifecycle ?? (isRoot ? 'resident' : 'terminal'),
112
+ mode,
113
+ lifecycle,
114
+ // Born already acked to its initial persona: a fresh node has been "given
115
+ // guidance" for the state it starts in (its bearings carry it), so the
116
+ // persona injector sees no drift on its first turn boundary.
117
+ persona_ack: { mode, lifecycle },
76
118
  status: 'active',
77
119
  parent,
120
+ spawned_by: spawnedBy,
78
121
  passive_default: opts.passiveDefault ?? false,
79
122
  intent: null,
80
123
  pi_session_id: null,
124
+ pi_session_file: null,
81
125
  launch: opts.launch,
82
126
  };
127
+ // Validate BEFORE minting: a bad parent must leave no half-born orphan row or
128
+ // dirs behind, so the parent's existence is checked before createNode
129
+ // scaffolds anything on disk or in the db.
130
+ if (parent !== null && getNode(parent) === null) {
131
+ throw new Error(`cannot spawn under unknown parent node: ${parent}`);
132
+ }
83
133
  createNode(meta);
84
134
  if (parent !== null) {
85
- if (getNode(parent) === null) {
86
- throw new Error(`cannot spawn under unknown parent node: ${parent}`);
87
- }
88
135
  // The load-bearing seed: parent subscribes (active) to child so it learns
89
136
  // when the work finishes. This mirrors spawn structure into the spine.
137
+ // A root (parent=null) gets NO subscription — nobody is woken by it.
90
138
  subscribe(parent, meta.node_id, true);
91
- // Audit-only provenance.
92
- recordSpawn(meta.node_id, parent);
139
+ }
140
+ // Audit-only provenance edge — recorded for a root too (from its spawner).
141
+ if (spawnedBy !== null && spawnedBy !== undefined && getNode(spawnedBy) !== null) {
142
+ recordSpawn(meta.node_id, spawnedBy);
93
143
  }
94
144
  return meta;
95
145
  }
@@ -0,0 +1,25 @@
1
+ import { type Mode, type Lifecycle } from '../canvas/index.js';
2
+ /** The two-axis persona state the injector keys on. */
3
+ export interface Persona {
4
+ mode: Mode;
5
+ lifecycle: Lifecycle;
6
+ }
7
+ export interface PersonaDriftResult {
8
+ from: Persona;
9
+ to: Persona;
10
+ /** The built transition guidance to inject for `to`. */
11
+ guidance: string;
12
+ }
13
+ /** Build the injected transition prompt for a `from → to` persona change.
14
+ * Concatenates the relevant section per changed axis (both when both changed).
15
+ * Pure read of the node's roadmap/memory for the base→orchestrator case. */
16
+ export declare function transitionGuidance(nodeId: string, from: Persona, to: Persona): string;
17
+ /** Compare a node's live {mode,lifecycle} against its `persona_ack` (the last
18
+ * state it was given guidance for). Returns the transition + built guidance
19
+ * when they differ, else null. Does NOT mutate — the caller delivers the
20
+ * guidance, then `commitPersonaAck`s the new state. An unset `persona_ack`
21
+ * (legacy node) defaults to the current persona, so it reads as no drift and
22
+ * never fabricates spurious guidance. */
23
+ export declare function personaDrift(nodeId: string): PersonaDriftResult | null;
24
+ /** Commit the persona state the node has now been given guidance for. */
25
+ export declare function commitPersonaAck(nodeId: string, to: Persona): void;
@@ -0,0 +1,139 @@
1
+ // persona.ts — the CENTRALIZED persona-transition injector.
2
+ //
3
+ // A node has two orthogonal, independently switchable axes:
4
+ // • mode — base (hands-on, finishes in one window) ↔ orchestrator
5
+ // (delegates, holds a roadmap, survives refresh cycles + yields)
6
+ // • lifecycle — terminal (owes a final up the spine, reaps when done) ↔
7
+ // resident (interactable, stays dormant, never forced to submit)
8
+ //
9
+ // Whenever EITHER axis changes from the value the node was last GIVEN guidance
10
+ // for, the node must be prompt-injected with guidance for its new state —
11
+ // automatically, here, not by each state-changing command. Commands just call
12
+ // `updateNode({ mode|lifecycle })`; this module is the single source of the
13
+ // transition prose, delivered from exactly two sites:
14
+ // • the stophook turn_end hook (self-changes this turn + external changes
15
+ // while the node is active), and
16
+ // • the revive kickoff (external changes made while the node was dormant).
17
+ //
18
+ // The `persona_ack` meta field records the last {mode,lifecycle} the node was
19
+ // given guidance for (born equal to its initial persona at spawn, so a fresh
20
+ // worker never gets spurious guidance). `personaDrift` compares live meta to it;
21
+ // the caller delivers the guidance, then commits the ack.
22
+ import { getNode, updateNode } from '../canvas/index.js';
23
+ import { loadKernel, loadPersona, loadLifecycleFragment } from '../personas/index.js';
24
+ import { resolveSkill } from '../resolver.js';
25
+ import { readText } from '../fs-utils.js';
26
+ import { parseFrontmatter } from '../frontmatter.js';
27
+ import { readRoadmap, roadmapPath } from './roadmap.js';
28
+ import { orchestratorContextNote } from './bearings.js';
29
+ import { memoryPath, memoryDir, userMemoryPath, userMemoryDir, projectMemoryPath, projectMemoryDir, } from './memory.js';
30
+ // ---------------------------------------------------------------------------
31
+ // base→orchestrator guidance (the roadmap-shaping dump) — MOVED here from
32
+ // promote.ts so the injector is the one place that builds it.
33
+ // ---------------------------------------------------------------------------
34
+ /** Load a skill's body text by name, or null if it can't be resolved. Used to
35
+ * inline a kind's roadmap-shaping skill into the orchestration guidance. */
36
+ function loadSkillBody(name) {
37
+ try {
38
+ const skill = resolveSkill(name, {});
39
+ return parseFrontmatter(readText(skill.path)).body.trim();
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /** The base→orchestrator guidance dump, specialized to the node's kind: the
46
+ * shared kernel + that kind's roadmap-shaping skill + the roadmap scaffold the
47
+ * node must author + the orchestrator context-dir framing + the three memory
48
+ * stores. The node is now a delegator whose scarce resource is its own context
49
+ * window. (Lifecycle is left to its own section — promotion no longer forces
50
+ * resident, so this never asserts residency.) */
51
+ function orchestrationGuidance(nodeId, kind, cwd) {
52
+ const kernel = loadKernel();
53
+ const orch = loadPersona(kind, 'orchestrator');
54
+ const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
55
+ ? orch.frontmatter['roadmapSkill']
56
+ : undefined;
57
+ const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
58
+ const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
59
+ const rmPath = roadmapPath(nodeId);
60
+ const parts = [
61
+ `You are now a ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
62
+ 'Your job is to manage context and delegate — not to do the goal yourself.',
63
+ '',
64
+ kernel,
65
+ ];
66
+ if (skillBody) {
67
+ parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
68
+ }
69
+ parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '',
70
+ // The orchestrator framing for the context dir — the missing guidance a
71
+ // promoted node never got at spawn (it spawned as a base worker). Same note
72
+ // a born-orchestrator gets in its <crtr-context> bearings block.
73
+ orchestratorContextNote(nodeId), '', 'Your long-term memory now exists across three seeded stores (write to them directly), each a different scope per "Your long-term memory" above:', ` • user-global \`${userMemoryDir()}\` (index \`${userMemoryPath()}\`) — who the human is, how they like to work; loaded into every orchestrator everywhere.`, ` • project \`${projectMemoryDir(cwd)}\` (index \`${projectMemoryPath(cwd)}\`) — facts bound to this repo; loaded into every orchestrator working here.`, ` • node-local \`${memoryDir(nodeId)}\` (index \`${memoryPath(nodeId)}\`) — facts specific to this goal; they die with this node.`, 'A memory\'s `type` decides which store it lands in (see "Your long-term memory"). These same paths ride into every future wake in your `<crtr-context>` block.', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
74
+ return parts.join('\n');
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // The other three transitions — short, prescriptive, audience = the node's
78
+ // agent (decision-first; one well-placed "don't").
79
+ // ---------------------------------------------------------------------------
80
+ /** orchestrator → base (demote): hands-on again, finish in-window. */
81
+ function baseModeGuidance() {
82
+ return ('You are HANDS-ON again — base mode. Do the work yourself in THIS window and finish it here; ' +
83
+ 'stop delegating by default. You no longer drive a roadmap, so `crtr node yield` is not your exit. ' +
84
+ 'Spawn a child only for a cleanly separable unit, never as your first move.');
85
+ }
86
+ // The lifecycle transition prose is the SAME contract that's baked into the
87
+ // static system prompt at birth — so both load from the one source
88
+ // (`personas/lifecycle/{terminal,resident}.md`) and can never drift. The flip
89
+ // only re-delivers that fragment as the node's new-state steer.
90
+ /** terminal → resident: interactable, never forced to submit. */
91
+ function residentLifecycleGuidance() {
92
+ return loadLifecycleFragment('resident');
93
+ }
94
+ /** resident → terminal: owes a final, reaps when done. */
95
+ function terminalLifecycleGuidance() {
96
+ return loadLifecycleFragment('terminal');
97
+ }
98
+ /** Build the injected transition prompt for a `from → to` persona change.
99
+ * Concatenates the relevant section per changed axis (both when both changed).
100
+ * Pure read of the node's roadmap/memory for the base→orchestrator case. */
101
+ export function transitionGuidance(nodeId, from, to) {
102
+ const sections = [];
103
+ if (from.mode !== to.mode) {
104
+ if (to.mode === 'orchestrator') {
105
+ const node = getNode(nodeId);
106
+ sections.push(orchestrationGuidance(nodeId, node?.kind ?? 'general', node?.cwd ?? process.cwd()));
107
+ }
108
+ else {
109
+ sections.push(baseModeGuidance());
110
+ }
111
+ }
112
+ if (from.lifecycle !== to.lifecycle) {
113
+ sections.push(to.lifecycle === 'resident' ? residentLifecycleGuidance() : terminalLifecycleGuidance());
114
+ }
115
+ return sections.join('\n\n');
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // Detector + ack commit.
119
+ // ---------------------------------------------------------------------------
120
+ /** Compare a node's live {mode,lifecycle} against its `persona_ack` (the last
121
+ * state it was given guidance for). Returns the transition + built guidance
122
+ * when they differ, else null. Does NOT mutate — the caller delivers the
123
+ * guidance, then `commitPersonaAck`s the new state. An unset `persona_ack`
124
+ * (legacy node) defaults to the current persona, so it reads as no drift and
125
+ * never fabricates spurious guidance. */
126
+ export function personaDrift(nodeId) {
127
+ const meta = getNode(nodeId);
128
+ if (meta === null)
129
+ return null;
130
+ const to = { mode: meta.mode, lifecycle: meta.lifecycle };
131
+ const from = meta.persona_ack ?? { mode: meta.mode, lifecycle: meta.lifecycle };
132
+ if (from.mode === to.mode && from.lifecycle === to.lifecycle)
133
+ return null;
134
+ return { from, to, guidance: transitionGuidance(nodeId, from, to) };
135
+ }
136
+ /** Commit the persona state the node has now been given guidance for. */
137
+ export function commitPersonaAck(nodeId, to) {
138
+ updateNode(nodeId, { persona_ack: to });
139
+ }
@@ -0,0 +1,287 @@
1
+ import { type NodeRow, type FocusRow } from '../canvas/index.js';
2
+ import { homeSessionOf } from './nodes.js';
3
+ export { homeSessionOf };
4
+ export type { FocusRow };
5
+ /** The focus a node occupies, or null. UNIQUE(node_id) ⇒ at most one. */
6
+ export declare function focusOf(nodeId: string): FocusRow | null;
7
+ /** Is this node on a viewport? */
8
+ export declare function isFocused(nodeId: string): boolean;
9
+ /** The focus realized by a given pane (`%id`), or null. */
10
+ export declare function focusByPane(pane: string): FocusRow | null;
11
+ /** The set of node ids currently on some focus. */
12
+ export declare function focusedNodes(): Set<string>;
13
+ /** Every focus row (every live viewport). */
14
+ export declare function listFocuses(): FocusRow[];
15
+ /** The cached LOCATION as stored on a node row: the authoritative `pane` handle
16
+ * plus its derived window/session cache. */
17
+ export interface CachedLocation {
18
+ pane: string | null;
19
+ tmux_session: string | null;
20
+ window: string | null;
21
+ }
22
+ /** What `reconcile` resolved from tmux for the cached location. The shell does
23
+ * the two driver reads; the pure decision interprets them.
24
+ * - `paneLoc`: `paneLocation(cached.pane)` — the pane's CURRENT session/window,
25
+ * or null when the pane is gone. Only meaningful when `cached.pane != null`.
26
+ * - `windowPane`: `paneOfWindow(cached.tmux_session, cached.window)` — the live
27
+ * window's active pane, for the legacy backfill case (`cached.pane == null`). */
28
+ export interface LiveProbe {
29
+ paneLoc: {
30
+ session: string;
31
+ window: string;
32
+ } | null;
33
+ windowPane: string | null;
34
+ }
35
+ /** The presence patch `reconcile` should write, or `{ kind: 'none' }` for a no-op.
36
+ * - `none` — the cache already matches reality (or there's nothing to do).
37
+ * - `gone` — the durable pane is gone → null the whole LOCATION.
38
+ * - `follow` — the pane moved (user move) → re-point the cache at its new
39
+ * window/session, keeping the same pane id.
40
+ * - `backfill` — a legacy row had no pane but a live window → adopt the
41
+ * window's active pane as the durable handle (begins populating
42
+ * `pane` for pre-existing nodes). */
43
+ export type ReconcileDecision = {
44
+ kind: 'none';
45
+ } | {
46
+ kind: 'gone';
47
+ } | {
48
+ kind: 'follow';
49
+ pane: string;
50
+ tmux_session: string;
51
+ window: string;
52
+ } | {
53
+ kind: 'backfill';
54
+ pane: string;
55
+ tmux_session: string;
56
+ window: string;
57
+ };
58
+ /** PURE reconciliation decision (§2.4) — unit-testable without a live tmux.
59
+ * Given the cached row LOCATION and what tmux currently reports, decide the
60
+ * presence patch. Mirrors the pure-core/impure-shell split (cf. `livenessVerdict`
61
+ * vs `handleLiveWindow`): this is the decision, `reconcile` wires it to the
62
+ * driver reads + `setPresence`. */
63
+ export declare function reconcileDecision(cached: CachedLocation, live: LiveProbe): ReconcileDecision;
64
+ /** Reconcile a node's LOCATION against tmux reality (§2.4) — the impure shell.
65
+ * Reads `row.pane`, resolves its CURRENT session/window via the driver, and
66
+ * writes the resulting presence patch through `setPresence` (never a raw UPDATE):
67
+ * - pane moved → FOLLOW (re-point window/session, keep the pane id)
68
+ * - pane gone → null the whole LOCATION
69
+ * - legacy/no pane + live window → backfill the pane from `paneOfWindow`
70
+ * A no-op when there's nothing to resolve (genuinely no pane, or the cache is
71
+ * already current). Call this before any swap/kill/focus/revive so the act lands
72
+ * on the pane's current window, never a stale one. */
73
+ export declare function reconcile(nodeId: string): void;
74
+ /** Reconcile a FOCUS's derived `session` cache against tmux reality (§2.4, Q4) —
75
+ * the focus-row analogue of `reconcile`. A focus is anchored on its durable
76
+ * `%pane_id`; `session` is a derived cache. If the user moved the focus pane to
77
+ * another session, re-point the cache so a resume-into-focus lands in the pane's
78
+ * CURRENT session. A no-op when the focus has no pane, the cache is already
79
+ * current, or the pane is GONE — in the gone case reconcileFocus does NOT null
80
+ * the row; the caller (reviveIntoPlacement) instead falls to the backstage
81
+ * branch via `paneExists(pane)` being false. */
82
+ export declare function reconcileFocus(focusId: string): void;
83
+ /** Is this node's pane (its LOCATION) alive? The v3 PRIMARY liveness probe,
84
+ * PURE / non-mutating so the daemon can gate on it without side effects:
85
+ * - `pane != null` → `paneExists(pane)` (display-message on the `%id`), so a
86
+ * user moving the pane to another window/session never reads as "gone".
87
+ * - `pane == null` → window-keyed FALLBACK (`windowAlive`) for legacy/no-pane
88
+ * rows that haven't been backfilled yet.
89
+ * Accepts a node id (re-reads the row) or a `NodeRow` already in hand. */
90
+ export declare function isNodePaneAlive(node: string | NodeRow): boolean;
91
+ /** The launch recipe `reviveIntoPlacement` plays into a pane. `command` is the
92
+ * full shell string (`piCommand(argv)`); `env`/`cwd`/`name` describe the
93
+ * window/pane; `resuming` is carried through for the caller's ReviveResult. */
94
+ export interface ReviveLaunch {
95
+ command: string;
96
+ env: Record<string, string>;
97
+ cwd: string;
98
+ name: string;
99
+ resuming: boolean;
100
+ }
101
+ /** Where a revive physically landed: the new/derived window, the session it ran
102
+ * in, and the durable pane id. */
103
+ export interface PlacementResult {
104
+ window: string | null;
105
+ session: string;
106
+ pane: string | null;
107
+ }
108
+ /** The PURE revive-target decision (§1.4/§5.1) — THE assertion that the
109
+ * "unbidden windows" bug is structurally dead. Given a node's focus (or null),
110
+ * whether that focus's pane is still alive, and the node's durable REVIVE-HOME
111
+ * (`home_session`), decide WHERE a revive must land:
112
+ * - occupies a LIVE focus → resume IN PLACE in that focus pane (no new window).
113
+ * - otherwise → a new window in `homeSession`, and NOTHING ELSE.
114
+ *
115
+ * The backstage branch's session is `homeSession` ONLY — never
116
+ * `meta.tmux_session`, the field focus taints to a user session. For a
117
+ * post-Step-1 child `homeSession` is the backstage `crtr` (never a user
118
+ * session), so a non-focused child — INCLUDING a once-focused-now-unfocused
119
+ * child whose `tmux_session` was tainted — can NEVER revive into a user session.
120
+ * A root's `homeSession` is its own session, so reviving a root into its own
121
+ * session is correct, not the bug. */
122
+ export type ReviveTargetDecision = {
123
+ kind: 'focus-pane';
124
+ pane: string;
125
+ session: string;
126
+ } | {
127
+ kind: 'backstage';
128
+ session: string;
129
+ };
130
+ export declare function reviveTarget(focus: FocusRow | null, focusPaneAlive: boolean, homeSession: string): ReviveTargetDecision;
131
+ /** Place a reviving node into its CORRECT location (§1.4) — the single decision
132
+ * that replaces revive.ts's old `session = meta.tmux_session ?? nodeSession()` +
133
+ * `openNodeWindow`. Reconcile first (§2.4), then dispatch on `reviveTarget`:
134
+ * - the node occupies a LIVE focus → `reconcileFocus` (resolve the pane's
135
+ * CURRENT session, Q4) and `respawn-pane -k` the pi INTO that focus pane —
136
+ * no new window (F3 resume-in-place).
137
+ * - otherwise → the node is NOT focused (or its focus pane already collapsed,
138
+ * the Step-5 limitation: remain-on-exit lands in Step 6), so it may ONLY
139
+ * (re)appear in its durable REVIVE-HOME: a fresh window in `homeSession`.
140
+ * **There is NO code path here by which a non-focused node's new-window
141
+ * targets a user session** — `openNodeWindow`'s session is `homeSession` and
142
+ * nothing else. That is the structural bug-kill.
143
+ *
144
+ * `setPresence` (the one atomic LOCATION write) records where the node landed.
145
+ * CRTR_ROOT_SESSION is forced to `homeSession` in BOTH branches so the node's
146
+ * children always flow to the backstage, never into the focus session. */
147
+ export declare function reviveIntoPlacement(nodeId: string, launch: ReviveLaunch): PlacementResult;
148
+ /** Relocate a node's still-running agent to the background `crtr` session,
149
+ * freeing the foreground pane WITHOUT killing the pi. `break-pane` moves the
150
+ * pane out of the foreground window into a fresh window in the shared backstage
151
+ * (the pi keeps generating); the node becomes a background window — switchable
152
+ * but not rendered, like any other node. Reconcile first (act on the pane's
153
+ * CURRENT location, §2.4) and again after (presence FOLLOWS the move). No-op
154
+ * (false) when there is no live pane to relocate or tmux refuses the break.
155
+ * `pane` is the authoritative node pane the caller acts on (the Alt+C menu's
156
+ * `#{pane_id}`); falls back to the node's durable handle. */
157
+ export declare function detachToBackground(nodeId: string, pane?: string): boolean;
158
+ /** A reviver: resume a DORMANT node into its backstage placement (a fresh `crtr`
159
+ * window via reviveIntoPlacement). Injected so placement.ts need not import
160
+ * revive.ts (which imports placement.ts — a cycle). The node's landed pane is
161
+ * read back from its row afterwards. */
162
+ export type Reviver = (nodeId: string) => void;
163
+ /** Result of a focus/retarget op. */
164
+ export interface FocusResult {
165
+ focused: boolean;
166
+ session: string | null;
167
+ inPlace: boolean;
168
+ revived: boolean;
169
+ }
170
+ /** PURE disposition of a focus's outgoing occupant after a retarget swap (§2.5/
171
+ * §1.3): a still-generating node moves to backstage (F2); a holder pane or a
172
+ * done/dormant node has its (now-backstage) pane reaped (Invariant P: a
173
+ * not-focused + not-generating node has NO pane). Unit-testable in isolation. */
174
+ export type OutgoingAction = {
175
+ kind: 'backstage';
176
+ } | {
177
+ kind: 'kill';
178
+ };
179
+ export declare function outgoingDisposition(o: {
180
+ exists: boolean;
181
+ generating: boolean;
182
+ }): OutgoingAction;
183
+ /** Open a NEW viewport (§2.3, F4) — the ONLY path a new pane appears in a user
184
+ * session. Default: `splitWindow(callerPane)` beside (Q3); `newWindow` opens a
185
+ * fresh window in the caller pane's session instead. Arms `remain-on-exit` on
186
+ * the new pane's window (F3) and inserts a focuses row anchored on it, occupied
187
+ * by a HOLDER until retargetFocus swaps a real node in. A benign long-sleep
188
+ * holds the pane open until the swap; retargetFocus reaps it. Returns the row,
189
+ * or null if tmux failed. */
190
+ export declare function openFocus(callerPane: string, opts?: {
191
+ newWindow?: boolean;
192
+ }): FocusRow | null;
193
+ /** Register the FOREGROUND root's pane as focus #1 at boot (§2.6). The inline
194
+ * root owns the user's viewport, so its own pane becomes a durable focus — with
195
+ * `remain-on-exit` so a clean exit FREEZES the pane rather than detaching the
196
+ * terminal (F1). A background `--root` does NOT call this (§6): it stays a plain
197
+ * window until the user `node focus`es it. No-op when the pane or this node is
198
+ * already a focus. Mirrors focus.ptr via setFocus (the transitional bridge). */
199
+ export declare function registerRootFocus(nodeId: string, pane: string, session: string | null, window: string | null): FocusRow | null;
200
+ /** retargetFocus — the unified hot-swap (§2.5, Invariant P + Q5). Swap `incoming`
201
+ * onto focus `focusId`'s viewport, keeping the screen position invariant (no new
202
+ * window). One sqlite txn updates the focus row + BOTH nodes' presence:
203
+ * - Q5: if `incoming` already occupies ANOTHER focus, VACATE it first (close
204
+ * its row + kill its pane — the node MOVES here, no auto-retarget).
205
+ * - resolve `incoming`'s live pin pane (a backstage pane), else `revive` it
206
+ * into the backstage and read back its pane.
207
+ * - `swapPaneInPlace(pin, focusPane)`: incoming → the viewport slot; the
208
+ * outgoing occupant → incoming's old (backstage) slot, %ids preserved
209
+ * (cross-session swap confirmed by the spike).
210
+ * - outgoing still generating → backstage (F2); else reap its now-backstage
211
+ * pane (Invariant P). A holder occupant (no node row) is always reaped.
212
+ * Arms remain-on-exit on the viewport (F3) and mirrors focus.ptr (setFocus). */
213
+ export declare function retargetFocus(focusId: string, incoming: string, revive: Reviver): FocusResult;
214
+ /** The front door for `node focus` / `node cycle` (§2.3): resolve which focus the
215
+ * caller's pane acts on, then retarget `nodeId` onto it.
216
+ * - `newPane` → `openFocus` a fresh viewport beside the caller (F4), then
217
+ * retarget into it.
218
+ * - else → retarget the caller pane's focus IN PLACE (`focusByPane`); if the
219
+ * caller's pane is not yet a viewport, adopt it as one (occupied by whatever
220
+ * node sits there now — `callerNode`, else resolved by pane).
221
+ * - no caller pane (not in tmux) → best-effort: mirror focus.ptr, report
222
+ * not-in-place. */
223
+ export declare function focus(nodeId: string, opts: {
224
+ pane?: string;
225
+ newPane?: boolean;
226
+ callerNode?: string;
227
+ revive: Reviver;
228
+ }): FocusResult;
229
+ /** Tear a node off its placement (close/reset teardown, §2.3, flow (e)).
230
+ * Reconcile first (follow a manual move / backfill a legacy pane), close the
231
+ * focus row it occupies (if any), kill its pane (pane-keyed via the durable
232
+ * `%id` — the window collapses once its last pane goes), and null its LOCATION.
233
+ * Mirrors focus.ptr when this node was the current focus. Best-effort tmux; the
234
+ * DB writes always land. The pane kill is the sole teardown unit (Q1/§6: a
235
+ * split-pane focus returns its space to the surviving split; a standalone-window
236
+ * focus closes the window). */
237
+ export declare function tearDownNode(nodeId: string): void;
238
+ /** Demote's in-pane relaunch (§2.3, flow (e)): respawn `nodeId`'s launch into an
239
+ * EXISTING `pane`, keeping the durable `%id` (respawn-pane -k), and record its
240
+ * presence keyed on that pane. The session/window are DERIVED from the pane
241
+ * itself (paneLocation), so the recycled node's LOCATION follows the pane it was
242
+ * recycled into. `launch.env` is passed through verbatim — the caller (demote)
243
+ * already sets CRTR_ROOT_SESSION (children → backstage) + FRONT_DOOR. Detached
244
+ * respawn, since the pane is often the caller's own. Returns whether the respawn
245
+ * dispatched. */
246
+ export declare function recycleFocusPane(nodeId: string, pane: string, launch: ReviveLaunch): boolean;
247
+ /** §1.6 lifecycle successor — hand a truly-done focused node's viewport to its
248
+ * manager. Repoints the focus row `focusId` to `managerId` (a DB swap of the
249
+ * occupant). Two takeover realizations, split on the manager's liveness:
250
+ * - DORMANT manager (dead pi): the row repoint is all this does; the manager,
251
+ * woken by the finished node's `push final` landing in its inbox, is revived
252
+ * by the external daemon INTO this node's now-frozen focus pane
253
+ * (remain-on-exit), where reviveIntoPlacement's focus-pane branch resumes it
254
+ * in place — no new window, no taint. (UNCHANGED — the canonical takeover.)
255
+ * - LIVE manager (pi alive in the backstage, the normal multi-child state):
256
+ * the daemon never revives it (it only respawns dead-pi nodes), so we must
257
+ * bring it into the viewport SYNCHRONOUSLY here — swap its backstage pane
258
+ * into the focus slot (MAJOR 1). Otherwise the manager runs off-screen
259
+ * forever while %m sits orphaned in the viewport and the focus row lies
260
+ * about LOCATION.
261
+ * Returns false — the caller closes the focus (Q1) — when there is no manager,
262
+ * the manager IS this node, or the manager already occupies another viewport
263
+ * (UNIQUE node_id: do NOT move it, §1.6 edge).
264
+ *
265
+ * Why the live swap is NOT the forbidden self-saw: `swap-pane -d` only EXCHANGES
266
+ * two panes' slot positions; it never respawns or kills the finishing node's own
267
+ * pi. The forbidden move is a synchronous `respawn-pane -k %m` from inside %m —
268
+ * we never do that here. After the swap, %m (the dying node's pane) sits in the
269
+ * manager's old backstage slot; the caller nulls this node's presence so nothing
270
+ * tracks the corpse. */
271
+ export declare function handFocusToManager(focusId: string, managerId: string | null): boolean;
272
+ export interface SpreadResult {
273
+ window: string | null;
274
+ session: string | null;
275
+ /** Child node ids whose panes were joined into the target window. */
276
+ joined: string[];
277
+ focused: boolean;
278
+ }
279
+ /** Join each of `childIds`' live panes into `targetId`'s window, lay them out
280
+ * (target wide on the left, children stacked right), and focus it. Reconcile
281
+ * drives both the target resolution and the per-join fix-up (a joined pane keeps
282
+ * its `%id` but changes window, so its LOCATION must FOLLOW — else the daemon
283
+ * reads it dormant). Caller revives dormant nodes first so they have live panes.
284
+ * No-op result when the target has no live pane. */
285
+ export declare function spreadNode(targetId: string, childIds: string[], opts?: {
286
+ mainPaneWidth?: string;
287
+ }): SpreadResult;