@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
@@ -1,6 +1,6 @@
1
- // demote.ts — the "graduate this agent" action behind `crtr node demote`.
1
+ // recycle.ts — the "finalize + reboot this pane" action behind `crtr node recycle`.
2
2
  //
3
- // Demote finishes the agent occupying a tmux pane and recycles that pane for
3
+ // Recycle FINALIZES the agent occupying a tmux pane and recycles that pane for
4
4
  // fresh work, in three steps:
5
5
  //
6
6
  // 1. Finalize — push the agent's last surfaced message as a `final` report so
@@ -8,6 +8,10 @@
8
8
  // 2. Close — kill the agent's pi (respawn-pane -k tears it down in place).
9
9
  // 3. Recycle — boot a fresh resident root in that same pane (a new `crtr`).
10
10
  //
11
+ // NOT to be confused with `node demote` (flip-to-terminal IN PLACE, which keeps
12
+ // the agent focused and running): recycle ENDS this agent and boots a brand-new
13
+ // resident root here.
14
+ //
11
15
  // The agent's real conversation lives inside pi (not on disk), so the final
12
16
  // body is its newest report (which, on a natural stop, IS its last assistant
13
17
  // message) — falling back to a short note when it never reported.
@@ -16,10 +20,11 @@ import { join } from 'node:path';
16
20
  import { getNode, setPresence, updateNode, setFocusOccupant, fullName } from '../canvas/index.js';
17
21
  import { reportsDir } from '../canvas/paths.js';
18
22
  import { pushFinal } from '../feed/feed.js';
19
- import { spawnNode, nodeSession } from './nodes.js';
23
+ import { spawnNode, nodeSession, rootOfSpine } from './nodes.js';
20
24
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
21
25
  import { FRONT_DOOR_ENV } from './front-door.js';
22
26
  import { focusOf, recycleFocusPane, piCommand, paneLocation } from './placement.js';
27
+ import { hostFor } from './host.js';
23
28
  import { ensureDaemon } from '../../daemon/manage.js';
24
29
  /** The agent's most recent surfaced message: the newest reports/*.md body with
25
30
  * its YAML frontmatter stripped. Empty string when the node never reported. */
@@ -49,18 +54,18 @@ function lastReportBody(nodeId) {
49
54
  }
50
55
  /** Finish `nodeId` and recycle its pane into a fresh root. `callerPane` is the
51
56
  * tmux pane the agent occupies (the Alt+C menu passes it as `#{pane_id}`).
52
- * Best-effort; `demoted:false` when there is no pane to act on. */
53
- export async function demoteNode(nodeId, callerPane) {
57
+ * Best-effort; `recycled:false` when there is no pane to act on. */
58
+ export async function recycleNode(nodeId, callerPane) {
54
59
  const meta = getNode(nodeId);
55
60
  if (meta === null)
56
- return { demoted: false, finalized: false, newRoot: null, delivered: [] };
61
+ return { recycled: false, finalized: false, newRoot: null, delivered: [] };
57
62
  const pane = callerPane ?? process.env['TMUX_PANE'];
58
63
  if (pane === undefined || pane === '') {
59
- return { demoted: false, finalized: false, newRoot: null, delivered: [] };
64
+ return { recycled: false, finalized: false, newRoot: null, delivered: [] };
60
65
  }
61
66
  // 1. Finalize — fan the agent's last message out as a `final`, mark it done.
62
67
  const body = lastReportBody(nodeId) ||
63
- `Closed via demote — no final summary was authored by ${meta.name}.`;
68
+ `Closed via recycle — no final summary was authored by ${meta.name}.`;
64
69
  let delivered = [];
65
70
  let finalized = false;
66
71
  try {
@@ -69,6 +74,17 @@ export async function demoteNode(nodeId, callerPane) {
69
74
  finalized = true;
70
75
  }
71
76
  catch { /* recycle the pane even if the report failed */ }
77
+ // A broker node has NO tmux pane, so recycleFocusPane below (respawn-pane -k)
78
+ // never kills its engine — route its teardown through the Host seam so the
79
+ // broker PROCESS exits and releases the sole .jsonl writer (mirrors the T12
80
+ // close.ts/reset.ts fix; review reuse MINOR-3). Status is already flipped done
81
+ // by pushFinal above (crash-safe order: the daemon won't revive a done node).
82
+ if (meta.host_kind === 'broker') {
83
+ try {
84
+ hostFor(meta).teardown(nodeId);
85
+ }
86
+ catch { /* best-effort */ }
87
+ }
72
88
  // Capture M's focus viewport (if any) BEFORE nulling — the fresh root inherits
73
89
  // it (the SAME focus row + pane). The demoted node no longer holds a pane: it is
74
90
  // being reclaimed.
@@ -93,7 +109,7 @@ export async function demoteNode(nodeId, callerPane) {
93
109
  parent: null,
94
110
  launch,
95
111
  });
96
- // REVIVE-HOME: a demote-recycled root's durable revive target is the session
112
+ // REVIVE-HOME: a recycled root's durable revive target is the session
97
113
  // of the pane it was recycled into (the one place home_session is rewritten
98
114
  // after birth). Falls back to the backstage when the pane can't be located.
99
115
  updateNode(root.node_id, { home_session: loc?.session ?? nodeSession() });
@@ -107,9 +123,9 @@ export async function demoteNode(nodeId, callerPane) {
107
123
  }
108
124
  const fresh = getNode(root.node_id);
109
125
  const inv = buildPiArgv(fresh);
110
- const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), [FRONT_DOOR_ENV]: '1' };
126
+ const env = { ...inv.env, CRTR_ROOT_SESSION: nodeSession(), CRTR_SUBTREE: rootOfSpine(root.node_id), [FRONT_DOOR_ENV]: '1' };
111
127
  const ok = recycleFocusPane(root.node_id, pane, {
112
128
  command: piCommand(inv.argv), env, cwd: meta.cwd, name: fullName(fresh), resuming: false,
113
129
  });
