@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
@@ -1,67 +1,91 @@
1
- // Root reset — the `/new` equivalent.
1
+ // Root reset + relaunch — the `/new` equivalents, plus clean-exit termination.
2
2
  //
3
3
  // A live pi process is bound to one node via CRTR_NODE_ID (set at launch, not
4
- // rebindable mid-process). When the user runs `/new`, the conversation is reset
5
- // but the process — and thus the node id — stays the same. To make `/new`
6
- // behave like re-running `crtr` (a brand-new graph on the canvas) we reset the
7
- // root in place: reap its entire descendant sub-DAG, detach its subscriptions,
8
- // and wipe its working state, then re-point it at a fresh base persona and the
9
- // new pi session id. The node keeps its id; from the dashboard/nav it is a
10
- // pristine root with an empty graph.
4
+ // rebindable mid-process). When the user runs `/new`, the conversation resets
5
+ // but the OS process — and thus the node id — stays the same. To make `/new`
6
+ // behave like re-running `crtr` we have two strategies:
7
+ //
8
+ // relaunchRoot (option C) for a ROOT in a tmux pane: PARK the old root
9
+ // (mark done, keep its id/edges/pi_session_id intact as history), mint a
10
+ // FRESH node id, and re-exec pi in the current pane bound to the new id.
11
+ // The old id never changes meaning; external refs stay valid.
12
+ // • resetRoot (fallback) — for a non-root child (session-id refresh only) or
13
+ // a root with no pane (no tmux): the legacy in-place reset of the SAME id.
14
+ //
15
+ // Termination semantics: a pi that ends cleanly resolves its node to `done`
16
+ // (markCleanExitDone); only a true crash leaves it `dead`. A force-kill
17
+ // (closeWindow / respawn-pane -k) fires NO clean session_shutdown, so reaped
18
+ // descendants are marked `done` explicitly here.
11
19
  //
12
20
  // Best-effort throughout: a tmux/fs failure on one node never aborts the reset.
13
21
  import { existsSync, rmSync } from 'node:fs';
