@crouton-kit/crouter 0.3.14 → 0.3.16

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 (224) 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/attention.js +76 -7
  35. package/dist/commands/canvas-prune.d.ts +2 -0
  36. package/dist/commands/canvas-prune.js +66 -0
  37. package/dist/commands/canvas.js +5 -8
  38. package/dist/commands/chord.d.ts +2 -0
  39. package/dist/commands/chord.js +143 -0
  40. package/dist/commands/daemon.js +8 -5
  41. package/dist/commands/dashboard.js +2 -0
  42. package/dist/commands/human/prompts.js +28 -27
  43. package/dist/commands/human/queue.js +30 -14
  44. package/dist/commands/human/shared.d.ts +26 -21
  45. package/dist/commands/human/shared.js +45 -67
  46. package/dist/commands/human.js +4 -14
  47. package/dist/commands/node.d.ts +11 -0
  48. package/dist/commands/node.js +221 -99
  49. package/dist/commands/pkg/market-inspect.js +6 -4
  50. package/dist/commands/pkg/market-manage.js +10 -6
  51. package/dist/commands/pkg/market.js +2 -4
  52. package/dist/commands/pkg/plugin-inspect.js +6 -4
  53. package/dist/commands/pkg/plugin-manage.js +12 -7
  54. package/dist/commands/pkg/plugin.js +2 -4
  55. package/dist/commands/pkg.js +0 -4
  56. package/dist/commands/push.js +178 -15
  57. package/dist/commands/revive.js +5 -3
  58. package/dist/commands/skill/author.js +6 -4
  59. package/dist/commands/skill/find.js +8 -5
  60. package/dist/commands/skill/read.js +2 -0
  61. package/dist/commands/skill/state.js +6 -4
  62. package/dist/commands/skill.js +0 -6
  63. package/dist/commands/sys/config.js +21 -7
  64. package/dist/commands/sys/doctor.js +2 -0
  65. package/dist/commands/sys/update.js +4 -0
  66. package/dist/commands/sys.js +0 -6
  67. package/dist/commands/tmux-spread.d.ts +2 -0
  68. package/dist/commands/tmux-spread.js +129 -0
  69. package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
  70. package/dist/core/__tests__/child-followup.test.d.ts +1 -0
  71. package/dist/core/__tests__/child-followup.test.js +83 -0
  72. package/dist/core/__tests__/close.test.d.ts +1 -0
  73. package/dist/core/__tests__/close.test.js +148 -0
  74. package/dist/core/__tests__/context-intro.test.d.ts +1 -0
  75. package/dist/core/__tests__/context-intro.test.js +196 -0
  76. package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
  77. package/dist/core/__tests__/daemon-boot.test.js +93 -0
  78. package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
  79. package/dist/core/__tests__/daemon-liveness.test.js +223 -0
  80. package/dist/core/__tests__/focuses.test.d.ts +1 -0
  81. package/dist/core/__tests__/focuses.test.js +196 -0
  82. package/dist/core/__tests__/fork.test.d.ts +1 -0
  83. package/dist/core/__tests__/fork.test.js +91 -0
  84. package/dist/core/__tests__/home-session.test.d.ts +1 -0
  85. package/dist/core/__tests__/home-session.test.js +153 -0
  86. package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
  87. package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
  88. package/dist/core/__tests__/keystone.test.d.ts +1 -0
  89. package/dist/core/__tests__/keystone.test.js +185 -0
  90. package/dist/core/__tests__/kickoff.test.d.ts +1 -0
  91. package/dist/core/__tests__/kickoff.test.js +89 -0
  92. package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
  93. package/dist/core/__tests__/lifecycle.test.js +178 -0
  94. package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
  95. package/dist/core/__tests__/listing-completeness.test.js +31 -0
  96. package/dist/core/__tests__/memory.test.d.ts +1 -0
  97. package/dist/core/__tests__/memory.test.js +152 -0
  98. package/dist/core/__tests__/migration.test.d.ts +1 -0
  99. package/dist/core/__tests__/migration.test.js +238 -0
  100. package/dist/core/__tests__/pane-column.test.d.ts +1 -0
  101. package/dist/core/__tests__/pane-column.test.js +153 -0
  102. package/dist/core/__tests__/passive-subscription.test.js +24 -1
  103. package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
  104. package/dist/core/__tests__/persona-compose.test.js +53 -0
  105. package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
  106. package/dist/core/__tests__/persona-subkind.test.js +62 -0
  107. package/dist/core/__tests__/persona.test.d.ts +1 -0
  108. package/dist/core/__tests__/persona.test.js +107 -0
  109. package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
  110. package/dist/core/__tests__/placement-focus.test.js +266 -0
  111. package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
  112. package/dist/core/__tests__/placement-reconcile.test.js +212 -0
  113. package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
  114. package/dist/core/__tests__/placement-revive.test.js +238 -0
  115. package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
  116. package/dist/core/__tests__/placement-teardown.test.js +178 -0
  117. package/dist/core/__tests__/prune.test.d.ts +1 -0
  118. package/dist/core/__tests__/prune.test.js +116 -0
  119. package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
  120. package/dist/core/__tests__/push-final-guard.test.js +71 -0
  121. package/dist/core/__tests__/relaunch.test.d.ts +1 -0
  122. package/dist/core/__tests__/relaunch.test.js +334 -0
  123. package/dist/core/__tests__/reset.test.js +26 -7
  124. package/dist/core/__tests__/revive.test.d.ts +1 -0
  125. package/dist/core/__tests__/revive.test.js +217 -0
  126. package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
  127. package/dist/core/__tests__/spawn-root.test.js +73 -0
  128. package/dist/core/__tests__/steer-note.test.d.ts +1 -0
  129. package/dist/core/__tests__/steer-note.test.js +39 -0
  130. package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
  131. package/dist/core/__tests__/stop-guard.test.js +82 -0
  132. package/dist/core/__tests__/subcommand-tier.test.js +35 -33
  133. package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
  134. package/dist/core/__tests__/tmux-surface.test.js +105 -0
  135. package/dist/core/__tests__/unknown-path.test.js +8 -2
  136. package/dist/core/canvas/attention.d.ts +10 -0
  137. package/dist/core/canvas/attention.js +40 -0
  138. package/dist/core/canvas/canvas.d.ts +66 -7
  139. package/dist/core/canvas/canvas.js +209 -21
  140. package/dist/core/canvas/db.d.ts +8 -0
  141. package/dist/core/canvas/db.js +205 -4
  142. package/dist/core/canvas/focuses.d.ts +22 -0
  143. package/dist/core/canvas/focuses.js +81 -0
  144. package/dist/core/canvas/index.d.ts +3 -0
  145. package/dist/core/canvas/index.js +3 -0
  146. package/dist/core/canvas/labels.d.ts +27 -0
  147. package/dist/core/canvas/labels.js +36 -0
  148. package/dist/core/canvas/render.js +25 -10
  149. package/dist/core/canvas/telemetry.d.ts +14 -0
  150. package/dist/core/canvas/telemetry.js +35 -0
  151. package/dist/core/canvas/types.d.ts +115 -12
  152. package/dist/core/command.d.ts +25 -1
  153. package/dist/core/command.js +23 -15
  154. package/dist/core/config.js +36 -2
  155. package/dist/core/feed/feed.js +3 -3
  156. package/dist/core/feed/inbox.d.ts +3 -1
  157. package/dist/core/feed/inbox.js +45 -5
  158. package/dist/core/feed/passive.js +24 -11
  159. package/dist/core/help.d.ts +26 -13
  160. package/dist/core/help.js +44 -37
  161. package/dist/core/personas/index.d.ts +1 -1
  162. package/dist/core/personas/index.js +1 -1
  163. package/dist/core/personas/loader.d.ts +40 -1
  164. package/dist/core/personas/loader.js +63 -1
  165. package/dist/core/personas/resolve.d.ts +13 -6
  166. package/dist/core/personas/resolve.js +46 -34
  167. package/dist/core/runtime/bearings.d.ts +20 -0
  168. package/dist/core/runtime/bearings.js +92 -0
  169. package/dist/core/runtime/close.d.ts +14 -0
  170. package/dist/core/runtime/close.js +151 -0
  171. package/dist/core/runtime/demote.js +24 -12
  172. package/dist/core/runtime/front-door.js +1 -1
  173. package/dist/core/runtime/kickoff.d.ts +23 -6
  174. package/dist/core/runtime/kickoff.js +92 -36
  175. package/dist/core/runtime/launch.d.ts +26 -12
  176. package/dist/core/runtime/launch.js +78 -19
  177. package/dist/core/runtime/lifecycle.d.ts +13 -0
  178. package/dist/core/runtime/lifecycle.js +86 -0
  179. package/dist/core/runtime/memory.d.ts +43 -0
  180. package/dist/core/runtime/memory.js +165 -0
  181. package/dist/core/runtime/naming.d.ts +22 -0
  182. package/dist/core/runtime/naming.js +166 -0
  183. package/dist/core/runtime/nodes.d.ts +39 -1
  184. package/dist/core/runtime/nodes.js +69 -10
  185. package/dist/core/runtime/persona.d.ts +25 -0
  186. package/dist/core/runtime/persona.js +139 -0
  187. package/dist/core/runtime/placement.d.ts +299 -0
  188. package/dist/core/runtime/placement.js +688 -0
  189. package/dist/core/runtime/promote.d.ts +14 -7
  190. package/dist/core/runtime/promote.js +57 -67
  191. package/dist/core/runtime/reset.d.ts +47 -4
  192. package/dist/core/runtime/reset.js +223 -52
  193. package/dist/core/runtime/revive.d.ts +26 -2
  194. package/dist/core/runtime/revive.js +166 -39
  195. package/dist/core/runtime/spawn.d.ts +20 -5
  196. package/dist/core/runtime/spawn.js +163 -43
  197. package/dist/core/runtime/stop-guard.d.ts +1 -1
  198. package/dist/core/runtime/stop-guard.js +18 -8
  199. package/dist/core/runtime/tmux-chrome.d.ts +1 -0
  200. package/dist/core/runtime/tmux-chrome.js +4 -0
  201. package/dist/core/runtime/tmux.d.ts +113 -20
  202. package/dist/core/runtime/tmux.js +221 -39
  203. package/dist/core/spawn.js +15 -0
  204. package/dist/daemon/crtrd.d.ts +12 -1
  205. package/dist/daemon/crtrd.js +152 -34
  206. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
  207. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
  208. package/dist/pi-extensions/canvas-commands.js +16 -13
  209. package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
  210. package/dist/pi-extensions/canvas-context-intro.js +164 -0
  211. package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
  212. package/dist/pi-extensions/canvas-goal-capture.js +15 -1
  213. package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
  214. package/dist/pi-extensions/canvas-nav.d.ts +12 -4
  215. package/dist/pi-extensions/canvas-nav.js +594 -262
  216. package/dist/pi-extensions/canvas-resume.d.ts +22 -0
  217. package/dist/pi-extensions/canvas-resume.js +173 -0
  218. package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
  219. package/dist/pi-extensions/canvas-stophook.js +340 -228
  220. package/dist/types.d.ts +28 -0
  221. package/dist/types.js +16 -0
  222. package/package.json +2 -2
  223. package/dist/core/runtime/presence.d.ts +0 -38
  224. package/dist/core/runtime/presence.js +0 -154
