@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -32,15 +32,20 @@ export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
32
32
  export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
33
33
  export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
34
34
  export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
35
+ export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
35
36
  export const CANVAS_COMMANDS_PATH = resolveExtension('canvas-commands');
36
37
  export const CANVAS_RESUME_PATH = resolveExtension('canvas-resume');
38
+ export const CANVAS_VIEW_PATH = resolveExtension('canvas-view');
37
39
  /** The canvas extensions every node loads, in order: stophook (routing +
38
40
  * telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
39
41
  * graph chrome), goal-capture (persist the first user message as the goal),
40
42
  * passive-context (drain passive backlog as pre-text on the next message),
41
43
  * context-intro (inject the <crtr-context> bearings block as its own session
42
- * message, once per brand-new chat), commands (the /promote slash-command),
43
- * resume (the /resume-node whole-canvas picker `crtr node focus`).
44
+ * message, once per brand-new chat), doc-substrate (the unified document
45
+ * substrate's two hooks: ## Skills + ## Preferences at boot, on-read context
46
+ * injection), commands (the /promote slash-command), resume (the /resume-node
47
+ * whole-canvas picker → `crtr node focus`), view (the /view popup → `crtr
48
+ * view pick` / `crtr view run <name>`).
44
49
  * All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
45
50
  * reads the raw user text. */