14
- import { getNode, updateNode, setStatus, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, } from '../canvas/index.js';
15
- import { closeWindow, windowAlive } from './tmux.js';
22
+ import { getNode, updateNode, setPresence, clearPid, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
23
+ import { transition } from './lifecycle.js';
24
+ import { paneLocation, nodeSession } from './tmux.js';
25
+ import { tearDownNode } from './placement.js';
16
26
  import { buildLaunchSpec } from './launch.js';
17
27
  import { roadmapPath } from './roadmap.js';
18
- /** Reset a root node to a pristine, empty graph (the `/new` semantics).
28
+ import { spawnNode, newNodeId } from './nodes.js';
29
+ import { setFocus } from './presence.js';
30
+ import { relaunchRootInPane } from './revive.js';
31
+ // ---------------------------------------------------------------------------
32
+ // reapDescendants — tear down a root's descendant sub-DAG (shared helper)
33
+ // ---------------------------------------------------------------------------
34
+ /** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
35
+ * on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
36
+ * window (closes the daemon revive race). Edges are LEFT INTACT — descendants
37
+ * keep parent=rootId. No wipe. Returns the reaped ids.
38
+ *
39
+ * Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
40
+ * -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
41
+ * quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
42
+ * `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
43
+ * fallback, so both leave their descendants `done`. */
44
+ export function reapDescendants(rootId) {
45
+ const reaped = [];
46
+ for (const id of view(rootId)) {
47
+ try {
48
+ // Reap BEFORE tearing down the placement (the crash-safety invariant the
49
+ // `reap` event encodes): a non-supervised status + cleared intent first, so
50
+ // the daemon can't revive a descendant mid-teardown. tearDownNode then
51
+ // closes any focus row it held, kills its pane (pane-keyed), and nulls its
52
+ // LOCATION.
53
+ transition(id, 'reap');
54
+ tearDownNode(id);
55
+ reaped.push(id);
56
+ }
57
+ catch {
58
+ /* one bad node never aborts the reap */
59
+ }
60
+ }
61
+ return reaped;
62
+ }
63
+ /** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
64
+ * now used as the no-pane fallback and the non-root session-id refresh).
19
65
  *
20
66
  * For a non-root (spawned child), a `/new` is not a graph reset — we only
21
- * refresh its session id so a later `--resume` wakes the right conversation. */
22
- export function resetRoot(nodeId, newSessionId) {
67
+ * refresh its session id so a later `--session <id>` wakes the right conversation. */
68
+ export function resetRoot(nodeId, newSessionId, newSessionFile) {
23
69
  const meta = getNode(nodeId);
24
70
  if (meta === null)
25
71
  return { reaped: [], detached: [], reset: false };
26
72
  // Only roots own a graph in the "ran crtr again" sense.
27
73
  if (meta.parent != null) {
28
74
  if (newSessionId !== undefined) {
29
- try {
30
- updateNode(nodeId, { pi_session_id: newSessionId });
31
- }
32
- catch { /* */ }
75
+ updateNode(nodeId, {
76
+ pi_session_id: newSessionId,
77
+ ...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
78
+ });
33
79
  }
34
80
  return { reaped: [], detached: [], reset: false };
35
81
  }
36
- // 1) Reap the descendant sub-DAG. Mark dead + clear intent FIRST, then kill
37
- // the window: the daemon revives on a window-gone + intent==='refresh'
38
- // (or 'idle-release'), so flipping to dead before the window dies closes
39
- // the race where a descendant mid-yield gets revived as we tear it down.
40
- const reaped = [];
41
- for (const id of view(nodeId)) {
42
- try {
43
- const dmeta = getNode(id);
44
- setStatus(id, 'dead');
45
- updateNode(id, { intent: null });
46
- if (dmeta !== null && windowAlive(dmeta.tmux_session, dmeta.window)) {
47
- closeWindow(dmeta.window);
48
- }
49
- reaped.push(id);
50
- }
51
- catch {
52
- /* one bad node never aborts the reset */
53
- }
54
- }
82
+ // 1) Reap the descendant sub-DAG (mark done + kill windows; shared helper).
83
+ const reaped = reapDescendants(nodeId);
55
84
  // 2) Detach the root's own subscriptions so its view is empty.
56
85
  const detached = [];
57
86
  for (const sub of subscriptionsOf(nodeId)) {
58
- try {
59
- unsubscribe(nodeId, sub.node_id);
60
- detached.push(sub.node_id);
61
- }
62
- catch {
63
- /* */
64
- }
87
+ unsubscribe(nodeId, sub.node_id);
88
+ detached.push(sub.node_id);
65
89
  }
66
90
  // 3) Wipe the root's working state (reports / inbox / roadmap).
67
91
  for (const p of [
@@ -78,20 +102,167 @@ export function resetRoot(nodeId, newSessionId) {
78
102
  /* */
79
103
  }
80
104
  }
81
- // 4) Re-point the root at a fresh base persona + the new pi session id.
105
+ // 4) Re-point the root at a fresh base persona + the new pi session id. A
106
+ // root is resident by definition (this only runs on roots — see the early
107
+ // return above), so resetting to base/resident is the model, not a bypass.
108
+ // Re-seed persona_ack to the fresh persona so the pristine `/new`
109
+ // conversation never gets a spurious mode/lifecycle transition steer (the
110
+ // persona injector compares against this ack).
111
+ const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
112
+ updateNode(nodeId, {
113
+ mode: 'base',
114
+ lifecycle: 'resident',
115
+ persona_ack: { mode: 'base', lifecycle: 'resident' },
116
+ launch,
117
+ ...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
118
+ ...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
119
+ });
120
+ transition(nodeId, 'revive');
121
+ return { reaped, detached, reset: true };
122
+ }
123
+ /** The single entry the stophook calls on a detected `/new` (session id change).
124
+ * Policy lives here so the stophook stays thin and this stays unit-testable:
125
+ * - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
126
+ * - root + pane present → relaunchRoot(nodeId, pane) (option C)
127
+ * - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
128
+ * On a respawn-dispatch failure the live pi never died, so we degrade to the
129
+ * legacy in-place reset. */
130
+ export function handleNewSession(nodeId, newSessionId, pane, deps = {}, newSessionFile) {
131
+ const meta = getNode(nodeId);
132
+ if (meta === null)
133
+ return { path: 'noop' };
134
+ // Non-root child: a `/new` only refreshes its session id (unchanged).
135
+ if (meta.parent != null) {
136
+ resetRoot(nodeId, newSessionId, newSessionFile);
137
+ return { path: 'reset-child' };
138
+ }
139
+ // Root with no pane (not inside tmux): in-place reset fallback. Option C needs
140
+ // a pane to respawn into; resetRoot needs the new session id (available here
141
+ // because the trigger is session_start).
142
+ if (pane === undefined || pane.trim() === '') {
143
+ resetRoot(nodeId, newSessionId, newSessionFile);
144
+ return { path: 'reset-root' };
145
+ }
146
+ // Root with a pane: option C relaunch. relaunchRoot self-rolls-back its DB
147
+ // writes on a respawn-dispatch failure and rethrows; we then degrade to the
148
+ // legacy in-place reset (the live pi is still alive, never killed).
149
+ try {
150
+ const result = relaunchRoot(nodeId, pane, deps);
151
+ if (result === null)
152
+ return { path: 'noop' }; // defensive guard hit (e.g. rapid double /new)
153
+ return { path: 'relaunch', newNodeId: result.newNodeId };
154
+ }
155
+ catch {
156
+ setFocus(nodeId);
157
+ resetRoot(nodeId, newSessionId, newSessionFile);
158
+ return { path: 'reset-root' };
159
+ }
160
+ }
161
+ /** Park the old root + create+launch a fresh root in `pane` (option C). All DB
162
+ * writes are synchronous and happen BEFORE the respawn (the respawn kills the
163
+ * caller). Returns the new node id, or null on a defensive guard (not a root /
164
+ * already parked). Throws only if the respawn dispatch fails — and self-rolls-
165
+ * back its writes first so the caller can degrade to resetRoot. */
166
+ export function relaunchRoot(oldId, pane, deps = {}) {
167
+ const oldMeta = getNode(oldId);
168
+ if (oldMeta === null || oldMeta.parent != null)
169
+ return null; // defensive: not a root
170
+ if (oldMeta.status === 'done')
171
+ return null; // defensive: already parked (rapid double /new)
172
+ const respawn = deps.relaunchRootInPane ?? relaunchRootInPane;
173
+ // Resolve where the new pi will live (pane authoritative; fall back to old
174
+ // meta when paneLocation can't resolve, e.g. outside a live tmux server).
175
+ const loc = paneLocation(pane) ?? {
176
+ session: oldMeta.tmux_session ?? null,
177
+ window: oldMeta.window ?? null,
178
+ };
179
+ const newId = newNodeId();
180
+ const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
181
+ // Park-old + mint-new is the single most fragile spot in the runtime, so it is
182
+ // ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
183
+ // failure anywhere — including the respawn DISPATCH — rolls the whole thing
184
+ // back, leaving the old root EXACTLY as it was (no hand-rolled compensation).
185
+ // Only the *detached* respawn (the async pane kill) lands outside the txn — it
186
+ // must, since it kills this caller, and by then COMMIT has made the new state
187
+ // durable. setFocus is a file write, not in the txn; the catch restores it.
188
+ const db = openDb();
189
+ db.exec('BEGIN');
82
190
  try {
83
- const { launch } = buildLaunchSpec(meta.kind, 'base');
84
- updateNode(nodeId, {
191
+ // 1) Reap descendants (mark done + kill windows, keep edges, no wipe).
192
+ reapDescendants(oldId);
193
+ // 2) Create the fresh root node (new id, empty context dir via
194
+ // ensureNodeDirs) seeded active; `yield` adds the refresh safety net so
195
+ // that if the pane dies before boot the daemon revives it in a new window.
196
+ spawnNode({
197
+ kind: oldMeta.kind,
85
198
  mode: 'base',
86
199
  lifecycle: 'resident',
87
- intent: null,
88
- status: 'active',
200
+ cwd: oldMeta.cwd,
201
+ name: oldMeta.kind,
202
+ parent: null,
203
+ spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
204
+ nodeId: newId,
89
205
  launch,
90
- ...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
91
206
  });
207
+ transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
208
+ setPresence(newId, { tmux_session: loc.session, window: loc.window });
209
+ // REVIVE-HOME: the relaunched root's durable revive target is the session
210
+ // of the pane it is respawned into (same pane-recycle rule as demote).
211
+ updateNode(newId, { home_session: loc.session ?? nodeSession() });
212
+ clearPid(newId); // no pi yet → daemon 'leave' until boot records the pid
213
+ // 3) Park the old root: reap (done + intent cleared) and detach its window so
214
+ // it never claims the pane, but KEEP pi_session_id (resumable),
215
+ // parent=null, and all edges.
216
+ transition(oldId, 'reap');
217
+ setPresence(oldId, { window: null, tmux_session: null });
218
+ // 4) Focus follows content (file write — restored by the catch on rollback).
219
+ setFocus(newId);
220
+ // 5) Re-exec pi in this pane bound to newId; the dispatch is the LAST thing
221
+ // inside the txn. If it throws the txn rolls back (old root untouched); on
222
+ // success we COMMIT and the async detached kill of this pane lands after.
223
+ respawn(newId, pane);
224
+ db.exec('COMMIT');
92
225
  }
93
- catch {
94
- /* */
226
+ catch (err) {
227
+ // Dispatch failed (or a write threw) — the live pi never died. Roll the whole
228
+ // transaction back so the old root is FULLY restored, then degrade.
229
+ try {
230
+ db.exec('ROLLBACK');
231
+ }
232
+ catch { /* */ }
233
+ // The rolled-back new node's row is gone, but spawnNode already scaffolded its
234
+ // on-disk dir (ensureNodeDirs). With no row, prune never sees it — so remove
235
+ // the orphan dir here, otherwise it is permanent disk litter on this rare path.
236
+ try {
237
+ rmSync(nodeDir(newId), { recursive: true, force: true });
238
+ }
239
+ catch { /* */ }
240
+ try {
241
+ setFocus(oldId);
242
+ }
243
+ catch { /* */ } // focus is a file op, outside the txn
244
+ throw err instanceof Error ? err : new Error(String(err));
95
245
  }
96
- return { reaped, detached, reset: true };
246
+ return { newNodeId: newId };
247
+ }
248
+ // ---------------------------------------------------------------------------
249
+ // markCleanExitDone — the clean-exit→done termination guard
250
+ // ---------------------------------------------------------------------------
251
+ /** Resolve a cleanly-exiting node to `done`. Returns true iff it transitioned.
252
+ * Guard: only a real quit, and only a node still active|idle with no pending
253
+ * intent — so it never clobbers a node already routed by agent_end to done
254
+ * (push final), refresh (yield), or idle-release. Pure/DB-only (no pi/tmux) so
255
+ * the guard is unit-testable without a live pi. */
256
+ export function markCleanExitDone(nodeId, reason) {
257
+ if (reason !== 'quit')
258
+ return false; // new/reload/resume/fork → no-op
259
+ const meta = getNode(nodeId);
260
+ if (meta === null)
261
+ return false;
262
+ if (meta.status !== 'active' && meta.status !== 'idle')
263
+ return false; // already done/dead/canceled
264
+ if (meta.intent != null)
265
+ return false; // refresh / idle-release in flight
266
+ transition(nodeId, 'finalize');
267
+ return true;
97
268
  }
@@ -1,9 +1,21 @@
1
+ import { type NodeMeta } from '../canvas/index.js';
2
+ import { type RespawnPaneOpts } from './tmux.js';
3
+ /** Pick the `--session` source for a revive. resume=true prefers the absolute
4
+ * session-file path (immune to cwd; pi opens it directly) and keeps the bare
5
+ * session id as the fallback for older nodes booted before pi_session_file was
6
+ * captured. buildPiArgv prefers the path when both are present. resume=false (a
7
+ * refresh-yield) selects neither — the node re-reads its roadmap fresh. Pure so
8
+ * the path-vs-id selection is unit-testable without tmux. */
9
+ export declare function resumeArgs(meta: NodeMeta, resume: boolean): {
10
+ resumeSessionId?: string;
11
+ resumeSessionPath?: string;
12
+ };
1
13
  export interface ReviveResult {
2
14
  /** The new tmux window id, or null if openNodeWindow failed. */
3
15
  window: string | null;
4
16
  /** The tmux session the node was placed in. */
5
17
  session: string;
6
- /** True when pi was instructed to resume its saved conversation (`--resume`). */
18
+ /** True when pi was instructed to resume its saved conversation (`--session <id>`). */
7
19
  resumed: boolean;
8
20
  }
9
21
  /** Open a fresh background tmux window for `nodeId` and update canvas meta.
@@ -23,4 +35,16 @@ export declare function reviveNode(nodeId: string, opts: {
23
35
  * `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
24
36
  * Throws on unknown node or when the respawn could not be dispatched, so the
25
37
  * caller can fall back to a plain shutdown (daemon revives in a new window). */
26
- export declare function reviveInPlace(nodeId: string, pane: string): ReviveResult;
38
+ export declare function reviveInPlace(nodeId: string, pane: string, respawn?: (opts: RespawnPaneOpts) => boolean): ReviveResult;
39
+ /** Re-exec a FRESH pi for `nodeId` in EXISTING `pane` (respawn-pane -k), with
40
+ * NO prompt and NO resume — a clean root conversation (goal-capture /
41
+ * context-intro handle the first message + bearings, exactly like bare
42
+ * `crtr`). Unlike reviveInPlace: no buildReviveKickoff prompt, no cycles bump,
43
+ * and it sets CRTR_FRONT_DOOR=1 (REQUIRED — src/core/runtime/CLAUDE.md: any
44
+ * path that boots a pi must guard against a removed/renamed subcommand
45
+ * fork-bombing). Throws if the respawn could not be dispatched.
46
+ *
47
+ * Used by relaunchRoot (reset.ts) for the `/new`-in-a-root relaunch. Kept
48
+ * SEPARATE from reviveInPlace so the refresh-yield path's exact semantics
49
+ * (kickoff + cycle bump) are untouched. */
50
+ export declare function relaunchRootInPane(nodeId: string, pane: string): void;
@@ -2,16 +2,55 @@
2
2
  // window. Used by both the supervisor daemon (on crash/refresh detection) and
3
3
  // the explicit `crtr canvas revive` command.
4
4
  //
5
- // A revive always opens a NEW window: the old one is gone (crashed, or the
6
- // node exited with intent=refresh). The node's persisted LaunchSpec and cwd
7
- // are the canonical recipe; reviveNode replays them faithfully.
5
+ // A revive replays the node's persisted LaunchSpec + cwd (the canonical recipe)
6
+ // and routes PLACEMENT through reviveIntoPlacement (§1.4): a non-focused node
7
+ // opens a fresh background window in its home_session (the backstage `crtr` for
8
+ // a child — NEVER a user session); a node that occupies a LIVE focus resumes IN
9
+ // PLACE in that focus pane (respawn-pane -k, no new window). reviveNode never
10
+ // targets meta.tmux_session, so a background revive can no longer open an
11
+ // unbidden window in the user's session.
8
12
  //
9
- // resume=true → `pi --resume <pi_session_id>` — wakes the saved conversation.
13
+ // resume=true → `pi --session <path|id>` — wakes the saved conversation,
14
+ // preferring the absolute session-file path (cwd-immune) over
15
+ // the bare session id.
10
16
  // resume=false → fresh pi invocation — the node re-reads its roadmap/context dir.
11
- import { getNode, updateNode, } from '../canvas/index.js';
17
+ import { getNode, updateNode, setPresence, clearPid, fullName, } from '../canvas/index.js';
18
+ import { transition } from './lifecycle.js';
12
19
  import { buildPiArgv } from './launch.js';
13
- import { buildReviveKickoff } from './kickoff.js';
14
- import { ensureSession, openNodeWindow, piCommand, respawnPane, nodeSession, } from './tmux.js';
20
+ import { buildReviveKickoff, drainBearings } from './kickoff.js';
21
+ import { FRONT_DOOR_ENV } from './front-door.js';
22
+ import { piCommand, respawnPane, nodeSession } from './tmux.js';
23
+ import { reviveIntoPlacement, reconcile, isNodePaneAlive, homeSessionOf } from './placement.js';
24
+ /** signal-0 liveness probe for a pi pid (mirrors the daemon's isPidAlive). A
25
+ * null pid (legacy / never-booted) reads dead. */
26
+ function pidAlive(pid) {
27
+ if (pid == null)
28
+ return false;
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch (e) {
34
+ return e.code === 'EPERM';
35
+ }
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // resumeArgs — which session source a revive resumes from
39
+ // ---------------------------------------------------------------------------
40
+ /** Pick the `--session` source for a revive. resume=true prefers the absolute
41
+ * session-file path (immune to cwd; pi opens it directly) and keeps the bare
42
+ * session id as the fallback for older nodes booted before pi_session_file was
43
+ * captured. buildPiArgv prefers the path when both are present. resume=false (a
44
+ * refresh-yield) selects neither — the node re-reads its roadmap fresh. Pure so
45
+ * the path-vs-id selection is unit-testable without tmux. */
46
+ export function resumeArgs(meta, resume) {
47
+ if (!resume)
48
+ return {};
49
+ return {
50
+ resumeSessionId: meta.pi_session_id ?? undefined,
51
+ resumeSessionPath: meta.pi_session_file ?? undefined,
52
+ };
53
+ }
15
54
  // ---------------------------------------------------------------------------
16
55
  // reviveNode
17
56
  // ---------------------------------------------------------------------------
@@ -25,35 +64,63 @@ export function reviveNode(nodeId, opts) {
25
64
  if (meta === null) {
26
65
  throw new Error(`reviveNode: unknown node ${nodeId}`);
27
66
  }
28
- // The node lives in the shared global session. Prefer its stored session
29
- // (an inline root tracks its own real terminal session); fall back to the
30
- // shared node session.
31
- const session = meta.tmux_session ?? nodeSession();
32
- ensureSession(session, meta.cwd);
33
- // Decide whether to wake the saved pi conversation or start fresh.
34
- const resumeId = opts.resume && meta.pi_session_id != null
35
- ? meta.pi_session_id
36
- : undefined;
67
+ // Double-revive guard (pane-keyed, §2.4): reconcile FIRST so a user-moved pane
68
+ // isn't misread as "not yet revived", then probe pane-existence. A node whose
69
+ // pane is alive AND whose pi is still RUNNING was already revived by another
70
+ // path; re-launching would put a SECOND pi on the same session file — no-op.
71
+ // A FROZEN focus pane (remain-on-exit, F3) is pane-alive but pi-DEAD: that is
72
+ // the resume-into-focus case and MUST proceed (respawn-pane -k back into the
73
+ // frozen pane), so the guard gates on pi liveness too, not pane-existence alone.
74
+ reconcile(nodeId);
75
+ const live = getNode(nodeId) ?? meta;
76
+ if (isNodePaneAlive(nodeId) && pidAlive(live.pi_pid)) {
77
+ return {
78
+ window: live.window ?? null,
79
+ session: live.tmux_session ?? nodeSession(),
80
+ resumed: false,
81
+ };
82
+ }
83
+ // Every (re)launch is a new cycle — bump the counter so the editor label's
84
+ // trailing N advances. Mutate the in-memory meta too so buildPiArgv below
85
+ // builds the label with the incremented count.
86
+ meta.cycles = (meta.cycles ?? 0) + 1;
87
+ updateNode(nodeId, { cycles: meta.cycles });
88
+ // Decide whether to wake the saved pi conversation or start fresh. Prefer the
89
+ // absolute session-file path (cwd-immune); fall back to the bare id.
90
+ const resume = resumeArgs(meta, opts.resume);
91
+ const resuming = resume.resumeSessionPath !== undefined || resume.resumeSessionId !== undefined;
37
92
  // A fresh revive (no resume) gets a kickoff prompt so it re-reads its roadmap
38
- // and continues; resuming a saved conversation needs none.
39
- const inv = resumeId !== undefined
40
- ? buildPiArgv(meta, { resumeSessionId: resumeId })
41
- : buildPiArgv(meta, { prompt: buildReviveKickoff(meta) });
42
- const env = { ...inv.env, CRTR_ROOT_SESSION: session };
43
- const window = openNodeWindow({
44
- session,
45
- name: meta.name,
46
- cwd: meta.cwd,
47
- env,
93
+ // and continues; resuming a saved conversation needs none. drainBearings is the
94
+ // one-shot consuming step (yield note + feed cursor + persona ack); the builder
95
+ // is then pure.
96
+ let inv;
97
+ if (resuming) {
98
+ inv = buildPiArgv(meta, resume);
99
+ }
100
+ else {
101
+ const bearings = drainBearings(meta);
102
+ inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
103
+ }
104
+ // Placement owns WHERE this revive lands (§1.4): resume into a live focus pane
105
+ // if the node occupies one, else a fresh window in its home_session (the
106
+ // backstage `crtr` for a child — NEVER a user session). reviveIntoPlacement
107
+ // performs the one atomic setPresence; reviveNode keeps transition+clearPid
108
+ // around it (the crash-safety ordering, unchanged). THIS is the bug-kill: a
109
+ // non-focused background revive can no longer new-window into a user session.
110
+ transition(nodeId, 'revive');
111
+ const placed = reviveIntoPlacement(nodeId, {
48
112
  command: piCommand(inv.argv),
113
+ env: inv.env,
114
+ cwd: meta.cwd,
115
+ name: fullName(meta),
116
+ resuming,
49
117
  });
50
- updateNode(nodeId, {
51
- status: 'active',
52
- intent: null,
53
- window,
54
- tmux_session: session,
55
- });
56
- return { window, session, resumed: resumeId !== undefined };
118
+ // Window-backed launch: clear the stale pid so the daemon won't re-fire on
119
+ // it during the new pi's boot. The fresh pi re-records its pid on
120
+ // session_start; if it never boots, this window closes and the window-gone
121
+ // pass reaps it.
122
+ clearPid(nodeId);
123
+ return { window: placed.window, session: placed.session, resumed: resuming };
57
124
  }
58
125
  // ---------------------------------------------------------------------------
59
126
  // reviveInPlace — refresh-yield without churning the window
@@ -67,21 +134,81 @@ export function reviveNode(nodeId, opts) {
67
134
  * `pane` is the target pane id (the yielding node reads it from $TMUX_PANE).
68
135
  * Throws on unknown node or when the respawn could not be dispatched, so the
69
136
  * caller can fall back to a plain shutdown (daemon revives in a new window). */
70
- export function reviveInPlace(nodeId, pane) {
137
+ export function reviveInPlace(nodeId, pane, respawn = respawnPane) {
71
138
  const meta = getNode(nodeId);
72
139
  if (meta === null) {
73
140
  throw new Error(`reviveInPlace: unknown node ${nodeId}`);
74
141
  }
142
+ // A refresh-yield is a cycle too — advance the label's trailing N.
143
+ meta.cycles = (meta.cycles ?? 0) + 1;
144
+ updateNode(nodeId, { cycles: meta.cycles });
145
+ // The node's LOCATION — the session its pane physically lives in. The re-exec
146
+ // is IN PLACE (the pane never moves), so this is preserved unchanged below.
75
147
  const session = meta.tmux_session ?? nodeSession();
76
148
  // Fresh re-exec: same recipe as a no-resume reviveNode, with the kickoff so
77
- // the node rebuilds its bearings from disk.
78
- const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta) });
79
- const env = { ...inv.env, CRTR_ROOT_SESSION: session };
80
- const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
149
+ // the node rebuilds its bearings from disk. Drain the one-shot bearings first,
150
+ // then build purely.
151
+ const bearings = drainBearings(meta);
152
+ const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
153
+ // CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into — it must
154
+ // be the durable REVIVE-HOME (home_session), NOT the pane's live `session`. A
155
+ // FOCUSED child's pane is in a USER session (focus taints meta.tmux_session),
156
+ // so sourcing it from `session` would land any child it spawns in the user's
157
+ // session, re-tainting that child's home_session (A-MAJOR-1). home_session is
158
+ // the taint-immune backstage `crtr` for a child; for a root it equals its own
159
+ // session, so this is behavior-preserving there.
160
+ const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId) };
161
+ const ok = respawn({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
81
162
  if (!ok) {
82
163
  throw new Error(`reviveInPlace: respawn-pane dispatch failed for ${nodeId}`);
83
164
  }
84
- updateNode(nodeId, { status: 'active', intent: null, tmux_session: session });
165
+ // Deliberately DO NOT clear intent here, and DO NOT touch pi_pid. The detached
166
+ // respawn-pane can't confirm it actually replaced the pi (it kills this very
167
+ // process mid-flight), so clearing intent optimistically is how a failed
168
+ // refresh became a silent death: the fresh pi never boots, yet meta says the
169
+ // refresh completed. Instead we leave intent='refresh' (the fresh pi clears it
170
+ // on boot — the only proof the respawn worked) and leave pi_pid as the OLD
171
+ // pid. If the respawn succeeds, the old pi dies and the fresh one overwrites
172
+ // pid+intent within the daemon's grace window; if it fails, the old pid stays
173
+ // dead and the daemon's pi-liveness pass revives the node.
174
+ transition(nodeId, 'boot');
175
+ // tmux_session may have resolved to the shared session; window is unchanged
176
+ // (we re-execed in place), so preserve it explicitly.
177
+ setPresence(nodeId, { tmux_session: session, window: meta.window ?? null });
85
178
  // Window is unchanged (we re-execed in place); report the existing one.
86
179
  return { window: meta.window ?? null, session, resumed: false };
87
180
  }
181
+ // ---------------------------------------------------------------------------
182
+ // relaunchRootInPane — boot a CLEAN fresh root in the current pane (option C)
183
+ // ---------------------------------------------------------------------------
184
+ /** Re-exec a FRESH pi for `nodeId` in EXISTING `pane` (respawn-pane -k), with
185
+ * NO prompt and NO resume — a clean root conversation (goal-capture /
186
+ * context-intro handle the first message + bearings, exactly like bare
187
+ * `crtr`). Unlike reviveInPlace: no buildReviveKickoff prompt, no cycles bump,
188
+ * and it sets CRTR_FRONT_DOOR=1 (REQUIRED — src/core/runtime/CLAUDE.md: any
189
+ * path that boots a pi must guard against a removed/renamed subcommand
190
+ * fork-bombing). Throws if the respawn could not be dispatched.
191
+ *
192
+ * Used by relaunchRoot (reset.ts) for the `/new`-in-a-root relaunch. Kept
193
+ * SEPARATE from reviveInPlace so the refresh-yield path's exact semantics
194
+ * (kickoff + cycle bump) are untouched. */
195
+ export function relaunchRootInPane(nodeId, pane) {
196
+ const meta = getNode(nodeId);
197
+ if (meta === null) {
198
+ throw new Error(`relaunchRootInPane: unknown node ${nodeId}`);
199
+ }
200
+ // No prompt, no resume → a brand-new root conversation at cycle 0.
201
+ const inv = buildPiArgv(meta, {});
202
+ // Source CRTR_ROOT_SESSION from the durable REVIVE-HOME (home_session), the
203
+ // same taint-immunity rule as reviveInPlace. relaunchRootInPane runs only on a
204
+ // root, whose home_session IS its own session, so this is behavior-preserving
205
+ // — it keeps both in-pane revive paths sourced identically.
206
+ const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId), [FRONT_DOOR_ENV]: '1' };
207
+ const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
208
+ if (!ok) {
209
+ throw new Error(`relaunchRootInPane: respawn-pane dispatch failed for ${nodeId}`);
210
+ }
211
+ // Do NOT clear intent/pi_pid here — the fresh pi clears intent='refresh' on
212
+ // its session_start boot (the only proof the respawn worked), same dance as
213
+ // reviveInPlace.
214
+ }
@@ -1,10 +1,11 @@
1
1
  export declare function roadmapPath(nodeId: string): string;
2
2
  export declare function hasRoadmap(nodeId: string): boolean;
3
3
  export declare function readRoadmap(nodeId: string): string | null;
4
- /** Seed a fresh roadmap SCAFFOLD. No goal is required promotion lays this
5
- * down so the file exists for a refresh, and the owner authors the goal +
6
- * body as its next act (guided by its kind's roadmap skill). `goal`/
7
- * `exitCriteria` are optional overrides. Idempotent only if you intend it —
4
+ /** Seed a fresh, EXTREMELY BAREBONES roadmap scaffoldjust the section
5
+ * skeleton with one-line prompts. Promotion lays this down so the file exists
6
+ * for a refresh; the owner fleshes it out as its next act (guided by its
7
+ * kind's roadmap skill). `goal`/`exitCriteria` pre-fill those sections when
8
+ * known (e.g. from the node's goal doc). Idempotent only if you intend it —
8
9
  * call sites guard on hasRoadmap to avoid clobbering an evolved map. */
9
10
  export declare function seedRoadmap(nodeId: string, opts?: {
10
11
  goal?: string;