114
- return { demoted: ok, finalized, newRoot: root.node_id, delivered };
130
+ return { recycled: ok, finalized, newRoot: root.node_id, delivered };
115
131
  }
@@ -22,7 +22,8 @@
22
22
  import { existsSync, rmSync } from 'node:fs';
23
23
  import { getNode, updateNode, setPresence, clearPid, setFocusOccupant, subscriptionsOf, unsubscribe, view, reportsDir, inboxPath, nodeDir, openDb, } from '../canvas/index.js';
24
24
  import { transition } from './lifecycle.js';
25
- import { paneLocation, tearDownNode, focusOf } from './placement.js';
25
+ import { hostFor } from './host.js';
26
+ import { paneLocation, focusOf } from './placement.js';
26
27
  import { buildLaunchSpec } from './launch.js';
27
28
  import { roadmapPath } from './roadmap.js';
28
29
  import { spawnNode, newNodeId, nodeSession } from './nodes.js';
@@ -49,11 +50,12 @@ export function reapDescendants(rootId) {
49
50
  try {
50
51
  // Reap BEFORE tearing down the placement (the crash-safety invariant the
51
52
  // `cancel` event encodes): a non-supervised status + cleared intent first, so
52
- // the daemon can't revive a descendant mid-teardown. tearDownNode then
53
- // closes any focus row it held, kills its pane (pane-keyed), and nulls its
54
- // LOCATION.
55
- transition(id, 'cancel');
56
- tearDownNode(id);
53
+ // the daemon can't revive a descendant mid-teardown. Teardown then routes
54
+ // through the Host seam (hostFor): a tmux node closes any focus row it held,
55
+ // kills its pane (pane-keyed), and nulls its LOCATION; a broker node sends
56
+ // the `shutdown` frame so its PROCESS exits and releases the .jsonl writer.
57
+ const m = transition(id, 'cancel');
58
+ hostFor(m).teardown(id);
57
59
  reaped.push(id);
58
60
  }
59
61
  catch {
@@ -110,7 +112,7 @@ export function resetRoot(nodeId, newSessionId, newSessionFile) {
110
112
  // Re-seed persona_ack to the fresh persona so the pristine `/new`
111
113
  // conversation never gets a spurious mode/lifecycle transition steer (the
112
114
  // persona injector compares against this ack).
113
- const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false });
115
+ const { launch } = buildLaunchSpec(meta.kind, 'base', { lifecycle: 'resident', hasManager: false, model: meta.model_override ?? undefined });
114
116
  updateNode(nodeId, {
115
117
  mode: 'base',
116
118
  lifecycle: 'resident',
@@ -178,7 +180,7 @@ export function relaunchRoot(oldId, pane, deps = {}) {
178
180
  window: oldMeta.window ?? null,
179
181
  };
180
182
  const newId = newNodeId();
181
- const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false });
183
+ const { launch } = buildLaunchSpec(oldMeta.kind, 'base', { lifecycle: 'resident', hasManager: false, model: oldMeta.model_override ?? undefined });
182
184
  // Park-old + mint-new is the single most fragile spot in the runtime, so it is
183
185
  // ONE atomic unit: every ROW write below runs inside a sqlite transaction. A
184
186
  // failure anywhere — including the respawn DISPATCH — rolls the whole thing
@@ -204,6 +206,7 @@ export function relaunchRoot(oldId, pane, deps = {}) {
204
206
  parent: null,
205
207
  spawnedBy: oldId, // audit-only successor link; does NOT touch the spine
206
208
  nodeId: newId,
209
+ modelOverride: oldMeta.model_override,
207
210
  launch,
208
211
  });
