@crouton-kit/crouter 0.3.36 → 0.3.38

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 (177) hide show
  1. package/dist/build-root.js +3 -6
  2. package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
  3. package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
  5. package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
  6. package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
  7. package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
  8. package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
  9. package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
  10. package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
  11. package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
  12. package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
  13. package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
  14. package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
  15. package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
  16. package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
  17. package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
  18. package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
  19. package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
  20. package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
  21. package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
  22. package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
  23. package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
  24. package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
  25. package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
  26. package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
  27. package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
  28. package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
  29. package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
  30. package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
  31. package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
  32. package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
  33. package/dist/builtin-memory/internal/INDEX.md +1 -1
  34. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  35. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  36. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
  37. package/dist/builtin-views/canvas/core.mjs +3 -2
  38. package/dist/builtin-views/prompt-review/core.mjs +6 -134
  39. package/dist/builtin-views/settings/core.mjs +26 -94
  40. package/dist/builtin-views/settings/text.mjs +0 -5
  41. package/dist/builtin-views/settings/tui.mjs +1 -7
  42. package/dist/builtin-views/settings/web.jsx +3 -49
  43. package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
  44. package/dist/clients/attach/attach-cmd.js +429 -432
  45. package/dist/clients/web/web-cmd.js +7 -7
  46. package/dist/commands/__tests__/human.test.js +1 -15
  47. package/dist/commands/attention.js +9 -6
  48. package/dist/commands/canvas-history/search.js +2 -2
  49. package/dist/commands/canvas-history/show.js +3 -3
  50. package/dist/commands/canvas-history.js +1 -1
  51. package/dist/commands/canvas-issue.js +1 -0
  52. package/dist/commands/canvas-snapshot.js +1 -0
  53. package/dist/commands/canvas.js +10 -11
  54. package/dist/commands/chord.js +1 -0
  55. package/dist/commands/human/prompts.js +0 -1
  56. package/dist/commands/human/queue.js +1 -0
  57. package/dist/commands/memory/read.js +3 -3
  58. package/dist/commands/memory/shared.d.ts +13 -8
  59. package/dist/commands/memory/shared.js +22 -8
  60. package/dist/commands/memory/write.js +5 -3
  61. package/dist/commands/node-snapshot.js +1 -0
  62. package/dist/commands/node.js +52 -22
  63. package/dist/commands/profile/add-project.d.ts +1 -0
  64. package/dist/commands/profile/add-project.js +42 -0
  65. package/dist/commands/profile/delete.d.ts +1 -0
  66. package/dist/commands/profile/delete.js +39 -0
  67. package/dist/commands/profile/list.d.ts +1 -0
  68. package/dist/commands/profile/list.js +35 -0
  69. package/dist/commands/profile/new.d.ts +1 -0
  70. package/dist/commands/profile/new.js +50 -0
  71. package/dist/commands/profile/remove-project.d.ts +1 -0
  72. package/dist/commands/profile/remove-project.js +42 -0
  73. package/dist/commands/profile/rename.d.ts +1 -0
  74. package/dist/commands/profile/rename.js +42 -0
  75. package/dist/commands/profile/show.d.ts +1 -0
  76. package/dist/commands/profile/show.js +50 -0
  77. package/dist/commands/profile.d.ts +2 -0
  78. package/dist/commands/profile.js +33 -0
  79. package/dist/commands/push.d.ts +0 -1
  80. package/dist/commands/push.js +2 -228
  81. package/dist/commands/revive.js +2 -2
  82. package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
  83. package/dist/commands/surface.js +3 -2
  84. package/dist/commands/sys/config.js +6 -21
  85. package/dist/commands/sys/daemon.d.ts +2 -0
  86. package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
  87. package/dist/commands/sys/doctor.js +1 -0
  88. package/dist/commands/sys/prompt-review.js +90 -17
  89. package/dist/commands/sys.js +4 -3
  90. package/dist/core/__tests__/daemon-boot.test.js +1 -1
  91. package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
  92. package/dist/core/__tests__/daemon-wedge.test.js +87 -0
  93. package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
  94. package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
  95. package/dist/core/__tests__/history-inbox.test.js +105 -0
  96. package/dist/core/__tests__/kickoff.test.js +3 -3
  97. package/dist/core/__tests__/revive.test.js +39 -1
  98. package/dist/core/canvas/canvas.js +10 -9
  99. package/dist/core/canvas/db.js +11 -0
  100. package/dist/core/canvas/history.d.ts +1 -1
  101. package/dist/core/canvas/history.js +91 -3
  102. package/dist/core/canvas/paths.d.ts +6 -0
  103. package/dist/core/canvas/paths.js +9 -0
  104. package/dist/core/canvas/status-glyph.js +1 -0
  105. package/dist/core/canvas/types.d.ts +10 -0
  106. package/dist/core/config.d.ts +44 -1
  107. package/dist/core/config.js +155 -12
  108. package/dist/core/fault-classifier.d.ts +2 -2
  109. package/dist/core/feed/inbox.d.ts +7 -12
  110. package/dist/core/feed/inbox.js +45 -29
  111. package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
  112. package/dist/core/memory-resolver.d.ts +26 -15
  113. package/dist/core/memory-resolver.js +158 -40
  114. package/dist/core/preview-registry.js +14 -112
  115. package/dist/core/profiles/manifest.d.ts +33 -0
  116. package/dist/core/profiles/manifest.js +332 -0
  117. package/dist/core/profiles/select.d.ts +11 -0
  118. package/dist/core/profiles/select.js +115 -0
  119. package/dist/core/render.js +1 -1
  120. package/dist/core/runtime/bearings.d.ts +9 -0
  121. package/dist/core/runtime/bearings.js +31 -0
  122. package/dist/core/runtime/busy.d.ts +15 -0
  123. package/dist/core/runtime/busy.js +32 -1
  124. package/dist/core/runtime/close.d.ts +10 -0
  125. package/dist/core/runtime/close.js +18 -13
  126. package/dist/core/runtime/fault-recovery.js +4 -0
  127. package/dist/core/runtime/front-door.js +9 -2
  128. package/dist/core/runtime/kickoff.js +5 -7
  129. package/dist/core/runtime/launch.d.ts +25 -7
  130. package/dist/core/runtime/launch.js +42 -55
  131. package/dist/core/runtime/nodes.d.ts +7 -0
  132. package/dist/core/runtime/nodes.js +6 -0
  133. package/dist/core/runtime/persona.js +18 -32
  134. package/dist/core/runtime/placement.js +1 -1
  135. package/dist/core/runtime/revive.js +9 -1
  136. package/dist/core/runtime/spawn.d.ts +13 -0
  137. package/dist/core/runtime/spawn.js +25 -0
  138. package/dist/core/runtime/tmux.d.ts +2 -2
  139. package/dist/core/runtime/tmux.js +2 -2
  140. package/dist/core/scope.d.ts +14 -7
  141. package/dist/core/scope.js +76 -20
  142. package/dist/core/substrate/index.d.ts +3 -3
  143. package/dist/core/substrate/index.js +5 -3
  144. package/dist/core/substrate/on-read.js +3 -2
  145. package/dist/core/substrate/render.d.ts +16 -11
  146. package/dist/core/substrate/render.js +249 -62
  147. package/dist/core/substrate/schema.d.ts +11 -3
  148. package/dist/core/substrate/schema.js +25 -0
  149. package/dist/core/substrate/subject.d.ts +15 -2
  150. package/dist/core/substrate/subject.js +24 -2
  151. package/dist/daemon/crtrd-cli.js +1 -1
  152. package/dist/daemon/crtrd.d.ts +24 -0
  153. package/dist/daemon/crtrd.js +213 -5
  154. package/dist/daemon/manage.js +2 -2
  155. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  156. package/dist/pi-extensions/canvas-nav.js +2 -2
  157. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  158. package/dist/pi-extensions/canvas-stophook.js +59 -2
  159. package/dist/types.d.ts +57 -1
  160. package/dist/types.js +83 -1
  161. package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
  162. package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
  163. package/dist/web-client/index.html +2 -2
  164. package/package.json +2 -2
  165. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
  166. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
  167. package/dist/builtin-memory/crouter-development/personas.md +0 -112
  168. package/dist/builtin-personas/spine/no-manager.md +0 -2
  169. package/dist/commands/daemon.d.ts +0 -2
  170. package/dist/core/personas/index.d.ts +0 -13
  171. package/dist/core/personas/index.js +0 -11
  172. package/dist/core/personas/loader.d.ts +0 -159
  173. package/dist/core/personas/loader.js +0 -327
  174. package/dist/core/personas/resolve.d.ts +0 -59
  175. package/dist/core/personas/resolve.js +0 -376
  176. package/dist/web-client/assets/index-DUThOUzU.css +0 -2
  177. /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
