@crouton-kit/crouter 0.3.18 → 0.3.19

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 (257) 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/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -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 {
@@ -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,17 @@ 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';
30
42
  }
31
43
  /** Resolve a `--fork-from` value to the source pi gets as `--fork <path|id>`.
32
44
  * A live node id resolves to its captured session FILE (absolute, cwd-immune),
@@ -38,6 +50,13 @@ export interface SpawnChildResult {
38
50
  window: string | null;
39
51
  session: string;
40
52
  }
53
+ /** Resolve who a spawn is attributed to. A managed child needs a spine parent
54
+ * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
55
+ * does not: it is top-level by definition and the spawner identity is
56
+ * provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
57
+ * spawner (regression: `crtr node new --root` from outside a node used to
58
+ * throw here). */
59
+ export declare function resolveSpawner(parent: string | undefined, ctxNodeId: string | null, root: boolean): string | null;
41
60
  /** Spawn a node from a live node. By default a managed terminal worker in a
42
61
  * background window, with the spawner auto-subscribed (active) via spawnNode.
43
62
  * 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/
@@ -156,9 +171,15 @@ 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,
162
183
  launch,
163
184
  });
164
185
  // Persist the task as the child's goal for a fresh revive to re-read.
@@ -168,6 +189,19 @@ export function spawnChild(opts) {
168
189
  // — not in buildPiArgv — so a bad reference fails the spawn loudly before any
169
190
  // window opens, rather than after pi is already booting.
170
191
  const forkFrom = opts.forkFrom !== undefined ? resolveForkSource(opts.forkFrom) : undefined;
192
+ // A fork inherits the SOURCE node's entire first-person conversation, so the
193
+ // identity re-assertion must ride the STRONGEST channel too, not only the
194
+ // session-start bearings (a trailing custom_message the model weighs only by
195
+ // recency). Prepend it to the kickoff PROMPT — the message that actually
196
+ // triggers the fork's first turn, so the model acts on "you are node X, a
197
+ // FORK of <source>, NOT them" as live instruction. When the daemon births this
198
+ // node from a scheduled `spawn` wake, the <crtr-wake> provenance block leads
199
+ // (so "a timer birthed you" precedes the task) — mirroring the fork-identity
200
+ // prepend. Only the pi argv prompt is reframed; the persisted goal (writeGoal
201
+ // above) keeps the raw task.
202
+ const wakeBlock = opts.wakeOrigin !== undefined ? `${buildWakeBearings(opts.wakeOrigin)}\n\n` : '';
203
+ const idBlock = forkFrom !== undefined ? `${buildIdentityAssertion(meta.node_id)}\n\n` : '';
204
+ const kickoff = `${wakeBlock}${idBlock}${opts.prompt}`;
171
205
  // A child created DIRECTLY as an orchestrator (mode='orchestrator') boots
172
206
  // with the orchestrator persona but bypasses promote(), which is where a
173
207
  // roadmap scaffold would normally be seeded. Lay one down here (goal
@@ -176,14 +210,8 @@ export function spawnChild(opts) {
176
210
  if (mode === 'orchestrator' && !hasRoadmap(meta.node_id)) {
177
211
  seedRoadmap(meta.node_id, { goal: opts.prompt.trim() });
178
212
  }
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
- }
213
+ // (The three scoped long-term memory stores are seeded for EVERY node at birth
214
+ // in spawnNode no orchestrator-gated seeding needed here.)
187
215
  // A managed CHILD lands in the shared global session: inherited from the
188
216
  // parent's CRTR_ROOT_SESSION, else the default node session. A --root spawned
189
217
  // from inside tmux instead opens its window in the CALLER'S CURRENT session,
@@ -205,8 +233,33 @@ export function spawnChild(opts) {
205
233
  // backstage, never a user session — this is what keeps a background revive
206
234
  // off the user's screen (the focus taint cannot reach it).
207
235
  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' };
236
+ const inv = buildPiArgv(meta, { prompt: kickoff, forkFrom });
237
+ // Belt-and-suspenders backstage routing emitted at EVERY launch site: the
238
+ // authoritative CRTR_ROOT_SESSION (this subtree's shared backstage) +
239
+ // CRTR_SUBTREE (this node's spine root). Hoisted ABOVE the host branch so the
240
+ // broker launch and the tmux command consume ONE authoritative env — the
241
+ // broker host merges inv.env, so a --headless --root broker would otherwise
242
+ // inherit the SPAWNER's subtree id via the lossy nodeEnv passthrough (review
243
+ // reuse MINOR-2). FRONT_DOOR is added per-consumer below (the broker host sets
244
+ // it itself; the tmux env const adds it).
245
+ inv.env = { ...inv.env, CRTR_ROOT_SESSION: childSession, CRTR_SUBTREE: rootOfSpine(meta.node_id) };
246
+ // Birth LAUNCH branch on the PERSISTED host_kind (set at birth by spawnNode).
247
+ // A broker birth diverts AWAY from the tmux pane: the headless broker host
248
+ // launches a detached broker process (which records its own pid as pi_pid via
249
+ // the stophook) and returns placement fields all null — so we open NO tmux
250
+ // window and write NO tmux placement (presence stays null for a broker). The
251
+ // tmux path below is left completely UNCHANGED (byte-identical) for every
252
+ // non-broker spawn. Mirrors reviveNode's `hostFor(meta).launch(...)` shape.
253
+ if (meta.host_kind === 'broker') {
254
+ const placed = headlessBrokerHost.launch(meta.node_id, inv, {
255
+ cwd: meta.cwd,
256
+ name: fullName(meta),
257
+ resuming: false,
258
+ });
259
+ const saved = getNode(meta.node_id);
260
+ return { node: saved, window: placed.window, session: placed.session };
261
+ }
262
+ const env = { ...inv.env, [FRONT_DOOR_ENV]: '1' };
210
263
  const command = piCommand(inv.argv);
211
264
  // openNodeWindow now returns {window, pane}; pane is unused until the
212
265
  // 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;
@@ -191,6 +191,39 @@ export function paneExists(pane) {
191
191
  const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
192
192
  return r.ok && r.stdout === pane;
193
193
  }
194
+ /** Every live pane id on the server (`list-panes -a`), as a Set for membership
195
+ * probes. Returns null when tmux is unreachable (no server / transient failure)
196
+ * so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
197
+ * never mass-delete, on a failed probe. One subprocess call total, so batch
198
+ * liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
199
+ * display-message each. */
200
+ export function listLivePanes() {
201
+ const r = tmux(['list-panes', '-a', '-F', '#{pane_id}']);
202
+ if (!r.ok)
203
+ return null;
204
+ return new Set(r.stdout.split('\n').filter((p) => p !== ''));
205
+ }
206
+ /** The working directory of a pane (`display-message -p -t <pane>
207
+ * '#{pane_current_path}'`). Used to preserve a view monitor's cwd across a
208
+ * view-cycle respawn so project-scoped views still resolve. Null if tmux fails. */
209
+ export function paneCurrentPath(pane) {
210
+ const r = tmux(['display-message', '-p', '-t', pane, '#{pane_current_path}']);
211
+ return r.ok && r.stdout !== '' ? r.stdout : null;
212
+ }
213
+ /** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
214
+ * Used to tag a pane with the view id it currently hosts (`@crtr_view`) so the
215
+ * view-nav cycle can read it back and switch to the next/prev view in place.
216
+ * Best-effort; never throws. */
217
+ export function setPaneOption(pane, name, value) {
218
+ return tmux(['set-option', '-p', '-t', pane, name, value]).ok;
219
+ }
220
+ /** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
221
+ * <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
222
+ * error so an unset option yields an empty string. undefined if tmux fails. */
223
+ export function getPaneOption(pane, name) {
224
+ const r = tmux(['show-options', '-p', '-t', pane, '-q', '-v', name]);
225
+ return r.ok ? r.stdout : undefined;
226
+ }
194
227
  /** Relocate a pane into another session as its own window WITHOUT killing the
195
228
  * process in it — `break-pane -d` moves the pane out of its current window (the
196
229
  * pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
@@ -375,12 +408,13 @@ export function installMenuBinding() {
375
408
  // a whole-canvas picker (incl. dormant nodes) and revives the choice via
376
409
  // `crtr node focus` — the only sync-safe open (routes through reviveNode).
377
410
  { name: 'resume node', key: 'r', cmd: `send-keys -t '#{pane_id}' '/resume-node' Enter` },
378
- // `d` demotes the agent to TERMINAL in place: no finalize, no kill — it keeps
379
- // running where it is, and because it is now terminal it is forced to push a
380
- // final up the spine when it finishes. `D` ALSO detaches it to the background
381
- // `crtr` session (frees the pane; the pi keeps generating). Neither ends it.
382
- { name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' >/dev/null 2>&1"` },
383
- { name: 'detach to background', key: 'D', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
411
+ // `d` runs `node demote`: flip the agent to TERMINAL in place no finalize,
412
+ // no kill — it keeps running where it is, and because it is now terminal it
413
+ // is forced to push a final up the spine when it finishes. `D` runs `node
414
+ // demote --detach`, which ALSO detaches it to the background `crtr` session
415
+ // (frees the pane; the pi keeps generating). Neither ends it.
416
+ { name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node demote --pane '#{pane_id}' >/dev/null 2>&1"` },
417
+ { name: 'detach to background', key: 'D', cmd: `run-shell "crtr node demote --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
384
418
  // Close cascades down the subscribes_to spine (kills the subtree's windows,
385
419
  // marks them canceled); revivable. Output discarded — the keypress just acts.
386
420
  { name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
@@ -455,3 +489,43 @@ export function installNavBindings() {
455
489
  ]).ok;
456
490
  return next && prev;
457
491
  }
492
+ // ---------------------------------------------------------------------------
493
+ // View-nav bindings — Alt+V then ] / [ cycle the view hosted in a MONITOR pane
494
+ // to the next/prev available view, in place. A VIEW-PREFIXED CHORD, not a bare
495
+ // Alt pair: Alt+V switches into a private one-shot `crtr-view` key table, then
496
+ // ] (next) / [ (prev) fire the cycle — mirroring node cycle's bracket DIRECTION
497
+ // grammar (Alt+] next / Alt+[ prev) while NAMESPACING the brackets so they can
498
+ // never shadow root bindings. This is collision-proof: only ONE root key (M-v)
499
+ // is claimed, and bare Alt pairs are a minefield in real configs (e.g. Alt+,/.
500
+ // and Alt+]/.[ commonly bound to window/pane nav). The bracket lives in the
501
+ // private table, so it also sidesteps the M-[ vs CSI-introducer ambiguity that
502
+ // dogs installNavBindings' Alt+[. Each key shells `crtr view cycle`, passing the
503
+ // active pane; cycle reads the pane's @crtr_view tag and respawns it on the
504
+ // next/prev view. Output discarded so the keypress never pops a results view.
505
+ // Installed at root boot alongside the node nav + Alt+C menu.
506
+ //
507
+ // NOTE: `M-v` is a GLOBAL root binding — it intercepts Alt+V in every pane/app
508
+ // (e.g. swallowed from the pi editor), the same tradeoff crtr already takes for
509
+ // M-c / M-] / M-[. Intended: crtr owns a small set of Alt chords server-wide.
510
+ // ---------------------------------------------------------------------------
511
+ /** Bind Alt+V → (], [) to the view-monitor cycle. `M-v` enters the private
512
+ * `crtr-view` key table (switch-client -T), then ] cycles next / [ cycles prev.
513
+ * Best-effort; false if any of the three binds fail. Deliberately distinct from
514
+ * installNavBindings' bare Alt+]/Alt+[ (node cycle): the two cycles coexist on
515
+ * the same server — brackets alone walk the node graph, Alt+V-then-bracket flips
516
+ * view monitors. The bracket keys are bound in the private table, so they NEVER
517
+ * shadow the user's root ]/[ and carry no CSI-introducer ambiguity. */
518
+ export function installViewNavBindings() {
519
+ const enter = tmux([
520
+ 'bind-key', '-n', 'M-v', 'switch-client', '-T', 'crtr-view',
521
+ ]).ok;
522
+ const next = tmux([
523
+ 'bind-key', '-T', 'crtr-view', ']', 'run-shell',
524
+ `crtr view cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`,
525
+ ]).ok;
526
+ const prev = tmux([
527
+ 'bind-key', '-T', 'crtr-view', '[', 'run-shell',
528
+ `crtr view cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`,
529
+ ]).ok;
530
+ return enter && next && prev;
531
+ }
@@ -1,5 +1,7 @@
1
1
  import type { Scope } from '../types.js';
