@crouton-kit/crouter 0.3.14 → 0.3.16

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 (224) 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 +45 -67
  46. package/dist/commands/human.js +4 -14
  47. package/dist/commands/node.d.ts +11 -0
  48. package/dist/commands/node.js +221 -99
  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 +129 -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 +196 -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 +266 -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 +178 -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 +334 -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 +105 -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 +205 -4
  142. package/dist/core/canvas/focuses.d.ts +22 -0
  143. package/dist/core/canvas/focuses.js +81 -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 +24 -12
  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 +26 -12
  176. package/dist/core/runtime/launch.js +78 -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 +39 -1
  184. package/dist/core/runtime/nodes.js +69 -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 +299 -0
  188. package/dist/core/runtime/placement.js +688 -0
  189. package/dist/core/runtime/promote.d.ts +14 -7
  190. package/dist/core/runtime/promote.js +57 -67
  191. package/dist/core/runtime/reset.d.ts +47 -4
  192. package/dist/core/runtime/reset.js +223 -52
  193. package/dist/core/runtime/revive.d.ts +26 -2
  194. package/dist/core/runtime/revive.js +166 -39
  195. package/dist/core/runtime/spawn.d.ts +20 -5
  196. package/dist/core/runtime/spawn.js +163 -43
  197. package/dist/core/runtime/stop-guard.d.ts +1 -1
  198. package/dist/core/runtime/stop-guard.js +18 -8
  199. package/dist/core/runtime/tmux-chrome.d.ts +1 -0
  200. package/dist/core/runtime/tmux-chrome.js +4 -0
  201. package/dist/core/runtime/tmux.d.ts +113 -20
  202. package/dist/core/runtime/tmux.js +221 -39
  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 +594 -262
  216. package/dist/pi-extensions/canvas-resume.d.ts +22 -0
  217. package/dist/pi-extensions/canvas-resume.js +173 -0
  218. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  219. package/dist/pi-extensions/canvas-stophook.js +340 -228
  220. package/dist/types.d.ts +28 -0
  221. package/dist/types.js +16 -0
  222. package/package.json +2 -2
  223. package/dist/core/runtime/presence.d.ts +0 -38
  224. package/dist/core/runtime/presence.js +0 -154
@@ -1,21 +1,28 @@
1
1
  import { type NodeMeta } from '../canvas/index.js';
2
2
  export interface PromoteResult {
3
3
  meta: NodeMeta;
4
- /** Orchestration guidance to surface into the node's current context now. */
5
- guidance: string;
6
4
  roadmapWritten: boolean;
7
5
  /** Absolute path to the node's roadmap doc (context/roadmap.md). */
8
6
  roadmapPath: string;
9
7
  /** Absolute path to the node's goal doc (context/initial-prompt.md). */
10
8
  goalPath: string;
9
+ /** Absolute path to the node-local memory index (context/memory/MEMORY.md). */
10
+ memoryPath: string;
11
+ /** Absolute path to the user-global memory index (<crtrHome>/memory/MEMORY.md). */
12
+ userMemoryPath: string;
13
+ /** Absolute path to the project memory index (<crtrHome>/projects/<key>/memory/MEMORY.md). */
14
+ projectMemoryPath: string;
11
15
  }
12
- /** Promote a node to resident orchestrator, optionally specializing its kind
13
- * (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
14
- * re-promoting just rewrites the spec + returns fresh guidance. Seeds a
15
- * roadmap SCAFFOLD if absent (a boss with no map is a failure mode) no goal
16
- * is forced here; authoring the goal + roadmap is the node's next act. */
16
+ /** Promote a node to an orchestrator (mode→orchestrator), optionally
17
+ * specializing its kind (e.g. a `general` worker becoming a
18
+ * `developer.orchestrator`) and optionally also making it resident. Idempotent:
19
+ * re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
20
+ * boss with no map is a failure mode) no goal is forced here; authoring the
21
+ * goal + roadmap is the node's next act. The transition guidance is injected
22
+ * centrally by the persona injector at the next turn boundary, not returned. */
17
23
  export declare function promote(nodeId: string, opts?: {
18
24
  kind?: string;
25
+ resident?: boolean;
19
26
  }): PromoteResult;
