@crouton-kit/crouter 0.3.13 → 0.3.15

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 (233) hide show
  1. package/dist/build-root.d.ts +8 -0
  2. package/dist/build-root.js +30 -0
  3. package/dist/builtin-personas/design/base.md +3 -7
  4. package/dist/builtin-personas/design/orchestrator.md +4 -3
  5. package/dist/builtin-personas/developer/base.md +3 -7
  6. package/dist/builtin-personas/developer/orchestrator.md +5 -4
  7. package/dist/builtin-personas/explore/base.md +3 -7
  8. package/dist/builtin-personas/explore/orchestrator.md +1 -5
  9. package/dist/builtin-personas/general/base.md +2 -4
  10. package/dist/builtin-personas/general/orchestrator.md +2 -4
  11. package/dist/builtin-personas/lifecycle/resident.md +2 -0
  12. package/dist/builtin-personas/lifecycle/terminal.md +6 -0
  13. package/dist/builtin-personas/orchestration-kernel.md +42 -3
  14. package/dist/builtin-personas/plan/base.md +3 -5
  15. package/dist/builtin-personas/plan/orchestrator.md +5 -4
  16. package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
  17. package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
  18. package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
  19. package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
  20. package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
  21. package/dist/builtin-personas/review/base.md +3 -5
  22. package/dist/builtin-personas/review/orchestrator.md +2 -6
  23. package/dist/builtin-personas/runtime-base.md +3 -19
  24. package/dist/builtin-personas/spec/base.md +3 -5
  25. package/dist/builtin-personas/spec/orchestrator.md +4 -3
  26. package/dist/builtin-personas/spine/has-manager.md +10 -0
  27. package/dist/builtin-personas/spine/no-manager.md +2 -0
  28. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
  29. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
  30. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
  31. package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
  32. package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
  33. package/dist/cli.js +6 -29
  34. package/dist/commands/__tests__/human.test.js +73 -2
  35. package/dist/commands/attention.js +76 -7
  36. package/dist/commands/canvas-prune.d.ts +2 -0
  37. package/dist/commands/canvas-prune.js +66 -0
  38. package/dist/commands/canvas.js +5 -8
  39. package/dist/commands/chord.d.ts +2 -0
  40. package/dist/commands/chord.js +143 -0
  41. package/dist/commands/daemon.js +8 -5
  42. package/dist/commands/dashboard.js +2 -0
  43. package/dist/commands/human/prompts.js +28 -27
  44. package/dist/commands/human/queue.d.ts +1 -0
  45. package/dist/commands/human/queue.js +105 -2
  46. package/dist/commands/human/shared.d.ts +28 -18
  47. package/dist/commands/human/shared.js +53 -60
  48. package/dist/commands/human.js +6 -14
  49. package/dist/commands/node.d.ts +11 -0
  50. package/dist/commands/node.js +381 -87
  51. package/dist/commands/pkg/market-inspect.js +6 -4
  52. package/dist/commands/pkg/market-manage.js +10 -6
  53. package/dist/commands/pkg/market.js +2 -4
  54. package/dist/commands/pkg/plugin-inspect.js +6 -4
  55. package/dist/commands/pkg/plugin-manage.js +12 -7
  56. package/dist/commands/pkg/plugin.js +2 -4
  57. package/dist/commands/pkg.js +0 -4
  58. package/dist/commands/push.js +178 -15
  59. package/dist/commands/revive.js +5 -3
  60. package/dist/commands/skill/author.js +6 -4
  61. package/dist/commands/skill/find.js +8 -5
  62. package/dist/commands/skill/read.js +2 -0
  63. package/dist/commands/skill/state.js +6 -4
  64. package/dist/commands/skill.js +0 -6
  65. package/dist/commands/sys/config.js +21 -7
  66. package/dist/commands/sys/doctor.js +2 -0
  67. package/dist/commands/sys/update.js +4 -0
  68. package/dist/commands/sys.js +0 -6
  69. package/dist/commands/tmux-spread.d.ts +2 -0
  70. package/dist/commands/tmux-spread.js +130 -0
  71. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  72. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  73. package/dist/core/__tests__/child-followup.test.js +83 -0
  74. package/dist/core/__tests__/close.test.d.ts +1 -0
  75. package/dist/core/__tests__/close.test.js +148 -0
  76. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  77. package/dist/core/__tests__/context-intro.test.js +196 -0
  78. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  80. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  81. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  82. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  83. package/dist/core/__tests__/focuses.test.js +259 -0
  84. package/dist/core/__tests__/fork.test.d.ts +1 -0
  85. package/dist/core/__tests__/fork.test.js +91 -0
  86. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  87. package/dist/core/__tests__/home-session.test.js +153 -0
  88. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  89. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  90. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  91. package/dist/core/__tests__/keystone.test.js +185 -0
  92. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  93. package/dist/core/__tests__/kickoff.test.js +89 -0
  94. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  95. package/dist/core/__tests__/lifecycle.test.js +178 -0
  96. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  97. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  98. package/dist/core/__tests__/memory.test.d.ts +1 -0
  99. package/dist/core/__tests__/memory.test.js +152 -0
  100. package/dist/core/__tests__/migration.test.d.ts +1 -0
  101. package/dist/core/__tests__/migration.test.js +238 -0
  102. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  103. package/dist/core/__tests__/pane-column.test.js +153 -0
  104. package/dist/core/__tests__/passive-subscription.test.d.ts +1 -0
  105. package/dist/core/__tests__/passive-subscription.test.js +164 -0
  106. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  107. package/dist/core/__tests__/persona-compose.test.js +53 -0
  108. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  109. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  110. package/dist/core/__tests__/persona.test.d.ts +1 -0
  111. package/dist/core/__tests__/persona.test.js +107 -0
  112. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  113. package/dist/core/__tests__/placement-focus.test.js +244 -0
  114. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  115. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  116. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  117. package/dist/core/__tests__/placement-revive.test.js +238 -0
  118. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  119. package/dist/core/__tests__/placement-teardown.test.js +183 -0
  120. package/dist/core/__tests__/prune.test.d.ts +1 -0
  121. package/dist/core/__tests__/prune.test.js +116 -0
  122. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  123. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  124. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  125. package/dist/core/__tests__/relaunch.test.js +328 -0
  126. package/dist/core/__tests__/reset.test.js +26 -7
  127. package/dist/core/__tests__/revive.test.d.ts +1 -0
  128. package/dist/core/__tests__/revive.test.js +217 -0
  129. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  130. package/dist/core/__tests__/spawn-root.test.js +73 -0
  131. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  132. package/dist/core/__tests__/steer-note.test.js +39 -0
  133. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  134. package/dist/core/__tests__/stop-guard.test.js +82 -0
  135. package/dist/core/__tests__/subcommand-tier.test.d.ts +1 -0
  136. package/dist/core/__tests__/subcommand-tier.test.js +99 -0
  137. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  138. package/dist/core/__tests__/tmux-surface.test.js +106 -0
  139. package/dist/core/__tests__/unknown-path.test.js +8 -2
  140. package/dist/core/canvas/attention.d.ts +10 -0
  141. package/dist/core/canvas/attention.js +40 -0
  142. package/dist/core/canvas/canvas.d.ts +66 -7
  143. package/dist/core/canvas/canvas.js +209 -21
  144. package/dist/core/canvas/db.d.ts +8 -0
  145. package/dist/core/canvas/db.js +206 -4
  146. package/dist/core/canvas/focuses.d.ts +22 -0
  147. package/dist/core/canvas/focuses.js +80 -0
  148. package/dist/core/canvas/index.d.ts +3 -0
  149. package/dist/core/canvas/index.js +3 -0
  150. package/dist/core/canvas/labels.d.ts +27 -0
  151. package/dist/core/canvas/labels.js +36 -0
  152. package/dist/core/canvas/paths.d.ts +4 -0
  153. package/dist/core/canvas/paths.js +6 -0
  154. package/dist/core/canvas/render.js +25 -10
  155. package/dist/core/canvas/telemetry.d.ts +14 -0
  156. package/dist/core/canvas/telemetry.js +35 -0
  157. package/dist/core/canvas/types.d.ts +115 -12
  158. package/dist/core/command.d.ts +25 -1
  159. package/dist/core/command.js +48 -7
  160. package/dist/core/config.js +36 -2
  161. package/dist/core/feed/feed.js +14 -12
  162. package/dist/core/feed/inbox.d.ts +3 -1
  163. package/dist/core/feed/inbox.js +45 -5
  164. package/dist/core/feed/passive.d.ts +17 -0
  165. package/dist/core/feed/passive.js +92 -0
  166. package/dist/core/help.d.ts +59 -13
  167. package/dist/core/help.js +73 -28
  168. package/dist/core/personas/index.d.ts +1 -1
  169. package/dist/core/personas/index.js +1 -1
  170. package/dist/core/personas/loader.d.ts +40 -1
  171. package/dist/core/personas/loader.js +63 -1
  172. package/dist/core/personas/resolve.d.ts +13 -6
  173. package/dist/core/personas/resolve.js +46 -34
  174. package/dist/core/runtime/bearings.d.ts +20 -0
  175. package/dist/core/runtime/bearings.js +92 -0
  176. package/dist/core/runtime/close.d.ts +14 -0
  177. package/dist/core/runtime/close.js +151 -0
  178. package/dist/core/runtime/demote.d.ts +14 -0
  179. package/dist/core/runtime/demote.js +120 -0
  180. package/dist/core/runtime/front-door.js +1 -1
  181. package/dist/core/runtime/kickoff.d.ts +32 -6
  182. package/dist/core/runtime/kickoff.js +111 -37
  183. package/dist/core/runtime/launch.d.ts +29 -6
  184. package/dist/core/runtime/launch.js +85 -13
  185. package/dist/core/runtime/lifecycle.d.ts +13 -0
  186. package/dist/core/runtime/lifecycle.js +86 -0
  187. package/dist/core/runtime/memory.d.ts +43 -0
  188. package/dist/core/runtime/memory.js +165 -0
  189. package/dist/core/runtime/naming.d.ts +22 -0
  190. package/dist/core/runtime/naming.js +166 -0
  191. package/dist/core/runtime/nodes.d.ts +32 -1
  192. package/dist/core/runtime/nodes.js +60 -10
  193. package/dist/core/runtime/persona.d.ts +25 -0
  194. package/dist/core/runtime/persona.js +139 -0
  195. package/dist/core/runtime/placement.d.ts +287 -0
  196. package/dist/core/runtime/placement.js +663 -0
  197. package/dist/core/runtime/presence.d.ts +7 -32
  198. package/dist/core/runtime/presence.js +90 -110
  199. package/dist/core/runtime/promote.d.ts +18 -7
  200. package/dist/core/runtime/promote.js +70 -65
  201. package/dist/core/runtime/reset.d.ts +47 -4
  202. package/dist/core/runtime/reset.js +223 -52
  203. package/dist/core/runtime/revive.d.ts +26 -2
  204. package/dist/core/runtime/revive.js +166 -39
  205. package/dist/core/runtime/roadmap.d.ts +5 -4
  206. package/dist/core/runtime/roadmap.js +9 -16
  207. package/dist/core/runtime/spawn.d.ts +20 -5
  208. package/dist/core/runtime/spawn.js +169 -44
  209. package/dist/core/runtime/stop-guard.d.ts +1 -1
  210. package/dist/core/runtime/stop-guard.js +18 -8
  211. package/dist/core/runtime/tmux.d.ts +106 -21
  212. package/dist/core/runtime/tmux.js +249 -45
  213. package/dist/core/spawn.js +15 -0
  214. package/dist/daemon/crtrd.d.ts +12 -1
  215. package/dist/daemon/crtrd.js +152 -34
  216. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  217. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  218. package/dist/pi-extensions/canvas-commands.d.ts +34 -0
  219. package/dist/pi-extensions/canvas-commands.js +103 -0
  220. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  221. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  222. package/dist/pi-extensions/canvas-goal-capture.d.ts +21 -0
  223. package/dist/pi-extensions/canvas-goal-capture.js +67 -0
  224. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  225. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  226. package/dist/pi-extensions/canvas-nav.js +586 -262
  227. package/dist/pi-extensions/canvas-passive-context.d.ts +32 -0
  228. package/dist/pi-extensions/canvas-passive-context.js +114 -0
  229. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  230. package/dist/pi-extensions/canvas-stophook.js +344 -228
  231. package/dist/types.d.ts +28 -0
  232. package/dist/types.js +16 -0
  233. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  // The canvas: one global graph of nodes + edges. Phase 0 of the pi-native
