@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
@@ -0,0 +1,165 @@
1
+ // MEMORY.md + memory/ — an orchestrator's persistent file-based memory.
2
+ //
3
+ // One layout, three scopes. Each store is a `memory/` directory of one-fact
4
+ // files (each with typed frontmatter and [[wikilinks]]) indexed by a single
5
+ // MEMORY.md that holds one pointer line per memory and NEVER any content — the
6
+ // architecture in examples/memory-instructions.md. The pointer lines are the
7
+ // load-bearing read: a node's <crtr-context> bearings block extracts every
8
+ // applicable store's pointer lines each brand-new chat (see canvas-context-intro
9
+ // + bearings), so the indexes must stay lean; the detail files load on demand
10
+ // mid-session.
11
+ //
12
+ // The three scopes differ only in WHERE they live and HOW LONG they outlast a
13
+ // node — the `type` taxonomy in each memory's frontmatter drives which store a
14
+ // fact lands in (the mapping lives in the orchestration kernel's "Your long-term
15
+ // memory"). ALL THREE live under the canvas home (crtrHome), all machine-local:
16
+ //
17
+ // user-global <crtrHome>/memory/ — who the human is, how
18
+ // they like to work; loaded into EVERY orchestrator everywhere.
19
+ // project <crtrHome>/projects/<key>/memory/ — facts bound to one
20
+ // repo; loaded into orchestrators whose cwd resolves to that project. <key>
21
+ // is the git-repo-root (walked up from the cwd), else the cwd, mangled.
22
+ // node-local <crtrHome>/nodes/<id>/context/memory/ — facts specific to this
23
+ // node's goal; dies with the node.
24
+ //
25
+ // An ORCHESTRATOR-only artifact — the resident, multi-cycle nodes that survive
26
+ // refreshes and accumulate durable lessons/preferences; terminal workers are
27
+ // one-shot and get none. All three stores are seeded the moment a node becomes
28
+ // an orchestrator (promotion, or born one — where the roadmap is seeded too),
29
+ // guarded so a re-seed never clobbers an evolved memory.
30
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
31
+ import { join, dirname, resolve } from 'node:path';
32
+ import { contextDir, crtrHome } from '../canvas/index.js';
33
+ import { mangleCwd } from '../artifact.js';
34
+ // ---------------------------------------------------------------------------
35
+ // Index template + generic store ops (shared by all three scopes).
36
+ // ---------------------------------------------------------------------------
37
+ /** Build the seed contents of a fresh MEMORY.md index. Deliberately tiny: the
38
+ * bearings block only ever extracts the pointer lines, so this prose never
39
+ * rides into context — it's only for a human/agent opening the file directly,
40
+ * and the how-to lives once in the orchestrator kernel ("Your long-term
41
+ * memory"), not here. `holds` is a short scope hint so the empty index still
42
+ * orients a fresh write. */
43
+ function indexTemplate(holds) {
44
+ return ('# memory index — one pointer line per memory (`- [Title](slug.md) — hook`); ' +
45
+ `how-to in "Your long-term memory". Holds ${holds}.\n\n(no memories yet)\n`);
46
+ }
47
+ /** The node-local index template. Named export kept for callers/tests that
48
+ * assert the seeded node store verbatim. */
49
+ export const MEMORY_TEMPLATE = indexTemplate('your saved memories');
50
+ /** The user-global index template — framed around the human, not a goal. */
51
+ export const USER_MEMORY_TEMPLATE = indexTemplate('your saved memories about the human — who they are and how they like to work');
52
+ /** The project index template — framed around the repo. */
53
+ export const PROJECT_MEMORY_TEMPLATE = indexTemplate('your saved memories about this project');
54
+ /** The MEMORY.md index path inside a memory `dir`. */
55
+ function indexPathOf(dir) {
56
+ return join(dir, 'MEMORY.md');
57
+ }
58
+ /** Seed `dir` + its MEMORY.md index with `template` IFF the index is absent.
59
+ * Idempotent and guarded so it never clobbers an evolved memory; creating the
60
+ * dir up front lets the node write detail files into it directly (no mkdir).
61
+ * Returns true when it seeded, false when an index already existed. */
62
+ function seedStore(dir, template) {
63
+ const idx = indexPathOf(dir);
64
+ if (existsSync(idx))
65
+ return false;
66
+ mkdirSync(dir, { recursive: true });
67
+ writeFileSync(idx, template);
68
+ return true;
69
+ }
70
+ /** Read a store's MEMORY.md index, or null when it doesn't exist. */
71
+ function readStore(dir) {
72
+ const idx = indexPathOf(dir);
73
+ return existsSync(idx) ? readFileSync(idx, 'utf8') : null;
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // node-local store — <crtrHome>/nodes/<id>/context/memory/ (facts for this goal)
77
+ // ---------------------------------------------------------------------------
78
+ /** The node-local memory directory in a node's context dir — holds MEMORY.md
79
+ * (the index) and the one-fact detail files it points at. */
80
+ export function memoryDir(nodeId) {
81
+ return join(contextDir(nodeId), 'memory');
82
+ }
83
+ /** The node-local MEMORY.md index path (inside the memory dir). */
84
+ export function memoryPath(nodeId) {
85
+ return indexPathOf(memoryDir(nodeId));
86
+ }
87
+ /** Whether the node has a node-local memory store. This is ALSO the
88
+ * orchestrator gate: only orchestrators are ever seeded one, so a node with no
89
+ * node-local store is a terminal worker (no memory framing at all). */
90
+ export function hasMemory(nodeId) {
91
+ return existsSync(memoryPath(nodeId));
92
+ }
93
+ /** Read the node-local MEMORY.md index, or null when it doesn't exist. */
94
+ export function readMemory(nodeId) {
95
+ return readStore(memoryDir(nodeId));
96
+ }
97
+ /** Seed the node-local memory dir + index IF the node has none yet. */
98
+ export function seedMemory(nodeId) {
99
+ return seedStore(memoryDir(nodeId), MEMORY_TEMPLATE);
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // user-global store — <crtrHome>/memory/ (who the human is, how they work)
103
+ // ---------------------------------------------------------------------------
104
+ /** The user-global memory directory — one per machine, key-less, loaded into
105
+ * every orchestrator everywhere. */
106
+ export function userMemoryDir() {
107
+ return join(crtrHome(), 'memory');
108
+ }
109
+ /** The user-global MEMORY.md index path. */
110
+ export function userMemoryPath() {
111
+ return indexPathOf(userMemoryDir());
112
+ }
113
+ export function hasUserMemory() {
114
+ return existsSync(userMemoryPath());
115
+ }
116
+ /** Read the user-global MEMORY.md index, or null when it doesn't exist. */
117
+ export function readUserMemory() {
118
+ return readStore(userMemoryDir());
119
+ }
120
+ /** Seed the user-global memory dir + index IF absent. */
121
+ export function seedUserMemory() {
122
+ return seedStore(userMemoryDir(), USER_MEMORY_TEMPLATE);
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // project store — <crtrHome>/projects/<key>/memory/ (facts bound to one repo)
126
+ // ---------------------------------------------------------------------------
127
+ /** The git repo root containing `cwd` — walk up for a `.git` entry — or null
128
+ * when `cwd` is not inside a repo. `.git` may be a dir (normal) or a file
129
+ * (worktree/submodule); existsSync catches both. */
130
+ function gitRoot(cwd) {
131
+ let dir = resolve(cwd);
132
+ for (;;) {
133
+ if (existsSync(join(dir, '.git')))
134
+ return dir;
135
+ const parent = dirname(dir);
136
+ if (parent === dir)
137
+ return null; // hit the filesystem root
138
+ dir = parent;
139
+ }
140
+ }
141
+ /** The project key for `cwd`: its git-repo-root when inside a repo, else the
142
+ * cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
143
+ * This keys the per-project memory store under <crtrHome>/projects/. */
144
+ export function projectKey(cwd) {
145
+ return mangleCwd(gitRoot(cwd) ?? cwd);
146
+ }
147
+ /** The project memory directory for `cwd`. */
148
+ export function projectMemoryDir(cwd) {
149
+ return join(crtrHome(), 'projects', projectKey(cwd), 'memory');
150
+ }
151
+ /** The project MEMORY.md index path for `cwd`. */
152
+ export function projectMemoryPath(cwd) {
153
+ return indexPathOf(projectMemoryDir(cwd));
154
+ }
155
+ export function hasProjectMemory(cwd) {
156
+ return existsSync(projectMemoryPath(cwd));
157
+ }
158
+ /** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
159
+ export function readProjectMemory(cwd) {
160
+ return readStore(projectMemoryDir(cwd));
161
+ }
162
+ /** Seed the project memory dir + index for `cwd` IF absent. */
163
+ export function seedProjectMemory(cwd) {
164
+ return seedStore(projectMemoryDir(cwd), PROJECT_MEMORY_TEMPLATE);
165
+ }
@@ -0,0 +1,22 @@
1
+ import type { NodeMeta } from '../canvas/index.js';
2
+ /** Coerce arbitrary text into a 3-5 word kebab-case name, or '' if nothing
3
+ * usable survives. Lowercases, keeps [a-z0-9], collapses everything else to a
4
+ * single hyphen, and clamps to the first 5 words. */
5
+ export declare function sanitizeSessionName(raw: string): string;
6
+ /** Local fallback: derive a name straight from the prompt (no pi call). Drops
7
+ * stop-words, takes the first few content words. */
8
+ export declare function slugFromPrompt(prompt: string): string;
9
+ /** Synchronously ask pi for a 2-4 word kebab name for `prompt`. Blocks up to
10
+ * NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
11
+ * output) falls back to a local slug. Returns '' only for an empty prompt. */
12
+ export declare function generateSessionName(prompt: string): string;
13
+ /** Asynchronously generate a name for `prompt` and persist it to the node's
14
+ * meta as `description` — only if the node has none yet (so a later message
15
+ * never clobbers it). Non-blocking: safe to call from inside a live pi event
16
+ * loop. Best-effort; swallows all errors.
17
+ *
18
+ * `onNamed` (optional) fires with the freshly-persisted meta the moment the
19
+ * name lands — the bare-root path passes a callback that calls
20
+ * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
21
+ * same session, instead of waiting for the next revive/cycle. */
22
+ export declare function generateAndPersistName(nodeId: string, prompt: string, onNamed?: (meta: NodeMeta) => void): void;
@@ -0,0 +1,166 @@
1
+ // Session naming — turn a node's first prompt into a short, human-readable
2
+ // handle for the editor label.
3
+ //
4
+ // A node's editor label is `<kind> (<mode>) <name> <cycle>` (see editorLabel in
5
+ // launch.ts). The `<name>` is a 3-5 word kebab-case "description" derived from
6
+ // the first prompt by asking pi headlessly (`pi -p`), persisted on the node's
7
+ // meta so it survives revives and shows in every cycle.
8
+ //
9
+ // Two entry points:
10
+ // • generateSessionName — synchronous (spawnSync). For the CLI spawn paths
11
+ // (spawnChild / bootRoot) that run outside any pi event loop, where a brief
12
+ // block before launching the worker is fine and lets the FIRST pi session
13
+ // already carry the name.
14
+ // • generateAndPersistName — async (execFile, non-blocking). For the bare-root
15
+ // case where the prompt only arrives as the first interactive message inside
16
+ // a live pi process; it must never block the event loop. Persists the name
17
+ // to meta so the label picks it up on the next cycle.
18
+ //
19
+ // Both are best-effort: a failed/slow/garbled pi call falls back to a local slug
20
+ // of the prompt, so a node always gets a sane name.
21
+ import { spawnSync, execFile } from 'node:child_process';
22
+ import { getNode, updateNode } from '../canvas/index.js';
23
+ /** Cap on prompt text fed to the namer — a name needs only the gist. */
24
+ const PROMPT_CAP = 2000;
25
+ /** Wall-clock budget for the headless pi call before we fall back to a slug. */
26
+ const NAME_TIMEOUT_MS = 20_000;
27
+ const NAME_SYSTEM_PROMPT = 'You name coding-agent work sessions. This name is a label used to identify the ' +
28
+ 'session at a glance among many other concurrent programming sessions, so it must ' +
29
+ 'describe what the task is about. Reply with ONLY a concise 3-5 word name in ' +
30
+ 'kebab-case: lowercase words joined by single hyphens (e.g. `refactor-auth-token-flow`, ' +
31
+ '`add-csv-export-endpoint`). No punctuation, quotes, prose, or trailing text. ' +
32
+ 'Output JUST the name, nothing else.';
33
+ /** Put the raw task text FIRST in a delimited block, then the instruction, so the
34
+ * model reads the content before being told what to do and never mistakes the
35
+ * prompt's own text for the instruction. The prompt is capped first, so the
36
+ * closing tag is always present. */
37
+ function nameUserPrompt(prompt) {
38
+ return `<prompt>\n${prompt.slice(0, PROMPT_CAP)}\n</prompt>\n\nName this session based on the task above. The name should describe what the task is about, so it can be identified among many other programming sessions. Output JUST the name, nothing else.`;
39
+ }
40
+ /** A short stop-word set so the local-slug fallback skips filler words. */
41
+ const STOPWORDS = new Set([
42
+ 'the', 'a', 'an', 'and', 'or', 'but', 'to', 'of', 'in', 'on', 'for', 'with',
43
+ 'is', 'are', 'be', 'this', 'that', 'it', 'as', 'at', 'by', 'from', 'into',
44
+ 'please', 'can', 'you', 'i', 'we', 'my', 'our', 'me', 'so', 'then',
45
+ ]);
46
+ /** Coerce arbitrary text into a 3-5 word kebab-case name, or '' if nothing
47
+ * usable survives. Lowercases, keeps [a-z0-9], collapses everything else to a
48
+ * single hyphen, and clamps to the first 5 words. */
49
+ export function sanitizeSessionName(raw) {
50
+ const firstLine = (raw ?? '').split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
51
+ const words = firstLine
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9]+/g, '-')
54
+ .split('-')
55
+ .filter((w) => w !== '');
56
+ return words.slice(0, 5).join('-');
57
+ }
58
+ /** Local fallback: derive a name straight from the prompt (no pi call). Drops
59
+ * stop-words, takes the first few content words. */
60
+ export function slugFromPrompt(prompt) {
61
+ const words = (prompt ?? '')
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9]+/g, ' ')
64
+ .split(' ')
65
+ .filter((w) => w !== '' && !STOPWORDS.has(w));
66
+ const picked = (words.length > 0 ? words : (prompt ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(' ').filter(Boolean))
67
+ .slice(0, 3);
68
+ return sanitizeSessionName(picked.join('-')) || 'session';
69
+ }
70
+ /** Default namer model — Anthropic's small/fast model. Naming is a one-line
71
+ * classification, so we pin Haiku (cheap, quick) instead of inheriting the
72
+ * node's heavyweight default. Override with CRTR_NAME_MODEL. */
73
+ const DEFAULT_NAME_MODEL = 'anthropic/claude-haiku-4-5';
74
+ /** The pi argv for a headless name request. Stripped down (no tools, session,
75
+ * context files, extensions, skills, templates, themes) so it's fast and
76
+ * side-effect free. Pinned to Haiku with thinking off — naming is a trivial
77
+ * classification that never needs a reasoning budget. Override the model with
78
+ * CRTR_NAME_MODEL. */
79
+ function nameArgs(prompt) {
80
+ const override = process.env['CRTR_NAME_MODEL'];
81
+ const model = override !== undefined && override.trim() !== '' ? override.trim() : DEFAULT_NAME_MODEL;
82
+ const argv = [
83
+ '-p',
84
+ '--no-session',
85
+ '--no-context-files',
86
+ '--no-extensions',
87
+ '--no-skills',
88
+ '--no-prompt-templates',
89
+ '--no-themes',
90
+ '--no-tools',
91
+ '--mode', 'text',
92
+ // Naming is a trivial one-line classification — no thinking budget, ever.
93
+ '--thinking', 'off',
94
+ '--model', model,
95
+ ];
96
+ argv.push('--system-prompt', NAME_SYSTEM_PROMPT);
97
+ argv.push(nameUserPrompt(prompt));
98
+ return argv;
99
+ }
100
+ /** Synchronously ask pi for a 2-4 word kebab name for `prompt`. Blocks up to
101
+ * NAME_TIMEOUT_MS; on any failure (non-zero exit, timeout, empty/garbled
102
+ * output) falls back to a local slug. Returns '' only for an empty prompt. */
103
+ export function generateSessionName(prompt) {
104
+ const body = (prompt ?? '').trim();
105
+ if (body === '')
106
+ return '';
107
+ try {
108
+ const r = spawnSync('pi', nameArgs(body), {
109
+ encoding: 'utf8',
110
+ timeout: NAME_TIMEOUT_MS,
111
+ // Don't inherit a TUI; capture stdout only.
112
+ stdio: ['ignore', 'pipe', 'ignore'],
113
+ });
114
+ if (r.status === 0 && typeof r.stdout === 'string') {
115
+ const name = sanitizeSessionName(r.stdout);
116
+ if (name !== '')
117
+ return name;
118
+ }
119
+ }
120
+ catch {
121
+ // fall through to slug
122
+ }
123
+ return slugFromPrompt(body);
124
+ }
125
+ /** Asynchronously generate a name for `prompt` and persist it to the node's
126
+ * meta as `description` — only if the node has none yet (so a later message
127
+ * never clobbers it). Non-blocking: safe to call from inside a live pi event
128
+ * loop. Best-effort; swallows all errors.
129
+ *
130
+ * `onNamed` (optional) fires with the freshly-persisted meta the moment the
131
+ * name lands — the bare-root path passes a callback that calls
132
+ * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
133
+ * same session, instead of waiting for the next revive/cycle. */
134
+ export function generateAndPersistName(nodeId, prompt, onNamed) {
135
+ const body = (prompt ?? '').trim();
136
+ if (body === '')
137
+ return;
138
+ const persist = (name) => {
139
+ try {
140
+ const meta = getNode(nodeId);
141
+ if (meta === null)
142
+ return;
143
+ if ((meta.description ?? '').trim() !== '')
144
+ return; // already named
145
+ const clean = sanitizeSessionName(name);
146
+ const updated = updateNode(nodeId, { description: clean !== '' ? clean : slugFromPrompt(body) });
147
+ onNamed?.(updated);
148
+ }
149
+ catch {
150
+ // best-effort
151
+ }
152
+ };
153
+ try {
154
+ execFile('pi', nameArgs(body), { encoding: 'utf8', timeout: NAME_TIMEOUT_MS }, (err, stdout) => {
155
+ if (err || typeof stdout !== 'string') {
156
+ persist(slugFromPrompt(body));
157
+ return;
158
+ }
159
+ const name = sanitizeSessionName(stdout);
160
+ persist(name !== '' ? name : slugFromPrompt(body));
161
+ });
162
+ }
163
+ catch {
164
+ persist(slugFromPrompt(body));
165
+ }
166
+ }
@@ -1,9 +1,34 @@
1
1
  import { type NodeMeta, type Mode, type Lifecycle, type LaunchSpec } from '../canvas/index.js';