20
27
  export interface YieldResult {
21
28
  meta: NodeMeta;
@@ -1,71 +1,36 @@
1
- // Promotion — terminal → resident, and the worker→orchestrator polymorph.
1
+ // Promotion — the worker→orchestrator polymorph (mode→orchestrator).
2
2
  //
3
3
  // Two stages (the pi-mode-switch pattern):
4
- // 1. Promotion → guidance dump (mid-turn, ephemeral). This call flips the
5
- // node's mode/lifecycle and (optionally) its KIND, REWRITES its launch
6
- // spec to that kind's orchestrator persona (so the next revive comes back
7
- // as that orchestrator), seeds a roadmap scaffold, and RETURNS kind-
8
- // specific orchestration + roadmap-shaping guidance which enters the
9
- // current context so the node can author its roadmap before any refresh.
4
+ // 1. Promotion → mode flips to orchestrator (mid-turn). This call flips the
5
+ // node's mode and (optionally) its KIND, REWRITES its launch spec to that
6
+ // kind's orchestrator persona (so the next revive comes back as that
7
+ // orchestrator), and seeds a roadmap scaffold + the three memory stores.
8
+ // The transition guidance the node needs is injected CENTRALLY by the
9
+ // persona injector (runtime/persona.ts) at the turn boundary promote()
10
+ // itself no longer returns or hand-emits guidance.
10
11
  // 2. Refresh → persona swap (permanent). On the next fresh revive the node
11
12
  // starts with the orchestrator system prompt baked in (because the launch
12
- // spec now says orchestrator). The guidance dump bridges until then.
13
+ // spec now says orchestrator). The injected guidance bridges until then.
14
+ //
15
+ // Mode and lifecycle are ORTHOGONAL: promotion flips mode only. Lifecycle stays
16
+ // whatever it was (a promoted child is terminal/orchestrator — still reports up
17
+ // + reaps) unless the caller passes `resident:true` to also make it resident.
13
18
  //
14
19
  // Trigger is persistence-need (deliberate, or a refresh-yield with open work),
15
20
  // never the mere act of spawning a child.
16
- import { getNode, updateNode, hasActiveLiveSubscription } from '../canvas/index.js';
21
+ import { getNode, updateNode } from '../canvas/index.js';
22
+ import { transition } from './lifecycle.js';
17
23
  import { buildLaunchSpec } from './launch.js';
18
- import { loadKernel, loadPersona } from '../personas/index.js';
19
- import { resolveSkill } from '../resolver.js';
20
- import { readText } from '../fs-utils.js';
21
- import { parseFrontmatter } from '../frontmatter.js';
22
- import { hasRoadmap, seedRoadmap, readRoadmap, roadmapPath } from './roadmap.js';
24
+ import { hasRoadmap, seedRoadmap, roadmapPath } from './roadmap.js';
25
+ import { seedMemory, memoryPath, seedUserMemory, userMemoryPath, seedProjectMemory, projectMemoryPath, } from './memory.js';
23
26
  import { readGoal, goalPath } from './kickoff.js';
24
- /** Load a skill's body text by name, or null if it can't be resolved. Used to
25
- * inline a kind's roadmap-shaping skill into the promotion guidance dump. */
26
- function loadSkillBody(name) {
27
- try {
28
- const skill = resolveSkill(name, {});
29
- return parseFrontmatter(readText(skill.path)).body.trim();
30
- }
31
- catch {
32
- return null;
33
- }
34
- }
35
- /** Build the mid-turn guidance dump, specialized to the node's (possibly
36
- * just-chosen) kind: the shared kernel + that kind's roadmap-shaping skill
37
- * (auto-loaded now, before the persona swap bakes in on revive) + the roadmap
38
- * scaffold the node must author. No goal is assumed — writing it is step one. */
39
- function orchestrationGuidance(nodeId, kind) {
40
- const kernel = loadKernel();
41
- const orch = loadPersona(kind, 'orchestrator');
42
- const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
43
- ? orch.frontmatter['roadmapSkill']
44
- : undefined;
45
- const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
46
- const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
47
- const rmPath = roadmapPath(nodeId);
48
- const goal = readGoal(nodeId);
49
- const parts = [
50
- `You are now a RESIDENT ${kind.toUpperCase()} ORCHESTRATOR. Your scarce resource is your own context window.`,
51
- 'Your job is to manage context and delegate — not to do the goal yourself.',
52
- '',
53
- kernel,
54
- ];
55
- if (goal !== null && goal.trim() !== '') {
56
- parts.push('', `--- Your goal (${goalPath(nodeId)}) ---`, '', goal.trim());
57
- }
58
- if (skillBody) {
59
- parts.push('', `--- How to shape a ${kind} roadmap (skill: ${roadmapSkill}) ---`, '', skillBody);
60
- }
61
- 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, '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
62
- return parts.join('\n');
63
- }
64
- /** Promote a node to resident orchestrator, optionally specializing its kind
65
- * (e.g. a `general` worker becoming a `developer.orchestrator`). Idempotent:
66
- * re-promoting just rewrites the spec + returns fresh guidance. Seeds a
67
- * roadmap SCAFFOLD if absent (a boss with no map is a failure mode) — no goal
68
- * is forced here; authoring the goal + roadmap is the node's next act. */
27
+ /** Promote a node to an orchestrator (mode→orchestrator), optionally
28
+ * specializing its kind (e.g. a `general` worker becoming a
29
+ * `developer.orchestrator`) and optionally also making it resident. Idempotent:
30
+ * re-promoting just rewrites the spec. Seeds a roadmap SCAFFOLD if absent (a
31
+ * boss with no map is a failure mode) — no goal is forced here; authoring the
32
+ * goal + roadmap is the node's next act. The transition guidance is injected
33
+ * centrally by the persona injector at the next turn boundary, not returned. */
69
34
  export function promote(nodeId, opts = {}) {
70
35
  const node = getNode(nodeId);
71
36
  if (node === null)
@@ -76,7 +41,13 @@ export function promote(nodeId, opts = {}) {
76
41
  // *next* revive comes back orchestrating in that kind (polymorph stage 2).
77
42
  // nodeEnv reads meta.{kind,mode}, so CRTR_KIND/CRTR_MODE flip immediately for
78
43
  // the live process's children too.
79
- const { launch } = buildLaunchSpec(targetKind, 'orchestrator');
44
+ // Bake the node's post-promote lifecycle + spine into the rebuilt prompt:
45
+ // lifecycle becomes resident only when the caller asked (else it keeps its
46
+ // current value); spine is fixed by parent-ness (immutable).
47
+ const { launch } = buildLaunchSpec(targetKind, 'orchestrator', {
48
+ lifecycle: opts.resident === true ? 'resident' : node.lifecycle,
49
+ hasManager: node.parent !== null,
50
+ });
80
51
  // Seed a barebones roadmap scaffold if absent so the file exists for a
81
52
  // refresh. Pre-fill its Goal from the node's goal doc when present (set at
82
53
  // spawn, or captured from the first user message); the node fleshes out the
@@ -87,13 +58,30 @@ export function promote(nodeId, opts = {}) {
87
58
  seedRoadmap(nodeId, goal !== null && goal.trim() !== '' ? { goal: goal.trim() } : {});
88
59
  roadmapWritten = true;
89
60
  }
90
- const meta = updateNode(nodeId, { kind: targetKind, lifecycle: 'resident', mode: 'orchestrator', launch });
61
+ // Seed all three scoped memory stores alongside the roadmap — user-global,
62
+ // project (keyed off this node's cwd), and node-local. Each is a durable,
63
+ // refresh-surviving artifact; each guarded so a re-seed never clobbers an
64
+ // evolved memory.
65
+ seedUserMemory();
66
+ seedProjectMemory(node.cwd);
67
+ seedMemory(nodeId);
68
+ // Flip mode→orchestrator + kind + launch spec. Lifecycle is independent:
69
+ // only set resident when the caller asked for it (the common self-promotion
70
+ // stays terminal/orchestrator — it still reports up + reaps).
71
+ const meta = updateNode(nodeId, {
72
+ kind: targetKind,
73
+ mode: 'orchestrator',
74
+ launch,
75
+ ...(opts.resident === true ? { lifecycle: 'resident' } : {}),
76
+ });
91
77
  return {
92
78
  meta,
93
- guidance: orchestrationGuidance(nodeId, targetKind),
94
79
  roadmapWritten,
95
80
  roadmapPath: roadmapPath(nodeId),
96
81
  goalPath: goalPath(nodeId),
82
+ memoryPath: memoryPath(nodeId),
83
+ userMemoryPath: userMemoryPath(),
84
+ projectMemoryPath: projectMemoryPath(node.cwd),
97
85
  };
98
86
  }
99
87
  /** Request a refresh-yield: discard in-memory context and revive fresh against
@@ -107,14 +95,16 @@ export function requestYield(nodeId, opts = {}) {
107
95
  if (node === null)
108
96
  throw new Error(`unknown node: ${nodeId}`);
109
97
  let promoted = false;
110
- if (node.lifecycle === 'terminal') {
111
- // Yielding with open work must survive a context reset promote
112
- // (optionally specializing the kind).
98
+ if (node.mode !== 'orchestrator') {
99
+ // A yield needs a ROADMAP to refresh against i.e. orchestrator mode, not
100
+ // resident lifecycle. Ensure orchestrator (which seeds the roadmap + memory)
101
+ // WITHOUT forcing resident: a terminal/orchestrator yields fine, since the
102
+ // daemon's refresh-revive keys on intent='refresh', not lifecycle.
113
103
  promote(nodeId, opts.kind !== undefined ? { kind: opts.kind } : {});
114
104
  promoted = true;
115
105
  }
116
106
  // Mark the intent; the stophook enacts the shutdown, the daemon the revive.
117
- const meta = updateNode(nodeId, { intent: 'refresh' });
118
- void hasActiveLiveSubscription; // (open-work signal, reserved for future gating)
107
+ transition(nodeId, 'yield');
108
+ const meta = getNode(nodeId);
119
109
  return { meta, promoted, willRefresh: true };
120
110
  }
@@ -1,13 +1,56 @@
1
+ /** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
2
+ * on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
3
+ * window (closes the daemon revive race). Edges are LEFT INTACT — descendants
4
+ * keep parent=rootId. No wipe. Returns the reaped ids.
5
+ *
6
+ * Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
7
+ * -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
8
+ * quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
9
+ * `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
10
+ * fallback, so both leave their descendants `done`. */
11
+ export declare function reapDescendants(rootId: string): string[];
1
12
  export interface ResetRootResult {
2
- /** Descendant node ids torn down (window killed + marked dead). */
13
+ /** Descendant node ids torn down (window killed + marked done). */
3
14
  reaped: string[];
4
15
  /** Direct subscriptions dropped off the root. */
5
16
  detached: string[];
6
17
  /** True when the node was a root and a full reset ran. */
7
18
  reset: boolean;
8
19
  }
9
- /** Reset a root node to a pristine, empty graph (the `/new` semantics).
20
+ /** Reset a root node to a pristine, empty graph (the legacy `/new` semantics
21
+ * now used as the no-pane fallback and the non-root session-id refresh).
10
22
  *
11
23
  * For a non-root (spawned child), a `/new` is not a graph reset — we only
12
- * refresh its session id so a later `--resume` wakes the right conversation. */
13
- export declare function resetRoot(nodeId: string, newSessionId?: string): ResetRootResult;
24
+ * refresh its session id so a later `--session <id>` wakes the right conversation. */
25
+ export declare function resetRoot(nodeId: string, newSessionId?: string, newSessionFile?: string | null): ResetRootResult;
26
+ /** Injectable respawn seam — tests pass a double since tmux isn't available. */
27
+ export interface RelaunchDeps {
28
+ relaunchRootInPane?: (nodeId: string, pane: string) => void;
29
+ }
30
+ export type HandleNewSessionPath = 'relaunch' | 'reset-root' | 'reset-child' | 'noop';
31
+ export interface HandleNewSessionResult {
32
+ path: HandleNewSessionPath;
33
+ newNodeId?: string;
34
+ }
35
+ /** The single entry the stophook calls on a detected `/new` (session id change).
36
+ * Policy lives here so the stophook stays thin and this stays unit-testable:
37
+ * - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
38
+ * - root + pane present → relaunchRoot(nodeId, pane) (option C)
39
+ * - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
40
+ * On a respawn-dispatch failure the live pi never died, so we degrade to the
41
+ * legacy in-place reset. */
42
+ export declare function handleNewSession(nodeId: string, newSessionId: string, pane: string | undefined, deps?: RelaunchDeps, newSessionFile?: string | null): HandleNewSessionResult;
43
+ /** Park the old root + create+launch a fresh root in `pane` (option C). All DB
44
+ * writes are synchronous and happen BEFORE the respawn (the respawn kills the
45
+ * caller). Returns the new node id, or null on a defensive guard (not a root /
46
+ * already parked). Throws only if the respawn dispatch fails — and self-rolls-
47
+ * back its writes first so the caller can degrade to resetRoot. */
48
+ export declare function relaunchRoot(oldId: string, pane: string, deps?: RelaunchDeps): {
49
+ newNodeId: string;
50
+ } | null;
51
+ /** Resolve a cleanly-exiting node to `done`. Returns true iff it transitioned.
52
+ * Guard: only a real quit, and only a node still active|idle with no pending
53
+ * intent — so it never clobbers a node already routed by agent_end to done
54
+ * (push final), refresh (yield), or idle-release. Pure/DB-only (no pi/tmux) so
55
+ * the guard is unit-testable without a live pi. */
56
+ export declare function markCleanExitDone(nodeId: string, reason: unknown): boolean;
@@ -1,67 +1,89 @@
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, setFocusOccupant, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
23
+ import { transition } from './lifecycle.js';
24
+ import { paneLocation, tearDownNode, focusOf } from './placement.js';
16
25
  import { buildLaunchSpec } from './launch.js';
17
26
  import { roadmapPath } from './roadmap.js';
18
- /** Reset a root node to a pristine, empty graph (the `/new` semantics).
27
+ import { spawnNode, newNodeId, nodeSession } from './nodes.js';
28
+ import { relaunchRootInPane } from './revive.js';
29
+ // ---------------------------------------------------------------------------
30
+ // reapDescendants — tear down a root's descendant sub-DAG (shared helper)
31
+ // ---------------------------------------------------------------------------
32
+ /** Reap the descendant sub-DAG of `rootId`: mark each **done** (the user moved
33
+ * on — a clean teardown, NOT a fault) + clear intent FIRST, then kill its
34
+ * window (closes the daemon revive race). Edges are LEFT INTACT — descendants
35
+ * keep parent=rootId. No wipe. Returns the reaped ids.
36
+ *
37
+ * Why `done`, and why marking is STILL explicit: a `closeWindow`/`respawn-pane
38
+ * -k` kill is abrupt and fires NO clean `session_shutdown`, so the general
39
+ * quit→done rule does NOT auto-resolve a force-killed descendant — we mark it
40
+ * `done` here. Shared by relaunchRoot (option C) and resetRoot's in-place
41
+ * fallback, so both leave their descendants `done`. */
42
+ export function reapDescendants(rootId) {
43
+ const reaped = [];
44
+ for (const id of view(rootId)) {
45
+ try {
46
+ // Reap BEFORE tearing down the placement (the crash-safety invariant the
47
+ // `reap` event encodes): a non-supervised status + cleared intent first, so
48
+ // the daemon can't revive a descendant mid-teardown. tearDownNode then
49
+ // closes any focus row it held, kills its pane (pane-keyed), and nulls its
50
+ // LOCATION.
51
+ transition(id, 'reap');
52
+ tearDownNode(id);
53
+ reaped.push(id);
54
+ }
55
+ catch {
56
+ /* one bad node never aborts the reap */
57
+ }
58
+ }
59
+ return reaped;
60
+ }
61
+ /** Reset a root node to a pristine, empty graph (the legacy `/new` semantics —
62
+ * now used as the no-pane fallback and the non-root session-id refresh).
19
63
  *
20
64
  * 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) {
65
+ * refresh its session id so a later `--session <id>` wakes the right conversation. */
66
+ export function resetRoot(nodeId, newSessionId, newSessionFile) {
23
67
  const meta = getNode(nodeId);
24
68
  if (meta === null)
25
69
  return { reaped: [], detached: [], reset: false };
26
70
  // Only roots own a graph in the "ran crtr again" sense.
27
71
  if (meta.parent != null) {
28
72
  if (newSessionId !== undefined) {
29
- try {
30
- updateNode(nodeId, { pi_session_id: newSessionId });
31
- }
32
- catch { /* */ }
73
+ updateNode(nodeId, {
74
+ pi_session_id: newSessionId,
75
+ ...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
76
+ });
33
77
  }
34
78
  return { reaped: [], detached: [], reset: false };
35
79
  }
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
- }
80
+ // 1) Reap the descendant sub-DAG (mark done + kill windows; shared helper).
81
+ const reaped = reapDescendants(nodeId);
55
82
  // 2) Detach the root's own subscriptions so its view is empty.
56
83
  const detached = [];
57
84
  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
- }
85
+ unsubscribe(nodeId, sub.node_id);
86
+ detached.push(sub.node_id);
65
87
  }