@@ -5,6 +5,10 @@ const POLICIES = {
5
5
  auth: { kind: 'auth', gate: 'none', max: 0, baseDelayMs: 0, capDelayMs: 0, fatal: true },
6
6
  protocol: { kind: 'protocol', gate: 'none', max: 0, baseDelayMs: 0, capDelayMs: 0, fatal: true },
7
7
  other: { kind: 'other', gate: 'none', max: 0, baseDelayMs: 0, capDelayMs: 0, fatal: true },
8
+ // A wedge is a daemon-detected stall, never auto-retried — recovery is a
9
+ // deliberate agent/operator decision (kill the runaway subprocess), not a
10
+ // provider back-off.
11
+ wedged: { kind: 'wedged', gate: 'none', max: 0, baseDelayMs: 0, capDelayMs: 0, fatal: true },
8
12
  };
9
13
  export function policyFor(kind) {
10
14
  return POLICIES[kind];
@@ -4,6 +4,9 @@
4
4
  // crtr [dir] → root pinned to dir
5
5
  // crtr [dir] ["prompt"] → root with a starter prompt
6
6
  // crtr --name NAME ... → named root
7
+ // crtr --profile <id-or-name> → root under an explicit profile (else the
8
+ // startup selector: MRU covering cwd, else a
9
+ // synchronous create-or-root prompt)
7
10
  // crtr <subcommand> ... → falls through to the normal dispatcher
8
11
  // crtr -h | --help → root help (dispatcher)
9
12
  //
@@ -25,13 +28,14 @@ function isDir(p) {
25
28
  * positional dir/prompt). A leading token in this set still boots a root —
26
29
  * without it, `crtr --name X` would fall through to the dispatcher and error as
27
30
  * an unknown subcommand. */
28
- const FRONT_DOOR_FLAGS = new Set(['--name', '--kind']);
31
+ const FRONT_DOOR_FLAGS = new Set(['--name', '--kind', '--profile']);
29
32
  /** Parse `[dir] [prompt]` positionals + the front-door flags out of the leftover
30
33
  * tokens after the bare `crtr`. */
31
34
  function parseRootArgs(tokens) {
32
35
  let cwd = process.cwd();
33
36
  let name;
34
37
  let kind;
38
+ let profile;
35
39
  const positionals = [];
36
40
  for (let i = 0; i < tokens.length; i++) {
37
41
  const t = tokens[i];
@@ -41,6 +45,9 @@ function parseRootArgs(tokens) {
41
45
  else if (t === '--kind') {
42
46
  kind = tokens[++i];
43
47
  }
48
+ else if (t === '--profile') {
49
+ profile = tokens[++i];
50
+ }
44
51
  else if (t.startsWith('--')) {
45
52
  // ignore unknown flags for the front door
46
53
  }
@@ -53,7 +60,7 @@ function parseRootArgs(tokens) {
53
60
  cwd = resolvePath(positionals.shift());
54
61
  }
55
62
  const prompt = positionals.length > 0 ? positionals.join(' ') : undefined;
56
- return { cwd, prompt, name, kind };
63
+ return { cwd, prompt, name, kind, profile };
57
64
  }
58
65
  /** Env marker set on every pi the front door boots. Its presence means we are
59
66
  * already inside a front-door-booted root, so a nested front-door launch must
@@ -107,8 +107,8 @@ export function drainBearings(meta) {
107
107
  // Consume the one-shot yield note (deleted on read) BEFORE the kickoff lists
108
108
  // the context dir, so it never shows up there.
109
109
  const yieldMsg = consumeYieldMessage(nodeId);
110
- // Drain the feed: read unread since the cursor and advance it past them, so a
111
- // later `crtr feed read` shows only what arrives afterward.
110
+ // Drain the feed: read unread since the cursor and advance it past them, so
111
+ // only entries that arrive after this point still count as unread.
112
112
  const cursor = readCursor(nodeId);
113
113
  const entries = readInboxSince(nodeId, cursor);
114
114
  let unreadDigest = null;
@@ -178,10 +178,8 @@ function feedBlock(nodeId, unreadDigest) {
178
178
  const subj = n === 1 ? 'it is' : 'they are';
179
179
  const pron = n === 1 ? 'it' : 'they';
180
180
  const verb = n === 1 ? 'pushes' : 'push';
181
- // State aliveness + the automatic wake at the source. Bare status ("— active")
182
- // left earlier revives unsure whether the worker was really live, so they
183
- // burned a turn on `feed read`/`feed peek` to confirm. Asserting it here
184
- // removes the reason to check.
181
+ // State aliveness + the automatic wake at the source explicitly, so there is
182
+ // no reason to check, poll, or verify separately.
185
183
  lines.push(`Awaiting ${n} node${n === 1 ? '' : 's'} — ${subj} alive and running right now, and will wake you the moment ${pron} ${verb}. The wake is automatic; nothing to check, poll, or verify.`);
186
184
  for (const m of awaiting)
187
185
  lines.push(` - ${m.name} (${m.node_id}) — ${m.status}`);
@@ -203,7 +201,7 @@ function feedBlock(nodeId, unreadDigest) {
203
201
  lines.push('', `Report history on disk (collapsed): ${total} report${total === 1 ? '' : 's'} across ${history.length} subscribed node${history.length === 1 ? '' : 's'}. This is a catch-up index for reports your cursor already passed; read only what matters.`, ...visible.map((h) => {
204
202
  const ref = `${h.nodeId}:reports/${h.latest}`;
205
203
  return ` - ${h.name} (${h.nodeId}): ${h.count} report${h.count === 1 ? '' : 's'}; latest ${reportKind(h.latest)}: ${ref}`;
206
- }), ...(hidden > 0 ? [` - … ${hidden} more subscribed node${hidden === 1 ? '' : 's'} with reports hidden`] : []), 'Expand: `crtr canvas history read <ref>` for one listed report; `crtr canvas history show <node-id> --type report` for that node\'s full report list; `crtr feed read --all` for cursor-independent inbox history.');
204
+ }), ...(hidden > 0 ? [` - … ${hidden} more subscribed node${hidden === 1 ? '' : 's'} with reports hidden`] : []), 'Expand: `crtr canvas history read <ref>` for one listed report; `crtr canvas history show <node-id> --type report` for that node\'s full report list; `crtr canvas history show <node-id> --type inbox` for cursor-independent inbox history including full message bodies.');
207
205
  }
208
206
  return `<feed>\n${lines.join('\n')}\n</feed>`;
209
207
  }
@@ -49,13 +49,31 @@ export declare function equivalentOtherProviderModel(currentModel: string, faile
49
49
  * strong/medium/light, ALWAYS anthropic (ignores the env/config default).
50
50
  * 4. Anything else → pass through unchanged. */
51
51
  export declare function normalizeModel(model: string): string;
52
- /** Compose a node's full pi launch recipe from its persona. The system prompt
53
- * is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
54
- * (terminal/resident — the finish contract) and spine position (hasManager
55
- * whether the push-up family is taught at all). Callers pass the authoritative
56
- * lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
57
- * prompt faithfully. The two canvas extensions are always first; persona-
58
- * declared extensions follow. */
52
+ /** The minimal static system prompt every node launches with a thin launcher
53
+ * frame keeping the `\n\nGuidelines:` anchor the `before_agent_start` substrate
54
+ * hook (canvas-doc-substrate.ts) splices its `<memory kind="preference">` block
55
+ * before. ALL runtime/spine/lifecycle/kind prompting now lives as gated builtin
56
+ * memory docs (src/builtin-memory/), spliced in fresh every turn this scaffold
57
+ * must never duplicate that prose. Exported so `sys prompt-review` can render
58
+ * through the exact same static+spliced pipeline boot uses, so the two can
59
+ * never drift. */
60
+ export declare const LAUNCH_SYSTEM_PROMPT: string;
61
+ /** Compose a node's full pi launch recipe from the kind registry. The system
62
+ * prompt is now a minimal static scaffold (`LAUNCH_SYSTEM_PROMPT`) — runtime/
63
+ * spine/lifecycle/kind prose is spliced per-turn from gated builtin memory docs,
64
+ * not baked in here. Launch KNOBS (model/tools/extensions) still come from the
65
+ * kind registry (`resolveKindConfig`), keyed on the FULL kind string (top-level
66
+ * or sub-kind). Model precedence, strongest to weakest:
67
+ * 1. Caller-supplied `opts.model` (durable on `meta.model_override`, re-passed
68
+ * on every polymorph).
69
+ * 2. The kind's registered model — mode-aware: orchestrator mode prefers
70
+ * `orchestratorModel`, falling back to `model`; base mode always uses
71
+ * `model`.
72
+ * 3. Unset → pi default.
73
+ * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
74
+ * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
75
+ * as-given — it is no longer persona-frontmatter-derived. The two canvas
76
+ * extensions are always first; kind-declared extensions follow. */
59
77
  export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
60
78
  lifecycle: Lifecycle;
61
79
  hasManager: boolean;
@@ -7,15 +7,13 @@
7
7
  // for a refresh-yield.
8
8
  // The spec is rewritten on every polymorph (base→orchestrator) so a node
9
9
  // always comes back as its *current* self.
10
- import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
10
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
11
11
  import { dirname, join } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
- import { readConfig, configPath as userConfigPath } from '../config.js';
14
- import { resolve as resolvePersona } from '../personas/index.js';
13
+ import { readMergedLaunchConfig, resolveKindConfig } from '../config.js';
15
14
  import { nodeEnv } from './nodes.js';
16
15
  import { editorLabel } from '../canvas/index.js';
17
16
  import { nodeDir } from '../canvas/paths.js';
18
- import { defaultScopeConfig } from '../../types.js';
19
17
  // ---------------------------------------------------------------------------
20
18
  // The two canvas pi-extensions every node loads. They self-gate on the live
21
19
  // {kind,mode} env, so the worker→orchestrator polymorph flips hook behavior
@@ -71,37 +69,13 @@ export const CANVAS_EXTENSIONS = [
71
69
  // xhigh — confirmed via `pi --help`), so the resolved spec is exactly the
72
70
  // string buildPiArgv passes to `--model`; no extra launch plumbing.
73
71
  // ---------------------------------------------------------------------------
74
- /** Cached user config for model resolution. The launch path is hot and reads
75
- * config synchronously, so cache the merged config and invalidate on mtime/size
76
- * changes rather than rereading on every normalizeModel/buildLaunchSpec call. */
77
- let cachedUserConfig = null;
78
- function readUserConfigCached() {
79
- const path = userConfigPath('user');
80
- if (path === null)
81
- return defaultScopeConfig();
82
- try {
83
- const stat = statSync(path);
84
- if (cachedUserConfig !== null &&
85
- cachedUserConfig.path === path &&
86
- cachedUserConfig.mtimeMs === stat.mtimeMs &&
87
- cachedUserConfig.size === stat.size) {
88
- return cachedUserConfig.config;
89
- }
90
- const config = readConfig('user');
91
- cachedUserConfig = { path, mtimeMs: stat.mtimeMs, size: stat.size, config };
92
- return config;
93
- }
94
- catch (error) {
95
- if (error.code === 'ENOENT') {
96
- cachedUserConfig = null;
97
- return defaultScopeConfig();
98
- }
99
- throw error;
100
- }
101
- }
102
- const DEFAULT_MODEL_LADDERS = defaultScopeConfig().modelLadders;
72
+ /** Model-ladder resolution goes through `readMergedLaunchConfig` the SAME
73
+ * project-stack > profile > user > builtin precedence `resolveKindConfig`
74
+ * already uses for `kinds` so a project/profile override of `modelLadders`
75
+ * is honored identically to a kind override, instead of this file reading
76
+ * only user config. */
103
77
  function modelLadders() {
104
- return readUserConfigCached().modelLadders ?? DEFAULT_MODEL_LADDERS;
78
+ return readMergedLaunchConfig().modelLadders;
105
79
  }
106
80
  function ladderFor(provider, ladders = modelLadders()) {
107
81
  return ladders[provider];
@@ -112,7 +86,7 @@ function ladderFor(provider, ladders = modelLadders()) {
112
86
  * defaultProvider in config.json wins over this env. */
113
87
  export const OPENAI_PROVIDER_ENV = 'CRTR_MODEL_PROVIDER';
114
88
  function defaultProvider() {
115
- const cfgProvider = readUserConfigCached().modelLadders.defaultProvider;
89
+ const cfgProvider = readMergedLaunchConfig().modelLadders.defaultProvider;
116
90
  if (cfgProvider === 'anthropic' || cfgProvider === 'openai')
117
91
  return cfgProvider;
118
92
  return (process.env[OPENAI_PROVIDER_ENV] ?? '').toLowerCase() === 'openai' ? 'openai' : 'anthropic';
@@ -198,31 +172,44 @@ export function normalizeModel(model) {
198
172
  // ---------------------------------------------------------------------------
199
173
  // Build the launch spec from {kind, mode}
200
174
  // ---------------------------------------------------------------------------
201
- /** Compose a node's full pi launch recipe from its persona. The system prompt
202
- * is composed from FOUR inputs: kind×mode (the persona body) plus lifecycle
203
- * (terminal/resident — the finish contract) and spine position (hasManager
204
- * whether the push-up family is taught at all). Callers pass the authoritative
205
- * lifecycle + hasManager (`parent !== null`) so a polymorph/flip rebuilds the
206
- * prompt faithfully. The two canvas extensions are always first; persona-
207
- * declared extensions follow. */
175
+ /** The minimal static system prompt every node launches with a thin launcher
176
+ * frame keeping the `\n\nGuidelines:` anchor the `before_agent_start` substrate
177
+ * hook (canvas-doc-substrate.ts) splices its `<memory kind="preference">` block
178
+ * before. ALL runtime/spine/lifecycle/kind prompting now lives as gated builtin
179
+ * memory docs (src/builtin-memory/), spliced in fresh every turn this scaffold
180
+ * must never duplicate that prose. Exported so `sys prompt-review` can render
181
+ * through the exact same static+spliced pipeline boot uses, so the two can
182
+ * never drift. */
183
+ export const LAUNCH_SYSTEM_PROMPT = 'You are a node in the crtr canvas — an agentic runtime.\n\nGuidelines:\n' +
184
+ '- Use the crtr canvas and memory substrate according to injected context.';
185
+ /** Compose a node's full pi launch recipe from the kind registry. The system
186
+ * prompt is now a minimal static scaffold (`LAUNCH_SYSTEM_PROMPT`) — runtime/
187
+ * spine/lifecycle/kind prose is spliced per-turn from gated builtin memory docs,
188
+ * not baked in here. Launch KNOBS (model/tools/extensions) still come from the
189
+ * kind registry (`resolveKindConfig`), keyed on the FULL kind string (top-level
190
+ * or sub-kind). Model precedence, strongest to weakest:
191
+ * 1. Caller-supplied `opts.model` (durable on `meta.model_override`, re-passed
192
+ * on every polymorph).
193
+ * 2. The kind's registered model — mode-aware: orchestrator mode prefers
194
+ * `orchestratorModel`, falling back to `model`; base mode always uses
195
+ * `model`.
196
+ * 3. Unset → pi default.
197
+ * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
198
+ * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
199
+ * as-given — it is no longer persona-frontmatter-derived. The two canvas
200
+ * extensions are always first; kind-declared extensions follow. */
208
201
  export function buildLaunchSpec(kind, mode, opts) {
209
- const p = resolvePersona(kind, mode, { lifecycle: opts.lifecycle, hasManager: opts.hasManager });
210
- const cfg = readUserConfigCached();
211
- // Precedence, from strongest to weakest:
212
- // 1. Caller-supplied override (durable on `meta.model_override`, re-passed on
213
- // every polymorph).
214
- // 2. Configured persona strength for this kind.
215
- // 3. The persona file's own frontmatter model.
216
- // 4. Unset → pi default.
217
- const chosenModel = opts.model ?? cfg.personaStrengths[kind] ?? p.model;
202
+ const kindConfig = resolveKindConfig(kind);
203
+ const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
204
+ const chosenModel = opts.model ?? kindModel;
218
205
  const launch = {
219
206
  model: chosenModel !== undefined ? normalizeModel(chosenModel) : undefined,
220
- tools: p.tools,
221
- extensions: [...CANVAS_EXTENSIONS, ...p.extensions],
222
- systemPrompt: p.systemPrompt,
207
+ tools: kindConfig?.tools ?? [],
208
+ extensions: [...CANVAS_EXTENSIONS, ...(kindConfig?.extensions ?? [])],
209
+ systemPrompt: LAUNCH_SYSTEM_PROMPT,
223
210
  env: { ...(opts.extraEnv ?? {}) },
224
211
  };
225
- return { launch, lifecycle: p.lifecycle };
212
+ return { launch, lifecycle: opts.lifecycle };
226
213
  }
227
214
  // ---------------------------------------------------------------------------
228
215
  // Build the pi argv to launch / revive a node
@@ -47,6 +47,13 @@ export interface SpawnNodeOpts {
47
47
  * persona default. Persisted to `meta.model_override` so polymorphs preserve
48
48
  * it. Omit to use the persona default. */
49
49
  modelOverride?: string | null;
50
+ /** The profile-directory id (`<slug>-<id>`) this node runs under; null for a
51
+ * root/no-profile node. Written to `meta.profile_id` and the row, and mirrored
52
+ * into the pi process env as `CRTR_PROFILE_ID` by `nodeEnv` — the runtime
53
+ * → resolver seam every downstream memory/gate consumer reads. Callers
54
+ * resolve/validate the operand (via `loadProfileManifest`) before it reaches
55
+ * here; this layer just persists the already-resolved id. */
56
+ profile_id?: string | null;
50
57
  /** Override the generated id (e.g. when a caller pre-allocates one). */
51
58
  nodeId?: string;
52
59
  }
@@ -64,6 +64,11 @@ export function nodeEnv(meta) {
64
64
  // resolves into the REPO, not here — agents must address artifacts via this.
65
65
  CRTR_CONTEXT_DIR: contextDir(meta.node_id),
66
66
  CRTR_CYCLES: (meta.cycles ?? 0).toString(),
67
+ // The runtime→resolver seam: memory-resolver.ts/scope.ts/config.ts read
68
+ // this process-wide env var (Phase 5) to pull in the node's profile memory
69
+ // store and multi-project pointer set. '' (never omitted) for a root/
70
+ // no-profile node, matching every other CRTR_* scalar here.
71
+ CRTR_PROFILE_ID: meta.profile_id ?? '',
67
72
  };
68
73
  if (meta.parent)
69
74
  env['CRTR_PARENT_NODE_ID'] = meta.parent;
@@ -105,6 +110,7 @@ export function spawnNode(opts) {
105
110
  // Broker is the only host: every node runs on the headless broker; a tmux
106
111
  // pane is only an attach viewer.
107
112
  host_kind: 'broker',
113
+ profile_id: opts.profile_id ?? null,
108
114
  kind: opts.kind,
109
115
  mode,
110
116
  lifecycle,
@@ -20,7 +20,6 @@
20
20
  // worker never gets spurious guidance). `personaDrift` compares live meta to it;
21
21
  // the caller delivers the guidance, then commits the ack.
22
22
  import { getNode, updateNode } from '../canvas/index.js';
23
- import { loadKernel, loadPersona, loadLifecycleFragment } from '../personas/index.js';
24
23
  import { resolveMemoryDoc } from '../memory-resolver.js';
25
24
  import { readRoadmap, roadmapPath } from './roadmap.js';
26
25
  import { orchestratorContextNote } from './bearings.js';
@@ -28,40 +27,30 @@ import { orchestratorContextNote } from './bearings.js';
28
27
  // base→orchestrator guidance (the roadmap-shaping dump) — MOVED here from
29
28
  // promote.ts so the injector is the one place that builds it.
30
29
  // ---------------------------------------------------------------------------
31
- /** Coerce a frontmatter scalar to its string form. Strings pass through;
32
- * number/boolean coerce via String(); null/undefined and non-scalars are
33
- * dropped (→ null). This lets numeric/boolean `roadmapKnowledge` knowledge-doc
34
- * pointers resolve the same way string pointers do. */
35
- function scalarToString(v) {
36
- if (typeof v === 'string')
37
- return v;
38
- if (typeof v === 'number' || typeof v === 'boolean')
39
- return String(v);
40
- return null;
41
- }
42
- /** Load a knowledge doc's body text by name, or null if it can't be resolved.
43
- * Used to inline a kind's roadmap-shaping knowledge doc into the orchestration
44
- * guidance. */
45
- function loadRoadmapKnowledgeBody(name) {
30
+ /** Load a builtin/user/project memory doc's body by its normalized name, or ''
31
+ * if it can't be resolved. The single source for the lifecycle/spine/
32
+ * orchestration-kernel fragments this injector re-delivers on a transition —
33
+ * the SAME docs Phase 2 authored under `src/builtin-memory/`, so static
34
+ * (system-prompt splice) and transition prose can never drift. */
35
+ function loadMemoryBody(name) {
46
36
  try {
47
37
  return resolveMemoryDoc(name).body.trim();
48
38
  }
49
39
  catch {
50
- return null;
40
+ return '';
51
41
  }
52
42
  }
53
43
  /** The base→orchestrator guidance dump, specialized to the node's kind: the
54
- * shared kernel + that kind's roadmap-shaping memory doc + the roadmap scaffold
55
- * the node must author + the orchestrator context-dir framing + a short pointer at
56
- * the substrate memory flow (promotion re-informs of nothing about the stores,
57
- * it only adds the across-cycles relevance). The node is now a delegator whose
58
- * scarce resource is its own context window. (Lifecycle is left to its own
59
- * section promotion no longer forces resident, so this never asserts residency.) */
44
+ * shared orchestration-kernel doc + the roadmap scaffold the node must author +
45
+ * the orchestrator context-dir framing + a short pointer at the substrate
46
+ * memory flow (promotion re-informs of nothing about the stores, it only adds
47
+ * the across-cycles relevance). The node is now a delegator whose scarce
48
+ * resource is its own context window. Roadmap-shaping guidance is just another orchestrator-gated memory
49
+ * doc the node already gets via the boot-render splice (kinds/<kind>/orchestrator
50
+ * gated `{kind, mode: orchestrator}`). (Lifecycle is left to its own section —
51
+ * promotion no longer forces resident, so this never asserts residency.) */
60
52
  function orchestrationGuidance(nodeId, kind) {
61
- const kernel = loadKernel();
62
- const orch = loadPersona(kind, 'orchestrator');
63
- const roadmapKnowledge = scalarToString(orch?.frontmatter?.['roadmapKnowledge']) ?? undefined;
64
- const roadmapKnowledgeBody = roadmapKnowledge ? loadRoadmapKnowledgeBody(roadmapKnowledge) : null;
53
+ const kernel = loadMemoryBody('orchestration-kernel');
65
54
  const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
66
55
  const rmPath = roadmapPath(nodeId);
67
56
  const parts = [
@@ -70,9 +59,6 @@ function orchestrationGuidance(nodeId, kind) {
70
59
  '',
71
60
  kernel,
72
61
  ];
73
- if (roadmapKnowledgeBody) {
74
- parts.push('', `--- How to shape a ${kind} roadmap (knowledge: ${roadmapKnowledge}) ---`, '', roadmapKnowledgeBody);
75
- }
76
62
  parts.push('', `Your roadmap scaffold (\`${rmPath}\`) — author it now: state the goal, exit criteria, and the phase skeleton, using the approach above. Current contents:`, '', roadmap, '',
77
63
  // The orchestrator framing for the context dir — the missing guidance a
78
64
  // promoted node never got at spawn (it spawned as a base worker). Same note
@@ -96,11 +82,11 @@ function baseModeGuidance() {
96
82
  // only re-delivers that fragment as the node's new-state steer.
97
83
  /** terminal → resident: interactable, never forced to submit. */
98
84
  function residentLifecycleGuidance() {
99
- return loadLifecycleFragment('resident');
85
+ return loadMemoryBody('lifecycle/resident');
100
86
  }
101
87
  /** resident → terminal: owes a final, reaps when done. */
102
88
  function terminalLifecycleGuidance() {
103
- return loadLifecycleFragment('terminal');
89
+ return loadMemoryBody('lifecycle/terminal');
104
90
  }
105
91
  /** Build the injected transition prompt for a `from → to` persona change.
106
92
  * Concatenates the relevant section per changed axis (both when both changed).
@@ -38,7 +38,7 @@ import { isPidAlive } from '../canvas/pid.js';
38
38
  // verbs — respawnPane, respawnPaneDetached, swapPaneInPlace, breakPaneToSession,
39
39
  // joinPane, setWindowOption, setRemainOnExit — are deleted). `breakPane` +
40
40
  // `selectLayout` are FRESH minimal viewer-chrome helpers (not the deleted
41
- // engine-in-pane `breakPaneToSession`): `canvas tmux-spread` breaks the caller's
41
+ // engine-in-pane `breakPaneToSession`): `surface tmux-spread` breaks the caller's
42
42
  // viewer pane into its own window and tiles sibling viewers beside it.
43
43
  // `piCommand` + `paneCurrentPath` are KEPT: the sessionless surface-view system
44
44
  // (view-cycle.ts / view-run.ts) builds its pane commands with them, and the §5.1
@@ -17,8 +17,9 @@
17
17
  //
18
18
  // reviveNode remains the ONLY sanctioned launcher of the node engine.
19
19
  import { existsSync } from 'node:fs';
20
- import { getNode, updateNode, clearPid, fullName, cancelDeadlinesFor, } from '../canvas/index.js';
20
+ import { getNode, updateNode, clearPid, fullName, cancelDeadlinesFor, subscribersOf, } from '../canvas/index.js';
21
21
  import { transition } from './lifecycle.js';
22
+ import { fanDoctrineWake } from './close.js';
22
23
  import { buildPiArgv } from './launch.js';
23
24
  import { buildReviveKickoff, drainBearings } from './kickoff.js';
24
25
  import { headlessBrokerHost } from './host.js';
@@ -163,6 +164,13 @@ export function reviveNode(nodeId, opts) {
163
164
  catch (error) {
164
165
  try {
165
166
  transition(nodeId, 'crash');
167
+ // Doctrine wake: a launch-refusal here marks the node dead with NOTHING
168
+ // else fanning that out — the daemon's own auto-revive is the most common
169
+ // caller (superviseTick, best-effort try/catch, stderr-only on failure), so
170
+ // without this a manager waiting dormant on this child hangs forever,
171
+ // never told it will not come back on its own. Mirrors closeNode's step-4
172
+ // fan-out (same subscribers table, same active/passive split).
173
+ fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead. It will NOT resume on its own.`, { reason: 'child-crashed', child: nodeId });
166
174
  }
167
175
  catch {
168
176
  /* best-effort cleanup */
@@ -6,6 +6,11 @@ export interface BootRootOpts {
6
6
  name?: string;
7
7
  /** Optional starter prompt (bare `crtr` requires none). */
8
8
  prompt?: string;
9
+ /** Explicit `--profile <id-or-name>` on the front-door invocation, validated
10
+ * through `loadProfileManifest`. Omit to run the MRU/create-or-root startup
11
+ * selector for `cwd` (see `selectProfileForCwd`) — a root has no spawner to
12
+ * inherit from, so this is the front door's only source of profile identity. */
13
+ profile?: string | null;
9
14
  }
10
15
  /** Create the front-door root: launch its broker engine, then exec `crtr surface attach`
11
16
  * inline so THIS terminal becomes the broker's controller-viewer. Does not
@@ -41,6 +46,14 @@ export interface SpawnChildOpts {
41
46
  * persona's declared default. Persisted to `meta.model_override` so it
42
47
  * survives polymorphs. Omit to use the persona default. */
43
48
  model?: string;
49
+ /** Select the profile this node runs under — an exact profile id or a unique
50
+ * manifest name, validated through `loadProfileManifest`. Omit to INHERIT the
51
+ * spawner's current `profile_id` (managed child or --root alike — --root only
52
+ * means top-level, it does not reset to no profile). Passed through unchanged
53
+ * into a deferred trigger recipe so a triggered birth re-resolves the SAME
54
+ * choice at fire time (an explicit value re-validates; inheritance re-reads
55
+ * the spawner's THEN-current profile). */
56
+ profile?: string | null;
44
57
  }
45
58
  /** Resolve a `--fork-from` value to an ABSOLUTE `.jsonl` source path for the
46
59
  * broker fork (`SessionManager.forkFrom`, which loads a file — never a bare id).
@@ -14,6 +14,8 @@ import { readdirSync, existsSync } from 'node:fs';
14
14
  import { isAbsolute, resolve, join } from 'node:path';
15
15
  import { homedir } from 'node:os';
16
16
  import { spawnNode, currentNodeContext, rootOfSpine } from './nodes.js';
17
+ import { loadProfileManifest } from '../profiles/manifest.js';
18
+ import { selectProfileForCwd } from '../profiles/select.js';
17
19
  import { buildLaunchSpec, buildPiArgv } from './launch.js';
18
20
  import { writeGoal } from './kickoff.js';
19
21
  import { hasRoadmap, seedRoadmap } from './roadmap.js';
@@ -61,6 +63,10 @@ export async function bootRoot(opts) {
61
63
  }
62
64
  catch { /* daemon is best-effort */ }
63
65
  const kind = opts.kind ?? 'general';
66
+ // The front door's only source of profile identity — a root has no spawner
67
+ // to inherit from. Runs BEFORE spawnNode: explicit --profile > MRU profile
68
+ // covering cwd > a synchronous create-or-root prompt (or root, headless).
69
+ const profileId = await selectProfileForCwd(opts.cwd, opts.profile);
64
70
  // A born-resident root starts in base mode; it earns the orchestrator persona
65
71
  // the first time it delegates (or on promotion). Resident lifecycle either way.
66
72
  const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
@@ -75,6 +81,7 @@ export async function bootRoot(opts) {
75
81
  cwd: opts.cwd,
76
82
  name: opts.name ?? kind,
77
83
  parent: null,
84
+ profile_id: profileId,
78
85
  launch,
79
86
  });
80
87
  // Persist the spawning prompt as the goal so a fresh revive can re-read its
@@ -216,6 +223,22 @@ export function resolveForkSource(value) {
216
223
  // .jsonl (the broker fork won't resolve a uuid).
217
224
  return resolveSessionUuid(v);
218
225
  }
226
+ /** Resolve the profile a spawned child runs under: an explicit `--profile`
227
+ * operand (id or name) validated through `loadProfileManifest`, else INHERIT
228
+ * the spawner's current `profile_id` (null when the spawner has none). `--root`
229
+ * never resets this to null on its own; only an explicit override does. When
230
+ * there is NO spawner at all — `crtr node new --root` run directly from a
231
+ * shell, not from inside a node — there is no profile to inherit, so this runs
232
+ * the same startup selector the front door uses (MRU covering `cwd`, else the
233
+ * synchronous create-or-root prompt / headless root default). */
234
+ async function resolveProfileId(explicit, spawner, cwd) {
235
+ if (explicit !== undefined && explicit !== null && explicit !== '') {
236
+ return loadProfileManifest(explicit).profileId;
237
+ }
238
+ if (spawner !== null)
239
+ return getNode(spawner)?.profile_id ?? null;
240
+ return selectProfileForCwd(cwd);
241
+ }
219
242
  /** Resolve who a spawn is attributed to. A managed child needs a spine parent
220
243
  * (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
221
244
  * does not: it is top-level by definition and the spawner identity is
@@ -256,6 +279,7 @@ export async function spawnChild(opts) {
256
279
  // Born WITHOUT a name — the canvas-goal-capture extension names it async from
257
280
  // its first message (the kickoff task) inside its own pi process, so spawn
258
281
  // never blocks on the LLM naming round-trip (the 2-3s freeze it used to cost).
282
+ const profileId = await resolveProfileId(opts.profile, spawner, opts.cwd);
259
283
  const meta = spawnNode({
260
284
  kind: opts.kind,
261
285
  mode,
@@ -267,6 +291,7 @@ export async function spawnChild(opts) {
267
291
  // A child's parent IS its manager.
268
292
  parent: root ? null : spawner,
269
293
  spawnedBy: root ? (spawner ?? undefined) : undefined,
294
+ profile_id: profileId,
270
295
  // Persist the RAW fork reference (not the resolved path) as provenance, so
271
296
  // the boot intro can detect this is a fork and re-assert the node's own
272
297
  // identity over the source's copied-in conversation.
@@ -78,7 +78,7 @@ export declare function renameWindow(window: string, name: string): boolean;
78
78
  * the break never falls back to tmux's global current pane. The pane keeps its
79
79
  * durable `%id` across the move (only its window changes), so callers can keep
80
80
  * using the same pane handle. Returns the new `{window, pane}` location, or null
81
- * if tmux fails. Used by `canvas tmux-spread` to lift the caller's viewer into a
81
+ * if tmux fails. Used by `surface tmux-spread` to lift the caller's viewer into a
82
82
  * fresh window before tiling sibling viewers beside it. */
83
83
  export declare function breakPane(pane: string): {
84
84
  window: string;
@@ -187,7 +187,7 @@ export declare function windowAlive(session: string | null | undefined, window:
187
187
  * to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
188
188
  export declare function selectWindow(session: string, window: string): boolean;
189
189
  /** Apply a named tmux layout to a window (`tmux select-layout -t <window>
190
- * <layout>`). `canvas tmux-spread` calls it with `tiled` to evenly grid every
190
+ * <layout>`). `surface tmux-spread` calls it with `tiled` to evenly grid every
191
191
  * viewer pane in the spread window (and between splits, to redistribute space so
192
192
  * the next split has room). Best-effort; false if tmux fails. */
193
193
  export declare function selectLayout(window: string, layout: string): boolean;
@@ -176,7 +176,7 @@ export function renameWindow(window, name) {
176
176
  * the break never falls back to tmux's global current pane. The pane keeps its
177
177
  * durable `%id` across the move (only its window changes), so callers can keep
178
178
  * using the same pane handle. Returns the new `{window, pane}` location, or null
179
- * if tmux fails. Used by `canvas tmux-spread` to lift the caller's viewer into a
179
+ * if tmux fails. Used by `surface tmux-spread` to lift the caller's viewer into a
180
180
  * fresh window before tiling sibling viewers beside it. */
181
181
  export function breakPane(pane) {
182
182
  const r = tmux([
@@ -369,7 +369,7 @@ export function selectWindow(session, window) {
369
369
  return tmux(['select-window', '-t', `${session}:${window}`]).ok;
370
370
  }
371
371
  /** Apply a named tmux layout to a window (`tmux select-layout -t <window>
372
- * <layout>`). `canvas tmux-spread` calls it with `tiled` to evenly grid every
372
+ * <layout>`). `surface tmux-spread` calls it with `tiled` to evenly grid every
373
373
  * viewer pane in the spread window (and between splits, to redistribute space so
374
374
  * the next split has room). Best-effort; false if tmux fails. */
375
375
  export function selectLayout(window, layout) {
@@ -5,14 +5,21 @@ export declare function builtinPiPackagesDir(): string;
5
5
  export declare function builtinPiPackageDir(name: string): string;
6
6
  export declare function userScopeRoot(): string;
7
7
  export declare function findProjectScopeRoot(startDir?: string): string | null;
8
- /** Every project scope root visible from `startDir`, nearest first. Each
9
- * ancestor directory with a `.crouter/` contributes memory/preferences to the
10
- * current workspace; the nearest root still wins for writes and duplicate doc
11
- * names. The user-global `~/.crouter/` is excluded because it is the separate
12
- * user scope, not a project ancestor. */
13
- export declare function findProjectScopeRoots(startDir?: string): string[];
8
+ /** Every project scope root visible from `startDir`, nearest first — now a
9
+ * PROFILE-AWARE pointer walk, not a single-pointer ancestor walk. Pointers are
10
+ * `[startDir]` plus, when `profileId` resolves to a manifest, each of its
11
+ * `projects` in manifest order; each pointer independently walks upward
12
+ * collecting every ancestor `.crouter/` dir, the pointers' contributions
13
+ * concatenate in pointer order, and the combined list dedupes to first
14
+ * occurrence — so a `.crouter/` shared by two pointers contributes once, at
15
+ * whichever pointer reached it first. `profileId` defaults to the process's
16
+ * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
17
+ * to the single `[startDir]` pointer, reproducing today's behavior exactly.
18
+ * Results are cached per (resolved startDir, profileId) — `resetScopeCache()`
19
+ * invalidates on any profile mutation that can change project dirs or names. */
20
+ export declare function findProjectScopeRoots(startDir?: string, profileId?: string | null): string[];
14
21
  export declare function projectScopeRoot(startDir?: string): string | null;
15
- export declare function projectScopeRoots(startDir?: string): string[];
22
+ export declare function projectScopeRoots(startDir?: string, profileId?: string | null): string[];
16
23
  export declare function scopeRoot(scope: Scope): string | null;
17
24
  export declare function requireScopeRoot(scope: Scope): string;
18
25
  export declare function ensureProjectScopeRoot(startDir?: string): string;