@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
@@ -6,26 +6,41 @@ import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier }
6
6
  import { ambiguous, notFound, usage } from './errors.js';
7
7
  import { warn } from './output.js';
8
8
  import { pluginMemoryDir, projectScopeRoot, projectScopeRoots, scopeMemoryDir } from './scope.js';
9
+ import { normalizeDocName, normalizeNameSegment } from './substrate/schema.js';
10
+ import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
9
11
  /** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
10
12
  export function memoryDocId(doc) {
11
13
  return `${doc.scope}/${doc.name}`;
12
14
  }
13
- /** The memory scopes in resolution precedence: project > user > builtin.
14
- * Project is included only when a project scope exists for the cwd. A single
15
- * `scope` narrows to just that scope. */
15
+ /** The selected profile id from the process env, or '' when none. The single
16
+ * seam every profile-aware memory/config path reads through see the plan's
17
+ * synthesis decision on why this is process env, not a threaded parameter. */
18
+ function selectedProfileId() {
19
+ return process.env['CRTR_PROFILE_ID'] || '';
20
+ }
21
+ /** The memory scopes in resolution precedence: project stack > profile > user >
22
+ * builtin. Project is included only when a project scope exists for the cwd
23
+ * (or the selected profile's project stack); profile is included only when a
24
+ * profile is selected. A single `scope` narrows to just that scope. */
16
25
  function scopesInPrecedence(scope) {
17
26
  if (scope)
18
27
  return [scope];
19
28
  const out = [];
20
29
  if (projectScopeRoot())
21
30
  out.push('project');
31
+ if (selectedProfileId() !== '')
32
+ out.push('profile');
22
33
  out.push('user');
23
34
  out.push('builtin');
24
35
  return out;
25
36
  }
26
37
  /** Memory loads project scopes as a stack, not a single nearest root: every
27
- * ancestor `.crouter/` from cwd upward contributes, nearest first. User and
28
- * builtin remain singleton scopes after that stack. */
38
+ * ancestor `.crouter/` from cwd upward (widened by a selected profile's
39
+ * `projects`) contributes, nearest first. Profile is a singleton store
40
+ * resolved through the centralized `loadProfileManifest` — never a raw path
41
+ * join — and drops out silently (never throws the resolver) when the
42
+ * selected profile id no longer resolves to a manifest. User and builtin
43
+ * remain singleton scopes after that. */
29
44
  function memorySourcesInPrecedence(scope) {
30
45
  const out = [];
31
46
  for (const s of scopesInPrecedence(scope)) {
@@ -38,6 +53,18 @@ function memorySourcesInPrecedence(scope) {
38
53
  });
39
54
  }
40
55
  }