2
2
  // agent runtime. Topology in sqlite (WAL), node flesh on disk.
3
3
  export * from './types.js';
4
+ export * from './labels.js';
4
5
  export * from './paths.js';
5
6
  export * from './canvas.js';
7
+ export * from './focuses.js';
8
+ export * from './telemetry.js';
6
9
  export { openDb, closeDb } from './db.js';
@@ -0,0 +1,27 @@
1
+ import type { NodeMeta } from './types.js';
2
+ /** The minimal shape `fullName` needs — satisfied by NodeMeta and by anything
3
+ * else carrying name + kind (description optional). */
4
+ export interface NamedNode {
5
+ name: string;
6
+ kind: string;
7
+ description?: string;
8
+ }
9
+ /** A node's human label: its explicit handle plus the pi-generated description
10
+ * of its first task, e.g. `fix-auth refactor-login-flow`. The handle is
11
+ * dropped when it's just the kind default (non-informative, and the kind is
12
+ * surfaced elsewhere), so a default-named node reads as its description alone
13
+ * (`refactor-login-flow`). Falls back to the bare name when no description
14
+ * exists yet (a node not yet named, e.g. a bare root before its first
15
+ * message). Never empty. */
16
+ export declare function fullName(node: NamedNode): string;
17
+ /** The pi session display name — the editor label in the top-left. Format is
18
+ * `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
19
+ * plus the pi-generated description of its first task (see fullName) and
20
+ * `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
21
+ * fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
22
+ * cycle working the auth refactor. The name segment is omitted while it
23
+ * collapses to the bare kind (a bare root before its first message is named).
24
+ * Recomputed from meta on every revive (and pushed live via pi.setSessionName
25
+ * when a bare root is named mid-session), so a base→orchestrator polymorph, a
26
+ * fresh cycle, or a first-message naming all update the label. */
27
+ export declare function editorLabel(meta: NodeMeta): string;
@@ -0,0 +1,36 @@
1
+ // labels.ts — human-facing node labels derived from a node's meta.
2
+ //
3
+ // One source of truth for "what do we call this node on screen". `fullName`
4
+ // combines the node's explicit handle (meta.name) with the pi-generated
5
+ // description of its first task (meta.description) — the same string the editor
6
+ // label, the canvas dashboard tree, the nav spine, and the tmux window tab all
7
+ // render, so a node reads identically everywhere it appears.
8
+ /** A node's human label: its explicit handle plus the pi-generated description
9
+ * of its first task, e.g. `fix-auth refactor-login-flow`. The handle is
10
+ * dropped when it's just the kind default (non-informative, and the kind is
11
+ * surfaced elsewhere), so a default-named node reads as its description alone
12
+ * (`refactor-login-flow`). Falls back to the bare name when no description
13
+ * exists yet (a node not yet named, e.g. a bare root before its first
14
+ * message). Never empty. */
15
+ export function fullName(node) {
16
+ const desc = (node.description ?? '').trim();
17
+ const handle = node.name && node.name !== node.kind ? node.name : '';
18
+ const combined = [handle, desc].filter((s) => s !== '').join(' ');
19
+ return combined !== '' ? combined : node.name;
20
+ }
21
+ /** The pi session display name — the editor label in the top-left. Format is
22
+ * `<kind> (<mode>) <fullName> <cycle>` where `<fullName>` is the node's handle
23
+ * plus the pi-generated description of its first task (see fullName) and
24
+ * `<cycle>` is the revive count (meta.cycles). So `developer (orchestrator)
25
+ * fix-auth refactor-auth-flow 2` reads as a developer orchestrator on its 2nd
26
+ * cycle working the auth refactor. The name segment is omitted while it
27
+ * collapses to the bare kind (a bare root before its first message is named).
28
+ * Recomputed from meta on every revive (and pushed live via pi.setSessionName
29
+ * when a bare root is named mid-session), so a base→orchestrator polymorph, a
30
+ * fresh cycle, or a first-message naming all update the label. */
31
+ export function editorLabel(meta) {
32
+ const base = `${meta.kind} (${meta.mode})`;
33
+ const full = fullName(meta);
34
+ const cycle = meta.cycles ?? 0;
35
+ return full !== '' && full !== meta.kind ? `${base} ${full} ${cycle}` : `${base} ${cycle}`;
36
+ }
@@ -8,6 +8,10 @@ export declare function jobDir(nodeId: string): string;
8
8
  export declare function reportsDir(nodeId: string): string;