209
212
  transition(newId, 'yield'); // active (from spawn) + intent=refresh safety net
@@ -1,4 +1,5 @@
1
1
  import { type NodeMeta } from '../canvas/index.js';
2
+ import type { WakeOrigin } from './bearings.js';
2
3
  import { type RespawnPaneOpts } from './placement.js';
3
4
  /** Pick the `--session` source for a revive. resume=true prefers the absolute
4
5
  * session-file path (immune to cwd; pi opens it directly) and keeps the bare
@@ -25,6 +26,7 @@ export interface ReviveResult {
25
26
  */
26
27
  export declare function reviveNode(nodeId: string, opts: {
27
28
  resume: boolean;
29
+ wakeReason?: WakeOrigin;
28
30
  }): ReviveResult;
29
31
  /** Re-exec a node's pi FRESH in its EXISTING tmux pane (the refresh-yield
30
32
  * path). Unlike `reviveNode`, this opens no new window: the pane's current pi
@@ -14,26 +14,15 @@
14
14
  // preferring the absolute session-file path (cwd-immune) over
15
15
  // the bare session id.
16
16
  // resume=false → fresh pi invocation — the node re-reads its roadmap/context dir.
17
- import { getNode, updateNode, setPresence, clearPid, fullName, } from '../canvas/index.js';
17
+ import { getNode, updateNode, setPresence, clearPid, fullName, cancelDeadlinesFor, } from '../canvas/index.js';
18
18
  import { transition } from './lifecycle.js';
19
19
  import { buildPiArgv } from './launch.js';
20
20
  import { buildReviveKickoff, drainBearings } from './kickoff.js';
21
21
  import { FRONT_DOOR_ENV } from './front-door.js';
22
- import { reviveIntoPlacement, reconcile, isNodePaneAlive, homeSessionOf, piCommand, respawnPane, } from './placement.js';
23
- import { nodeSession } from './nodes.js';
24
- /** signal-0 liveness probe for a pi pid (mirrors the daemon's isPidAlive). A
25
- * null pid (legacy / never-booted) reads dead. */
26
- function pidAlive(pid) {
27
- if (pid == null)
28
- return false;
29
- try {
30
- process.kill(pid, 0);
31
- return true;
32
- }
33
- catch (e) {
34
- return e.code === 'EPERM';
35
- }
36
- }
22
+ import { reconcile, piCommand, respawnPane, } from './placement.js';
23
+ import { hostFor } from './host.js';
24
+ import { nodeSession, childBackstageOf, rootOfSpine } from './nodes.js';
25
+ import { isPidAlive } from '../canvas/pid.js';
37
26
  // ---------------------------------------------------------------------------
38
27
  // resumeArgs — which session source a revive resumes from
39
28
  // ---------------------------------------------------------------------------