2
2
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
3
3
  export declare function newNodeId(): string;
4
+ /** Resolve the tmux session a freshly-born node's window/pane opens into — and
5
+ * thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
6
+ * unit-testable without a live tmux:
7
+ * - managed background child (`adoptCaller=false`) → the shared backstage:
8
+ * the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
9
+ * - independent `--root` / inline front door (`adoptCaller=true`) → the
10
+ * caller's CURRENT session when inside tmux (`here`), else the backstage.
11
+ * This is exactly the session each birth site already places the node into;
12
+ * centralizing it keeps `home_session` and the actual placement in lockstep. */
13
+ export declare function resolveBirthSession(opts: {
14
+ /** True for an independent root or the inline front door (both adopt the
15
+ * caller's session); false for a managed background child. */
16
+ adoptCaller: boolean;
17
+ /** The caller's current tmux location, or null when not inside tmux. */
18
+ here: {
19
+ session: string;
20
+ } | null;
21
+ /** The inherited CRTR_ROOT_SESSION (the backstage the subtree flows into). */
22
+ rootSession?: string | null;
23
+ }): string;
24
+ /** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
25
+ * before `home_session` existed have no such field in meta — they fall back to
26
+ * their last live LOCATION (`tmux_session`), then to the shared backstage
27
+ * (`nodeSession()`). The defaulted read for the placement layer; a present
28
+ * `home_session` is always returned verbatim. */
29
+ export declare function homeSessionOf(nodeId: string): string;
4
30
  export interface NodeContext {
5
31
  nodeId: string | null;
6
- parentNodeId: string | null;
7
32
  kind: string | null;
8
33
  mode: Mode | null;
9
34
  }