56
+ else if (s === 'profile') {
57
+ const profileId = selectedProfileId();
58
+ if (profileId !== '') {
59
+ try {
60
+ const { profileId: resolvedId } = loadProfileManifest(profileId);
61
+ out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [] });
62
+ }
63
+ catch {
64
+ // Missing/deleted/invalid selected profile: no profile memory source.
65
+ }
66
+ }
67
+ }
41
68
  else {
42
69
  out.push({
43
70
  scope: s,
@@ -56,7 +83,7 @@ function loadMemoryDoc(name, scope, path) {
56
83
  * subdirs supported), sorted by path-derived name. SKILL.md bundles are legacy
57
84
  * Agent Skills and are ignored; crouter memory docs are plain .md files under
58
85
  * memory/. */
59
- function listMemoryDocsInDir(scope, dir) {
86
+ function listMemoryDocsInDir(scope, dir, quiet = false) {
60
87
  if (!dir || !pathExists(dir))
61
88
  return [];
62
89
  const docs = [];
@@ -76,7 +103,8 @@ function listMemoryDocsInDir(scope, dir) {
76
103
  stack.push(join(d, e.name));
77
104
  else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'SKILL.md') {
78
105
  const file = join(d, e.name);
79
- const name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
106
+ const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
107
+ const name = normalizeDocName(raw);
80
108
  if (name)
81
109
  found.push({ path: file, name });
82
110
  }
@@ -86,35 +114,52 @@ function listMemoryDocsInDir(scope, dir) {
86
114
  // COLLECTION layer: the strict frontmatter parser throws on invalid YAML.
87
115
  // Isolate one malformed doc with a clear scoped notice + skip, so a single
88
116
  // bad file can't brick `memory list`/`find` or the substrate boot render.
117
+ // `quiet` suppresses the notice for a targeted resolve (a leaf-name read),
118
+ // where another doc's health is irrelevant noise before the result.
89
119
  try {
90
120
  docs.push(loadMemoryDoc(name, scope, path));
91
121
  }
92
122
  catch (e) {
93
123
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
94
- warn(`invalid frontmatter in ${path}: ${msg}`);
124
+ if (!quiet)
125
+ warn(`invalid frontmatter in ${path}: ${msg}`);
95
126
  }
96
127
  }
97
128
  return docs.sort((a, b) => a.name.localeCompare(b.name));
98
129
  }
99
130
  /** All native memory docs for a scope. Project scope is a nearest-first stack of
100
- * every ancestor `.crouter/memory/`; user and builtin are singleton stores. */
101
- export function listMemoryDocs(scope) {
131
+ * every ancestor `.crouter/memory/` (widened by a selected profile's project
132
+ * stack); profile is the selected profile's own singleton store, resolved
133
+ * through `loadProfileManifest`; user and builtin are singleton stores. */
134
+ export function listMemoryDocs(scope, quiet = false) {
102
135
  if (scope === 'project') {
103
- return projectScopeRoots().flatMap((root) => listMemoryDocsInDir('project', join(root, 'memory')));
136
+ return projectScopeRoots().flatMap((root) => listMemoryDocsInDir('project', join(root, 'memory'), quiet));
104
137
  }
105
- return listMemoryDocsInDir(scope, scopeMemoryDir(scope));
138
+ if (scope === 'profile') {
139
+ const profileId = selectedProfileId();
140
+ if (profileId === '')
141
+ return [];
142
+ try {
143
+ const { profileId: resolvedId } = loadProfileManifest(profileId);
144
+ return listMemoryDocsInDir('profile', profileMemoryDir(resolvedId), quiet);
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ }
150
+ return listMemoryDocsInDir(scope, scopeMemoryDir(scope), quiet);
106
151
  }
107
152
  /** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
108
153
  * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
109
154
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
110
155
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
111
- export function listPluginMemoryDocs(plugin, scope) {
156
+ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
112
157
  const dir = pluginMemoryDir(plugin);
113
158
  if (!pathExists(dir))
114
159
  return [];
115
160
  const docs = [];
116
161
  for (const file of walkFiles(dir, (n) => n.endsWith('.md') && n !== 'SKILL.md')) {
117
- const derived = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
162
+ const derived = normalizeDocName(relative(dir, file).replace(/\.md$/i, '').split(sep).join('/'));
118
163
  if (!derived)
119
164
  continue;
120
165
  const name = `${plugin.name}/${derived}`;
@@ -123,21 +168,84 @@ export function listPluginMemoryDocs(plugin, scope) {
123
168
  }
124
169
  catch (e) {
125
170
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
126
- warn(`invalid frontmatter in ${file}: ${msg}`);
171
+ if (!quiet)
172
+ warn(`invalid frontmatter in ${file}: ${msg}`);
127
173
  }
128
174
  }
129
175
  return docs.sort((a, b) => a.name.localeCompare(b.name));
130
176
  }
131
177
  /** All memory docs across the resolved sources, in precedence order: each
132
- * ancestor project `.crouter/` from nearest to farthest, then user, then
133
- * builtin. Within each source, native docs are emitted before enabled-plugin
134
- * docs, so native wins on the caller's first-wins dedup. */
135
- export function listAllMemoryDocs(scope) {
178
+ * ancestor project `.crouter/` from nearest to farthest, then the selected
179
+ * profile's memory (if any), then user, then builtin. Within each source,
180
+ * native docs are emitted before enabled-plugin docs, so native wins on the
181
+ * caller's first-wins dedup. */
182
+ export function listAllMemoryDocs(scope, quiet = false) {
136
183
  return memorySourcesInPrecedence(scope).flatMap((source) => [
137
- ...listMemoryDocsInDir(source.scope, source.memoryDir),
138
- ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope)),
184
+ ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet),
185
+ ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
139
186
  ]);
140
187
  }
188
+ /** Find the direct child of `dir` — a `.md` file (matched on name minus
189
+ * extension) or a directory — whose NORMALIZED display name equals
190
+ * `segment`. An exact literal match (no prefix to strip) wins over a
191
+ * normalized match, so a `spine` dir sitting beside a `01-spine` dir (a
192
+ * malformed corpus) never masks the literal one; a well-formed corpus has at
193
+ * most one candidate either way. Returns the absolute child path, or null. */
194
+ function matchNormalizedChild(dir, segment, want) {
195
+ let entries;
196
+ try {
197
+ entries = readdirSync(dir, { withFileTypes: true });
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ let normalizedHit = null;
203
+ for (const e of entries) {
204
+ if (want === 'dir') {
205
+ if (!e.isDirectory())
206
+ continue;
207
+ if (e.name === segment)
208
+ return join(dir, e.name);
209
+ if (normalizedHit === null && normalizeNameSegment(e.name) === segment) {
210
+ normalizedHit = join(dir, e.name);
211
+ }
212
+ }
213
+ else {
214
+ if (!(e.isFile() && e.name.endsWith('.md')))
215
+ continue;
216
+ const base = e.name.slice(0, -3);
217
+ if (base === segment)
218
+ return join(dir, e.name);
219
+ if (normalizedHit === null && normalizeNameSegment(base) === segment) {
220
+ normalizedHit = join(dir, e.name);
221
+ }
222
+ }
223
+ }
224
+ return normalizedHit;
225
+ }
226
+ /** Resolve a NORMALIZED (numeric-prefix-stripped) segment path against a
227
+ * physical memory dir: walk intermediate segments matching each as a
228
+ * directory by normalized name, then resolve the final segment as either a
229
+ * `.md` file or a directory (the bare-dir -> INDEX.md fallback needs the dir
230
+ * match even when a same-named file also exists). Either half is null when
231
+ * the segment path does not resolve that way. This is what makes
232
+ * `00-runtime-base.md` findable as `runtime-base` and `01-spine/00-has-manager`
233
+ * findable as `spine/has-manager` — the physical path keeps its pins, only
234
+ * lookup is prefix-blind. */
235
+ function resolveNormalizedPath(baseDir, segments) {
236
+ let curDir = baseDir;
237
+ for (let i = 0; i < segments.length - 1; i++) {
238
+ const next = matchNormalizedChild(curDir, segments[i], 'dir');
239
+ if (!next)
240
+ return { filePath: null, dirPath: null };
241
+ curDir = next;
242
+ }
243
+ const last = segments[segments.length - 1];
244
+ return {
245
+ filePath: matchNormalizedChild(curDir, last, 'file'),
246
+ dirPath: matchNormalizedChild(curDir, last, 'dir'),
247
+ };
248
+ }
141
249
  /** Direct full-path lookup of memory/<name>.md across scopes, precedence-first.
142
250
  * Returns every scope's hit in precedence order; the resolver takes the first
143
251
  * (highest-precedence) one — a fully-specified name is never ambiguous.
@@ -145,7 +253,12 @@ export function listAllMemoryDocs(scope) {
145
253
  * A directory INDEX is the cleaner contract: when `<name>.md` is absent but
146
254
  * `<name>/INDEX.md` exists, the bare dir name (`taste`) resolves to the dir's
147
255
  * INDEX doc — carrying the dir name as its identity. (`taste/INDEX` still
148
- * resolves directly as the file path.) */
256
+ * resolves directly as the file path.)
257
+ *
258
+ * `name`'s segments are already normalized (a caller-supplied identifier is
259
+ * never authored with a numeric prefix); resolution against the physical tree
260
+ * is prefix-blind via `resolveNormalizedPath` so a normalized name finds an
261
+ * `NN-`-pinned physical file/dir. */
149
262
  function findMemoryMatches(name, scope) {
150
263
  const matches = [];
151
264
  const segments = name.split('/');
@@ -156,15 +269,17 @@ function findMemoryMatches(name, scope) {
156
269
  // ancestor `.crouter/`, then user, then builtin.
157
270
  const dir = source.memoryDir;
158
271
  if (dir) {
159
- const path = join(dir, ...segments) + '.md';
160
- if (!isLegacySkillDoc && pathExists(path)) {
161
- matches.push(loadMemoryDoc(name, source.scope, path));
272
+ const { filePath, dirPath } = resolveNormalizedPath(dir, segments);
273
+ if (!isLegacySkillDoc && filePath !== null) {
274
+ matches.push(loadMemoryDoc(name, source.scope, filePath));
162
275
  continue;
163
276
  }
164
- const indexPath = join(dir, ...segments, 'INDEX.md');
165
- if (pathExists(indexPath)) {
166
- matches.push(loadMemoryDoc(name, source.scope, indexPath));
167
- continue;
277
+ if (dirPath !== null) {
278
+ const indexPath = join(dirPath, 'INDEX.md');
279
+ if (pathExists(indexPath)) {
280
+ matches.push(loadMemoryDoc(name, source.scope, indexPath));
281
+ continue;
282
+ }
168
283
  }
169
284
  }
170
285
  // Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
@@ -179,21 +294,22 @@ function findMemoryMatches(name, scope) {
179
294
  if (p.name !== pluginName)
180
295
  continue;
181
296
  const pdir = pluginMemoryDir(p);
297
+ const restSegments = rest ? rest.split('/') : [];
182
298
  if (rest) {
183
- const restSegments = rest.split('/');
184
- const ppath = join(pdir, ...restSegments) + '.md';
185
- if (restSegments.at(-1) !== 'SKILL' && pathExists(ppath)) {
186
- matches.push(loadMemoryDoc(name, source.scope, ppath));
299
+ const { filePath } = resolveNormalizedPath(pdir, restSegments);
300
+ if (restSegments.at(-1) !== 'SKILL' && filePath !== null) {
301
+ matches.push(loadMemoryDoc(name, source.scope, filePath));
187
302
  break;
188
303
  }
189
304
  }
190
305
  // Bare name -> <plugin>/memory/INDEX.md; slashed name -> dir INDEX.
191
- const pindex = rest
192
- ? join(pdir, ...rest.split('/'), 'INDEX.md')
193
- : join(pdir, 'INDEX.md');
194
- if (pathExists(pindex)) {
195
- matches.push(loadMemoryDoc(name, source.scope, pindex));
196
- break;
306
+ const pIndexDir = rest ? resolveNormalizedPath(pdir, restSegments).dirPath : pdir;
307
+ if (pIndexDir !== null) {
308
+ const pindex = join(pIndexDir, 'INDEX.md');
309
+ if (pathExists(pindex)) {
310
+ matches.push(loadMemoryDoc(name, source.scope, pindex));
311
+ break;
312
+ }
197
313
  }
198
314
  }
199
315
  }
@@ -207,7 +323,9 @@ function findMemoryByLeaf(leaf, scope) {
207
323
  return [];
208
324
  let all;
209
325
  try {
210
- all = listAllMemoryDocs(scope);
326
+ // Quiet: a targeted read must not spew other docs' frontmatter warnings
327
+ // before its own result (esp. a not_found) — corpus health is `lint`'s job.
328
+ all = listAllMemoryDocs(scope, true);
211
329
  }
212
330
  catch {
213
331
  return [];
@@ -57,7 +57,6 @@ export const HELP_ICON = String.fromCodePoint(HELP_ICON_CP);
57
57
  * private-use-area icons chosen per family concept (see command-icons.md). */
58
58
  const FAMILY_ICON_CP = {
59
59
  canvas: 0xf0e8,
60
- feed: 0xf09e,
61
60
  human: 0xf007,
62
61
  memory: 0xf02d,
63
62
  node: 0xf192,
@@ -80,17 +79,14 @@ const LEAF_ICON_CP = {
80
79
  'canvas history search': 0xf002,
81
80
  'canvas history read': 0xf02d,
82
81
  'canvas history show': 0xf1da,
83
- 'canvas daemon start': 0xf04b,
84
- 'canvas daemon status': 0xf111,
85
- 'canvas daemon stop': 0xf04d,
82
+ 'sys daemon start': 0xf04b,
83
+ 'sys daemon status': 0xf111,
84
+ 'sys daemon stop': 0xf04d,
86
85
  'canvas chord': 0xf001,
87
- 'canvas tmux-spread': 0xf009,
86
+ 'surface tmux-spread': 0xf009,
88
87
  'canvas issue new': 0xf188,
89
88
  'canvas prune': 0xf0c4,
90
89
  'canvas rebuild-index': 0xf0ad,
91
- 'feed read': 0xf02d,
92
- 'feed message': 0xf0e0,
93
- 'feed peek': 0xf06e,
94
90
  'human ask': 0xf059,
95
91
  'human review': 0xf044,
96
92
  'human notify': 0xf0f3,
@@ -262,7 +258,7 @@ function summarizeMemoryLint(text) {
262
258
  return null;
263
259
  }
264
260
  // ---------------------------------------------------------------------------
265
- // Shared table/count parsing helpers for the canvas/node/feed runtime cockpit
261
+ // Shared table/count parsing helpers for the canvas/node runtime cockpit
266
262
  // batch below. These parse the exact markdown shapes core/render.ts's generic
267
263
  // renderArray/renderResult produce (a `- field: N` bullet for a scalar count,
268
264
  // an `N <label>:` count lead-in followed by a GFM table for an array of
@@ -738,7 +734,7 @@ function summarizeCanvasHistoryShow(text) {
738
734
  const digest = summarizeTopCounts(typeCounts, 5, (name, count) => `${name}s×${count}`);
739
735
  return [{ text: `${countMatch[1]} artifacts` }, { text: digest, tone: 'muted' }];
740
736
  }
741
- /** `crtr canvas daemon start` (src/commands/daemon.ts:40, `spawnDaemon()` in
737
+ /** `crtr sys daemon start` (src/commands/sys/daemon.ts:40, `spawnDaemon()` in
742
738
  * src/daemon/manage.ts:89) has no custom `render`; the generic renderer
743
739
  * bullets `{started:true,pid}` or `{started:false,existing_pid}`. `pid` is
744
740
  * unconditionally set alongside `started:true` (manage.ts:125) — a
@@ -747,7 +743,7 @@ function summarizeCanvasHistoryShow(text) {
747
743
  * false branch (a `readPidfile() ?? undefined` race), so that one stays
748
744
  * optional. Returns null when `started` isn't a plain boolean bullet, or
749
745
  * is `true` without a `pid` bullet. */
750
- function summarizeCanvasDaemonStart(text) {
746
+ function summarizeSysDaemonStart(text) {
751
747
  const m = /^- started:\s*(true|false)\s*$/m.exec(text);
752
748
  if (m === null)
753
749
  return null;
@@ -760,14 +756,14 @@ function summarizeCanvasDaemonStart(text) {
760
756
  const pid = /^- existing_pid:\s*(\d+)\s*$/m.exec(text)?.[1];
761
757
  return [{ text: `already running${pid !== undefined ? ` · pid ${pid}` : ''}`, tone: 'muted' }];
762
758
  }
763
- /** `crtr canvas daemon status` (src/commands/daemon.ts:65) has no custom
759
+ /** `crtr sys daemon status` (src/commands/sys/daemon.ts:65) has no custom
764
760
  * `render`; the generic renderer bullets `{running:true,pid}` or
765
761
  * `{running:false}`. `pid` is unconditionally set alongside `running:true`
766
762
  * (daemon.ts:70 — the true branch only fires once `readPidfile()` already
767
763
  * returned a pid), so a true-with-no-pid shape is unrecognized. Returns
768
764
  * null when `running` isn't a plain boolean bullet, or is `true` without a
769
765
  * `pid` bullet. */
770
- function summarizeCanvasDaemonStatus(text) {
766
+ function summarizeSysDaemonStatus(text) {
771
767
  const m = /^- running:\s*(true|false)\s*$/m.exec(text);
772
768
  if (m === null)
773
769
  return null;
@@ -779,13 +775,13 @@ function summarizeCanvasDaemonStatus(text) {
779
775
  }
780
776
  return [{ text: 'not running', tone: 'error' }];
781
777
  }
782
- /** `crtr canvas daemon stop` (src/commands/daemon.ts:93) has no custom
778
+ /** `crtr sys daemon stop` (src/commands/sys/daemon.ts:93) has no custom
783
779
  * `render`; the generic renderer bullets `{stopped:true,pid}` or
784
780
  * `{stopped:false}`. `pid` is unconditionally set alongside `stopped:true`
785
781
  * (daemon.ts:98 — the signaled pid is echoed back in the same return), so a
786
782
  * true-with-no-pid shape is unrecognized. Returns null when `stopped` isn't
787
783
  * a plain boolean bullet, or is `true` without a `pid` bullet. */
788
- function summarizeCanvasDaemonStop(text) {
784
+ function summarizeSysDaemonStop(text) {
789
785
  const m = /^- stopped:\s*(true|false)\s*$/m.exec(text);
790
786
  if (m === null)
791
787
  return null;
@@ -982,97 +978,6 @@ function summarizeNodeTriggersList(text) {
982
978
  out.push({ text: `… ${remaining} more`, tone: 'muted' });
983
979
  return out;
984
980
  }
985
- /** `crtr feed read` (src/commands/push.ts:209) has two shapes: drained
986
- * (`${n} unread pointer(s) drained from ${node_id}'s inbox.\n\n${digest}`,
987
- * where the digest — `coalesce()`, src/core/feed/inbox.ts:225 — groups by
988
- * sender as `From ${sender} — ${N} update(s):` sections) or empty (`No
989
- * unread pointers for ${node_id} (unread=0).\n\n${explanation}`).
990
- * `coalesce()` always groups drained entries under at least one
991
- * From-section, so a positive drained count with zero parsed senders is a
992
- * shape mismatch, not a 0-sender success. Returns null on any other
993
- * shape. */
994
- function summarizeFeedRead(text) {
995
- const lines = text.split('\n');
996
- const first = lines[0] ?? '';
997
- const drained = /^(\d+) unread pointers? drained from (.+)'s inbox\.$/.exec(first);
998
- if (drained !== null) {
999
- const n = Number(drained[1]);
1000
- const senders = [...new Set([...text.matchAll(/^From (.+?) — \d+ update/gm)].map((m) => m[1]))];
1001
- if (n > 0 && senders.length === 0)
1002
- return null;
1003
- const out = [{ text: `${n} pointer${n === 1 ? '' : 's'} drained from ${senders.length} node${senders.length === 1 ? '' : 's'}` }];
1004
- if (senders.length > 0) {
1005
- const shown = senders.slice(0, 5);
1006
- const remaining = senders.length - shown.length;
1007
- out.push({ text: remaining > 0 ? `${shown.join(', ')}, … ${remaining} more` : shown.join(', '), tone: 'muted' });
1008
- }
1009
- return out;
1010
- }
1011
- if (/^No unread pointers for .+ \(unread=0\)\.$/.test(first)) {
1012
- return [{ text: 'nothing unread', tone: 'muted' }];
1013
- }
1014
- return null;
1015
- }
1016
- /** Teaser cap for `feed message`'s body preview — mirrors
1017
- * MEMORY_READ_CONTENT_LINES's role for a different leaf. */
1018
- const FEED_MESSAGE_TEASER_LINES = 3;
1019
- /** `crtr feed message` (src/commands/push.ts:264) has two shapes: found
1020
- * (`Full message ${id} (from ${from}):\n\n${body}`) or not-found (a short
1021
- * line — left as null, the generic fallback is fine for one line). */
1022
- function summarizeFeedMessage(text) {
1023
- const lines = text.split('\n');
1024
- const head = /^Full message (\S+) \(from (.+)\):$/.exec(lines[0] ?? '');
1025
- if (head === null)
1026
- return null;
1027
- const [, id, from] = head;
1028
- const bodyLines = lines.slice(1);
1029
- let idx = 0;
1030
- while (idx < bodyLines.length && bodyLines[idx].trim() === '')
1031
- idx++;
1032
- const content = bodyLines.slice(idx);
1033
- const teaser = content.slice(0, FEED_MESSAGE_TEASER_LINES);
1034
- const hasMore = content.slice(FEED_MESSAGE_TEASER_LINES).some((l) => l.trim() !== '');
1035
- if (hasMore)
1036
- teaser.push('…');
1037
- const out = [{ text: `${id} · from ${from}` }];
1038
- if (teaser.some((l) => l.trim() !== '')) {
1039
- out.push({ text: '' });
1040
- for (const l of teaser)
1041
- out.push({ text: l });
1042
- }
1043
- return out;
1044
- }
1045
- /** `crtr feed peek` (src/commands/push.ts:351) has two shapes: zero-kids
1046
- * (`No nodes below you (0 subscriptions, ${unread} unread). ${tail}` — the
1047
- * "0" is a hardcoded literal, not a variable) or nonzero (`${verdictLine}
1048
- * \n\n${N} node(s) below you — ${working} working, ${done} done, ${dead}
1049
- * dead, ${unread} unread:\n${rows}`). Returns null on any other shape. */
1050
- function summarizeFeedPeek(text) {
1051
- const zero = /^No nodes below you \(0 subscriptions, (\d+) unread\)\./.exec(text.split('\n')[0] ?? '');
1052
- if (zero !== null) {
1053
- const unread = Number(zero[1]);
1054
- return unread > 0
1055
- ? [{ text: `0 below · ${unread} unread`, tone: 'warning' }]
1056
- : [{ text: 'PASS · nothing below', tone: 'success' }];
1057
- }
1058
- const m = /(\d+) nodes? below you — (\d+) working, (\d+) done, (\d+) dead, (\d+) unread:/.exec(text);
1059
- if (m === null)
1060
- return null;
1061
- const [, , workingStr, doneStr, deadStr, unreadStr] = m;
1062
- const working = Number(workingStr);
1063
- const done = Number(doneStr);
1064
- const dead = Number(deadStr);
1065
- const unread = Number(unreadStr);
1066
- const parts = [`${working} working`, `${done} done`];
1067
- if (dead > 0)
1068
- parts.push(`${dead} dead`);
1069
- const lines = [{ text: parts.join(' · ') }];
1070
- if (dead > 0)
1071
- lines.push({ text: `${dead} dead — will not wake you`, tone: 'warning' });
1072
- else if (unread > 0)
1073
- lines.push({ text: `${unread} unread`, tone: 'warning' });
1074
- return lines;
1075
- }
1076
981
  /** `crtr push final` (makeTierLeaf's shared render, src/commands/push.ts:161)
1077
982
  * leads with a fixed sentence ('Result recorded — node finished; ...')
1078
983
  * followed unconditionally by `\n\n- report: ${report_path}` (`report_path`
@@ -1996,18 +1901,15 @@ const SUMMARIZERS = [
1996
1901
  { path: 'canvas history search', run: summarizeCanvasHistorySearch },
1997
1902
  { path: 'canvas history read', run: summarizeCanvasHistoryRead },
1998
1903
  { path: 'canvas history show', run: summarizeCanvasHistoryShow },
1999
- { path: 'canvas daemon start', run: summarizeCanvasDaemonStart },
2000
- { path: 'canvas daemon status', run: summarizeCanvasDaemonStatus },
2001
- { path: 'canvas daemon stop', run: summarizeCanvasDaemonStop },
1904
+ { path: 'sys daemon start', run: summarizeSysDaemonStart },
1905
+ { path: 'sys daemon status', run: summarizeSysDaemonStatus },
1906
+ { path: 'sys daemon stop', run: summarizeSysDaemonStop },
2002
1907
  { path: 'canvas issue new', run: summarizeCanvasIssueNew },
2003
1908
  { path: 'node new', run: summarizeNodeNew },
2004
1909
  { path: 'node msg', run: summarizeNodeMsg },
2005
1910
  { path: 'node yield', run: summarizeNodeYield },
2006
1911
  { path: 'node lifecycle close', run: summarizeNodeLifecycleClose },
2007
1912
  { path: 'node triggers list', run: summarizeNodeTriggersList },
2008
- { path: 'feed read', run: summarizeFeedRead },
2009
- { path: 'feed message', run: summarizeFeedMessage },
2010
- { path: 'feed peek', run: summarizeFeedPeek },
2011
1913
  { path: 'push urgent', run: summarizePushUrgent },
2012
1914
  { path: 'push final', run: summarizePushFinal },
2013
1915
  { path: 'human ask', run: summarizeHumanAsk },
@@ -0,0 +1,33 @@
1
+ import type { ProfileManifest } from '../../types.js';
2
+ export declare function profilesRoot(): string;
3
+ export declare function profileRoot(profileId: string): string;
4
+ export declare function profileManifestPath(profileId: string): string;
5
+ export declare function profileMemoryDir(profileId: string): string;
6
+ export interface ProfileEntry {
7
+ profileId: string;
8
+ manifest: ProfileManifest;
9
+ }
10
+ /** Every profile with a readable manifest, in directory-listing order.
11
+ * Enumerates ONLY immediate children of `profilesRoot()` — never recurses,
12
+ * never trusts a caller-supplied id. A dir whose name doesn't match the
13
+ * generated id shape, or whose `profile.json` is missing/corrupt, is skipped
14
+ * rather than crashing the whole listing. */
15
+ export declare function listProfiles(): ProfileEntry[];
16
+ /** Resolve a CLI operand to its profile — exact id first (checked against the
17
+ * actually-enumerated dirs, never a blind join), then a unique manifest
18
+ * `name` match. Ambiguous names fail listing every matching id; no match
19
+ * fails naming `profile list`/`profile new` as recovery. This is the ONLY
20
+ * function every command leaf and future runtime consumer (Phase 5/6) should
21
+ * call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
22
+ * safe profile id. */
23
+ export declare function loadProfileManifest(profileIdOrName: string): ProfileEntry;
24
+ /** Hold the per-profile manifest lock for the duration of `fn`. ALL manifest
25
+ * mutations (create/rename/add-project/remove-project/delete/last-used)
26
+ * below run inside this. */
27
+ export declare function withProfileManifestLock<T>(profileId: string, fn: () => T): T;
28
+ export declare function createProfile(name: string, projects?: string[]): ProfileEntry;
29
+ export declare function updateProfileLastUsed(profileId: string): ProfileEntry;
30
+ export declare function renameProfile(profileId: string, name: string): ProfileEntry;
31
+ export declare function addProfileProject(profileId: string, dir: string): ProfileEntry;
32
+ export declare function removeProfileProject(profileId: string, dir: string): ProfileEntry;
33
+ export declare function deleteProfile(profileId: string): void;