@@ -2,15 +2,17 @@
2
2
  //
3
3
  // pi-only. No claude branch — we are a super-opinionated system. A node's
4
4
  // LaunchSpec (persisted in meta.json) is the canonical recipe the daemon
5
- // replays to revive it faithfully: `--resume` to wake a done/idle node (keeps
6
- // its conversation), or fresh (against the context dir) for a refresh-yield.
5
+ // replays to revive it faithfully: `--session <id>` to wake a done/idle node
6
+ // (keeps its conversation), or fresh (against the context dir) for a refresh-yield.
7
7
  // The spec is rewritten on every polymorph (base→orchestrator) so a node
8
8
  // always comes back as its *current* self.
9
- import { existsSync } from 'node:fs';
9
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { resolve as resolvePersona } from '../personas/index.js';
13
13
  import { nodeEnv } from './nodes.js';
14
+ import { editorLabel } from '../canvas/index.js';
15
+ import { nodeDir } from '../canvas/paths.js';
14
16
  // ---------------------------------------------------------------------------
15
17
  // The two canvas pi-extensions every node loads. They self-gate on the live
16
18
  // {kind,mode} env, so the worker→orchestrator polymorph flips hook behavior
@@ -29,20 +31,27 @@ export const CANVAS_INBOX_WATCHER_PATH = resolveExtension('canvas-inbox-watcher'
29
31
  export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
30
32
  export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
31
33
  export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
34
+ export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
32
35
  export const CANVAS_COMMANDS_PATH = resolveExtension('canvas-commands');
36
+ export const CANVAS_RESUME_PATH = resolveExtension('canvas-resume');
33
37
  /** The canvas extensions every node loads, in order: stophook (routing +
34
38
  * telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
35
39
  * graph chrome), goal-capture (persist the first user message as the goal),
36
40
  * passive-context (drain passive backlog as pre-text on the next message),
37
- * commands (the /promote slash-command). All self-gate on CRTR_NODE_ID.
38
- * goal-capture precedes passive-context so it reads the raw user text. */
41
+ * context-intro (inject the <crtr-context> bearings block as its own session
42
+ * message, once per brand-new chat), commands (the /promote slash-command),
43
+ * resume (the /resume-node whole-canvas picker → `crtr node focus`).
44
+ * All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
45
+ * reads the raw user text. */
39
46
  export const CANVAS_EXTENSIONS = [
40
47
  CANVAS_STOPHOOK_PATH,
41
48
  CANVAS_INBOX_WATCHER_PATH,
42
49
  CANVAS_NAV_PATH,
43
50
  CANVAS_GOAL_CAPTURE_PATH,
44
51
  CANVAS_PASSIVE_CONTEXT_PATH,
52
+ CANVAS_CONTEXT_INTRO_PATH,
45
53
  CANVAS_COMMANDS_PATH,
54
+ CANVAS_RESUME_PATH,
46
55
  ];
47
56
  /** Bare model aliases resolve to the anthropic provider under pi (avoids the
48
57
  * bedrock default). Anything with a `/` or an unknown name passes through. */
@@ -55,10 +64,15 @@ export function normalizeModel(model) {
55
64
  // ---------------------------------------------------------------------------
56
65
  // Build the launch spec from {kind, mode}
57
66
  // ---------------------------------------------------------------------------
58
- /** Compose a node's full pi launch recipe from its persona. The two canvas
59
- * extensions are always first; persona-declared extensions follow. */
60
- export function buildLaunchSpec(kind, mode, opts = {}) {
61
- const p = resolvePersona(kind, mode);
67
+ /** Compose a node's full pi launch recipe from its persona. The system prompt
68
+ * is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
69
+ * (terminal/resident the finish contract) and spine position (hasManager —
70
+ * whether the push-up family is taught at all). Callers pass the authoritative
71
+ * lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
72
+ * prompt faithfully. The two canvas extensions are always first; persona-
73
+ * declared extensions follow. */
74
+ export function buildLaunchSpec(kind, mode, opts) {
75
+ const p = resolvePersona(kind, mode, { lifecycle: opts.lifecycle, hasManager: opts.hasManager });
62
76
  const launch = {
63
77
  model: p.model !== undefined ? normalizeModel(p.model) : undefined,
64
78
  tools: p.tools,
@@ -68,16 +82,34 @@ export function buildLaunchSpec(kind, mode, opts = {}) {
68
82
  };
69
83
  return { launch, lifecycle: p.lifecycle, skills: p.skills };
70
84
  }
71
- /** The pi session display name — the editor label in the top-left. Shows the
72
- * node's name plus its current mode so base vs orchestrator reads at a glance
73
- * (e.g. `developer (orchestrator)`). Recomputed from `meta.mode` on every
74
- * revive, so a base→orchestrator polymorph updates the label. */
75
- export function editorLabel(meta) {
76
- return `${meta.name} (${meta.mode})`;
85
+ // ---------------------------------------------------------------------------
86
+ // Build the pi argv to launch / revive a node
87
+ // ---------------------------------------------------------------------------
88
+ /** Persist a node's (possibly large) system prompt to a file in its node dir and
89
+ * return the absolute path, so callers can pass a short path to pi instead of
90
+ * the inline text. Returns null if the write fails — the caller then falls back
91
+ * to passing the prompt inline. Rewritten every launch so a polymorph's updated
92
+ * prompt always lands. */
93
+ function writeSystemPromptFile(nodeId, prompt) {
94
+ try {
95
+ const dir = nodeDir(nodeId);
96
+ mkdirSync(dir, { recursive: true });
97
+ const p = join(dir, 'system-prompt.md');
98
+ writeFileSync(p, prompt, 'utf8');
99
+ return p;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
77
104
  }
78
105
  /** Construct the pi invocation for a node.
79
106
  * - fresh start: pass `prompt` (the node's first user message), no resume.
80
- * - revive idle/done: pass `resumeSessionId` to `--resume` (keeps conversation).
107
+ * - fork start: pass `forkFrom` (absolute .jsonl path or partial uuid) to `--fork`
108
+ * — pi COPIES that conversation into a NEW session for this node, then `prompt`
109
+ * is delivered as the next message. One-shot at birth: the node thereafter
110
+ * captures its OWN pi_session_file and revives by `--session` like any other.
111
+ * - revive idle/done: pass `resumeSessionPath` (absolute .jsonl path, preferred)
112
+ * or `resumeSessionId` (bare uuid fallback) to `--session` (keeps conversation).
81
113
  * - refresh-yield: fresh again (no resume) — the node re-reads its roadmap. */
82
114
  export function buildPiArgv(meta, opts = {}) {
83
115
  const spec = meta.launch;
@@ -86,14 +118,41 @@ export function buildPiArgv(meta, opts = {}) {
86
118
  argv.push('-e', ext);
87
119
  }
88
120
  argv.push('-n', editorLabel(meta));
89
- if (opts.resumeSessionId !== undefined)
90
- argv.push('--resume', opts.resumeSessionId);
121
+ // pi's `--resume` is a bare toggle that opens the interactive picker; the
122
+ // flag that resumes a *specific* session is `--session <path|id>`. Prefer the
123
+ // absolute FILE path when present: pi resolves a bare id cwd-relative first
124
+ // and shows a cross-project "Fork? [y/N]" prompt when the revive cwd differs
125
+ // from the session's creation cwd, whereas a path (contains `/` or ends
126
+ // `.jsonl`) is opened directly — immune to any cwd discrepancy. The bare uuid
127
+ // is the fallback for older nodes booted before pi_session_file was captured.
128
+ // `--fork <path|id>` is the spawn-time branch: pi copies the source session
129
+ // into a fresh one for this node (the source is untouched), then delivers the
130
+ // kickoff prompt as the next message. Mutually exclusive with `--session`
131
+ // (resume) — fork wins when both are somehow set, but in practice a spawn
132
+ // never resumes and a revive never forks.
133
+ if (opts.forkFrom !== undefined && opts.forkFrom !== '') {
134
+ argv.push('--fork', opts.forkFrom);
135
+ }
136
+ else {
137
+ const resumeArg = opts.resumeSessionPath ?? opts.resumeSessionId;
138
+ if (resumeArg !== undefined)
139
+ argv.push('--session', resumeArg);
140
+ }
91
141
  if (spec?.model !== undefined)
92
142
  argv.push('--model', spec.model);
93
143
  if (spec?.tools !== undefined && spec.tools.length > 0)
94
144
  argv.push('--tools', spec.tools.join(','));
95
145
  if (spec?.systemPrompt !== undefined && spec.systemPrompt !== '') {
96
- argv.push('--append-system-prompt', spec.systemPrompt);
146
+ // pi's --append-system-prompt reads a FILE when the arg is an existing path,
147
+ // else treats the arg as literal text. Pass the prompt as a file path, not
148
+ // inline: an orchestrator persona is ~17KB, and passed inline it inflates the
149
+ // `tmux new-window 'pi …'` command past tmux's command-length limit, so the
150
+ // spawn dies with "command too long" and the node is marked dead before pi
151
+ // ever starts (base workers fit, orchestrator children don't). Writing it to
152
+ // the node dir keeps the command tiny. Falls back to inline if the write
153
+ // fails (e.g. an ephemeral meta with no node dir).
154
+ const promptArg = writeSystemPromptFile(meta.node_id, spec.systemPrompt) ?? spec.systemPrompt;
155
+ argv.push('--append-system-prompt', promptArg);
97
156
  }
98
157
  if (opts.prompt !== undefined && opts.prompt !== '')
99
158
  argv.push(opts.prompt);
@@ -0,0 +1,13 @@
1
+ import type { NodeMeta } from '../canvas/types.js';
2
+ /** The lifecycle events — the only vocabulary for moving a node's status/intent.
3
+ * Each maps (in the table below) to a target status and/or intent plus the set
4
+ * of from-statuses it is legal from. */
5
+ export type LifecycleEvent = 'finalize' | 'reap' | 'cancel' | 'crash' | 'yield' | 'release' | 'revive' | 'boot';
6
+ /** Enact a lifecycle event on a node: validate the from-status against the
7
+ * table, then write status+intent in ONE atomic statement (so they can never
8
+ * disagree). Returns the hydrated node view after the write.
9
+ *
10
+ * Throws on an unknown node, or on an ILLEGAL move (e.g. `finalize` on a `dead`
11
+ * node) — illegal states are unrepresentable. The throw is a real signal:
12
+ * callers that previously swallowed db-mutation errors now surface them. */
13
+ export declare function transition(nodeId: string, event: LifecycleEvent): NodeMeta;
@@ -0,0 +1,86 @@
1
+ // lifecycle.ts — the node status×intent state machine.
2
+ //
3
+ // ONE place defines which (status, intent) moves are legal and enacts them.
4
+ // Before this, ~a dozen scattered setStatus()/setIntent() pairs across
5
+ // reset/close/revive/feed/daemon/stophook/queue/promote re-derived the lifecycle
6
+ // by hand, with no shared definition of "what move is legal." Here the legal
7
+ // transition TABLE is the definition, and `transition(id, event)` is the single
8
+ // writer of status+intent: it validates the from-status, then writes both fields
9
+ // in ONE atomic statement (built on Phase 2's WAL'd row setters) so the two can
10
+ // never disagree.
11
+ //
12
+ // This mirrors persona.ts: persona.ts is the single source of transition PROSE;
13
+ // lifecycle.ts is the single source of which status/intent move is LEGAL. Two
14
+ // parallel, legible state machines instead of scattered enactment.
15
+ //
16
+ // Crash-safety invariant (was a comment repeated in reset/close/reapDescendants):
17
+ // "flip status to a non-supervised value + clear intent BEFORE killing the
18
+ // window" — the daemon only ever revives active|idle nodes, so a teardown must
19
+ // leave the node done/canceled first to close the revive race. That invariant is
20
+ // now the DEFINITION of the `reap`/`cancel` events: callers flip via transition()
21
+ // and only THEN kill the window.
22
+ //
23
+ // Layering note: lifecycle.ts is runtime, but it is the canvas write surface's
24
+ // `transition` verb (the only writer of status+intent), so it owns its atomic
25
+ // row UPDATE directly via openDb — the one sanctioned exception to "only
26
+ // canvas.ts touches the db" (see canvas/CLAUDE.md), exactly as db.ts's backfill
27
+ // is the sanctioned exception for a data migration.
28
+ import { openDb, getNode } from '../canvas/index.js';
29
+ const ANY = '*';
30
+ /** The supervised statuses — a live node the daemon watches. */
31
+ const LIVE = ['active', 'idle'];
32
+ /** The legal transition table — derived directly from the (status, intent) pairs
33
+ * the runtime actually wrote at its audited call sites, so behavior is preserved
34
+ * by construction. Each entry's comment names its writer(s). */
35
+ const TRANSITIONS = {
36
+ // feed.push(final) · queue.cancelJob · markCleanExitDone (clean quit).
37
+ finalize: { status: 'done', intent: 'done', from: LIVE },
38
+ // reapDescendants · relaunchRoot park-old. Forced teardown → done, intent cleared.
39
+ reap: { status: 'done', intent: null, from: ANY },
40
+ // closeNode cascade. Forced teardown → canceled, intent cleared.
41
+ cancel: { status: 'canceled', intent: null, from: ANY },
42
+ // daemon superviseTick: window gone with no yield/release intent. Intent KEPT
43
+ // (the dead log line still reports it).
44
+ crash: { status: 'dead', from: LIVE },
45
+ // requestYield · relaunchRoot new-node safety net. Status KEPT (already active).
46
+ yield: { intent: 'refresh', from: LIVE },
47
+ // stophook idle-release: free the window, stay woken by the inbox.
48
+ release: { status: 'idle', intent: 'idle-release', from: LIVE },
49
+ // reviveNode · resetRoot · stophook boot-confirm (clear a pending refresh net).
50
+ revive: { status: 'active', intent: null, from: ANY },
51
+ // reviveInPlace: re-exec a fresh pi in the SAME pane. Status (re)affirmed
52
+ // active; intent KEPT so a pending refresh survives as proof-of-boot until the
53
+ // fresh pi's session_start clears it (a premature clear is how a failed
54
+ // respawn became a silent death — see revive.ts).
55
+ boot: { status: 'active', from: LIVE },
56
+ };
57
+ /** Enact a lifecycle event on a node: validate the from-status against the
58
+ * table, then write status+intent in ONE atomic statement (so they can never
59
+ * disagree). Returns the hydrated node view after the write.
60
+ *
61
+ * Throws on an unknown node, or on an ILLEGAL move (e.g. `finalize` on a `dead`
62
+ * node) — illegal states are unrepresentable. The throw is a real signal:
63
+ * callers that previously swallowed db-mutation errors now surface them. */
64
+ export function transition(nodeId, event) {
65
+ const spec = TRANSITIONS[event];
66
+ const cur = getNode(nodeId);
67
+ if (cur === null)
68
+ throw new Error(`transition: unknown node ${nodeId}`);
69
+ if (spec.from !== ANY && !spec.from.includes(cur.status)) {
70
+ throw new Error(`illegal lifecycle transition: '${event}' from status='${cur.status}' (node ${nodeId})`);
71
+ }
72
+ const writeStatus = Object.prototype.hasOwnProperty.call(spec, 'status');
73
+ const writeIntent = Object.prototype.hasOwnProperty.call(spec, 'intent');
74
+ const db = openDb();
75
+ if (writeStatus && writeIntent) {
76
+ db.prepare('UPDATE nodes SET status = ?, intent = ? WHERE node_id = ?')
77
+ .run(spec.status, spec.intent ?? null, nodeId);
78
+ }
79
+ else if (writeStatus) {
80
+ db.prepare('UPDATE nodes SET status = ? WHERE node_id = ?').run(spec.status, nodeId);
81
+ }
82
+ else if (writeIntent) {
83
+ db.prepare('UPDATE nodes SET intent = ? WHERE node_id = ?').run(spec.intent ?? null, nodeId);
84
+ }
85
+ return getNode(nodeId);
86
+ }
@@ -0,0 +1,43 @@
1
+ /** The node-local index template. Named export kept for callers/tests that
2
+ * assert the seeded node store verbatim. */
3
+ export declare const MEMORY_TEMPLATE: string;
4
+ /** The user-global index template — framed around the human, not a goal. */
5
+ export declare const USER_MEMORY_TEMPLATE: string;
6
+ /** The project index template — framed around the repo. */
7
+ export declare const PROJECT_MEMORY_TEMPLATE: string;
8
+ /** The node-local memory directory in a node's context dir — holds MEMORY.md
9
+ * (the index) and the one-fact detail files it points at. */
10
+ export declare function memoryDir(nodeId: string): string;
11
+ /** The node-local MEMORY.md index path (inside the memory dir). */
12
+ export declare function memoryPath(nodeId: string): string;
13
+ /** Whether the node has a node-local memory store. This is ALSO the
14
+ * orchestrator gate: only orchestrators are ever seeded one, so a node with no
15
+ * node-local store is a terminal worker (no memory framing at all). */
16
+ export declare function hasMemory(nodeId: string): boolean;
17
+ /** Read the node-local MEMORY.md index, or null when it doesn't exist. */
18
+ export declare function readMemory(nodeId: string): string | null;
19
+ /** Seed the node-local memory dir + index IF the node has none yet. */
20
+ export declare function seedMemory(nodeId: string): boolean;
21
+ /** The user-global memory directory — one per machine, key-less, loaded into
22
+ * every orchestrator everywhere. */
23
+ export declare function userMemoryDir(): string;
24
+ /** The user-global MEMORY.md index path. */
25
+ export declare function userMemoryPath(): string;
26
+ export declare function hasUserMemory(): boolean;
27
+ /** Read the user-global MEMORY.md index, or null when it doesn't exist. */
28
+ export declare function readUserMemory(): string | null;
29
+ /** Seed the user-global memory dir + index IF absent. */
30
+ export declare function seedUserMemory(): boolean;
31
+ /** The project key for `cwd`: its git-repo-root when inside a repo, else the
32
+ * cwd itself, mangled into a flat directory name (reuses artifact mangleCwd).
33
+ * This keys the per-project memory store under <crtrHome>/projects/. */
34
+ export declare function projectKey(cwd: string): string;
35
+ /** The project memory directory for `cwd`. */
36
+ export declare function projectMemoryDir(cwd: string): string;
37
+ /** The project MEMORY.md index path for `cwd`. */
38
+ export declare function projectMemoryPath(cwd: string): string;
39
+ export declare function hasProjectMemory(cwd: string): boolean;
40
+ /** Read the project MEMORY.md index for `cwd`, or null when it doesn't exist. */
41
+ export declare function readProjectMemory(cwd: string): string | null;
42
+ /** Seed the project memory dir + index for `cwd` IF absent. */
43
+ export declare function seedProjectMemory(cwd: string): boolean;
@@ -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;