@@ -21,8 +46,14 @@ export interface SpawnNodeOpts {
21
46
  lifecycle?: Lifecycle;
22
47
  cwd: string;
23
48
  name?: string;
49
+ /** Editor-label handle (2-4 word kebab-case) for the node's first prompt. */
50
+ description?: string;
24
51
  /** Parent node id. Omit for a user-opened root. */
25
52
  parent?: string | null;
53
+ /** Who spawned me (the `spawned_by` provenance edge), when it differs from
54
+ * `parent` — e.g. an independent root (parent=null) still records its
55
+ * spawner. Defaults to `parent`. */
56
+ spawnedBy?: string | null;
26
57
  /** New subscriptions this node opens default to passive when true. */
27
58
  passiveDefault?: boolean;
28
59
  /** Resolved pi launch recipe (from resolve(kind,mode)). */
@@ -14,10 +14,42 @@
14
14
  // is also recorded.
15
15
  import { randomBytes } from 'node:crypto';
16
16
  import { createNode, getNode, subscribe, recordSpawn, } from '../canvas/index.js';
17
+ import { nodeSession } from './tmux.js';
17
18
  /** Generate a node id in the same shape as job ids (time-sortable + random). */
18
19
  export function newNodeId() {
19
20
  return `${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
20
21
  }
22
+ // ---------------------------------------------------------------------------
23
+ // REVIVE-HOME (home_session) — the durable session a node is (re)opened into
24
+ // ---------------------------------------------------------------------------
25
+ /** Resolve the tmux session a freshly-born node's window/pane opens into — and
26
+ * thus its durable REVIVE-HOME (`home_session`). Pure so the birth decision is
27
+ * unit-testable without a live tmux:
28
+ * - managed background child (`adoptCaller=false`) → the shared backstage:
29
+ * the inherited `CRTR_ROOT_SESSION`, else `nodeSession()` (`crtr`).
30
+ * - independent `--root` / inline front door (`adoptCaller=true`) → the
31
+ * caller's CURRENT session when inside tmux (`here`), else the backstage.
32
+ * This is exactly the session each birth site already places the node into;
33
+ * centralizing it keeps `home_session` and the actual placement in lockstep. */
34
+ export function resolveBirthSession(opts) {
35
+ const backstage = opts.rootSession !== undefined && opts.rootSession !== null && opts.rootSession !== ''
36
+ ? opts.rootSession
37
+ : nodeSession();
38
+ if (opts.adoptCaller && opts.here !== null)
39
+ return opts.here.session;
40
+ return backstage;
41
+ }
42
+ /** A node's durable REVIVE-HOME, with the legacy back-compat default. Nodes born
43
+ * before `home_session` existed have no such field in meta — they fall back to
44
+ * their last live LOCATION (`tmux_session`), then to the shared backstage
45
+ * (`nodeSession()`). The defaulted read for the placement layer; a present
46
+ * `home_session` is always returned verbatim. */
47
+ export function homeSessionOf(nodeId) {
48
+ const meta = getNode(nodeId);
49
+ if (meta === null)
50
+ return nodeSession();
51
+ return meta.home_session ?? meta.tmux_session ?? nodeSession();
52
+ }
21
53
  /** Read the current node's identity from the environment. A spawned pi process
22
54
  * runs with CRTR_NODE_ID set; its own `crtr` invocations spawn children under
23
55
  * it by reading CRTR_NODE_ID as the parent. */
@@ -25,7 +57,6 @@ export function currentNodeContext() {
25
57
  const env = process.env;
26
58
  return {
27
59
  nodeId: env['CRTR_NODE_ID'] ?? null,
28
- parentNodeId: env['CRTR_NODE_ID'] ?? null, // a child's parent is the live node
29
60
  kind: env['CRTR_KIND'] ?? null,
30
61
  mode: env['CRTR_MODE'] ?? null,
31
62
  };
@@ -63,33 +94,52 @@ export function nodeEnv(meta) {
63
94
  export function spawnNode(opts) {
64
95
  const parent = opts.parent ?? null;
65
96
  const isRoot = parent === null;
97
+ // Provenance is independent of the spine: a root has no parent but still
98
+ // records who spawned it. A child's spawner is its parent unless overridden.
99
+ const spawnedBy = opts.spawnedBy ?? parent;
100
+ const mode = opts.mode ?? 'base';
101
+ // A user-opened root is resident (a conversation you live in); a spawned node
102
+ // is terminal until it must persist (promotion handles that later).
103
+ const lifecycle = opts.lifecycle ?? (isRoot ? 'resident' : 'terminal');
66
104
  const meta = {
67
105
  node_id: opts.nodeId ?? newNodeId(),
68
106
  name: opts.name ?? opts.kind,
107
+ description: opts.description,
108
+ cycles: 0,
69
109
  created: new Date().toISOString(),
70
110
  cwd: opts.cwd,
71
111
  kind: opts.kind,
72
- mode: opts.mode ?? 'base',
73
- // A user-opened root is resident (a conversation you live in); a spawned
74
- // node is terminal until it must persist (promotion handles that later).
75
- lifecycle: opts.lifecycle ?? (isRoot ? 'resident' : 'terminal'),
112
+ mode,
113
+ lifecycle,
114
+ // Born already acked to its initial persona: a fresh node has been "given
115
+ // guidance" for the state it starts in (its bearings carry it), so the
116
+ // persona injector sees no drift on its first turn boundary.
117
+ persona_ack: { mode, lifecycle },
76
118
  status: 'active',
77
119
  parent,
120
+ spawned_by: spawnedBy,
78
121
  passive_default: opts.passiveDefault ?? false,
79
122
  intent: null,
80
123
  pi_session_id: null,
124
+ pi_session_file: null,
81
125
  launch: opts.launch,
82
126
  };
127
+ // Validate BEFORE minting: a bad parent must leave no half-born orphan row or
128
+ // dirs behind, so the parent's existence is checked before createNode
129
+ // scaffolds anything on disk or in the db.
130
+ if (parent !== null && getNode(parent) === null) {
131
+ throw new Error(`cannot spawn under unknown parent node: ${parent}`);
132
+ }
83
133
  createNode(meta);
84
134
  if (parent !== null) {
85
- if (getNode(parent) === null) {
86
- throw new Error(`cannot spawn under unknown parent node: ${parent}`);
87
- }
88
135
  // The load-bearing seed: parent subscribes (active) to child so it learns
89
136
  // when the work finishes. This mirrors spawn structure into the spine.
137
+ // A root (parent=null) gets NO subscription — nobody is woken by it.
90
138
  subscribe(parent, meta.node_id, true);
91
- // Audit-only provenance.
92
- recordSpawn(meta.node_id, parent);
139
+ }
140
+ // Audit-only provenance edge — recorded for a root too (from its spawner).
141
+ if (spawnedBy !== null && spawnedBy !== undefined && getNode(spawnedBy) !== null) {
142
+ recordSpawn(meta.node_id, spawnedBy);
93
143
  }
94
144
  return meta;
95
145
  }
@@ -0,0 +1,25 @@
1
+ import { type Mode, type Lifecycle } from '../canvas/index.js';
2
+ /** The two-axis persona state the injector keys on. */
3
+ export interface Persona {
4
+ mode: Mode;
5
+ lifecycle: Lifecycle;
6
+ }
7
+ export interface PersonaDriftResult {
8
+ from: Persona;
9
+ to: Persona;
10
+ /** The built transition guidance to inject for `to`. */
11
+ guidance: string;
12
+ }
13
+ /** Build the injected transition prompt for a `from → to` persona change.
14
+ * Concatenates the relevant section per changed axis (both when both changed).
15
+ * Pure read of the node's roadmap/memory for the base→orchestrator case. */
16
+ export declare function transitionGuidance(nodeId: string, from: Persona, to: Persona): string;
17
+ /** Compare a node's live {mode,lifecycle} against its `persona_ack` (the last
18
+ * state it was given guidance for). Returns the transition + built guidance
19
+ * when they differ, else null. Does NOT mutate — the caller delivers the
20
+ * guidance, then `commitPersonaAck`s the new state. An unset `persona_ack`
21
+ * (legacy node) defaults to the current persona, so it reads as no drift and
22
+ * never fabricates spurious guidance. */
23
+ export declare function personaDrift(nodeId: string): PersonaDriftResult | null;
24
+ /** Commit the persona state the node has now been given guidance for. */
25
+ export declare function commitPersonaAck(nodeId: string, to: Persona): void;