9
9
  export declare function nodeMetaPath(nodeId: string): string;
10
10
  export declare function inboxPath(nodeId: string): string;
11
+ /** Passive-subscription accumulator. Pushes from publishers this node subscribes
12
+ * to PASSIVELY land here instead of inbox.jsonl — the inbox-watcher never polls
13
+ * it, so they never wake the node. Drained as XML pre-text on the next message. */
14
+ export declare function passivePath(nodeId: string): string;
11
15
  export declare function transcriptPath(nodeId: string): string;
12
16
  export declare function sessionPtrPath(nodeId: string): string;
13
17
  /** Create the full directory skeleton for a node. Idempotent. */
@@ -44,6 +44,12 @@ export function nodeMetaPath(nodeId) {
44
44
  export function inboxPath(nodeId) {
45
45
  return join(nodeDir(nodeId), 'inbox.jsonl');
46
46
  }
47
+ /** Passive-subscription accumulator. Pushes from publishers this node subscribes
48
+ * to PASSIVELY land here instead of inbox.jsonl — the inbox-watcher never polls
49
+ * it, so they never wake the node. Drained as XML pre-text on the next message. */
50
+ export function passivePath(nodeId) {
51
+ return join(nodeDir(nodeId), 'passive.jsonl');
52
+ }
47
53
  export function transcriptPath(nodeId) {
48
54
  return join(nodeDir(nodeId), 'transcript.jsonl');
49
55
  }
@@ -15,6 +15,7 @@
15
15
  import { existsSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
18
+ import { fullName } from './labels.js';
18
19
  import { jobDir } from './paths.js';
19
20
  import { countAsks } from './attention.js';
20
21
  // ---------------------------------------------------------------------------
@@ -25,6 +26,7 @@ const STATUS_GLYPH = {
25
26
  idle: '○',
26
27
  done: '✓',
27
28
  dead: '✗',
29
+ canceled: '⊘',
28
30
  };
29
31
  function readNodeTelemetry(nodeId) {
30
32
  const path = join(jobDir(nodeId), 'telemetry.json');
@@ -58,7 +60,7 @@ function nodeLine(nodeId, indent, connector) {
58
60
  const ctx = fmtCtx(tel.tokens_in);
59
61
  const asks = countAsks(nodeId);
60
62
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
61
- return `${indent}${connector}${glyph} ${node.name} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`;
63
+ return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`;
62
64
  }
63
65
  /**
64
66
  * Recursively walk the subscription sub-DAG rooted at `nodeId`, appending
@@ -110,7 +112,7 @@ export function renderTree(rootId) {
110
112
  const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
111
113
  const glyph = STATUS_GLYPH[node.status] ?? '?';
112
114
  const out = [];
113
- out.push(`${glyph} ${node.name} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
115
+ out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}`);
114
116
  // visited starts with root already rendered (walkTree doesn't re-emit root).
115
117
  const visited = new Set([rootId]);
116
118
  const children = subscriptionsOf(rootId);
@@ -138,14 +140,24 @@ export function renderForest() {
138
140
  // a spawned_by edge + subscribe). Fall back to parent===null because querying
139
141
  // the full edge table would require opening the db here.
140
142
  //
141
- // Fine to use parent===null: roots are created by `node session` / `node new`
143
+ // Fine to use parent===null: roots are created by bare `crtr` / `node new --root`
142
144
  // without a parent; non-roots always have a parent.
143
- const roots = all.filter((n) => n.parent === null);
144
- // If for some reason we have no parent===null nodes (unusual: e.g., all nodes
145
- // were created by hand with a parent), fall back to all nodes.
146
- const renderRoots = roots.length > 0 ? roots : all;
145
+ //
146
+ // Filter to LIVE roots: each `/new` parks a `done` root (option C relaunch)
147
+ // with parent===null, so an unfiltered forest would render every parked root
148
+ // as a sibling tree and clutter the dashboard. Showing only active|idle roots
149
+ // drops parked (`done`) roots and, as a bonus, stray `dead`/`canceled` roots.
150
+ // Parked roots stay reachable by id (inspect / revive / focus).
151
+ const roots = all.filter((n) => n.parent === null && (n.status === 'active' || n.status === 'idle'));
152
+ // No LIVE roots: render an empty/placeholder forest rather than resurrecting
153
+ // parked (`done`) / dead / canceled roots. The live-only filter is the intent;
154
+ // falling back to all-status roots would re-clutter the dashboard with the very
155
+ // parked trees the filter drops (e.g. a sole root `/quit`'d with no `/new`).
156
+ // Parked roots stay reachable by id (inspect / revive / focus).
157
+ if (roots.length === 0)
158
+ return '(no live roots)';
147
159
  const parts = [];
148
- for (const r of renderRoots) {
160
+ for (const r of roots) {
149
161
  parts.push(renderTree(r.node_id));
150
162
  }
151
163
  return parts.join('\n\n');
@@ -160,7 +172,7 @@ export function dashboardRows(rootId) {
160
172
  const tel = readNodeTelemetry(id);
161
173
  return [{
162
174
  node_id: id,
163
- name: node.name,
175
+ name: fullName(node),
164
176
  status: node.status,
165
177
  kind: node.kind,
166
178
  mode: node.mode,
@@ -173,9 +185,12 @@ export function dashboardRows(rootId) {
173
185
  export function dashboardRowsAll() {
174
186
  return listNodes().flatMap((row) => {
175
187
  const tel = readNodeTelemetry(row.node_id);
188
+ // listNodes() returns the db projection (no description); read the meta to
189
+ // get the full label. Falls back to the row name if the meta is gone.
190
+ const meta = getNode(row.node_id);
176
191
  return [{
177
192
  node_id: row.node_id,
178
- name: row.name,
193
+ name: meta !== null ? fullName(meta) : row.name,
179
194
  status: row.status,
180
195
  kind: row.kind,
181
196
  mode: row.mode,
@@ -0,0 +1,14 @@
1
+ export interface Telemetry {
2
+ tokens_in?: number;
3
+ tokens_out?: number;
4
+ /** Live context-window size from the last turn_end (pi's getContextUsage). */
5
+ context_tokens?: number;
6
+ model?: string;
7
+ updated_at?: string;
8
+ }
9
+ /** Read a node's telemetry record. Missing/corrupt → {}. Never throws. */
10
+ export declare function readTelemetry(nodeId: string): Telemetry;
11
+ /** The node's current context-window size in tokens, or null when unknown.
12
+ * Prefers the live `context_tokens` gauge; falls back to cumulative
13
+ * `tokens_in` (the dashboard's coarse proxy) only when no live gauge exists. */
14
+ export declare function readContextTokens(nodeId: string): number | null;
@@ -0,0 +1,35 @@
1
+ // telemetry.ts — read a node's job/telemetry.json (written by canvas-stophook
2
+ // on every turn_end). Best-effort throughout: a missing or corrupt file yields
3
+ // an empty record, never a throw.
4
+ //
5
+ // tokens_in / tokens_out cumulative, non-cached throughput across the session.
6
+ // context_tokens the LIVE context-window gauge (pi's footer figure)
7
+ // captured on the last turn_end — the accurate measure
8
+ // of how full the node's window currently is. Absent
9
+ // until the first turn_end records a usable gauge.
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { jobDir } from './paths.js';
13
+ /** Read a node's telemetry record. Missing/corrupt → {}. Never throws. */
14
+ export function readTelemetry(nodeId) {
15
+ const path = join(jobDir(nodeId), 'telemetry.json');
16
+ if (!existsSync(path))
17
+ return {};
18
+ try {
19
+ return JSON.parse(readFileSync(path, 'utf8'));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ /** The node's current context-window size in tokens, or null when unknown.
26
+ * Prefers the live `context_tokens` gauge; falls back to cumulative
27
+ * `tokens_in` (the dashboard's coarse proxy) only when no live gauge exists. */
28
+ export function readContextTokens(nodeId) {
29
+ const tel = readTelemetry(nodeId);
30
+ if (typeof tel.context_tokens === 'number')
31
+ return tel.context_tokens;
32
+ if (typeof tel.tokens_in === 'number')
33
+ return tel.tokens_in;
34
+ return null;
35
+ }
@@ -1,6 +1,7 @@
1
1
  /** What a node is doing right now. UI shows active+idle; `done` is hidden but
2
- * revivable; only `dead` is a fault. */
3
- export type NodeStatus = 'active' | 'idle' | 'done' | 'dead';
2
+ * revivable; `canceled` is a user-closed node (also hidden, also revivable —
3
+ * not a fault); only `dead` is a fault. */
4
+ export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
4
5
  /** Does stopping finalize the node? terminal = worker (finalizes on push --final);
5
6
  * resident = manager/orchestrator (stays dormant, woken by inbox). */
6
7
  export type Lifecycle = 'terminal' | 'resident';
@@ -25,11 +26,21 @@ export interface LaunchSpec {
25
26
  /** Extra env injected into the pi process. */
26
27
  env: Record<string, string>;
27
28
  }
28
- /** A node's `meta.json`source of truth for its canvas row. Files for flesh,
29
- * sqlite for skeleton: the db indexes the queryable subset of these fields. */
30
- export interface NodeMeta {
29
+ /** A node's DURABLE IDENTITY the subset that `meta.json` persists on disk.
30
+ * Written rarely (birth, polymorph, session-id capture, naming); never touched
31
+ * by a status flip, an intent change, a focus swap, or a pid stamp. The db row
32
+ * indexes the queryable identity columns; `rebuildIndex()` re-derives them from
33
+ * here. (Live runtime state lives in NodeRuntime, authoritative in the row.) */
34
+ export interface NodeIdentity {
31
35
  node_id: string;
32
36
  name: string;
37
+ /** A 2-4 word kebab-case handle derived from the node's first prompt (named
38
+ * headlessly by pi; see runtime/naming.ts). Shown in the editor label. */
39
+ description?: string;
40
+ /** How many times this node has been (re)launched — born at 0, bumped on
41
+ * every revive. The trailing N in the editor label, so a refresh/crash cycle
42
+ * reads at a glance. */
43
+ cycles?: number;
33
44
  created: string;
34
45
  /** The dir this node is pinned to — its cwd (where pi runs, bash executes). */
35
46
  cwd: string;
@@ -37,24 +48,93 @@ export interface NodeMeta {
37
48
  kind: string;
38
49
  mode: Mode;
39
50
  lifecycle: Lifecycle;
40
- status: NodeStatus;
41
- /** spawned_by target who created me. Audit only; null for user-opened roots. */
51
+ /** The last persona state {mode,lifecycle} the node was GIVEN transition
52
+ * guidance for. Meta-only (not a db column). Born equal to the node's initial
53
+ * {mode,lifecycle} at spawn so a fresh node never gets spurious guidance. The
54
+ * persona injector (runtime/persona.ts) compares the live {mode,lifecycle}
55
+ * against this and, on drift, injects guidance for the new state then commits
56
+ * it here. */
57
+ persona_ack?: {
58
+ mode: Mode;
59
+ lifecycle: Lifecycle;
60
+ };
61
+ /** Spine parent — my manager (who subscribes to me); drives canvas nesting +
62
+ * orphaning. null for a root (top-level, no manager). */
42
63
  parent?: string | null;
64
+ /** Provenance — who spawned me (the `spawned_by` edge). Decoupled from
65
+ * `parent` so an INDEPENDENT root (parent=null) still records its lineage.
66
+ * Audit only; null for a user-opened root. Defaults to `parent` for a child. */
67
+ spawned_by?: string | null;
43
68
  /** New subscriptions this node opens default to passive when true. */
44
69
  passive_default?: boolean;
45
- /** Why the node last stopped (done | refresh). Drives reap-vs-revive. */
46
- intent?: ExitIntent;
47
- /** The pi session id for `--resume`. */
70
+ /** REVIVE-HOME the tmux session a node is (re)opened into when it must
71
+ * generate but is NOT focused (the durable revive target, distinct from the
72
+ * live LOCATION held by the runtime `tmux_session`). Set once at birth
73
+ * (managed child → the shared backstage `nodeSession()`; inline root → the
74
+ * adopted caller session; independent `--root` → the caller session), and
75
+ * rewritten only by demote-recycle. Durable identity (like `cwd`), never
76
+ * touched by a focus swap — this is what keeps a background revive off the
77
+ * user's session. Legacy metas omit it; readers default to
78
+ * `tmux_session ?? nodeSession()` (see `homeSessionOf`). */
79
+ home_session?: string;
80
+ /** The pi session id for `--session <id>` revival. */
48
81
  pi_session_id?: string | null;
82
+ /** Absolute path to pi's session `.jsonl` file, captured at session_start via
83
+ * ctx.sessionManager.getSessionFile(). Preferred over pi_session_id when
84
+ * resuming: pi resolves a BARE `--session <id>` relative to the launch cwd
85
+ * first (and shows an interactive cross-project "Fork? [y/N]" prompt when the
86
+ * revive cwd differs from the session's creation cwd), whereas an absolute
87
+ * PATH is opened directly — immune to any cwd discrepancy. Null for older
88
+ * nodes booted before this field existed → revive falls back to the bare id. */
89
+ pi_session_file?: string | null;
49
90
  /** Full pi launch recipe; rewritten on every polymorph. */
50
91
  launch?: LaunchSpec;
92
+ }
93
+ /** A node's LIVE RUNTIME state — authoritative in the WAL'd `nodes` row, each
94
+ * field mutated by exactly one atomic single-statement `UPDATE` (setStatus /
95
+ * setIntent / setPresence / recordPid+clearPid). NOT persisted to meta.json and
96
+ * NOT re-derivable by `rebuildIndex()` — it describes live process/presence
97
+ * state that is meaningless after the event that would lose the db; the daemon
98
+ * reconciles it from tmux reality, not from a stale file. */
99
+ export interface NodeRuntime {
100
+ /** What the node is doing right now. */
101
+ status: NodeStatus;
102
+ /** Why the node last stopped (done | refresh | idle-release). Drives reap-vs-revive.
103
+ * Optional on the hydrated view (a fresh construction omits it); the row
104
+ * column is always present, defaulting null. */
105
+ intent?: ExitIntent;
106
+ /** OS pid of the live pi process, recorded on boot (stophook session_start).
107
+ * The daemon's authoritative liveness signal: an inline root runs pi as a
108
+ * child of a persistent login shell, so its tmux window outlives a dead pi —
109
+ * window-existence alone can't detect the death, but a dead pid can. Cleared
110
+ * to null by a window-backed relaunch (reviveNode) until the fresh pi
111
+ * re-records it; left intact by an in-place respawn (reviveInPlace) so a
112
+ * failed respawn surfaces as a dead pid. */
113
+ pi_pid?: number | null;
51
114
  /** Presence: the tmux session (its root's home) and window this node renders
52
115
  * in while active. Cleared when the node goes done/dead and its window closes.
53
- * (Phase 5 promotes this to a dedicated presence registry.) */
116
+ * The row IS the presence registry (one atomic setPresence per move).
117
+ * v3: a DERIVED CACHE of `pane`'s current location — reconciled from the pane,
118
+ * never trusted when a user move could have desynced them. */
54
119
  tmux_session?: string | null;
55
120
  window?: string | null;
121
+ /** LOCATION's authoritative handle — the durable tmux `%pane_id` this node's
122
+ * pane is anchored on. tmux preserves it across `move-pane`/`join-pane`/
123
+ * `break-pane` and window renumbering, so `window`/`tmux_session` above are a
124
+ * cache reconciled from it and pane-existence is the liveness probe. A
125
+ * not-focused + not-generating node has `pane = null` (no pane). Authoritative
126
+ * in the row exactly like `window`/`tmux_session` — a RUNTIME field, NOT meta
127
+ * identity — written by the one atomic `setPresence` UPDATE. */
128
+ pane?: string | null;
56
129
  }
57
- /** The queryable projection of a NodeMeta stored as a canvas.db row. */
130
+ /** The hydrated node view `getNode()` returns: durable identity (from meta.json)
131
+ * ∪ live runtime (from the row). Keeps the historical `NodeMeta` name and field
132
+ * set so every `meta.X` read across the codebase typechecks unchanged — but
133
+ * `writeMeta` serializes only the NodeIdentity subset and the runtime fields are
134
+ * hydrated from the authoritative row. */
135
+ export type NodeMeta = NodeIdentity & NodeRuntime;
136
+ /** The queryable projection of a node stored as a canvas.db row: the indexed
137
+ * identity columns PLUS the authoritative runtime columns. */
58
138
  export interface NodeRow {
59
139
  node_id: string;
60
140
  name: string;
@@ -65,6 +145,13 @@ export interface NodeRow {
65
145
  cwd: string;
66
146
  parent: string | null;
67
147
  created: string;
148
+ /** Authoritative runtime columns (see NodeRuntime). */
149
+ intent: ExitIntent;
150
+ pi_pid: number | null;
151
+ window: string | null;
152
+ tmux_session: string | null;
153
+ /** The durable LOCATION handle (the tmux `%pane_id`); see NodeRuntime.pane. */
154
+ pane: string | null;
68
155
  }
69
156
  /** An edge as stored. For `subscribes_to`, `from` is the subscriber and `to`
70
157
  * is the publisher (A subscribes_to B ⇒ A receives B's output). For
@@ -78,6 +165,22 @@ export interface Edge {
78
165
  active: boolean;
79
166
  created: string;
80
167
  }
168
+ /** A FOCUS row as stored in the `focuses` table (canvas.db, migration v6): one
169
+ * durable on-screen viewport the user looks at, bound to one node. Plural —
170
+ * many focuses live at once across windows and sessions (the plural
171
+ * generalization of the old single `focus.ptr`). Anchored on the durable tmux
172
+ * `%pane_id`; `session` is a derived cache reconciled from the pane. `node_id`
173
+ * is UNIQUE — a node occupies at most one focus (Q5). */
174
+ export interface FocusRow {
175
+ /** Stable internal id for the viewport (the table's primary key). */
176
+ focus_id: string;
177
+ /** The durable `%pane_id` realizing the focus, or null before it is placed. */
178
+ pane: string | null;
179
+ /** Derived cache of the user session the pane lives in (reconciled from pane). */
180
+ session: string | null;
181
+ /** The node this focus shows. UNIQUE → a node occupies ≤1 focus. */
182
+ node_id: string;
183
+ }
81
184
  /** A subscription as seen from one endpoint. */
82
185
  export interface SubscriptionRef {
83
186
  /** The node id at the other end of the edge. */
@@ -1,4 +1,4 @@
1
- import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam } from './help.js';
1
+ import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam, SubTier } from './help.js';
2
2
  import { CrtrError } from './errors.js';
3
3
  /** Opt-in flag that surfaces a node as an editor slash command (a pi prompt
4
4
  * template / Claude Code command). When set, the bootstrap auto-writes a
@@ -19,6 +19,15 @@ export interface SlashSpec {
19
19
  export interface LeafDef {
20
20
  kind: 'leaf';
21
21
  name: string;
22
+ /** Short description for this node's <subcommand> row in its parent's -h. */
23
+ description?: string;
24
+ /** Selection rubric for the parent's listing — plainly states when to reach
25
+ * for this command (expansive with examples for judgment-heavy ones, concise
26
+ * for single-purpose). Rendered verbatim, no prefix. */
27
+ whenToUse?: string;
28
+ /** Visibility tier in ancestor listings (see SubTier). Default 'normal';
29
+ * 'hidden' keeps an internal leaf out of every listing. */
30
+ tier?: SubTier;
22
31
  help: LeafHelp;
23
32
  /** Opt into editor slash-command exposure (see SlashSpec). */
24
33
  slash?: SlashSpec;
@@ -31,6 +40,15 @@ export interface LeafDef {
31
40
  export interface BranchDef {
32
41
  kind: 'branch';
33
42
  name: string;
43
+ /** Short description for this node's <subcommand> row in its parent's -h.
44
+ * Unused on a top-level subtree (its root representation is its rootEntry). */
45
+ description?: string;
46
+ /** Selection rubric for the parent's listing — plainly states when to reach
47
+ * for this command (expansive with examples for judgment-heavy ones, concise
48
+ * for single-purpose). Rendered verbatim, no prefix. */
49
+ whenToUse?: string;
50
+ /** Visibility tier in ancestor listings (see SubTier). Default 'normal'. */
51
+ tier?: SubTier;
34
52
  help: BranchHelp;
35
53
  /** How this subtree represents itself one level up. Present on top-level
36
54
  * subtrees (assembled into root -h by defineRoot); omitted on nested
@@ -47,6 +65,9 @@ export interface RootDef {
47
65
  }
48
66
  export declare function defineLeaf(opts: {
49
67
  name: string;
68
+ description?: string;
69
+ whenToUse?: string;
70
+ tier?: SubTier;
50
71
  help: LeafHelp;
51
72
  slash?: SlashSpec;
52
73
  run: (input: Record<string, unknown>) => Promise<Record<string, unknown> | void>;
@@ -54,6 +75,9 @@ export declare function defineLeaf(opts: {
54
75
  }): LeafDef;
55
76
  export declare function defineBranch(opts: {
56
77
  name: string;
78
+ description?: string;
79
+ whenToUse?: string;
80
+ tier?: SubTier;
57
81
  help: BranchHelp;
58
82
  rootEntry?: RootEntry;
59
83
  slash?: SlashSpec;
@@ -17,16 +17,43 @@ export function defineLeaf(opts) {
17
17
  return {
18
18
  kind: 'leaf',
19
19
  name: opts.name,
20
+ description: opts.description,
21
+ whenToUse: opts.whenToUse,
22
+ tier: opts.tier,
20
23
  help: opts.help,
21
24
  slash: opts.slash,
22
25
  run: opts.run,
23
26
  render: opts.render,
24
27
  };
25
28
  }
29
+ /** Number of a node's own non-hidden subcommands (direct children). Leaves and
30
+ * childless branches return 0. */
31
+ function visibleSubCount(def) {
32
+ if (def.kind !== 'branch')
33
+ return 0;
34
+ return (def.help.listing ?? []).filter((c) => c.tier !== 'hidden').length;
35
+ }
26
36
  export function defineBranch(opts) {
37
+ // Assemble the parent-level listing straight from the child defs — each node
38
+ // owns its own description/whenToUse/tier, so the parent copies nothing
39
+ // (principle 16). Bottom-up construction guarantees a child branch's listing
40
+ // is already computed, so subCount is available here.
41
+ opts.help.listing = opts.children.map((c) => {
42
+ const subCount = visibleSubCount(c);
43
+ return {
44
+ name: c.name,
45
+ description: c.description ?? '',
46
+ whenToUse: c.whenToUse ?? '',
47
+ tier: c.tier ?? 'normal',
48
+ ...(subCount > 0 ? { subCount } : {}),
49
+ };
50
+ });
27
51
  return {
28
52
  kind: 'branch',
29
53
  name: opts.name,
54
+ description: opts.description,
55
+ whenToUse: opts.whenToUse,
56
+ tier: opts.tier,
30
57
  help: opts.help,
31
58
  rootEntry: opts.rootEntry,
32
59
  slash: opts.slash,
@@ -60,13 +87,27 @@ export function defineRoot(opts) {
60
87
  // and dynamic block all travel with it.
61
88
  const commands = opts.subtrees
62
89
  .filter((s) => s.rootEntry !== undefined)
63
- .map((s) => ({
64
- name: s.name,
65
- concept: s.rootEntry.concept,
66
- desc: s.rootEntry.desc,
67
- useWhen: s.rootEntry.useWhen,
68
- dynamicState: s.rootEntry.dynamicState,
69
- }));
90
+ .map((s) => {
91
+ // Promote this subtree's common/important children into root, and count
92
+ // how many other (non-hidden) direct subcommands stay behind `<name> -h`.
93
+ const visible = (s.help.listing ?? []).filter((c) => c.tier !== 'hidden');
94
+ const promoted = visible
95
+ .filter((c) => c.tier === 'common' || c.tier === 'important')
96
+ .map((c) => ({
97
+ path: `${s.name} ${c.name}`,
98
+ // important carries its shortform desc; common shows the bare path.
99
+ desc: c.tier === 'important' ? c.description : undefined,
100
+ }));
101
+ return {
102
+ name: s.name,
103
+ concept: s.rootEntry.concept,
104
+ desc: s.rootEntry.desc,
105
+ useWhen: s.rootEntry.useWhen,
106
+ dynamicState: s.rootEntry.dynamicState,
107
+ subcommands: promoted.length > 0 ? promoted : undefined,
108
+ otherSubcommandCount: visible.length - promoted.length,
109
+ };
110
+ });
70
111
  const help = {
71
112
  tagline: opts.tagline,
72
113
  commands,
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState } from '../types.js';
2
+ import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig } from '../types.js';
3
3
  import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
4
4
  import { scopeRoot, requireScopeRoot } from './scope.js';
5
5
  import { diag } from './io.js';
@@ -74,6 +74,39 @@ export function ensureScopeInitialized(scope, root) {
74
74
  writeJson(cfgPath, defaultScopeConfig());
75
75
  }
76
76
  }
77
+ /** Deep-merge one bind table over its built-in defaults: a user adding a single
78
+ * bind must not wipe the rest. Each user entry is validated (a `run` string is
79
+ * required) before it replaces/extends a key. */
80
+ function mergeBinds(base, over) {
81
+ const out = { ...base };
82
+ if (over !== null && typeof over === 'object') {
83
+ for (const [k, v] of Object.entries(over)) {
84
+ if (v !== null && typeof v === 'object' && typeof v.run === 'string') {
85
+ const b = v;
86
+ out[k] = {
87
+ run: b.run,
88
+ ...(b.confirm === true ? { confirm: true } : {}),
89
+ ...(typeof b.desc === 'string' ? { desc: b.desc } : {}),
90
+ };
91
+ }
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ /** Validate + deep-merge a user `canvasNav` block over the built-in defaults.
97
+ * An absent or malformed block falls back wholesale to defaults. */
98
+ function mergeCanvasNav(raw) {
99
+ const defaults = defaultCanvasNavConfig();
100
+ if (raw === null || typeof raw !== 'object')
101
+ return defaults;
102
+ const r = raw;
103
+ const prefixKey = typeof r.prefixKey === 'string' && r.prefixKey.trim() !== '' ? r.prefixKey : defaults.prefixKey;
104
+ return {
105
+ prefixKey,
106
+ prefixBinds: mergeBinds(defaults.prefixBinds, r.prefixBinds),
107
+ graphBinds: mergeBinds(defaults.graphBinds, r.graphBinds),
108
+ };
109
+ }
77
110
  function normalizeMode(value, fallback) {
78
111
  if (value === true)
79
112
  return 'notify';
@@ -103,7 +136,8 @@ function mergeConfig(partial) {
103
136
  const max_panes_per_window = typeof rawMaxPanes === 'number' && Number.isFinite(rawMaxPanes) && rawMaxPanes >= 1
104
137
  ? Math.floor(rawMaxPanes)
105
138
  : defaults.max_panes_per_window;
106
- return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window };
139
+ const canvasNav = mergeCanvasNav(partial.canvasNav);
140
+ return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav };
107
141
  }
108
142
  export function updateConfig(scope, mutate) {
109
143
  const cfg = readConfig(scope);