@crouton-kit/crouter 0.3.37 → 0.3.39

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 (143) hide show
  1. package/dist/build-root.js +1 -0
  2. package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +7 -0
  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-views/prompt-review/core.mjs +6 -134
  36. package/dist/builtin-views/settings/core.mjs +26 -94
  37. package/dist/builtin-views/settings/text.mjs +0 -5
  38. package/dist/builtin-views/settings/tui.mjs +1 -7
  39. package/dist/builtin-views/settings/web.jsx +3 -49
  40. package/dist/clients/attach/attach-cmd.js +416 -416
  41. package/dist/commands/memory/read.js +3 -3
  42. package/dist/commands/memory/shared.d.ts +13 -8
  43. package/dist/commands/memory/shared.js +22 -8
  44. package/dist/commands/memory/write.js +5 -3
  45. package/dist/commands/node.js +40 -11
  46. package/dist/commands/profile/add-project.d.ts +1 -0
  47. package/dist/commands/profile/add-project.js +42 -0
  48. package/dist/commands/profile/delete.d.ts +1 -0
  49. package/dist/commands/profile/delete.js +39 -0
  50. package/dist/commands/profile/list.d.ts +1 -0
  51. package/dist/commands/profile/list.js +35 -0
  52. package/dist/commands/profile/new.d.ts +1 -0
  53. package/dist/commands/profile/new.js +50 -0
  54. package/dist/commands/profile/remove-project.d.ts +1 -0
  55. package/dist/commands/profile/remove-project.js +42 -0
  56. package/dist/commands/profile/rename.d.ts +1 -0
  57. package/dist/commands/profile/rename.js +42 -0
  58. package/dist/commands/profile/show.d.ts +1 -0
  59. package/dist/commands/profile/show.js +50 -0
  60. package/dist/commands/profile.d.ts +2 -0
  61. package/dist/commands/profile.js +33 -0
  62. package/dist/commands/sys/config.js +6 -21
  63. package/dist/commands/sys/prompt-review.js +90 -17
  64. package/dist/core/canvas/canvas.js +10 -9
  65. package/dist/core/canvas/db.js +11 -0
  66. package/dist/core/canvas/history.js +1 -0
  67. package/dist/core/canvas/types.d.ts +10 -0
  68. package/dist/core/config.d.ts +44 -1
  69. package/dist/core/config.js +155 -12
  70. package/dist/core/hearth/provider.d.ts +8 -0
  71. package/dist/core/hearth/providers/__tests__/sweep-and-release.test.d.ts +1 -0
  72. package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +254 -0
  73. package/dist/core/hearth/providers/blaxel-home.d.ts +39 -0
  74. package/dist/core/hearth/providers/blaxel-home.js +271 -21
  75. package/dist/core/hearth/providers/blaxel.d.ts +5 -0
  76. package/dist/core/hearth/providers/blaxel.js +86 -4
  77. package/dist/core/hearth/providers/types.d.ts +16 -0
  78. package/dist/core/memory-resolver.d.ts +26 -15
  79. package/dist/core/memory-resolver.js +158 -40
  80. package/dist/core/profiles/manifest.d.ts +33 -0
  81. package/dist/core/profiles/manifest.js +332 -0
  82. package/dist/core/profiles/select.d.ts +11 -0
  83. package/dist/core/profiles/select.js +115 -0
  84. package/dist/core/runtime/bearings.d.ts +9 -0
  85. package/dist/core/runtime/bearings.js +31 -0
  86. package/dist/core/runtime/front-door.js +9 -2
  87. package/dist/core/runtime/launch.d.ts +25 -7
  88. package/dist/core/runtime/launch.js +42 -55
  89. package/dist/core/runtime/nodes.d.ts +7 -0
  90. package/dist/core/runtime/nodes.js +6 -0
  91. package/dist/core/runtime/persona.js +18 -32
  92. package/dist/core/runtime/spawn.d.ts +13 -0
  93. package/dist/core/runtime/spawn.js +25 -0
  94. package/dist/core/scope.d.ts +14 -7
  95. package/dist/core/scope.js +76 -20
  96. package/dist/core/substrate/index.d.ts +3 -3
  97. package/dist/core/substrate/index.js +5 -3
  98. package/dist/core/substrate/on-read.js +3 -2
  99. package/dist/core/substrate/render.d.ts +16 -11
  100. package/dist/core/substrate/render.js +249 -62
  101. package/dist/core/substrate/schema.d.ts +11 -3
  102. package/dist/core/substrate/schema.js +25 -0
  103. package/dist/core/substrate/subject.d.ts +15 -2
  104. package/dist/core/substrate/subject.js +24 -2
  105. package/dist/hearth/control-plane/__tests__/error-serialization.test.d.ts +1 -0
  106. package/dist/hearth/control-plane/__tests__/error-serialization.test.js +29 -0
  107. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +1 -0
  108. package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +44 -0
  109. package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +1 -0
  110. package/dist/hearth/control-plane/__tests__/wake-roll.test.js +230 -0
  111. package/dist/hearth/control-plane/db.js +27 -0
  112. package/dist/hearth/control-plane/hearth-target.d.ts +23 -0
  113. package/dist/hearth/control-plane/hearth-target.js +68 -0
  114. package/dist/hearth/control-plane/registry.d.ts +6 -1
  115. package/dist/hearth/control-plane/registry.js +7 -0
  116. package/dist/hearth/control-plane/relay.d.ts +4 -0
  117. package/dist/hearth/control-plane/relay.js +72 -3
  118. package/dist/hearth/control-plane/secrets.d.ts +14 -0
  119. package/dist/hearth/control-plane/secrets.js +21 -0
  120. package/dist/hearth/control-plane/server.js +140 -4
  121. package/dist/hearth/control-plane/serving.d.ts +15 -0
  122. package/dist/hearth/control-plane/serving.js +106 -0
  123. package/dist/hearth/control-plane/types.d.ts +20 -0
  124. package/dist/hearth/control-plane/wake.d.ts +8 -2
  125. package/dist/hearth/control-plane/wake.js +241 -3
  126. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  127. package/dist/types.d.ts +57 -1
  128. package/dist/types.js +83 -1
  129. package/dist/web-client/assets/{index-MSKSWwV0.js → index-CbO8L0mN.js} +4 -4
  130. package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
  131. package/dist/web-client/index.html +2 -2
  132. package/package.json +2 -2
  133. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
  134. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
  135. package/dist/builtin-memory/crouter-development/personas.md +0 -112
  136. package/dist/builtin-personas/spine/no-manager.md +0 -2
  137. package/dist/core/personas/index.d.ts +0 -13
  138. package/dist/core/personas/index.js +0 -11
  139. package/dist/core/personas/loader.d.ts +0 -159
  140. package/dist/core/personas/loader.js +0 -327
  141. package/dist/core/personas/resolve.d.ts +0 -59
  142. package/dist/core/personas/resolve.js +0 -376
  143. package/dist/web-client/assets/index-DUThOUzU.css +0 -2