2
2
  export declare function builtinSkillsRoot(): string;
3
+ export declare function builtinViewsRoot(): string;
4
+ export declare function builtinMemoryRoot(): string;
3
5
  export declare function userScopeRoot(): string;
4
6
  export declare function findProjectScopeRoot(startDir?: string): string | null;
5
7
  export declare function projectScopeRoot(startDir?: string): string | null;
@@ -9,6 +11,15 @@ export declare function ensureProjectScopeRoot(startDir?: string): string;
9
11
  export declare function pluginsDir(scope: Scope): string | null;
10
12
  export declare function marketplacesDir(scope: Scope): string | null;
11
13
  export declare function scopeSkillsDir(scope: Scope): string | null;
14
+ /** Where substrate memory documents live per scope. Builtin memory is the
15
+ * special case — its own package dir (builtinMemoryRoot, a sibling of
16
+ * builtin-skills), NOT scopeRoot('builtin')/memory — mirroring viewsDir's
17
+ * builtin handling. User and project memory live at scopeRoot(scope)/memory. */
18
+ export declare function scopeMemoryDir(scope: Scope): string | null;
19
+ /** Where view definition dirs live per scope. Builtin views sit directly under
20
+ * builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
21
+ * matching the loader's `<root>/<name>/view.mjs` resolution. */
22
+ export declare function viewsDir(scope: Scope): string | null;
12
23
  export declare function resolveScopeArg(scopeArg: string | undefined): Scope | 'all';
13
24
  export declare function listScopes(scopeArg: string | undefined): Scope[];
14
25
  export declare function resetScopeCache(): void;