66
88
  // 3) Wipe the root's working state (reports / inbox / roadmap).
67
89
  for (const p of [
@@ -78,20 +100,169 @@ export function resetRoot(nodeId, newSessionId) {
78
100
  /* */
79
101
  }
80
102
  }
81
- // 4) Re-point the root at a fresh base persona + the new pi session id.
103
+ // 4) Re-point the root at a fresh base persona + the new pi session id. A
104
+ // root is resident by definition (this only runs on roots — see the early
105
+ // return above), so resetting to base/resident is the model, not a bypass.
106
+ // Re-seed persona_ack to the fresh persona so the pristine `/new`
107
+ // conversation never gets a spurious mode/lifecycle transition steer (the
108
+ // persona injector compares against this ack).
109
+ const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
110
+ updateNode(nodeId, {
111
+ mode: 'base',
112
+ lifecycle: 'resident',
113
+ persona_ack: { mode: 'base', lifecycle: 'resident' },
114
+ launch,
115
+ ...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
116
+ ...(newSessionFile !== undefined ? { pi_session_file: newSessionFile } : {}),
117
+ });
118
+ transition(nodeId, 'revive');
119
+ return { reaped, detached, reset: true };
120
+ }
121
+ /** The single entry the stophook calls on a detected `/new` (session id change).
122
+ * Policy lives here so the stophook stays thin and this stays unit-testable:
123
+ * - non-root child → resetRoot(nodeId, newSessionId) (session-id refresh only)
124
+ * - root + pane present → relaunchRoot(nodeId, pane) (option C)
125
+ * - root + no pane (no tmux) → resetRoot(nodeId, newSessionId) (in-place fallback)
126
+ * On a respawn-dispatch failure the live pi never died, so we degrade to the
127
+ * legacy in-place reset. */
128
+ export function handleNewSession(nodeId, newSessionId, pane, deps = {}, newSessionFile) {
129
+ const meta = getNode(nodeId);
130
+ if (meta === null)
131
+ return { path: 'noop' };
132
+ // Non-root child: a `/new` only refreshes its session id (unchanged).
133
+ if (meta.parent != null) {
134
+ resetRoot(nodeId, newSessionId, newSessionFile);
135
+ return { path: 'reset-child' };
136
+ }
137
+ // Root with no pane (not inside tmux): in-place reset fallback. Option C needs
138
+ // a pane to respawn into; resetRoot needs the new session id (available here
139
+ // because the trigger is session_start).
140
+ if (pane === undefined || pane.trim() === '') {
141
+ resetRoot(nodeId, newSessionId, newSessionFile);
142
+ return { path: 'reset-root' };
143
+ }
144
+ // Root with a pane: option C relaunch. relaunchRoot self-rolls-back its DB
145
+ // writes on a respawn-dispatch failure and rethrows; we then degrade to the
146
+ // legacy in-place reset (the live pi is still alive, never killed).
147
+ try {
148
+ const result = relaunchRoot(nodeId, pane, deps);
149
+ if (result === null)
150
+ return { path: 'noop' }; // defensive guard hit (e.g. rapid double /new)
151
+ return { path: 'relaunch', newNodeId: result.newNodeId };
152
+ }
153
+ catch {
154
+ resetRoot(nodeId, newSessionId, newSessionFile);
155
+ return { path: 'reset-root' };
156
+ }
157
+ }
158
+ /** Park the old root + create+launch a fresh root in `pane` (option C). All DB
159
+ * writes are synchronous and happen BEFORE the respawn (the respawn kills the
160
+ * caller). Returns the new node id, or null on a defensive guard (not a root /
161
+ * already parked). Throws only if the respawn dispatch fails — and self-rolls-
162
+ * back its writes first so the caller can degrade to resetRoot. */
163
+ export function relaunchRoot(oldId, pane, deps = {}) {
164
+ const oldMeta = getNode(oldId);
165
+ if (oldMeta === null || oldMeta.parent != null)
166
+ return null; // defensive: not a root
167
+ if (oldMeta.status === 'done')
168
+ return null; // defensive: already parked (rapid double /new)
169
+ const respawn = deps.relaunchRootInPane ?? relaunchRootInPane;
170
+ // Resolve where the new pi will live (pane authoritative; fall back to old
171
+ // meta when paneLocation can't resolve, e.g. outside a live tmux server).
172
+ const loc = paneLocation(pane) ?? {
173
+ session: oldMeta.tmux_session ?? null,
174
+ window: oldMeta.window ?? null,
175
+ };
176
+ const newId = newNodeId();
177
+ const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
178
+ // Park-old + mint-new is the single most fragile spot in the runtime, so it is
179
+ // ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
180
+ // failure anywhere — including the respawn DISPATCH — rolls the whole thing
181
+ // back, leaving the old root EXACTLY as it was (no hand-rolled compensation).
182
+ // Only the *detached* respawn (the async pane kill) lands outside the txn — it
183
+ // must, since it kills this caller, and by then COMMIT has made the new state
184
+ // durable. The focus repoint (step 4) is INSIDE the txn, so a ROLLBACK undoes
185
+ // it automatically — there is no file to restore.
186
+ const db = openDb();
187
+ db.exec('BEGIN');
82
188
  try {
83
- const { launch } = buildLaunchSpec(meta.kind, 'base');
84
- updateNode(nodeId, {
189
+ // 1) Reap descendants (mark done + kill windows, keep edges, no wipe).
190
+ reapDescendants(oldId);
191
+ // 2) Create the fresh root node (new id, empty context dir via
192
+ // ensureNodeDirs) seeded active; `yield` adds the refresh safety net so
193
+ // that if the pane dies before boot the daemon revives it in a new window.
194
+ spawnNode({
195
+ kind: oldMeta.kind,
85
196
  mode: 'base',
86
197
  lifecycle: 'resident',
87
- intent: null,
88
- status: 'active',
198
+ cwd: oldMeta.cwd,
199
+ name: oldMeta.kind,
200
+ parent: null,
201
+ spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
202
+ nodeId: newId,
89
203
  launch,
90
- ...(newSessionId !== undefined ? { pi_session_id: newSessionId } : {}),
91
204
  });
205
+ transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
206
+ setPresence(newId, { tmux_session: loc.session, window: loc.window });
207
+ // REVIVE-HOME: the relaunched root's durable revive target is the session
208
+ // of the pane it is respawned into (same pane-recycle rule as demote).
209
+ updateNode(newId, { home_session: loc.session ?? nodeSession() });
210
+ clearPid(newId); // no pi yet → daemon 'leave' until boot records the pid
211
+ // 3) Park the old root: reap (done + intent cleared) and detach its window so
212
+ // it never claims the pane, but KEEP pi_session_id (resumable),
213
+ // parent=null, and all edges.
214
+ transition(oldId, 'reap');
215
+ setPresence(oldId, { window: null, tmux_session: null });
216
+ // 4) Focus follows content: repoint the old root's focus row to the new root
217
+ // (same pane — respawn-pane -k below keeps the %id). Inside the txn → the
218
+ // ROLLBACK path restores the old occupant automatically (no file to restore).
219
+ const oldFocus = focusOf(oldId);
220
+ if (oldFocus !== null)
221
+ setFocusOccupant(oldFocus.focus_id, newId);
222
+ // 5) Re-exec pi in this pane bound to newId; the dispatch is the LAST thing
223
+ // inside the txn. If it throws the txn rolls back (old root untouched); on
224
+ // success we COMMIT and the async detached kill of this pane lands after.
225
+ respawn(newId, pane);
226
+ db.exec('COMMIT');
92
227
  }
93
- catch {
94
- /* */
228
+ catch (err) {
229
+ // Dispatch failed (or a write threw) — the live pi never died. Roll the whole
230
+ // transaction back so the old root is FULLY restored, then degrade.
231
+ try {
232
+ db.exec('ROLLBACK');
233
+ }
234
+ catch { /* */ }
235
+ // The rolled-back new node's row is gone, but spawnNode already scaffolded its
236
+ // on-disk dir (ensureNodeDirs). With no row, prune never sees it — so remove
237
+ // the orphan dir here, otherwise it is permanent disk litter on this rare path.
238
+ try {
239
+ rmSync(nodeDir(newId), { recursive: true, force: true });
240
+ }
241
+ catch { /* */ }
242
+ // The focus repoint was inside the txn; ROLLBACK already restored the old
243
+ // occupant — nothing to undo here.
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 './placement.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;