46
51
  export const CANVAS_EXTENSIONS = [
@@ -50,12 +55,27 @@ export const CANVAS_EXTENSIONS = [
50
55
  CANVAS_GOAL_CAPTURE_PATH,
51
56
  CANVAS_PASSIVE_CONTEXT_PATH,
52
57
  CANVAS_CONTEXT_INTRO_PATH,
58
+ CANVAS_DOC_SUBSTRATE_PATH,
53
59
  CANVAS_COMMANDS_PATH,
54
60
  CANVAS_RESUME_PATH,
61
+ CANVAS_VIEW_PATH,
55
62
  ];
56
- /** Bare model aliases resolve to the anthropic provider under pi (avoids the
57
- * bedrock default). Anything with a `/` or an unknown name passes through. */
63
+ /** The named capability tiers a caller picks with `--model`, in descending
64
+ * strength. Each maps to a concrete pi model spec. `strong`/`medium`/`light`
65
+ * resolve to the latest opus/sonnet/haiku; `ultra` to the frontier model. */
66
+ export const MODEL_TIERS = {
67
+ ultra: 'anthropic/claude-fable-5',
68
+ strong: 'anthropic/opus',
69
+ medium: 'anthropic/sonnet',
70
+ light: 'anthropic/haiku',
71
+ };
72
+ /** Resolve a model token to the spec pi gets via `--model`. A named tier
73
+ * (ultra/strong/medium/light) maps to its concrete spec; a bare alias
74
+ * (sonnet/opus/haiku) resolves to the anthropic provider under pi (avoids the
75
+ * bedrock default); anything with a `/` or an unknown name passes through. */
58
76
  export function normalizeModel(model) {
77
+ if (model in MODEL_TIERS)
78
+ return MODEL_TIERS[model];
59
79
  const bare = new Set(['sonnet', 'opus', 'haiku']);
60
80
  if (bare.has(model))
61
81
  return `anthropic/${model}`;
@@ -73,8 +93,12 @@ export function normalizeModel(model) {
73
93
  * declared extensions follow. */
74
94
  export function buildLaunchSpec(kind, mode, opts) {
75
95
  const p = resolvePersona(kind, mode, { lifecycle: opts.lifecycle, hasManager: opts.hasManager });
96
+ // A caller-supplied override (durable on `meta.model_override`, re-passed on
97
+ // every polymorph) wins over the persona's declared default; absent both, the
98
+ // model is left unset and the node inherits pi's default.
99
+ const chosenModel = opts.model ?? p.model;
76
100
  const launch = {
77
- model: p.model !== undefined ? normalizeModel(p.model) : undefined,
101
+ model: chosenModel !== undefined ? normalizeModel(chosenModel) : undefined,
78
102
  tools: p.tools,
79
103
  extensions: [...CANVAS_EXTENSIONS, ...p.extensions],
80
104
  systemPrompt: p.systemPrompt,
@@ -158,3 +182,59 @@ export function buildPiArgv(meta, opts = {}) {
158
182
  argv.push(opts.prompt);
159
183
  return { argv, env: nodeEnv(meta) };
160
184
  }
185
+ /** Translate a `PiInvocation` (the recipe `buildPiArgv` produced) into the SDK
186
+ * config the broker hosts an engine with. This is the EXACT inverse of
187
+ * `buildPiArgv` and must track its flag set 1:1 — co-located here so the two
188
+ * never drift. Safe because `buildPiArgv` is the SOLE producer of these flags
189
+ * (we own both ends). SIDE EFFECT: merges `inv.env` into `process.env` so the
190
+ * in-process engine + the bound canvas extensions see the same env a forked pi
191
+ * would (CRTR_NODE_ID, CRTR_ROOT_SESSION, CRTR_SUBTREE, …). */
192
+ export function piInvocationToSdkConfig(inv) {
193
+ for (const [k, v] of Object.entries(inv.env))
194
+ process.env[k] = v;
195
+ const cfg = {
196
+ cwd: inv.env['CRTR_NODE_CWD'] ?? process.cwd(),
197
+ extensionPaths: [],
198
+ };
199
+ const argv = inv.argv;
200
+ for (let i = 0; i < argv.length; i++) {
201
+ const tok = argv[i];
202
+ switch (tok) {
203
+ case '-e':
204
+ cfg.extensionPaths.push(argv[++i]);
205
+ break;
206
+ case '-n':
207
+ cfg.editorName = argv[++i];
208
+ break;
209
+ case '--fork':
210
+ cfg.forkFrom = argv[++i];
211
+ break;
212
+ case '--session': {
213
+ const v = argv[++i];
214
+ // A path (contains `/` or ends `.jsonl`) resumes via SessionManager.open;
215
+ // a bare uuid is the legacy fallback. Same classification buildPiArgv
216
+ // documents (path preferred, immune to cwd discrepancy).
217
+ if (v.includes('/') || v.endsWith('.jsonl'))
218
+ cfg.resumeSessionPath = v;
219
+ else
220
+ cfg.resumeSessionId = v;
221
+ break;
222
+ }
223
+ case '--model':
224
+ cfg.model = argv[++i];
225
+ break;
226
+ case '--tools':
227
+ cfg.tools = argv[++i].split(',').filter((t) => t !== '');
228
+ break;
229
+ case '--append-system-prompt':
230
+ cfg.appendSystemPromptPath = argv[++i];
231
+ break;
232
+ default:
233
+ // buildPiArgv consumes every flag's value via the cases above, so any
234
+ // token reaching here is the single trailing positional (the prompt).
235
+ cfg.firstPrompt = tok;
236
+ break;
237
+ }
238
+ }
239
+ return cfg;
240
+ }
@@ -15,10 +15,11 @@
15
15
  //
16
16
  // Crash-safety invariant (was a comment repeated in reset/close/reapDescendants):
17
17
  // "flip status to a non-supervised value + clear intent BEFORE killing the
18
- // window" — the daemon only ever revives active|idle nodes, so a teardown must
18
+ // host" (tmux pane or headless broker) — the daemon only ever revives
19
+ // active|idle nodes, so a teardown must
19
20
  // leave the node done/canceled first to close the revive race. That invariant is
20
21
  // now the DEFINITION of the `cancel` event: callers flip via transition()
21
- // and only THEN kill the window.
22
+ // and only THEN tear the host down.
22
23
  //
23
24
  // Unification (A5, human-confirmed 2026-06-06): an externally-reaped node — torn
24
25
  // down because the user moved on (close cascade) OR because a root reset/relaunch
@@ -33,7 +34,7 @@
33
34
  // row UPDATE directly via openDb — the one sanctioned exception to "only
34
35
  // canvas.ts touches the db" (see canvas/CLAUDE.md), exactly as db.ts's backfill
35
36
  // is the sanctioned exception for a data migration.
36
- import { openDb, getNode } from '../canvas/index.js';
37
+ import { openDb, getNode, cancelWakesFor, cancelSelfAlarms } from '../canvas/index.js';
37
38
  const ANY = '*';
38
39
  /** The supervised statuses — a live node the daemon watches. */
39
40
  const LIVE = ['active', 'idle'];
@@ -47,12 +48,12 @@ const TRANSITIONS = {
47
48
  // of a node that did NOT finish its own work → canceled, intent cleared. (A5:
48
49
  // done is reserved for finalize; every external reap unifies on canceled.)
49
50
  cancel: { status: 'canceled', intent: null, from: ANY },
50
- // daemon superviseTick: window gone with no yield/release intent. Intent KEPT
51
- // (the dead log line still reports it).
51
+ // daemon superviseTick: engine container gone (tmux pane or broker process)
52
+ // with no yield/release intent. Intent KEPT (the dead log line still reports it).
52
53
  crash: { status: 'dead', from: LIVE },
53
54
  // requestYield · relaunchRoot new-node safety net. Status KEPT (already active).
54
55
  yield: { intent: 'refresh', from: LIVE },
55
- // stophook idle-release: free the window, stay woken by the inbox.
56
+ // stophook idle-release: free the host, stay woken by the inbox.
56
57
  release: { status: 'idle', intent: 'idle-release', from: LIVE },
57
58
  // reviveNode · resetRoot · stophook boot-confirm (clear a pending refresh net).
58
59
  revive: { status: 'active', intent: null, from: ANY },
@@ -90,5 +91,21 @@ export function transition(nodeId, event) {
90
91
  else if (writeIntent) {
91
92
  db.prepare('UPDATE nodes SET intent = ? WHERE node_id = ?').run(spec.intent ?? null, nodeId);
92
93
  }
94
+ // Wakeups cleanup — writes a DIFFERENT table (wakeups) AFTER the atomic
95
+ // status/intent write above, so the single-(status,intent)-writer rule holds.
96
+ // Event-gated; exactly ONE delegated wakeups helper per event, never inline
97
+ // wakeups SQL here (design §6.7 / D2 / D6, Q1/Q2):
98
+ // cancel → cancelWakesFor: reap node-anchored AND owner-armed detached
99
+ // wakes this node armed (ruling A) — covers all three cancel
100
+ // sites uniformly (close cascade · reapDescendants · relaunch
101
+ // park-old).
102
+ // finalize → cancelSelfAlarms: clear this node's one-shot self-alarms only;
103
+ // node-anchored declarative crons SURVIVE a finishing instance (Q1).
104
+ // crash → NOTHING. A fault is not a deliberate end-of-waiting; self-alarms
105
+ // and crons MUST survive instance death (D2 / Invariant E / AC-R3).
106
+ if (event === 'cancel')
107
+ cancelWakesFor(nodeId);
108
+ else if (event === 'finalize')
109
+ cancelSelfAlarms(nodeId);
93
110
  return getNode(nodeId);
94
111
  }
@@ -1,43 +1,3 @@
1
- /** The node-local index template. Named export kept for callers/tests that
2
- * assert the seeded node store verbatim. */
3
- export declare const MEMORY_TEMPLATE: string;
4
- /** The user-global index template — framed around the human, not a goal. */
5
- export declare const USER_MEMORY_TEMPLATE: string;
6
- /** The project index template — framed around the repo. */
7
- export declare const PROJECT_MEMORY_TEMPLATE: string;
8
- /** The node-local memory directory in a node's context dir — holds MEMORY.md
9
- * (the index) and the one-fact detail files it points at. */
1
+ /** The node-local memory directory in a node's context dir — holds substrate
2
+ * docs (.md files with kind/when/why frontmatter). */
10
3
  export declare function memoryDir(nodeId: string): string;
11
- /** The node-local MEMORY.md index path (inside the memory dir). */
12
- export declare function memoryPath(nodeId: string): string;
13
- /** Whether the node has a node-local memory store. This is ALSO the
14
- * orchestrator gate: only orchestrators are ever seeded one, so a node with no
15
- * node-local store is a terminal worker (no memory framing at all). */
16
- export declare function hasMemory(nodeId: string): boolean;
17
- /** Read the node-local MEMORY.md index, or null when it doesn't exist. */
18
- export declare function readMemory(nodeId: string): string | null;
19
- /** Seed the node-local memory dir + index IF the node has none yet. */
20
- export declare function seedMemory(nodeId: string): boolean;
21
- /** The user-global memory directory — one per machine, key-less, loaded into
22
- * every orchestrator everywhere. */
23
- export declare function userMemoryDir(): string;
24
- /** The user-global MEMORY.md index path. */
25
- export declare function userMemoryPath(): string;
26
- export declare function hasUserMemory(): boolean;
27
- /** Read the user-global MEMORY.md index, or null when it doesn't exist. */
28
- export declare function readUserMemory(): string | null;
29
- /** Seed the user-global memory dir + index IF absent. */
30
- export declare function seedUserMemory(): boolean;
31
- /** The project key for `cwd`: its git-repo-root when inside a repo, else the
32
- * cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
33
- * This keys the per-project memory store under <crtrHome>/projects/. */
34
- export declare function projectKey(cwd: string): string;
35
- /** The project memory directory for `cwd`. */
36
- export declare function projectMemoryDir(cwd: string): string;
37
- /** The project MEMORY.md index path for `cwd`. */
38
- export declare function projectMemoryPath(cwd: string): string;
39
- export declare function hasProjectMemory(cwd: string): boolean;
40
- /** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
41
- export declare function readProjectMemory(cwd: string): string | null;
42
- /** Seed the project memory dir + index for `cwd` IF absent. */
43
- export declare function seedProjectMemory(cwd: string): boolean;
@@ -1,165 +1,14 @@
1
- // MEMORY.md + memory/ an orchestrator's persistent file-based memory.
2
- //
3
- // One layout, three scopes. Each store is a `memory/` directory of one-fact
4
- // files (each with typed frontmatter and [[wikilinks]]) indexed by a single
5
- // MEMORY.md that holds one pointer line per memory and NEVER any content — the
6
- // architecture in examples/memory-instructions.md. The pointer lines are the
7
- // load-bearing read: a node's <crtr-context> bearings block extracts every
8
- // applicable store's pointer lines each brand-new chat (see canvas-context-intro
9
- // + bearings), so the indexes must stay lean; the detail files load on demand
10
- // mid-session.
11
- //
12
- // The three scopes differ only in WHERE they live and HOW LONG they outlast a
13
- // node — the `type` taxonomy in each memory's frontmatter drives which store a
14
- // fact lands in (the mapping lives in the orchestration kernel's "Your long-term
15
- // memory"). ALL THREE live under the canvas home (crtrHome), all machine-local:
16
- //
17
- // user-global <crtrHome>/memory/ — who the human is, how
18
- // they like to work; loaded into EVERY orchestrator everywhere.
19
- // project <crtrHome>/projects/<key>/memory/ — facts bound to one
20
- // repo; loaded into orchestrators whose cwd resolves to that project. <key>
21
- // is the git-repo-root (walked up from the cwd), else the cwd, mangled.
22
- // node-local <crtrHome>/nodes/<id>/context/memory/ — facts specific to this
23
- // node's goal; dies with the node.
24
- //
25
- // An ORCHESTRATOR-only artifact — the resident, multi-cycle nodes that survive
26
- // refreshes and accumulate durable lessons/preferences; terminal workers are
27
- // one-shot and get none. All three stores are seeded the moment a node becomes
28
- // an orchestrator (promotion, or born one — where the roadmap is seeded too),
29
- // guarded so a re-seed never clobbers an evolved memory.
30
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
31
- import { join, dirname, resolve } from 'node:path';
32
- import { contextDir, crtrHome } from '../canvas/index.js';
33
- import { mangleCwd } from '../artifact.js';
34
- // ---------------------------------------------------------------------------
35
- // Index template + generic store ops (shared by all three scopes).
36
- // ---------------------------------------------------------------------------
37
- /** Build the seed contents of a fresh MEMORY.md index. Deliberately tiny: the
38
- * bearings block only ever extracts the pointer lines, so this prose never
39
- * rides into context — it's only for a human/agent opening the file directly,
40
- * and the how-to lives once in the orchestrator kernel ("Your long-term
41
- * memory"), not here. `holds` is a short scope hint so the empty index still
42
- * orients a fresh write. */
43
- function indexTemplate(holds) {
44
- return ('# memory index — one pointer line per memory (`- [Title](slug.md) — hook`); ' +
45
- `how-to in "Your long-term memory". Holds ${holds}.\n\n(no memories yet)\n`);
46
- }
47
- /** The node-local index template. Named export kept for callers/tests that
48
- * assert the seeded node store verbatim. */
49
- export const MEMORY_TEMPLATE = indexTemplate('your saved memories');
50
- /** The user-global index template — framed around the human, not a goal. */
51
- export const USER_MEMORY_TEMPLATE = indexTemplate('your saved memories about the human — who they are and how they like to work');
52
- /** The project index template — framed around the repo. */
53
- export const PROJECT_MEMORY_TEMPLATE = indexTemplate('your saved memories about this project');
54
- /** The MEMORY.md index path inside a memory `dir`. */
55
- function indexPathOf(dir) {
56
- return join(dir, 'MEMORY.md');
57
- }
58
- /** Seed `dir` + its MEMORY.md index with `template` IFF the index is absent.
59
- * Idempotent and guarded so it never clobbers an evolved memory; creating the
60
- * dir up front lets the node write detail files into it directly (no mkdir).
61
- * Returns true when it seeded, false when an index already existed. */
62
- function seedStore(dir, template) {
63
- const idx = indexPathOf(dir);
64
- if (existsSync(idx))
65
- return false;
66
- mkdirSync(dir, { recursive: true });
67
- writeFileSync(idx, template);
68
- return true;
69
- }
70
- /** Read a store's MEMORY.md index, or null when it doesn't exist. */
71
- function readStore(dir) {
72
- const idx = indexPathOf(dir);
73
- return existsSync(idx) ? readFileSync(idx, 'utf8') : null;
74
- }
75
- // ---------------------------------------------------------------------------
76
- // node-local store — <crtrHome>/nodes/<id>/context/memory/ (facts for this goal)
77
- // ---------------------------------------------------------------------------
78
- /** The node-local memory directory in a node's context dir — holds MEMORY.md
79
- * (the index) and the one-fact detail files it points at. */
1
+ // memory.tsthe node-local substrate document store.
2
+ //
3
+ // One scope lives here: node-local memory, a `memory/` directory of substrate
4
+ // documents (.md files with typed frontmatter kind, when, why,
5
+ // system-prompt-visibility, gate) inside a node's context dir; it dies with the
6
+ // node. User-global and project memory are scope-resolved stores — see
7
+ // `scopeMemoryDir` in core/scope.ts.
8
+ import { join } from 'node:path';
9
+ import { contextDir } from '../canvas/index.js';
10
+ /** The node-local memory directory in a node's context dir — holds substrate
11
+ * docs (.md files with kind/when/why frontmatter). */
80
12
  export function memoryDir(nodeId) {
81
13
  return join(contextDir(nodeId), 'memory');
82
14
  }
83
- /** The node-local MEMORY.md index path (inside the memory dir). */
84
- export function memoryPath(nodeId) {
85
- return indexPathOf(memoryDir(nodeId));
86
- }
87
- /** Whether the node has a node-local memory store. This is ALSO the
88
- * orchestrator gate: only orchestrators are ever seeded one, so a node with no
89
- * node-local store is a terminal worker (no memory framing at all). */
90
- export function hasMemory(nodeId) {
91
- return existsSync(memoryPath(nodeId));
92
- }
93
- /** Read the node-local MEMORY.md index, or null when it doesn't exist. */
94
- export function readMemory(nodeId) {
95
- return readStore(memoryDir(nodeId));
96
- }
97
- /** Seed the node-local memory dir + index IF the node has none yet. */
98
- export function seedMemory(nodeId) {
99
- return seedStore(memoryDir(nodeId), MEMORY_TEMPLATE);
100
- }
101
- // ---------------------------------------------------------------------------
102
- // user-global store — <crtrHome>/memory/ (who the human is, how they work)
103
- // ---------------------------------------------------------------------------
104
- /** The user-global memory directory — one per machine, key-less, loaded into
105
- * every orchestrator everywhere. */
106
- export function userMemoryDir() {
107
- return join(crtrHome(), 'memory');
108
- }
109
- /** The user-global MEMORY.md index path. */
110
- export function userMemoryPath() {
111
- return indexPathOf(userMemoryDir());
112
- }
113
- export function hasUserMemory() {
114
- return existsSync(userMemoryPath());
115
- }
116
- /** Read the user-global MEMORY.md index, or null when it doesn't exist. */
117
- export function readUserMemory() {
118
- return readStore(userMemoryDir());
119
- }
120
- /** Seed the user-global memory dir + index IF absent. */
121
- export function seedUserMemory() {
122
- return seedStore(userMemoryDir(), USER_MEMORY_TEMPLATE);
123
- }
124
- // ---------------------------------------------------------------------------
125
- // project store — <crtrHome>/projects/<key>/memory/ (facts bound to one repo)
126
- // ---------------------------------------------------------------------------
127
- /** The git repo root containing `cwd` — walk up for a `.git` entry — or null
128
- * when `cwd` is not inside a repo. `.git` may be a dir (normal) or a file
129
- * (worktree/submodule); existsSync catches both. */
130
- function gitRoot(cwd) {
131
- let dir = resolve(cwd);
132
- for (;;) {
133
- if (existsSync(join(dir, '.git')))
134
- return dir;
135
- const parent = dirname(dir);
136
- if (parent === dir)
137
- return null; // hit the filesystem root
138
- dir = parent;
139
- }
140
- }
141
- /** The project key for `cwd`: its git-repo-root when inside a repo, else the
142
- * cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
143
- * This keys the per-project memory store under <crtrHome>/projects/. */
144
- export function projectKey(cwd) {
145
- return mangleCwd(gitRoot(cwd) ?? cwd);
146
- }
147
- /** The project memory directory for `cwd`. */
148
- export function projectMemoryDir(cwd) {
149
- return join(crtrHome(), 'projects', projectKey(cwd), 'memory');
150
- }
151
- /** The project MEMORY.md index path for `cwd`. */
152
- export function projectMemoryPath(cwd) {
153
- return indexPathOf(projectMemoryDir(cwd));
154
- }
155
- export function hasProjectMemory(cwd) {
156
- return existsSync(projectMemoryPath(cwd));
157
- }
158
- /** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
159
- export function readProjectMemory(cwd) {
160
- return readStore(projectMemoryDir(cwd));
161
- }
162
- /** Seed the project memory dir + index for `cwd` IF absent. */
163
- export function seedProjectMemory(cwd) {
164
- return seedStore(projectMemoryDir(cwd), PROJECT_MEMORY_TEMPLATE);
165
- }
@@ -1,6 +1,12 @@
1
1
  import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec } from '../canvas/index.js';
2
2
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
3
3
  export declare function newNodeId(): string;
4
+ /** The root of a node's spine: walk the `parent` column up to `parent == null`.
5
+ * Cycle-guarded (parents must not cycle, but never loop forever). Lives in the
6
+ * node layer (not placement) so the env-builder and placement both reach it
7
+ * without an import cycle — `CRTR_SUBTREE` is emitted as `rootOfSpine(nodeId)`
8
+ * at every launch site. */
9
+ export declare function rootOfSpine(nodeId: string): string;
4
10
  /** The single, shared tmux session that ALL canvas node windows live in.
5
11
  * Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
6
12
  * child opens a window here rather than cluttering the user's own working
@@ -34,6 +40,25 @@ export declare function resolveBirthSession(opts: {
34
40
  * (`nodeSession()`). The defaulted read for the placement layer; a present
35
41
  * `home_session` is always returned verbatim. */
36
42
  export declare function homeSessionOf(nodeId: string): string;
43
+ /** The session a node's CHILDREN spawn into — the value its pi process exports
44
+ * as CRTR_ROOT_SESSION (the subtree's backstage). NOT the same as where the
45
+ * node's OWN pane lives.
46
+ *
47
+ * For a managed CHILD this is its `home_session`: a child's home_session is
48
+ * ALWAYS the backstage it was born into (focus may move its live pane into a
49
+ * user session, but home_session stays the taint-immune backstage), so its
50
+ * descendants correctly inherit that backstage.
51
+ *
52
+ * For a ROOT (`parent === null`) `home_session` may be a USER session it
53
+ * ADOPTED — the inline front door keeps the root's own pane where the user is
54
+ * working (`home_session = cli`). Sourcing children's CRTR_ROOT_SESSION from
55
+ * that would exile the root's entire subtree into the user's session. A root's
56
+ * children must instead flow to the shared backstage `nodeSession()`, NEVER the
57
+ * user's session — mirroring spawnRoot's birth-time split (root pane → user
58
+ * session; children → nodeSession() via CRTR_ROOT_SESSION). The revive paths
59
+ * MUST honor the same split, or a refreshed front-door root re-points its whole
60
+ * subtree into the user's session on every yield (the bug this fixes). */
61
+ export declare function childBackstageOf(nodeId: string): string;
37
62
  export interface NodeContext {
38
63
  nodeId: string | null;
39
64
  kind: string | null;
@@ -61,10 +86,22 @@ export interface SpawnNodeOpts {
61
86
  * `parent` — e.g. an independent root (parent=null) still records its
62
87
  * spawner. Defaults to `parent`. */
63
88
  spawnedBy?: string | null;
89
+ /** Fork provenance — the `--fork-from` reference this node was forked from (a
90
+ * node id / session path / pi session uuid). Persisted to `meta.fork_from` so
91
+ * the boot intro can re-assert this node's OWN identity over the source's
92
+ * copied-in conversation. Omit for a fresh node. */
93
+ forkFrom?: string | null;
94
+ /** Which HOST launches + supervises this node: a tmux pane (default) or the
95
+ * headless broker. Persisted to `meta.host_kind` (NULL/'tmux' ⇒ tmux). */
96
+ hostKind?: 'tmux' | 'broker';
64
97
  /** New subscriptions this node opens default to passive when true. */
65
98
  passiveDefault?: boolean;
66
99
  /** Resolved pi launch recipe (from resolve(kind,mode)). */
67
100
  launch?: LaunchSpec;
101
+ /** Caller-pinned model tier (ultra/strong/medium/light) that overrides the
102
+ * persona default. Persisted to `meta.model_override` so polymorphs preserve
103
+ * it. Omit to use the persona default. */
104
+ modelOverride?: string | null;
68
105
  /** Override the generated id (e.g. when a caller pre-allocates one). */
69
106
  nodeId?: string;
70
107
  }
@@ -13,11 +13,31 @@
13
13
  // graph to mirror the spawn structure. A `spawned_by` audit edge
14
14
  // is also recorded.
15
15
  import { randomBytes } from 'node:crypto';
16
- import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
16
+ import { mkdirSync } from 'node:fs';
17
+ import { createNode, getNode, getRow, subscribe, recordSpawn, } from '../canvas/index.js';
18
+ import { memoryDir } from './memory.js';
17
19
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
18
20
  export function newNodeId() {
19
21
  return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
20
22
  }
23
+ /** The root of a node's spine: walk the `parent` column up to `parent == null`.
24
+ * Cycle-guarded (parents must not cycle, but never loop forever). Lives in the
25
+ * node layer (not placement) so the env-builder and placement both reach it
26
+ * without an import cycle — `CRTR_SUBTREE` is emitted as `rootOfSpine(nodeId)`
27
+ * at every launch site. */
28
+ export function rootOfSpine(nodeId) {
29
+ let cur = nodeId;
30
+ const seen = new Set();
31
+ for (;;) {
32
+ if (seen.has(cur))
33
+ return cur;
34
+ seen.add(cur);
35
+ const row = getRow(cur);
36
+ if (row === null || row.parent == null)
37
+ return cur;
38
+ cur = row.parent;
39
+ }
40
+ }
21
41
  /** The single, shared tmux session that ALL canvas node windows live in.
22
42
  * Overridable with CRTR_NODE_SESSION (default `crtr`). Every root and every
23
43
  * child opens a window here rather than cluttering the user's own working
@@ -59,6 +79,30 @@ export function homeSessionOf(nodeId) {
59
79
  return nodeSession();
60
80
  return meta.home_session ?? meta.tmux_session ?? nodeSession();
61
81
  }
82
+ /** The session a node's CHILDREN spawn into — the value its pi process exports
83
+ * as CRTR_ROOT_SESSION (the subtree's backstage). NOT the same as where the
84
+ * node's OWN pane lives.
85
+ *
86
+ * For a managed CHILD this is its `home_session`: a child's home_session is
87
+ * ALWAYS the backstage it was born into (focus may move its live pane into a
88
+ * user session, but home_session stays the taint-immune backstage), so its
89
+ * descendants correctly inherit that backstage.
90
+ *
91
+ * For a ROOT (`parent === null`) `home_session` may be a USER session it
92
+ * ADOPTED — the inline front door keeps the root's own pane where the user is
93
+ * working (`home_session = cli`). Sourcing children's CRTR_ROOT_SESSION from
94
+ * that would exile the root's entire subtree into the user's session. A root's
95
+ * children must instead flow to the shared backstage `nodeSession()`, NEVER the
96
+ * user's session — mirroring spawnRoot's birth-time split (root pane → user
97
+ * session; children → nodeSession() via CRTR_ROOT_SESSION). The revive paths
98
+ * MUST honor the same split, or a refreshed front-door root re-points its whole
99
+ * subtree into the user's session on every yield (the bug this fixes). */
100
+ export function childBackstageOf(nodeId) {
101
+ const meta = getNode(nodeId);
102
+ if (meta !== null && meta.parent === null)
103
+ return nodeSession();
104
+ return homeSessionOf(nodeId);
105
+ }
62
106
  /** Read the current node's identity from the environment. A spawned pi process
63
107
  * runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
64
108
  * it by reading CRTR_NODE_ID as the parent. */
@@ -92,6 +136,12 @@ export function nodeEnv(meta) {
92
136
  const rootSession = process.env['CRTR_ROOT_SESSION'];
93
137
  if (rootSession !== undefined && rootSession !== '')
94
138
  env['CRTR_ROOT_SESSION'] = rootSession;
139
+ // Propagate the subtree-root node id (additive in Phase 3; tmux still keys on
140
+ // CRTR_ROOT_SESSION). The broker uses it to group a subtree; each launch site
141
+ // also emits the authoritative rootOfSpine(nodeId) over this passthrough.
142
+ const subtree = process.env['CRTR_SUBTREE'];
143
+ if (subtree !== undefined && subtree !== '')
144
+ env['CRTR_SUBTREE'] = subtree;
95
145
  // Merge any launch-spec env last (it may override / extend).
96
146
  return { ...env, ...(meta.launch?.env ?? {}) };
97
147
  }
@@ -117,6 +167,7 @@ export function spawnNode(opts) {
117
167
  cycles: 0,
118
168
  created: new Date().toISOString(),
119
169
  cwd: opts.cwd,
170
+ host_kind: opts.hostKind ?? 'tmux',
120
171
  kind: opts.kind,
121
172
  mode,
122
173
  lifecycle,
@@ -127,6 +178,8 @@ export function spawnNode(opts) {
127
178
  status: 'active',
128
179
  parent,
129
180
  spawned_by: spawnedBy,
181
+ fork_from: opts.forkFrom,
182
+ model_override: opts.modelOverride ?? null,
130
183
  passive_default: opts.passiveDefault ?? false,
131
184
  intent: null,
132
185
  pi_session_id: null,
@@ -140,6 +193,12 @@ export function spawnNode(opts) {
140
193
  throw new Error(`cannot spawn under unknown parent node: ${parent}`);
141
194
  }
142
195
  createNode(meta);
196
+ // Create the node-local memory directory so substrate docs can be written
197
+ // directly into it without a separate mkdir. Skip for ephemeral human-bridge
198
+ // rows (kind 'human') — they are not agent nodes.
199
+ if (meta.kind !== 'human') {
200
+ mkdirSync(memoryDir(meta.node_id), { recursive: true });
201
+ }
143
202
  if (parent !== null) {
144
203
  // The load-bearing seed: parent subscribes (active) to child so it learns
145
204
  // when the work finishes. This mirrors spawn structure into the spine.
@@ -26,11 +26,22 @@ import { readText } from '../fs-utils.js';
26
26
  import { parseFrontmatter } from '../frontmatter.js';
27
27
  import { readRoadmap, roadmapPath } from './roadmap.js';
28
28
  import { orchestratorContextNote } from './bearings.js';
29
- import { memoryPath, memoryDir, userMemoryPath, userMemoryDir, projectMemoryPath, projectMemoryDir, } from './memory.js';
30
29
  // ---------------------------------------------------------------------------
31
30
  // base→orchestrator guidance (the roadmap-shaping dump) — MOVED here from
32
31
  // promote.ts so the injector is the one place that builds it.
33
32
  // ---------------------------------------------------------------------------
33
+ /** Coerce a frontmatter scalar to its string form, matching the legacy
34
+ * hand-rolled parser. Strings pass through; number/boolean coerce via
35
+ * String(); null/undefined and non-scalars are dropped (→ null). Without this,
36
+ * the `yaml` package's native scalar types would let a numeric/boolean
37
+ * `roadmapSkill` be silently dropped by a `typeof === 'string'` guard. */
38
+ function scalarToString(v) {
39
+ if (typeof v === 'string')
40
+ return v;
41
+ if (typeof v === 'number' || typeof v === 'boolean')
42
+ return String(v);
43
+ return null;
44
+ }
34
45
  /** Load a skill's body text by name, or null if it can't be resolved. Used to
35
46
  * inline a kind's roadmap-shaping skill into the orchestration guidance. */
36
47
  function loadSkillBody(name) {
@@ -44,16 +55,15 @@ function loadSkillBody(name) {
44
55
  }
45
56
  /** The base→orchestrator guidance dump, specialized to the node's kind: the
46
57
  * shared kernel + that kind's roadmap-shaping skill + the roadmap scaffold the
47
- * node must author + the orchestrator context-dir framing + the three memory
48
- * stores. The node is now a delegator whose scarce resource is its own context
49
- * window. (Lifecycle is left to its own section promotion no longer forces
50
- * resident, so this never asserts residency.) */
51
- function orchestrationGuidance(nodeId, kind, cwd) {
58
+ * node must author + the orchestrator context-dir framing + a short pointer at
59
+ * the substrate memory flow (promotion re-informs of nothing about the stores,
60
+ * it only adds the across-cycles relevance). The node is now a delegator whose
61
+ * scarce resource is its own context window. (Lifecycle is left to its own
62
+ * section — promotion no longer forces resident, so this never asserts residency.) */
63
+ function orchestrationGuidance(nodeId, kind) {
52
64
  const kernel = loadKernel();
53
65
  const orch = loadPersona(kind, 'orchestrator');
54
- const roadmapSkill = typeof orch?.frontmatter?.['roadmapSkill'] === 'string'
55
- ? orch.frontmatter['roadmapSkill']
56
- : undefined;
66
+ const roadmapSkill = scalarToString(orch?.frontmatter?.['roadmapSkill']) ?? undefined;
57
67
  const skillBody = roadmapSkill ? loadSkillBody(roadmapSkill) : null;
58
68
  const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
59
69
  const rmPath = roadmapPath(nodeId);
@@ -70,7 +80,7 @@ function orchestrationGuidance(nodeId, kind, cwd) {
70
80
  // The orchestrator framing for the context dir — the missing guidance a
71
81
  // promoted node never got at spawn (it spawned as a base worker). Same note
72
82
  // a born-orchestrator gets in its <crtr-context> bearings block.
73
- orchestratorContextNote(nodeId), '', 'Your long-term memory now exists across three seeded stores (write to them directly), each a different scope per "Your long-term memory" above:', ` • user-global \`${userMemoryDir()}\` (index \`${userMemoryPath()}\`) who the human is, how they like to work; loaded into every orchestrator everywhere.`, ` • project \`${projectMemoryDir(cwd)}\` (index \`${projectMemoryPath(cwd)}\`) facts bound to this repo; loaded into every orchestrator working here.`, ` node-local \`${memoryDir(nodeId)}\` (index \`${memoryPath(nodeId)}\`) facts specific to this goal; they die with this node.`, 'A memory\'s `type` decides which store it lands in (see "Your long-term memory"). These same paths ride into every future wake in your `<crtr-context>` block.', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
83
+ orchestratorContextNote(nodeId), '', 'Your three long-term memory scopes (user-global, project, node-local) already surface in your `<crtr-context>` block. What promotion changes is USE, not access: you now persist across refresh cycles, so node-local carries what THIS goal needs between your refreshes, and durable facts you commit to user-global or project outlive this node. Use `crtr memory write` to save a document; `crtr memory list` to browse; `crtr memory find` to search (see "Your long-term memory" above for the full substrate flow).', '', 'Then delegate each phase with `crtr node new --kind <kind>`. When your context fills, run `crtr node yield` to refresh against this roadmap.');
74
84
  return parts.join('\n');
75
85
  }
76
86
  // ---------------------------------------------------------------------------
@@ -103,7 +113,7 @@ export function transitionGuidance(nodeId, from, to) {
103
113
  if (from.mode !== to.mode) {
104
114
  if (to.mode === 'orchestrator') {
105
115
  const node = getNode(nodeId);
106
- sections.push(orchestrationGuidance(nodeId, node?.kind ?? 'general', node?.cwd ?? process.cwd()));
116
+ sections.push(orchestrationGuidance(nodeId, node?.kind ?? 'general'));
107
117
  }
108
118
  else {
109
119
  sections.push(baseModeGuidance());