@@ -73,7 +62,7 @@ export function reviveNode(nodeId, opts) {
73
62
  // frozen pane), so the guard gates on pi liveness too, not pane-existence alone.
74
63
  reconcile(nodeId);
75
64
  const live = getNode(nodeId) ?? meta;
76
- if (isNodePaneAlive(nodeId) && pidAlive(live.pi_pid)) {
65
+ if (hostFor(live).isAlive(nodeId) && isPidAlive(live.pi_pid)) {
77
66
  return {
78
67
  window: live.window ?? null,
79
68
  session: live.tmux_session ?? nodeSession(),
@@ -98,8 +87,12 @@ export function reviveNode(nodeId, opts) {
98
87
  inv = buildPiArgv(meta, resume);
99
88
  }
100
89
  else {
90
+ // A fresh revive: drain the one-shot bearings, then build the kickoff. When
91
+ // a scheduled bare self-alarm drove this revive, opts.wakeReason carries the
92
+ // wake provenance so the kickoff leads with a <crtr-wake> block ("a timer
93
+ // woke you"); every other reviveNode caller passes nothing → no block.
101
94
  const bearings = drainBearings(meta);
102
- inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
95
+ inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings, opts.wakeReason) });
103
96
  }
104
97
  // Placement owns WHERE this revive lands (§1.4): resume into a live focus pane
105
98
  // if the node occupies one, else a fresh window in its home_session (the
@@ -108,9 +101,14 @@ export function reviveNode(nodeId, opts) {
108
101
  // around it (the crash-safety ordering, unchanged). THIS is the bug-kill: a
109
102
  // non-focused background revive can no longer new-window into a user session.
110
103
  transition(nodeId, 'revive');
111
- const placed = reviveIntoPlacement(nodeId, {
112
- command: piCommand(inv.argv),
113
- env: inv.env,
104
+ // Cancel-on-wake (design §6.4, AC-E1): every revive-for-any-reason (an inbox
105
+ // event, a different wake, a manual focus) drops this node's pending deadline,
106
+ // so the deadline always belongs to the dormancy being left. Writes only the
107
+ // wakeups table, after the atomic transition above. (reviveInPlace's
108
+ // transition('boot') needs NO hook — a refresh is the same node continuing,
109
+ // not leaving a dormancy.)
110
+ cancelDeadlinesFor(nodeId);
111
+ const placed = hostFor(meta).launch(nodeId, inv, {
114
112
  cwd: meta.cwd,
115
113
  name: fullName(meta),
116
114
  resuming,
@@ -150,14 +148,16 @@ export function reviveInPlace(nodeId, pane, respawn = respawnPane) {
150
148
  // then build purely.
151
149
  const bearings = drainBearings(meta);
152
150
  const inv = buildPiArgv(meta, { prompt: buildReviveKickoff(meta, bearings) });
153
- // CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into — it must
154
- // be the durable REVIVE-HOME (home_session), NOT the pane's live `session`. A
155
- // FOCUSED child's pane is in a USER session (focus taints meta.tmux_session),
156
- // so sourcing it from `session` would land any child it spawns in the user's
157
- // session, re-tainting that child's home_session (A-MAJOR-1). home_session is
158
- // the taint-immune backstage `crtr` for a child; for a root it equals its own
159
- // session, so this is behavior-preserving there.
160
- const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId) };
151
+ // CRTR_ROOT_SESSION is the backstage this node's CHILDREN spawn into — NOT the
152
+ // pane's live `session`. A FOCUSED child's pane is in a USER session (focus
153
+ // taints meta.tmux_session), so sourcing it from `session` would land any
154
+ // child it spawns in the user's session, re-tainting that child's
155
+ // home_session (A-MAJOR-1). childBackstageOf is the taint-immune backstage:
156
+ // home_session for a managed child, but `nodeSession()` for a ROOT (whose
157
+ // home_session may itself be a user session it adopted via the inline front
158
+ // door sourcing children from it would re-point the root's whole subtree
159
+ // into the user's session on every refresh-yield).
160
+ const env = { ...inv.env, CRTR_ROOT_SESSION: childBackstageOf(nodeId), CRTR_SUBTREE: rootOfSpine(nodeId) };
161
161
  const ok = respawn({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
162
162
  if (!ok) {
163
163
  throw new Error(`reviveInPlace: respawn-pane dispatch failed for ${nodeId}`);
@@ -199,11 +199,11 @@ export function relaunchRootInPane(nodeId, pane) {
199
199
  }
200
200
  // No prompt, no resume → a brand-new root conversation at cycle 0.
201
201
  const inv = buildPiArgv(meta, {});
202
- // Source CRTR_ROOT_SESSION from the durable REVIVE-HOME (home_session), the
203
- // same taint-immunity rule as reviveInPlace. relaunchRootInPane runs only on a
204
- // root, whose home_session IS its own session, so this is behavior-preserving
205
- // — it keeps both in-pane revive paths sourced identically.
206
- const env = { ...inv.env, CRTR_ROOT_SESSION: homeSessionOf(nodeId), [FRONT_DOOR_ENV]: '1' };
202
+ // Source CRTR_ROOT_SESSION from childBackstageOf, the same backstage rule as
203
+ // reviveInPlace. relaunchRootInPane runs only on a root, whose children must
204
+ // flow to the shared backstage `nodeSession()` never the root's own adopted
205
+ // user session so both in-pane revive paths stay sourced identically.
206
+ const env = { ...inv.env, CRTR_ROOT_SESSION: childBackstageOf(nodeId), CRTR_SUBTREE: rootOfSpine(nodeId), [FRONT_DOOR_ENV]: '1' };
207
207
  const ok = respawnPane({ pane, cwd: meta.cwd, env, command: piCommand(inv.argv) });
208
208
  if (!ok) {
209
209
  throw new Error(`relaunchRootInPane: respawn-pane dispatch failed for ${nodeId}`);
@@ -1,3 +1,4 @@
1
+ import { type WakeOrigin } from './bearings.js';
1
2
  import { type NodeMeta, type Mode } from '../canvas/index.js';
2
3
  export interface BootRootOpts {
3
4
  cwd: string;
@@ -27,6 +28,21 @@ export interface SpawnChildOpts {
27
28
  * new session for the child — the source is untouched — then `prompt` is the
28
29
  * next message. A one-shot at birth; the child resumes its own session after. */
29
30
  forkFrom?: string;
31
+ /** Set ONLY by the daemon when a `spawn`/spawn-cron wake births this node: the
32
+ * provenance of the timer that fired (see WakeOrigin). In-memory only — it is
33
+ * NOT part of the stored recipe (the daemon spreads it in at fire time via
34
+ * `spawnChild({ ...recipe, wakeOrigin })`); `node new` never sets it. When set,
35
+ * a <crtr-wake> block is prepended to the kickoff so the newborn knows a clock
36
+ * birthed it. */
37
+ wakeOrigin?: WakeOrigin;
38
+ /** Which HOST launches + supervises this node: a tmux pane (default) or the
39
+ * headless broker. Persisted as `host_kind` at birth (resolved from
40
+ * `--headless` / the `headless` config default by the caller). */
41
+ hostKind?: 'tmux' | 'broker';
42
+ /** Pin the node to a model TIER (ultra/strong/medium/light), overriding the
43
+ * persona's declared default. Persisted to `meta.model_override` so it
44
+ * survives polymorphs. Omit to use the persona default. */
45
+ model?: string;
30
46
  }
31
47
  /** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
32
48
  * A live node id resolves to its captured session FILE (absolute, cwd-immune),
@@ -38,6 +54,13 @@ export interface SpawnChildResult {
38
54
  window: string | null;
39
55
  session: string;
40
56
  }
57
+ /** Resolve who a spawn is attributed to. A managed child needs a spine parent
58
+ * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
59
+ * does not: it is top-level by definition and the spawner identity is
60
+ * provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
61
+ * spawner (regression: `crtr node new --root` from outside a node used to
62
+ * throw here). */
63
+ export declare function resolveSpawner(parent: string | undefined, ctxNodeId: string | null, root: boolean): string | null;
41
64
  /** Spawn a node from a live node. By default a managed terminal worker in a
42
65
  * background window, with the spawner auto-subscribed (active) via spawnNode.
43
66
  * With `root`: an independent resident root — parent=null, NO subscription back
@@ -10,16 +10,17 @@
10
10
  // provenance via spawned_by) brought forefront for direct driving.
11
11
  import { spawnSync } from 'node:child_process';
12
12
  import { FRONT_DOOR_ENV } from './front-door.js';
13
- import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession } from './nodes.js';
13
+ import { spawnNode, currentNodeContext, resolveBirthSession, nodeSession, rootOfSpine } from './nodes.js';
14
14
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
15
15
  import { writeGoal } from './kickoff.js';
16
16
  import { hasRoadmap, seedRoadmap } from './roadmap.js';
17
- import { seedMemory, seedUserMemory, seedProjectMemory } from './memory.js';
18
17
  import { generateSessionName } from './naming.js';
19
- import { installMenuBinding, installNavBindings } from './tmux-chrome.js';
18
+ import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
19
+ import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
20
20
  import { setPresence, updateNode, getNode, fullName } from '../canvas/index.js';
21
21
  import { registerRootFocus, ensureSession, openNodeWindow, piCommand, currentTmux, inTmux, focusWindow, } from './placement.js';
22
22
  import { transition } from './lifecycle.js';
23
+ import { headlessBrokerHost } from './host.js';
23
24
  import { ensureDaemon } from '../../daemon/manage.js';
24
25
  /** Create a root node and bring up its pi. Returns the node; for 'inline' this
25
26
  * only returns after pi exits (it took over the terminal). */
@@ -57,8 +58,8 @@ export function bootRoot(opts) {
57
58
  // Every node window — root or child — lives in the one shared session.
58
59
  const session = nodeSession();
59
60
  ensureSession(session, opts.cwd);
60
- // Make the Alt+C action menu + Alt+] / Alt+[ nav keys live on this server
61
- // (idempotent, in-tmux only).
61
+ // Make the Alt+C action menu + Alt+] / Alt+[ node-nav keys + Alt+V then ]/[
62
+ // view-nav chord live on this server (idempotent, in-tmux only).
62
63
  if (inTmux()) {
63
64
  try {
64
65
  installMenuBinding();
@@ -68,6 +69,10 @@ export function bootRoot(opts) {
68
69
  installNavBindings();
69
70
  }
70
71
  catch { /* best-effort */ }
72
+ try {
73
+ installViewNavBindings();
74
+ }
75
+ catch { /* best-effort */ }
71
76
  }
72
77
  // inline: the root's pi takes over THIS terminal, so its own window stays
73
78
  // where the user is (its tmux_session tracks that real pane so supervision
@@ -93,7 +98,7 @@ export function bootRoot(opts) {
93
98
  }
94
99
  const withSession = getNode(meta.node_id);
95
100
  const inv = buildPiArgv(withSession, { prompt: opts.prompt });
96
- const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, [FRONT_DOOR_ENV]: '1' };
101
+ const env = { ...process.env, ...inv.env, CRTR_ROOT_SESSION: session, CRTR_SUBTREE: rootOfSpine(meta.node_id), [FRONT_DOOR_ENV]: '1' };
97
102
  const r = spawnSync('pi', inv.argv, { stdio: 'inherit', env });
98
103
  process.exit(r.status ?? 0);
99
104
  }
@@ -120,6 +125,19 @@ export function resolveForkSource(value) {
120
125
  // Not a known node — treat as a bare/partial pi session id for pi to resolve.
121
126
  return v;
122
127
  }
128
+ /** Resolve who a spawn is attributed to. A managed child needs a spine parent
129
+ * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
130
+ * does not: it is top-level by definition and the spawner identity is
131
+ * provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
132
+ * spawner (regression: `crtr node new --root` from outside a node used to
133
+ * throw here). */
134
+ export function resolveSpawner(parent, ctxNodeId, root) {
135
+ const spawner = parent ?? ctxNodeId ?? null;
136
+ if (!root && spawner === null) {
137
+ throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
138
+ }
139
+ return spawner;
140
+ }
123
141
  /** Spawn a node from a live node. By default a managed terminal worker in a
124
142
  * background window, with the spawner auto-subscribed (active) via spawnNode.
125
143
  * With `root`: an independent resident root — parent=null, NO subscription back
@@ -131,11 +149,8 @@ export function spawnChild(opts) {
131
149
  }
132
150
  catch { /* daemon is best-effort */ }
133
151
  const ctx = currentNodeContext();
134
- const spawner = opts.parent ?? ctx.nodeId;
135
- if (spawner === null || spawner === undefined) {
136
- throw new Error('spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent');
137
- }
138
152
  const root = opts.root === true;
153
+ const spawner = resolveSpawner(opts.parent, ctx.nodeId ?? null, root);
139
154
  const mode = opts.mode ?? 'base';
140
155
  // Lifecycle keys on ROOT-ness only, independent of mode: an independent root
141
156
  // (or `--root`) is resident (a conversation that persists, woken by inbox/
@@ -146,7 +161,7 @@ export function spawnChild(opts) {
146
161
  // Spine: a managed child reports up to its spawner (has a manager); an
147
162
  // independent root sits top-of-spine with nobody to push to. Mirrors the
148
163
  // `parent` set below (root ? null : spawner), so hasManager === parent!==null.
149
- const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root });
164
+ const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root, model: opts.model });
150
165
  // Name the worker from its task now, so its first editor label carries it.
151
166
  const meta = spawnNode({
152
167
  kind: opts.kind,
@@ -156,9 +171,16 @@ export function spawnChild(opts) {
156
171
  name: opts.name ?? opts.kind,
157
172
  description: generateSessionName(opts.prompt),
158
173
  // A root has no spine parent (top-level, nobody subscribes); it still
159
- // records spawned_by=spawner. A child's parent IS its manager.
174
+ // records spawned_by=spawner when a node (not a human shell) spawned it.
175
+ // A child's parent IS its manager.
160
176
  parent: root ? null : spawner,
161
- spawnedBy: root ? spawner : undefined,
177
+ spawnedBy: root ? (spawner ?? undefined) : undefined,
178
+ // Persist the RAW fork reference (not the resolved path) as provenance, so
179
+ // the boot intro can detect this is a fork and re-assert the node's own
180
+ // identity over the source's copied-in conversation.
181
+ forkFrom: opts.forkFrom,
182
+ hostKind: opts.hostKind,
183
+ modelOverride: opts.model,
162
184
  launch,
163
185
  });
164
186
  // Persist the task as the child's goal for a fresh revive to re-read.
@@ -168,6 +190,19 @@ export function spawnChild(opts) {
168
190
  // — not in buildPiArgv — so a bad reference fails the spawn loudly before any
169
191
  // window opens, rather than after pi is already booting.
170
192
  const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
193
+ // A fork inherits the SOURCE node's entire first-person conversation, so the
194
+ // identity re-assertion must ride the STRONGEST channel too, not only the
195
+ // session-start bearings (a trailing custom_message the model weighs only by
196
+ // recency). Prepend it to the kickoff PROMPT — the message that actually
197
+ // triggers the fork's first turn, so the model acts on "you are node X, a
198
+ // FORK of <source>, NOT them" as live instruction. When the daemon births this
199
+ // node from a scheduled `spawn` wake, the <crtr-wake> provenance block leads
200
+ // (so "a timer birthed you" precedes the task) — mirroring the fork-identity
201
+ // prepend. Only the pi argv prompt is reframed; the persisted goal (writeGoal
202
+ // above) keeps the raw task.
203
+ const wakeBlock = opts.wakeOrigin !== undefined ? `${buildWakeBearings(opts.wakeOrigin)}\n\n` : '';
204
+ const idBlock = forkFrom !== undefined ? `${buildIdentityAssertion(meta.node_id)}\n\n` : '';
205
+ const kickoff = `${wakeBlock}${idBlock}${opts.prompt}`;
171
206
  // A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
172
207
  // with the orchestrator persona but bypasses promote(), which is where a
173
208
  // roadmap scaffold would normally be seeded. Lay one down here (goal
@@ -176,14 +211,8 @@ export function spawnChild(opts) {
176
211
  if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
177
212
  seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
178
213
  }
179
- // Born an orchestrator also lay down its three scoped long-term memory
180
- // stores, the companions to the roadmap: user-global (key-less), project
181
- // (keyed off the child's cwd), and node-local. Each guarded against clobber.
182
- if (mode === 'orchestrator') {
183
- seedUserMemory();
184
- seedProjectMemory(opts.cwd);
185
- seedMemory(meta.node_id);
186
- }
214
+ // (The three scoped long-term memory stores are seeded for EVERY node at birth
215
+ // in spawnNode no orchestrator-gated seeding needed here.)
187
216
  // A managed CHILD lands in the shared global session: inherited from the
188
217
  // parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
189
218
  // from inside tmux instead opens its window in the CALLER'S CURRENT session,
@@ -205,8 +234,33 @@ export function spawnChild(opts) {
205
234
  // backstage, never a user session — this is what keeps a background revive
206
235
  // off the user's screen (the focus taint cannot reach it).
207
236
  updateNode(meta.node_id, { home_session: session });
208
- const inv = buildPiArgv(meta, { prompt: opts.prompt, forkFrom });
209
- const env = { ...inv.env, CRTR_ROOT_SESSION: childSession, [FRONT_DOOR_ENV]: '1' };
237
+ const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
238
+ // Belt-and-suspenders backstage routing emitted at EVERY launch site: the
239
+ // authoritative CRTR_ROOT_SESSION (this subtree's shared backstage) +
240
+ // CRTR_SUBTREE (this node's spine root). Hoisted ABOVE the host branch so the
241
+ // broker launch and the tmux command consume ONE authoritative env — the
242
+ // broker host merges inv.env, so a --headless --root broker would otherwise
243
+ // inherit the SPAWNER's subtree id via the lossy nodeEnv passthrough (review
244
+ // reuse MINOR-2). FRONT_DOOR is added per-consumer below (the broker host sets
245
+ // it itself; the tmux env const adds it).
246
+ inv.env = { ...inv.env, CRTR_ROOT_SESSION: childSession, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
247
+ // Birth LAUNCH branch on the PERSISTED host_kind (set at birth by spawnNode).
248
+ // A broker birth diverts AWAY from the tmux pane: the headless broker host
249
+ // launches a detached broker process (which records its own pid as pi_pid via
250
+ // the stophook) and returns placement fields all null — so we open NO tmux
251
+ // window and write NO tmux placement (presence stays null for a broker). The
252
+ // tmux path below is left completely UNCHANGED (byte-identical) for every
253
+ // non-broker spawn. Mirrors reviveNode's `hostFor(meta).launch(...)` shape.
254
+ if (meta.host_kind === 'broker') {
255
+ const placed = headlessBrokerHost.launch(meta.node_id, inv, {
256
+ cwd: meta.cwd,
257
+ name: fullName(meta),
258
+ resuming: false,
259
+ });
260
+ const saved = getNode(meta.node_id);
261
+ return { node: saved, window: placed.window, session: placed.session };
262
+ }
263
+ const env = { ...inv.env, [FRONT_DOOR_ENV]: '1' };
210
264
  const command = piCommand(inv.argv);
211
265
  // openNodeWindow now returns {window, pane}; pane is unused until the
212
266
  // placement layer lands, so destructure the window and proceed unchanged.
@@ -6,7 +6,7 @@ export interface StopSignals {
6
6
  }
7
7
  export type StopAction = {
8
8
  action: 'allow';
9
- reason: 'awaiting' | 'finished' | 'escalated' | 'dormant';
9
+ reason: 'awaiting' | 'scheduled' | 'finished' | 'escalated' | 'dormant';
10
10
  } | {
11
11
  action: 'reprompt';
12
12
  reason: 'stalled';
@@ -16,7 +16,7 @@
16
16
  // Also fine.
17
17
  // • otherwise → a TERMINAL node with nothing live to wait for and no
18
18
  // final pushed. Re-prompt it to finish or escalate.
19
- import { hasActiveLiveSubscription, getNode } from '../canvas/index.js';
19
+ import { hasActiveLiveSubscription, hasPendingSelfWake, getNode } from '../canvas/index.js';
20
20
  export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
21
21
  'Run `crtr push final "<result>"` if the work is done, or `crtr human ask` if you are blocked or need the user.';
22
22
  /** Decide what to do when a node stops. Pure given the canvas + this turn's
@@ -35,6 +35,11 @@ export function evaluateStop(nodeId, signals) {
35
35
  if (node !== null && node.lifecycle === 'resident') {
36
36
  return { action: 'allow', reason: 'dormant' };
37
37
  }
38
+ // A terminal node with a pending self-scheduled wakeup is legitimately
39
+ // waiting on the clock: the daemon's wakeup sweep resumes it when the wake
40
+ // fires, so it should idle-release rather than busy-wait holding pi/window.
41
+ if (hasPendingSelfWake(nodeId))
42
+ return { action: 'allow', reason: 'scheduled' };
38
43
  // A terminal node holding something live to wake it is legitimately awaiting.
39
44
  if (hasActiveLiveSubscription(nodeId))
40
45
  return { action: 'allow', reason: 'awaiting' };
@@ -1 +1 @@
1
- export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
1
+ export { installMenuBinding, installNavBindings, installViewNavBindings, sendKeysEnter } from './tmux.js';
@@ -1,4 +1,4 @@
1
1
  // tmux-chrome.ts — chrome seam (§2.1): stateless keybind/input verbs.
2
2
  // The ONLY non-placement module allowed to import the tmux driver, per the
3
3
  // §5.1 lint. Re-exports the menu/nav/send-keys verbs callers (spawn, chord) need.
4
- export { installMenuBinding, installNavBindings, sendKeysEnter } from './tmux.js';
4
+ export { installMenuBinding, installNavBindings, installViewNavBindings, sendKeysEnter } from './tmux.js';
@@ -96,6 +96,26 @@ export declare function paneLocation(pane: string): {
96
96
  * liveness. We therefore require the echoed `#{pane_id}` to equal the requested
97
97
  * pane: a live pane echoes its own id, a gone/bogus one yields empty. */
98
98
  export declare function paneExists(pane: string): boolean;
99
+ /** Every live pane id on the server (`list-panes -a`), as a Set for membership
100
+ * probes. Returns null when tmux is unreachable (no server / transient failure)
101
+ * so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
102
+ * never mass-delete, on a failed probe. One subprocess call total, so batch
103
+ * liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
104
+ * display-message each. */
105
+ export declare function listLivePanes(): Set<string> | null;
106
+ /** The working directory of a pane (`display-message -p -t <pane>
107
+ * '#{pane_current_path}'`). Used to preserve a view monitor's cwd across a
108
+ * view-cycle respawn so project-scoped views still resolve. Null if tmux fails. */
109
+ export declare function paneCurrentPath(pane: string): string | null;
110
+ /** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
111
+ * Used to tag a pane with the view id it currently hosts (`@crtr_view`) so the
112
+ * view-nav cycle can read it back and switch to the next/prev view in place.
113
+ * Best-effort; never throws. */
114
+ export declare function setPaneOption(pane: string, name: string, value: string): boolean;
115
+ /** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
116
+ * <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
117
+ * error so an unset option yields an empty string. undefined if tmux fails. */
118
+ export declare function getPaneOption(pane: string, name: string): string | undefined;
99
119
  /** Relocate a pane into another session as its own window WITHOUT killing the
100
120
  * process in it — `break-pane -d` moves the pane out of its current window (the
101
121
  * pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
@@ -197,3 +217,11 @@ export declare function installMenuBinding(): boolean;
197
217
  * terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
198
218
  */
199
219
  export declare function installNavBindings(): boolean;
220
+ /** Bind Alt+V → (], [) to the view-monitor cycle. `M-v` enters the private
221
+ * `crtr-view` key table (switch-client -T), then ] cycles next / [ cycles prev.
222
+ * Best-effort; false if any of the three binds fail. Deliberately distinct from
223
+ * installNavBindings' bare Alt+]/Alt+[ (node cycle): the two cycles coexist on
224
+ * the same server — brackets alone walk the node graph, Alt+V-then-bracket flips
225
+ * view monitors. The bracket keys are bound in the private table, so they NEVER
226
+ * shadow the user's root ]/[ and carry no CSI-introducer ambiguity. */
227
+ export declare function installViewNavBindings(): boolean;