@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
@@ -0,0 +1,92 @@
1
+ // bearings.ts — the <crtr-context> framing prose, shared by the two paths that
2
+ // deliver it so they can never drift:
3
+ //
4
+ // • the context-intro pi-extension injects buildContextBearings() as the
5
+ // node's first session message in every brand-new chat;
6
+ // • promote.ts folds orchestratorContextNote() into the promotion guidance
7
+ // dump, so a node that becomes an orchestrator MID-LIFE gets the
8
+ // orchestrator framing it never received at spawn — it spawned as a base
9
+ // worker, and the bearings already in its history carry only the base note.
10
+ //
11
+ // Base framing (every node): the context dir is durable, shared scratch — the
12
+ // one place other nodes on the canvas can read from, so it is for documents
13
+ // worth a shared reference, NOT a task tracker, and NOT a "future memory-wiped
14
+ // you" stash (a terminal worker has no future cycle — that framing only makes
15
+ // sense once a node is a resident orchestrator).
16
+ //
17
+ // Orchestrator addendum (resident orchestrators — i.e. nodes that have a
18
+ // node-local memory store): the dir ALSO survives refresh cycles, so it is where
19
+ // a future cycle of the orchestrator resumes; durable cross-goal lessons live in
20
+ // the three scoped memory stores, whose index pointer lines are inlined into
21
+ // <memory> (the how-to lives once in the kernel, not here).
22
+ import { contextDir, getNode } from '../canvas/index.js';
23
+ import { hasMemory, memoryDir, readMemory, hasUserMemory, userMemoryDir, readUserMemory, hasProjectMemory, projectMemoryDir, readProjectMemory, } from './memory.js';
24
+ /** Base framing — present for every node. No path baked in: the caller carries
25
+ * the dir in the <crtr-context dir="…"> attribute. */
26
+ export const BASE_CONTEXT_NOTE = 'This is your context directory — durable scratch space on disk, and the one place the other ' +
27
+ 'nodes on the canvas can read from. Put documents here that you want to share by reference ' +
28
+ 'instead of re-explaining them in a prompt: specs, designs, findings, notes worth pointing a ' +
29
+ 'sibling, child, or parent at. It is a shared document store, not a task tracker.';
30
+ /** Orchestrator-only framing: a resident orchestrator survives refresh cycles,
31
+ * so its context dir is also where a future cycle of itself resumes the work.
32
+ * Used inside the bearings block AND in the promotion guidance dump, so a
33
+ * promoted node gets the same note a born-orchestrator gets. */
34
+ export function orchestratorContextNote(nodeId) {
35
+ return (`Because you persist across refresh cycles, your context directory (${contextDir(nodeId)}) is ` +
36
+ `also where a future cycle of you resumes the work — keep the working notes and decisions a ` +
37
+ `refreshed you would need there, alongside the docs you share with the nodes you spawn.`);
38
+ }
39
+ /** One labeled store stanza inside <memory>: a compact `label · dir` header (the
40
+ * scope name + where to WRITE this kind of memory), then the LIVE pointer lines
41
+ * extracted fresh from the store's index — only lines matching `- [...` — with
42
+ * the index's how-to boilerplate dropped (it lives once in the kernel) and
43
+ * detail files loaded on demand. Falls back to `(empty)` when the index carries
44
+ * no pointers, which also covers the not-seeded / template-only case. */
45
+ function memoryStanza(label, dir, index) {
46
+ const pointers = (index ?? '')
47
+ .split('\n')
48
+ .filter((line) => /^\s*-\s*\[/.test(line))
49
+ .map((line) => line.trim());
50
+ const body = pointers.length > 0 ? pointers.join('\n') : '(empty)';
51
+ return `${label} · ${dir}\n${body}`;
52
+ }
53
+ /** The <memory> block (orchestrators only): the scoped stores merged, each a
54
+ * `label · dir` header over its live index pointer lines. A memory's `type`
55
+ * decides which store it lands in — the mapping + the how-to live once in the
56
+ * orchestration kernel ("Your long-term memory"); here we carry only the live
57
+ * data + a one-line pointer back to it. user-global rides in when the node has
58
+ * a user store, project when it has a project store, node-local always (the
59
+ * orchestrator gate). */
60
+ export function buildMemoryBlock(nodeId, cwd) {
61
+ const stanzas = [];
62
+ if (hasUserMemory()) {
63
+ stanzas.push(memoryStanza('user-global', userMemoryDir(), readUserMemory()));
64
+ }
65
+ if (hasProjectMemory(cwd)) {
66
+ stanzas.push(memoryStanza('project', projectMemoryDir(cwd), readProjectMemory(cwd)));
67
+ }
68
+ stanzas.push(memoryStanza('node-local', memoryDir(nodeId), readMemory(nodeId)));
69
+ const n = stanzas.length;
70
+ return ('<memory>\n' +
71
+ `Long-term memory, ${n} scope${n === 1 ? '' : 's'}. Each line ` +
72
+ '`- [Title](slug.md) — hook`; load a detail file by slug from the scope dir on demand. ' +
73
+ 'Write a new fact to the scope matching its `type` (see "Your long-term memory").\n\n' +
74
+ stanzas.join('\n\n') +
75
+ '\n</memory>');
76
+ }
77
+ /** The full <crtr-context> bearings block: base framing always, plus the
78
+ * orchestrator addendum + the merged three-store <memory> block when the node
79
+ * has a node-local memory store (the orchestrator gate). */
80
+ export function buildContextBearings(nodeId) {
81
+ const dir = contextDir(nodeId);
82
+ if (!hasMemory(nodeId)) {
83
+ // A terminal worker (no memory store): base framing only, no memory block.
84
+ return `<crtr-context dir="${dir}">\n${BASE_CONTEXT_NOTE}\n</crtr-context>`;
85
+ }
86
+ // An orchestrator: across-cycles framing + the merged three-store memory. The
87
+ // project store is keyed off the node's cwd (its working dir on disk).
88
+ const cwd = getNode(nodeId)?.cwd ?? process.cwd();
89
+ return (`<crtr-context dir="${dir}">\n` +
90
+ `${BASE_CONTEXT_NOTE}\n${orchestratorContextNote(nodeId)}\n${buildMemoryBlock(nodeId, cwd)}\n` +
91
+ '</crtr-context>');
92
+ }
@@ -0,0 +1,14 @@
1
+ export interface CloseNodeResult {
2
+ /** The focused node that was closed — the cascade root. */
3
+ root: string;
4
+ /** Every node torn down (root + cascaded descendants), in kill order
5
+ * (leaves first, root last). */
6
+ closed: string[];
7
+ /** Descendants left alive because a manager outside the subtree still
8
+ * subscribes to them. */
9
+ spared: string[];
10
+ }
11
+ /** Close `rootId` and its exclusive subtree. Best-effort throughout: a tmux/db
12
+ * failure on one node never aborts the cascade. Throws only on an unknown root
13
+ * so the command can surface a clean not-found error. */
14
+ export declare function closeNode(rootId: string): CloseNodeResult;
@@ -0,0 +1,151 @@
1
+ // close.ts — the "close this node + its subtree" action behind `crtr node close`.
2
+ //
3
+ // Closing a node tears down the focused node and every descendant it
4
+ // EXCLUSIVELY owns, walking DOWN the subscribes_to spine (subscriptionsOf = a
5
+ // node's reports/children). Nothing is deleted: pi_session_id, the canvas
6
+ // edges, and all on-disk state persist, so any closed node can later be revived
7
+ // (`crtr canvas revive` / focus → `pi --session <id>`). A close is a pause, not a reap.
8
+ //
9
+ // Per node, in this order — the order matters twice:
10
+ //
11
+ // 1. Mark `canceled` + clear intent. Done BEFORE the window dies: the daemon
12
+ // only ever revives an active|idle node, so flipping to canceled first
13
+ // closes the race where the supervisor sees a window-gone live node and
14
+ // either revives it or marks it dead (overwriting our canceled).
15
+ // 2. Kill its tmux PANE (the window closes once its last pane goes) — which
16
+ // kills pi and, with it, the inbox watcher. Pane-granular so that nodes
17
+ // the user co-located as panes in ONE window (via swap-pane focus) are not
18
+ // all taken down when one of them is closed.
19
+ // 3. Append the cancellation notice to its inbox AFTER the watcher is gone.
20
+ // The watcher advances its cursor when it READS an entry, so appending
21
+ // while it is still live would let it consume + skip the notice (cursor
22
+ // moves past it, never delivered). Killed first, the cursor stays put;
23
+ // on the node's next resume a fresh watcher seeds from that frozen cursor,
24
+ // finds the notice, and injects it — the agent learns its children died.
25
+ //
26
+ // The cascade is GUARDED: a descendant is closed only when EVERY node that
27
+ // subscribes to it (its managers, subscribersOf — active OR passive) is itself
28
+ // inside the closing set. A node still subscribed to by a manager outside the
29
+ // subtree is left running — "only kill the children if they are only subscribed
30
+ // to by the agent being closed", generalized to any depth via a fixpoint.
31
+ import { getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
32
+ import { transition } from './lifecycle.js';
33
+ import { tearDownNode } from './placement.js';
34
+ import { appendInbox } from '../feed/inbox.js';
35
+ /** The set of nodes to close: the root plus every descendant reachable down the
36
+ * subscriptions spine, all of whose managers are themselves in the set. Grown
37
+ * to a fixpoint — a node added this pass can qualify its own children next
38
+ * pass. Cycle-safe via the membership skip. */
39
+ function closingSet(root) {
40
+ const closing = new Set([root]);
41
+ let changed = true;
42
+ while (changed) {
43
+ changed = false;
44
+ for (const parent of [...closing]) {
45
+ for (const sub of subscriptionsOf(parent)) {
46
+ const child = sub.node_id;
47
+ if (closing.has(child))
48
+ continue;
49
+ // Close the child only if NOBODY outside the closing set subscribes to
50
+ // it. (subscriptionsOf always yields child→parent, so `parent` is one
51
+ // of child's managers and is in `closing` — the check is never vacuous.)
52
+ if (subscribersOf(child).every((m) => closing.has(m.node_id))) {
53
+ closing.add(child);
54
+ changed = true;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return closing;
60
+ }
61
+ /** BFS the closing set from root, then reverse: leaves die first, the focused
62
+ * root dies last ("cascades up"). The root being killed last also keeps the
63
+ * user's foreground window — the one they invoked the close from — open until
64
+ * every background descendant is gone. */
65
+ function killOrder(root, closing) {
66
+ const order = [];
67
+ const seen = new Set([root]);
68
+ const queue = [root];
69
+ while (queue.length > 0) {
70
+ const id = queue.shift();
71
+ order.push(id);
72
+ for (const sub of subscriptionsOf(id)) {
73
+ if (closing.has(sub.node_id) && !seen.has(sub.node_id)) {
74
+ seen.add(sub.node_id);
75
+ queue.push(sub.node_id);
76
+ }
77
+ }
78
+ }
79
+ // Any closing node a cycle kept BFS from reaching still gets torn down.
80
+ for (const id of closing)
81
+ if (!seen.has(id))
82
+ order.push(id);
83
+ return order.reverse();
84
+ }
85
+ /** The inbox notice a closed node reads on its next resume. */
86
+ function cancellationLabel(isRoot, deadChildren) {
87
+ const who = isRoot
88
+ ? 'You were CLOSED by the user from the canvas'
89
+ : 'You were CANCELED — an ancestor of yours was closed from the canvas';
90
+ if (deadChildren.length === 0) {
91
+ return `${who}. Your pi session is preserved; this resume reopened it.`;
92
+ }
93
+ const names = deadChildren.slice(0, 4).map((c) => {
94
+ const n = getNode(c);
95
+ return n !== null ? `${n.name} (${c})` : c;
96
+ });
97
+ const more = deadChildren.length > names.length ? ` +${deadChildren.length - names.length} more` : '';
98
+ return (`${who}. ${deadChildren.length} child node(s) you subscribe to were canceled with you and are no ` +
99
+ `longer running: ${names.join(', ')}${more}. Resuming will NOT restore them — re-spawn if you ` +
100
+ `still need that work.`);
101
+ }
102
+ /** Close `rootId` and its exclusive subtree. Best-effort throughout: a tmux/db
103
+ * failure on one node never aborts the cascade. Throws only on an unknown root
104
+ * so the command can surface a clean not-found error. */
105
+ export function closeNode(rootId) {
106
+ if (getNode(rootId) === null)
107
+ throw new Error(`closeNode: unknown node ${rootId}`);
108
+ const closing = closingSet(rootId);
109
+ const order = killOrder(rootId, closing);
110
+ // Descendants reachable from the subtree but kept alive (shared managers).
111
+ const spared = [];
112
+ for (const id of closing) {
113
+ for (const sub of subscriptionsOf(id)) {
114
+ if (!closing.has(sub.node_id) && !spared.includes(sub.node_id)) {
115
+ spared.push(sub.node_id);
116
+ }
117
+ }
118
+ }
119
+ const closed = [];
120
+ for (const id of order) {
121
+ try {
122
+ const m = getNode(id);
123
+ if (m === null)
124
+ continue;
125
+ // This node's reports that are dying with it (for the resume notice).
126
+ const deadChildren = subscriptionsOf(id)
127
+ .map((s) => s.node_id)
128
+ .filter((c) => closing.has(c));
129
+ // 1) Canceled + intent cleared BEFORE the window dies (daemon race).
130
+ transition(id, 'cancel');
131
+ // 2) Tear the node off its placement (pane-keyed): close any focus row it
132
+ // occupies, kill its PANE (the window closes once its last pane goes, so
133
+ // sibling nodes the user co-located in one window survive), and null its
134
+ // LOCATION (closing the focus row is the record — no pointer to clear).
135
+ tearDownNode(id);
136
+ // 3) Leave the resume notice AFTER the watcher is gone, so it survives.
137
+ appendInbox(id, {
138
+ from: null,
139
+ tier: 'normal',
140
+ kind: 'message',
141
+ label: cancellationLabel(id === rootId, deadChildren),
142
+ data: { reason: 'user-close', cascade_root: rootId, canceled_children: deadChildren },
143
+ });
144
+ closed.push(id);
145
+ }
146
+ catch {
147
+ /* one bad node never aborts the cascade */
148
+ }
149
+ }
150
+ return { root: rootId, closed, spared };
151
+ }
@@ -13,14 +13,13 @@
13
13
  // message) — falling back to a short note when it never reported.
14
14
  import { readdirSync, readFileSync, statSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
- import { getNode, updateNode } from '../canvas/index.js';
16
+ import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
17
17
  import { reportsDir } from '../canvas/paths.js';
18
18
  import { pushFinal } from '../feed/feed.js';
19
- import { spawnNode } from './nodes.js';
19
+ import { spawnNode, nodeSession } from './nodes.js';
20
20
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
21
- import { respawnPane, piCommand, paneLocation, nodeSession } from './tmux.js';
22
21
  import { FRONT_DOOR_ENV } from './front-door.js';
23
- import { getFocus, setFocus } from './presence.js';
22
+ import { focusOf, recycleFocusPane, piCommand, paneLocation } from './placement.js';
24
23
  import { ensureDaemon } from '../../daemon/manage.js';
25
24
  /** The agent's most recent surfaced message: the newest reports/*.md body with
26
25
  * its YAML frontmatter stripped. Empty string when the node never reported. */
@@ -70,20 +69,21 @@ export async function demoteNode(nodeId, callerPane) {
70
69
  finalized = true;
71
70
  }
72
71
  catch { /* recycle the pane even if the report failed */ }
73
- // The demoted node no longer holds a window — the pane is being reclaimed.
72
+ // Capture M's focus viewport (if any) BEFORE nulling — the fresh root inherits
73
+ // it (the SAME focus row + pane). The demoted node no longer holds a pane: it is
74
+ // being reclaimed.
75
+ const f = focusOf(nodeId);
74
76
  try {
75
- updateNode(nodeId, { window: null, tmux_session: null });
77
+ setPresence(nodeId, { pane: null, window: null, tmux_session: null });
76
78
  }
77
79
  catch { /* best-effort */ }
78
- if (getFocus() === nodeId)
79
- setFocus('');
80
80
  // 2 + 3. Recycle — boot a fresh resident root in the SAME pane.
81
81
  try {
82
82
  ensureDaemon();
83
83
  }
84
84
  catch { /* daemon is best-effort */ }
85
85
  const loc = paneLocation(pane);
86
- const { launch } = buildLaunchSpec('general', 'base');
86
+ const { launch } = buildLaunchSpec('general', 'base', { lifecycle: 'resident', hasManager: false });
87
87
  const root = spawnNode({
88
88
  kind: 'general',
89
89
  mode: 'base',
@@ -93,11 +93,23 @@ export async function demoteNode(nodeId, callerPane) {
93
93
  parent: null,
94
94
  launch,
95
95
  });
96
- if (loc !== null)
97
- updateNode(root.node_id, { tmux_session: loc.session, window: loc.window });
96
+ // REVIVE-HOME: a demote-recycled root's durable revive target is the session
97
+ // of the pane it was recycled into (the one place home_session is rewritten
98
+ // after birth). Falls back to the backstage when the pane can't be located.
99
+ updateNode(root.node_id, { home_session: loc?.session ?? nodeSession() });
100
+ // Hand the viewport to the fresh root: reuse M's focus row over the SAME pane
101
+ // (respawn-pane -k below keeps the %id), so the user keeps watching this slot.
102
+ if (f !== null) {
103
+ try {
104
+ setFocusOccupant(f.focus_id, root.node_id);
105
+ }
106
+ catch { /* best-effort */ }
107
+ }
98
108
  const fresh = getNode(root.node_id);
99
109
  const inv = buildPiArgv(fresh);
100
110
  const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
101
- const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
111
+ const ok = recycleFocusPane(root.node_id, pane, {
112
+ command: piCommand(inv.argv), env, cwd: meta.cwd, name: fullName(fresh), resuming: false,
113
+ });
102
114
  return { demoted: ok, finalized, newRoot: root.node_id, delivered };
103
115
  }
@@ -92,6 +92,6 @@ export function maybeBootRoot(root, argv) {
92
92
  // Unambiguous front-door launch → boot a resident root inline (exec pi in
93
93
  // this terminal). Does not return.
94
94
  const args = parseRootArgs(tokens);
95
- bootRoot({ ...args, placement: 'inline' });
95
+ bootRoot({ ...args });
96
96
  return true;
97
97
  }
@@ -24,9 +24,26 @@ export declare function writeYieldMessage(nodeId: string, text: string): void;
24
24
  export declare function consumeYieldMessage(nodeId: string): string | null;
25
25
  /** List the node's context/ dir (filenames, sorted). Empty when absent. */
26
26
  export declare function listContextDir(nodeId: string): string[];
27
- /** Build the auto-injected first message for a FRESH revive of `meta`. Reads
28
- * the node's goal, roadmap, context dir, feed, and one-shot yield message off
29
- * disk and frames them so the revived node can rebuild its bearings in one
30
- * turn. Side effects: consumes the yield message and advances the feed cursor
31
- * (both are "read" by surfacing them here). */
32
- export declare function buildReviveKickoff(meta: NodeMeta): string;
27
+ export interface ReviveBearings {
28
+ /** The one-shot yield note left by the prior self (already consumed/deleted). */
29
+ yieldMsg: string | null;
30
+ /** Coalesced digest of unread reports, or null when the feed was empty. The
31
+ * cursor has already been advanced past these. */
32
+ unreadDigest: string | null;
33
+ /** Persona-transition guidance to surface when the node's role was changed
34
+ * while it was away (its ack has already been committed), else null. */
35
+ driftGuidance: string | null;
36
+ }
37
+ /** Drain the one-shot revive bearings for `meta`: consume the yield note, advance
38
+ * the feed cursor past the unread reports, and capture+commit any external
39
+ * persona drift. The CONSUMING step of a fresh revive — the revive paths call it
40
+ * ONCE, then pass the result to buildReviveKickoff (which is then pure; building
41
+ * twice eats nothing). Calling drainBearings a second time would drain an
42
+ * already-empty note/feed, so ONLY the revive paths call it. */
43
+ export declare function drainBearings(meta: NodeMeta): ReviveBearings;
44
+ /** Assemble the auto-injected first message for a FRESH revive of `meta` from its
45
+ * already-drained `bearings` (see drainBearings) plus pure on-disk reads of the
46
+ * node's goal, roadmap, and context dir, framed so the revived node can rebuild
47
+ * its bearings in one turn. PURE: no state mutation, so calling it twice yields
48
+ * the same string and consumes nothing — drainBearings owns the one-shot reads. */
49
+ export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings): string;
@@ -6,18 +6,22 @@
6
6
  // conversation already holds the context).
7
7
  //
8
8
  // Layout (the framing a revived node sees):
9
- // <goal file=…>…</goal> the mandate it was spawned with
10
- // <roadmap file=…>…</roadmap> its evolving plan
9
+ // <roadmap file=…>…</roadmap> its evolving plan the source of truth
11
10
  // <context-dir path=…>…</context-dir> what artifacts exist on disk
12
11
  // <feed>Awaiting N nodes … digest</feed> who it waits on + unread reports
13
12
  // <yield-message>…</yield-message> the note its prior self left on yield
14
13
  //
15
- // The goal + yield-message are companion files in the node's context dir; the
14
+ // The roadmap (NOT the original spawn prompt) carries the goal on a refresh: its
15
+ // frozen core holds goal + exit criteria, its body the live plan. context/
16
+ // initial-prompt.md is NEVER injected into a node's prompts — it lives on disk
17
+ // purely as a log of the original mandate; by the time a node is running it is
18
+ // usually stale, and the roadmap is the doc the node keeps current. The
16
19
  // yield-message is one-shot (consumed on the next revive).
17
20
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
18
21
  import { join } from 'node:path';
19
- import { contextDir, getNode, subscriptionsOf, } from '../canvas/index.js';
22
+ import { contextDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
20
23
  import { readRoadmap, roadmapPath } from './roadmap.js';
24
+ import { personaDrift, commitPersonaAck } from './persona.js';
21
25
  import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
22
26
  // ---------------------------------------------------------------------------
23
27
  // Companion context files: the goal (the spawning mandate) and the one-shot
@@ -91,62 +95,114 @@ export function listContextDir(nodeId) {
91
95
  return [];
92
96
  return readdirSync(dir).sort();
93
97
  }
94
- // ---------------------------------------------------------------------------
95
- // Feed block who the node is awaiting, plus a drained digest of unread
96
- // reports. Draining here advances the cursor: the revived node has now "read"
97
- // the feed, so a later `crtr feed read` shows only what arrives afterward.
98
- // ---------------------------------------------------------------------------
99
- function feedBlock(nodeId) {
98
+ /** Drain the one-shot revive bearings for `meta`: consume the yield note, advance
99
+ * the feed cursor past the unread reports, and capture+commit any external
100
+ * persona drift. The CONSUMING step of a fresh revive — the revive paths call it
101
+ * ONCE, then pass the result to buildReviveKickoff (which is then pure; building
102
+ * twice eats nothing). Calling drainBearings a second time would drain an
103
+ * already-empty note/feed, so ONLY the revive paths call it. */
104
+ export function drainBearings(meta) {
105
+ const nodeId = meta.node_id;
106
+ // Consume the one-shot yield note (deleted on read) BEFORE the kickoff lists
107
+ // the context dir, so it never shows up there.
108
+ const yieldMsg = consumeYieldMessage(nodeId);
109
+ // Drain the feed: read unread since the cursor and advance it past them, so a
110
+ // later `crtr feed read` shows only what arrives afterward.
111
+ const cursor = readCursor(nodeId);
112
+ const entries = readInboxSince(nodeId, cursor);
113
+ let unreadDigest = null;
114
+ if (entries.length > 0) {
115
+ writeCursor(nodeId, entries[entries.length - 1].ts);
116
+ unreadDigest = coalesce(entries);
117
+ }
118
+ // Capture + commit any external persona drift (the second of the two delivery
119
+ // sites). Committing the ack here is the mutation; the guidance is surfaced by
120
+ // the pure builder from this captured value.
121
+ const drift = personaDrift(nodeId);
122
+ let driftGuidance = null;
123
+ if (drift !== null) {
124
+ driftGuidance = drift.guidance;
125
+ commitPersonaAck(nodeId, drift.to);
126
+ }
127
+ return { yieldMsg, unreadDigest, driftGuidance };
128
+ }
129
+ /** Render the <feed> block PURELY: the live "awaiting" roster (a read) plus the
130
+ * already-drained unread digest (from drainBearings). No cursor write here. */
131
+ function feedBlock(nodeId, unreadDigest) {
100
132
  // Awaiting = active subscriptions whose publisher is still live (active|idle).
101
133
  const awaiting = subscriptionsOf(nodeId)
102
134
  .filter((s) => s.active)
103
135
  .map((s) => getNode(s.node_id))
104
136
  .filter((m) => m !== null && (m.status === 'active' || m.status === 'idle'));
105
137
  const lines = [];
106
- lines.push(`Awaiting ${awaiting.length} node${awaiting.length === 1 ? '' : 's'}.`);
107
- for (const m of awaiting)
108
- lines.push(` - ${m.name} (${m.node_id}) ${m.status}`);
109
- const cursor = readCursor(nodeId);
110
- const entries = readInboxSince(nodeId, cursor);
111
- if (entries.length > 0) {
112
- writeCursor(nodeId, entries[entries.length - 1].ts);
113
- lines.push('', coalesce(entries));
138
+ if (awaiting.length > 0) {
139
+ const n = awaiting.length;
140
+ const subj = n === 1 ? 'it is' : 'they are';
141
+ const pron = n === 1 ? 'it' : 'they';
142
+ const verb = n === 1 ? 'pushes' : 'push';
143
+ // State aliveness + the automatic wake at the source. Bare status ("— active")
144
+ // left earlier revives unsure whether the worker was really live, so they
145
+ // burned a turn on `feed read`/`feed peek` to confirm. Asserting it here
146
+ // removes the reason to check.
147
+ lines.push(`Awaiting ${n} node${n === 1 ? '' : 's'} — ${subj} alive and running right now, and will wake you the moment ${pron} ${verb}. The wake is automatic; nothing to check, poll, or verify.`);
148
+ for (const m of awaiting)
149
+ lines.push(` - ${m.name} (${m.node_id}) — ${m.status}`);
150
+ lines.push('', unreadDigest ??
151
+ '(no unread reports yet — expected while they run: a worker leaves no pointer until it pushes, so an empty feed means still working, not stalled)');
114
152
  }
115
153
  else {
116
- lines.push('', '(no unread reports)');
154
+ lines.push('Awaiting 0 nodes.');
155
+ lines.push('', unreadDigest ?? '(no unread reports)');
117
156
  }
118
157
  return `<feed>\n${lines.join('\n')}\n</feed>`;
119
158
  }
120
159
  // ---------------------------------------------------------------------------
121
160
  // buildReviveKickoff — assemble the full fresh-revive first message.
122
161
  // ---------------------------------------------------------------------------
123
- /** Build the auto-injected first message for a FRESH revive of `meta`. Reads
124
- * the node's goal, roadmap, context dir, feed, and one-shot yield message off
125
- * disk and frames them so the revived node can rebuild its bearings in one
126
- * turn. Side effects: consumes the yield message and advances the feed cursor
127
- * (both are "read" by surfacing them here). */
128
- export function buildReviveKickoff(meta) {
162
+ /** Assemble the auto-injected first message for a FRESH revive of `meta` from its
163
+ * already-drained `bearings` (see drainBearings) plus pure on-disk reads of the
164
+ * node's goal, roadmap, and context dir, framed so the revived node can rebuild
165
+ * its bearings in one turn. PURE: no state mutation, so calling it twice yields
166
+ * the same string and consumes nothing — drainBearings owns the one-shot reads. */
167
+ export function buildReviveKickoff(meta, bearings) {
129
168
  const nodeId = meta.node_id;
130
- // Consume the one-shot yield note first so it never shows in the dir listing.
131
- const yieldMsg = consumeYieldMessage(nodeId);
132
169
  const parts = [
133
170
  `${REVIVE_KICKOFF_SENTINEL} — your previous in-memory ` +
134
171
  'context is gone, by design. Everything below was just read from disk; it is your ' +
135
172
  'full bearings. Rebuild from it and continue toward your goal.',
136
173
  ];
137
- const goal = readGoal(nodeId);
138
- if (goal !== null && goal.trim() !== '') {
139
- parts.push(`<goal file="${goalPath(nodeId)}">\n${goal.trim()}\n</goal>`);
140
- }
174
+ // The roadmap is the source of truth on a fresh revive: its frozen core holds
175
+ // the goal/exit criteria, its body the live plan the node kept current. The
176
+ // original spawn prompt (context/initial-prompt.md) is deliberately NOT injected
177
+ // — it lives on disk only as a log, and by now it is usually stale.
141
178
  const roadmap = readRoadmap(nodeId);
142
179
  parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${roadmap !== null && roadmap.trim() !== '' ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
143
180
  const files = listContextDir(nodeId);
144
181
  parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
145
- parts.push(feedBlock(nodeId));
146
- parts.push(yieldMsg !== null
147
- ? `<yield-message>\n${yieldMsg.trim()}\n</yield-message>`
182
+ parts.push(feedBlock(nodeId, bearings.unreadDigest));
183
+ parts.push(bearings.yieldMsg !== null
184
+ ? `<yield-message>\n${bearings.yieldMsg.trim()}\n</yield-message>`
148
185
  : '<yield-message/>');
149
- parts.push('If there is work to do, perform it. Otherwise stop `crtr push final "<result>"` ' +
150
- 'if the goal is met, or end your turn to stay dormant awaiting your workers.');
186
+ // A node that reports UP the spine (has subscribers awaiting its result)
187
+ // finishes with `push final`. A human-attended node (no subscribers a root
188
+ // conversation working directly with the user) has no result to submit and
189
+ // must not be told to finish: it stays resident and keeps working with the
190
+ // user.
191
+ const reportsUp = subscribersOf(nodeId).length > 0;
192
+ parts.push(reportsUp
193
+ ? 'If there is work to do, perform it. Otherwise stop — `crtr push final "<result>"` ' +
194
+ 'if the goal is met, or end your turn to stay dormant awaiting your workers.'
195
+ : 'If there is work to do, perform it. Otherwise end your turn — you are working ' +
196
+ 'directly with the user, so stay available and continue the conversation when they ' +
197
+ 'write back.');
198
+ // Persona-transition catch-up. If the node's mode/lifecycle was changed
199
+ // EXTERNALLY while it was dormant (e.g. a human ran `crtr node lifecycle` /
200
+ // `node promote --node` on it), it never saw the turn_end injector. drainBearings
201
+ // captured the guidance for its new persona and committed the ack (the second
202
+ // and only other delivery site); we just surface it. A clean fresh revive has
203
+ // no drift, so this is empty unless a real external change happened.
204
+ if (bearings.driftGuidance !== null) {
205
+ parts.push(`<persona-transition>\nYour role was changed while you were away. ${bearings.driftGuidance}\n</persona-transition>`);
206
+ }
151
207
  return parts.join('\n\n');
152
208
  }
@@ -1,23 +1,35 @@
1
- import type { NodeMeta, LaunchSpec, Mode } from '../canvas/index.js';
1
+ import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
2
2
  export declare const CANVAS_STOPHOOK_PATH: string;
3
3
  export declare const CANVAS_INBOX_WATCHER_PATH: string;
4
4
  export declare const CANVAS_NAV_PATH: string;
5
5
  export declare const CANVAS_GOAL_CAPTURE_PATH: string;
6
6
  export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
7
+ export declare const CANVAS_CONTEXT_INTRO_PATH: string;
7
8
  export declare const CANVAS_COMMANDS_PATH: string;
9
+ export declare const CANVAS_RESUME_PATH: string;
8
10
  /** The canvas extensions every node loads, in order: stophook (routing +
9
11
  * telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
10
12
  * graph chrome), goal-capture (persist the first user message as the goal),
11
13
  * passive-context (drain passive backlog as pre-text on the next message),
12
- * commands (the /promote slash-command). All self-gate on CRTR_NODE_ID.
13
- * goal-capture precedes passive-context so it reads the raw user text. */
14
+ * context-intro (inject the <crtr-context> bearings block as its own session
15
+ * message, once per brand-new chat), commands (the /promote slash-command),
16
+ * resume (the /resume-node whole-canvas picker → `crtr node focus`).
17
+ * All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
18
+ * reads the raw user text. */
14
19
  export declare const CANVAS_EXTENSIONS: string[];
15
20
  /** Bare model aliases resolve to the anthropic provider under pi (avoids the
16
21
  * bedrock default). Anything with a `/` or an unknown name passes through. */
17
22
  export declare function normalizeModel(model: string): string;
18
- /** Compose a node's full pi launch recipe from its persona. The two canvas
19
- * extensions are always first; persona-declared extensions follow. */
20
- export declare function buildLaunchSpec(kind: string, mode: Mode, opts?: {
23
+ /** Compose a node's full pi launch recipe from its persona. The system prompt
24
+ * is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
25
+ * (terminal/resident the finish contract) and spine position (hasManager —
26
+ * whether the push-up family is taught at all). Callers pass the authoritative
27
+ * lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
28
+ * prompt faithfully. The two canvas extensions are always first; persona-
29
+ * declared extensions follow. */
30
+ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
31
+ lifecycle: Lifecycle;
32
+ hasManager: boolean;
21
33
  extraEnv?: Record<string, string>;
22
34
  }): {
23
35
  launch: LaunchSpec;
@@ -30,16 +42,18 @@ export interface PiInvocation {
30
42
  /** env to merge into the process. */
31
43
  env: Record<string, string>;
32
44
  }
33
- /** The pi session display name — the editor label in the top-left. Shows the
34
- * node's name plus its current mode so base vs orchestrator reads at a glance
35
- * (e.g. `developer (orchestrator)`). Recomputed from `meta.mode` on every
36
- * revive, so a base→orchestrator polymorph updates the label. */
37
- export declare function editorLabel(meta: NodeMeta): string;
38
45
  /** Construct the pi invocation for a node.
39
46
  * - fresh start: pass `prompt` (the node's first user message), no resume.
40
- * - revive idle/done: pass `resumeSessionId` to `--resume` (keeps conversation).
47
+ * - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
48
+ * — pi COPIES that conversation into a NEW session for this node, then `prompt`
49
+ * is delivered as the next message. One-shot at birth: the node thereafter
50
+ * captures its OWN pi_session_file and revives by `--session` like any other.
51
+ * - revive idle/done: pass `resumeSessionPath` (absolute .jsonl path, preferred)
52
+ * or `resumeSessionId` (bare uuid fallback) to `--session` (keeps conversation).
41
53
  * - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
42
54
  export declare function buildPiArgv(meta: NodeMeta, opts?: {
43
55
  prompt?: string;
44
56
  resumeSessionId?: string;
57
+ resumeSessionPath?: string;
58
+ forkFrom?: string;
45
59
  }): PiInvocation;