@@ -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).
@@ -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.
@@ -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;
@@ -4,7 +4,19 @@ import { join, resolve, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { CRTR_DIR_NAME } from '../types.js';
6
6
  import { usage } from './errors.js';
7
- let cachedProjectRoot;
7
+ import { loadProfileManifest } from './profiles/manifest.js';
8
+ // Cache of resolved project-scope-root STACKS, keyed by (resolved cwd, profile
9
+ // id). A profile widens the pointer set beyond the bare cwd (see
10
+ // findProjectScopeRoots below), so the cache key must carry both — the same
11
+ // cwd resolves to a different stack under a different (or no) profile.
12
+ // `resetScopeCache()` (called by every profile mutation in
13
+ // src/core/profiles/manifest.ts) clears the whole cache; there is no
14
+ // per-entry invalidation because a profile edit can change which pointer set
15
+ // any cached cwd belongs to.
16
+ const scopeRootsCache = new Map();
17
+ function scopeRootsCacheKey(resolvedStartDir, profileId) {
18
+ return `${resolvedStartDir}\u0000${profileId ?? ''}`;
19
+ }
8
20
  export function builtinViewsRoot() {
9
21
  // Resolved relative to this file: dist/core/scope.js → dist/builtin-views/
10
22
  // (src/ at dev time).
@@ -47,20 +59,13 @@ function isProjectScopeDir(candidate, userRoot) {
47
59
  }
48
60
  }
49
61
  export function findProjectScopeRoot(startDir = process.cwd()) {
50
- if (cachedProjectRoot !== undefined)
51
- return cachedProjectRoot;
52
- const roots = findProjectScopeRoots(startDir);
53
- cachedProjectRoot = roots[0] ?? null;
54
- return cachedProjectRoot;
55
- }
56
- /** Every project scope root visible from `startDir`, nearest first. Each
57
- * ancestor directory with a `.crouter/` contributes memory/preferences to the
58
- * current workspace; the nearest root still wins for writes and duplicate doc
59
- * names. The user-global `~/.crouter/` is excluded because it is the separate
60
- * user scope, not a project ancestor. */
61
- export function findProjectScopeRoots(startDir = process.cwd()) {
62
+ return findProjectScopeRoots(startDir)[0] ?? null;
63
+ }
64
+ /** Walk `startDir` upward to the filesystem root, collecting every ancestor
65
+ * `.crouter/` dir (nearest first). The user-global `~/.crouter/` is excluded
66
+ * because it is the separate user scope, not a project ancestor. */
67
+ function collectAncestorScopeRoots(startDir, userRoot) {
62
68
  const roots = [];
63
- const userRoot = userScopeRoot();
64
69
  let dir = resolve(startDir);
65
70
  while (true) {
66
71
  const candidate = join(dir, CRTR_DIR_NAME);
@@ -72,11 +77,55 @@ export function findProjectScopeRoots(startDir = process.cwd()) {
72
77
  dir = parent;
73
78
  }
74
79
  }
80
+ /** Every project scope root visible from `startDir`, nearest first — now a
81
+ * PROFILE-AWARE pointer walk, not a single-pointer ancestor walk. Pointers are
82
+ * `[startDir]` plus, when `profileId` resolves to a manifest, each of its
83
+ * `projects` in manifest order; each pointer independently walks upward
84
+ * collecting every ancestor `.crouter/` dir, the pointers' contributions
85
+ * concatenate in pointer order, and the combined list dedupes to first
86
+ * occurrence — so a `.crouter/` shared by two pointers contributes once, at
87
+ * whichever pointer reached it first. `profileId` defaults to the process's
88
+ * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
89
+ * to the single `[startDir]` pointer, reproducing today's behavior exactly.
90
+ * Results are cached per (resolved startDir, profileId) — `resetScopeCache()`
91
+ * invalidates on any profile mutation that can change project dirs or names. */
92
+ export function findProjectScopeRoots(startDir = process.cwd(), profileId = process.env['CRTR_PROFILE_ID'] || null) {
93
+ const resolvedStart = resolve(startDir);
94
+ const key = scopeRootsCacheKey(resolvedStart, profileId);
95
+ const cached = scopeRootsCache.get(key);
96
+ if (cached !== undefined)
97
+ return cached;
98
+ const userRoot = userScopeRoot();
99
+ const pointers = [resolvedStart];
100
+ if (profileId !== null && profileId !== '') {
101
+ try {
102
+ const { manifest } = loadProfileManifest(profileId);
103
+ pointers.push(...manifest.projects);
104
+ }
105
+ catch {
106
+ // Missing/deleted/invalid profile: this is a hot path every memory and
107
+ // launch-config resolve runs through, so fall back to the bare
108
+ // `[startDir]` pointer rather than throwing.
109
+ }
110
+ }
111
+ const seen = new Set();
112
+ const roots = [];
113
+ for (const pointer of pointers) {
114
+ for (const root of collectAncestorScopeRoots(pointer, userRoot)) {
115
+ if (seen.has(root))
116
+ continue;
117
+ seen.add(root);
118
+ roots.push(root);
119
+ }
120
+ }
121
+ scopeRootsCache.set(key, roots);
122
+ return roots;
123
+ }
75
124
  export function projectScopeRoot(startDir) {
76
125
  return findProjectScopeRoot(startDir);
77
126
  }
78
- export function projectScopeRoots(startDir) {
79
- return findProjectScopeRoots(startDir);
127
+ export function projectScopeRoots(startDir, profileId) {
128
+ return findProjectScopeRoots(startDir, profileId);
80
129
  }
81
130
  export function scopeRoot(scope) {
82
131
  // The builtin scope has no scope-root dir: its only content is memory
@@ -96,9 +145,16 @@ export function ensureProjectScopeRoot(startDir = process.cwd()) {
96
145
  const found = findProjectScopeRoot(startDir);
97
146
  if (found)
98
147
  return found;
99
- // Initialize new project scope at startDir
100
- const root = join(resolve(startDir), CRTR_DIR_NAME);
101
- cachedProjectRoot = root;
148
+ // Initialize new project scope at startDir. `found` was falsy, which means
149
+ // the (resolvedStart, profileId) cache key above resolved to an EMPTY stack
150
+ // (no pointer had an existing `.crouter/` yet) — seed that same cache entry
151
+ // with the about-to-exist root so a caller that immediately writes under it
152
+ // (without an intervening disk rescan) sees it without a stale empty-array
153
+ // cache hit.
154
+ const resolvedStart = resolve(startDir);
155
+ const root = join(resolvedStart, CRTR_DIR_NAME);
156
+ const profileId = process.env['CRTR_PROFILE_ID'] || null;
157
+ scopeRootsCache.set(scopeRootsCacheKey(resolvedStart, profileId), [root]);
102
158
  return root;
103
159
  }
104
160
  export function pluginsDir(scope) {
@@ -154,5 +210,5 @@ export function listScopes(scopeArg) {
154
210
  return [v];
155
211
  }
156
212
  export function resetScopeCache() {
157
- cachedProjectRoot = undefined;
213
+ scopeRootsCache.clear();
158
214
  }
@@ -1,9 +1,9 @@
1
- export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
1
+ export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, normalizeNameSegment, normalizeDocName, } from './schema.js';
2
2
  export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from './schema.js';
3
- export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
3
+ export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
4
4
  export type { NodeConfigSubject } from './subject.js';
5
5
  export { gatePasses } from './gate.js';
6
6
  export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
7
7
  export type { Surface } from './ceiling.js';
8
- export { renderPreferencesSection, renderKnowledgeBlock, } from './render.js';
8
+ export { renderPreferencesSection, renderKnowledgeBlock, renderPreferencesForSubject, } from './render.js';
9
9
  export { renderOnReadDocs } from './on-read.js';
@@ -11,9 +11,11 @@ RUNGS, rungRank, rungAtLeast,
11
11
  // fallback floor
12
12
  FALLBACK_RUNG,
13
13
  // parse + render-shared helpers
14
- parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
15
- export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
14
+ parseSubstrateFrontmatter, parseSubstrateDoc, previewLine,
15
+ // display-name normalization
16
+ normalizeNameSegment, normalizeDocName, } from './schema.js';
17
+ export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
16
18
  export { gatePasses } from './gate.js';
17
19
  export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
18
- export { renderPreferencesSection, renderKnowledgeBlock, } from './render.js';
20
+ export { renderPreferencesSection, renderKnowledgeBlock, renderPreferencesForSubject, } from './render.js';
19
21
  export { renderOnReadDocs } from './on-read.js';
@@ -56,7 +56,7 @@ import { pathExists, readText, walkFiles } from '../fs-utils.js';
56
56
  import { parseFrontmatterGeneric } from '../frontmatter.js';
57
57
  import { evalCondition } from '../predicate.js';
58
58
  import { listAllMemoryDocs } from '../memory-resolver.js';
59
- import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
59
+ import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
60
60
  import { cachedSubstrateDocs } from './session-cache.js';
61
61
  // Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
62
62
  // file may live under a build/dependency tree; `.crouter` is NOT junk here — it
@@ -113,10 +113,11 @@ function isJunkAncestor(dir) {
113
113
  * (gate eval keys off the NODE subject, render off name/body/rung). */
114
114
  function loadPositionalDoc(file, memDir, scope) {
115
115
  try {
116
- const name = relative(memDir, file)
116
+ const raw = relative(memDir, file)
117
117
  .replace(/\.md$/i, '')
118
118
  .split(sep)
119
119
  .join('/');
120
+ const name = normalizeDocName(raw);
120
121
  if (name === '')
121
122
  return null;
122
123
  const { data, body } = parseFrontmatterGeneric(readText(file));
@@ -1,16 +1,21 @@
1
- /** The system-prompt memory block: the always-present memory-usage guidance,
2
- * then (when any are eligible) the `kind: preference` docs in one tree at their
3
- * effective rung (the preference default rung is `preview` → the routing line).
4
- * ALWAYS returns a non-empty `<memory kind="preference">` block — the guidance
5
- * frame is unconditional, so the system-prompt splice is never empty even when
6
- * no preference is eligible. */
1
+ import { type NodeConfigSubject, type SubstrateDoc } from './index.js';
7
2
  export declare function renderPreferencesSection(nodeId: string): string;
3
+ export declare function renderPreferencesForSubject(subject: NodeConfigSubject): {
4
+ block: string;
5
+ contentDocs: SubstrateDoc[];
6
+ tree: string;
7
+ };
8
8
  /** The `<memory kind="knowledge">` block embedded in the session_start bearings
9
9
  * message (bearings.ts pushes it as a SIBLING of `<crtr-bearings>`, or drops it
10
10
  * when ''). The consultable catalog: every eligible `kind: knowledge` resolver
11
- * doc at its effective rung (most surface only as `[+N more]` counts unless
12
- * author-promoted) PLUS the node-local memory docs (any kind), the latter
13
- * floored to `name` so a `none`-rung node-local doc still shows its name rather
14
- * than collapsing into a count. Procedural guidance and factual references both
15
- * live here as `knowledge`. Returns '' when nothing is eligible. */
11
+ * doc PLUS the node-local memory docs (any kind) resolver docs win
12
+ * first-wins dedup over a same-named node-local doc. GROUPED by rung exactly
13
+ * like the preference block: content prose first (general→specific,
14
+ * node-local last), then the preview/name catalog tree. Node-local docs are
15
+ * floored HERE a rung-none node-local doc's effective rung is bumped to
16
+ * `name` before grouping, so it still surfaces as a bare-name tree entry
17
+ * rather than collapsing into a `[+N more]` count (its body still never
18
+ * renders: floored docs never reach the `content` rung). Procedural guidance
19
+ * and factual references both live here as `knowledge`. Returns '' when
20
+ * nothing is eligible. */
16
21
  export declare function renderKnowledgeBlock(nodeId: string): string;