@north-light/crouter 0.3.208 → 0.3.210

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 (138) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/api/dto/broker-ops.d.ts +2 -12
  4. package/dist/api/dto/nodes.d.ts +16 -0
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +3 -2
  8. package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
  9. package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
  10. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
  11. package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
  12. package/dist/builtin-memory/04-base-worker.md +3 -2
  13. package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
  14. package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
  15. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
  16. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
  17. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
  18. package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
  19. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
  20. package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
  21. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
  22. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
  23. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
  31. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
  32. package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
  33. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
  34. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
  35. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
  36. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
  37. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
  38. package/dist/builtin-memory/advisor/council.md +0 -2
  39. package/dist/builtin-memory/design.md +3 -2
  40. package/dist/builtin-memory/development.md +3 -2
  41. package/dist/builtin-memory/insights/capture.md +1 -3
  42. package/dist/builtin-memory/insights/init.md +1 -3
  43. package/dist/builtin-memory/insights/listen.md +3 -2
  44. package/dist/builtin-memory/internal/INDEX.md +5 -4
  45. package/dist/builtin-memory/internal/agent-shaping.md +5 -4
  46. package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
  47. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
  48. package/dist/builtin-memory/internal/marketplaces.md +3 -2
  49. package/dist/builtin-memory/internal/memory-loading.md +31 -20
  50. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
  51. package/dist/builtin-memory/internal/plugins.md +7 -6
  52. package/dist/builtin-memory/internal/storage-tiers.md +3 -2
  53. package/dist/builtin-memory/plan/roadmap.md +3 -2
  54. package/dist/builtin-memory/spec/guide.md +0 -2
  55. package/dist/builtin-memory/spec/requirements.md +0 -2
  56. package/dist/builtin-memory/spec/roadmap.md +3 -2
  57. package/dist/builtin-memory/testing.md +1 -3
  58. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
  62. package/dist/clients/attach/viewer.js +366 -366
  63. package/dist/commands/memory/delete.js +3 -18
  64. package/dist/commands/memory/edit.js +23 -30
  65. package/dist/commands/memory/history.js +1 -1
  66. package/dist/commands/memory/lint.d.ts +7 -8
  67. package/dist/commands/memory/lint.js +178 -132
  68. package/dist/commands/memory/list.d.ts +0 -1
  69. package/dist/commands/memory/list.js +2 -12
  70. package/dist/commands/memory/move.d.ts +1 -0
  71. package/dist/commands/memory/move.js +195 -0
  72. package/dist/commands/memory/read.js +135 -141
  73. package/dist/commands/memory/shared.d.ts +18 -17
  74. package/dist/commands/memory/shared.js +93 -39
  75. package/dist/commands/memory/write.js +23 -33
  76. package/dist/commands/memory.js +5 -4
  77. package/dist/commands/pkg/browse/catalog.js +2 -4
  78. package/dist/commands/pkg/browse/doc-view.js +17 -11
  79. package/dist/commands/pkg/browse/model.d.ts +7 -9
  80. package/dist/commands/sys/migrate.d.ts +1 -0
  81. package/dist/commands/sys/migrate.js +106 -0
  82. package/dist/commands/sys/sync-deps.js +5 -10
  83. package/dist/commands/sys/sync-project-guidance.js +36 -16
  84. package/dist/commands/sys/sync-skills.js +8 -4
  85. package/dist/commands/sys.js +3 -2
  86. package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
  87. package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
  88. package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
  89. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
  90. package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
  91. package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
  92. package/dist/core/canvas/db.d.ts +3 -1
  93. package/dist/core/canvas/db.js +12 -2
  94. package/dist/core/memory/history.d.ts +4 -1
  95. package/dist/core/memory/history.js +1 -0
  96. package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
  97. package/dist/core/memory/inline-ref-inventory.js +23 -19
  98. package/dist/core/memory-resolver.d.ts +4 -4
  99. package/dist/core/memory-resolver.js +16 -43
  100. package/dist/core/runtime/bearings.d.ts +4 -3
  101. package/dist/core/runtime/bearings.js +4 -4
  102. package/dist/core/runtime/broker-extension-render.d.ts +3 -2
  103. package/dist/core/runtime/broker-extension-render.js +3 -3
  104. package/dist/core/runtime/memory.js +2 -3
  105. package/dist/core/substrate/index.d.ts +7 -4
  106. package/dist/core/substrate/index.js +6 -4
  107. package/dist/core/substrate/injected-store.d.ts +24 -12
  108. package/dist/core/substrate/injected-store.js +80 -33
  109. package/dist/core/substrate/listings.d.ts +21 -0
  110. package/dist/core/substrate/listings.js +88 -0
  111. package/dist/core/substrate/on-read-node.d.ts +5 -5
  112. package/dist/core/substrate/on-read-node.js +4 -5
  113. package/dist/core/substrate/on-read.d.ts +25 -4
  114. package/dist/core/substrate/on-read.js +81 -102
  115. package/dist/core/substrate/render-node.d.ts +5 -2
  116. package/dist/core/substrate/render-node.js +5 -3
  117. package/dist/core/substrate/render.d.ts +9 -8
  118. package/dist/core/substrate/render.js +104 -96
  119. package/dist/core/substrate/schema.d.ts +34 -18
  120. package/dist/core/substrate/schema.js +75 -32
  121. package/dist/core/substrate/surface-match.d.ts +32 -0
  122. package/dist/core/substrate/surface-match.js +179 -0
  123. package/dist/daemon/api/handlers/nodes.js +9 -0
  124. package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
  125. package/dist/migrations/001-surfaces-frontmatter.js +276 -0
  126. package/dist/migrations/convergent.d.ts +31 -0
  127. package/dist/migrations/convergent.js +71 -0
  128. package/dist/migrations/registry.d.ts +2 -0
  129. package/dist/migrations/registry.js +19 -0
  130. package/dist/migrations/types.d.ts +40 -0
  131. package/dist/migrations/types.js +11 -0
  132. package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
  133. package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
  134. package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
  135. package/package.json +1 -1
  136. package/runtime.lock.json +2 -2
  137. package/dist/core/substrate/ceiling.d.ts +0 -17
  138. package/dist/core/substrate/ceiling.js +0 -67
@@ -1,8 +1,8 @@
1
1
  // `crtr sys sync project-guidance` — one-way migration of a project's own
2
2
  // CLAUDE.md/AGENTS.md/.claude/rules into that SAME directory's OWN
3
- // `.crouter/memory/`. Each project guide becomes the root INDEX front door;
4
- // rules retain their explicit path routes, or use the `.` route (workspace
5
- // mount + any file read beneath the store's owning dir) when they are pathless.
3
+ // `.crouter/memory/`. Each project guide becomes the workspace front door
4
+ // (workspace-open + ./** read entries); rules keep their explicit path routes
5
+ // as read entries, or take the front-door pair when they are pathless.
6
6
  import { existsSync, readdirSync, statSync } from 'node:fs';
7
7
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
8
8
  import { defineLeaf } from '../../core/command.js';
@@ -218,9 +218,30 @@ function projectDocLabel(candidate) {
218
218
  .replace(/^\.$/, '');
219
219
  return relDir === '' ? rootLabel : normalizedName(`${rootLabel}/${relDir}`);
220
220
  }
221
+ /** The surfaces pair every workspace front door carries: whole-doc delivery
222
+ * when cwd/profile mounts the store, plus on any file read beneath the
223
+ * store's owning dir. */
224
+ function frontDoorSurfaces() {
225
+ return [
226
+ { on: 'workspace-open', at: 'content' },
227
+ { on: 'read', match: './**', at: 'content' },
228
+ ];
229
+ }
230
+ /** `./`-anchor a rule glob that could only match repo-relatively: any glob
231
+ * containing `/` that starts with neither `/`, `**`, nor `./`. Mirrors the
232
+ * surfaces migration's anchoring rule — this leaf MINTS new read globs, so it
233
+ * must anchor them the same way or the routes it writes silently never fire. */
234
+ function anchorReadGlob(g) {
235
+ const t = g.trim();
236
+ if (!t.includes('/'))
237
+ return t;
238
+ if (t.startsWith('/') || t.startsWith('**') || t.startsWith('./'))
239
+ return t;
240
+ return `./${t}`;
241
+ }
221
242
  /** AGENTS.md/CLAUDE.md → `<sourceDir>/.crouter/memory/INDEX.md`, the
222
243
  * workspace front door. Its explicit project identity keeps sibling roots
223
- * distinct; system none/file content plus `applies-to: "."` puts the wholly
244
+ * distinct; the workspace-open + ./** read entry pair puts the wholly
224
245
  * important guide in first-message context when cwd/profile mounts the store,
225
246
  * and surfaces it on any file read beneath the store's owning dir. */
226
247
  function prepareAgentsCandidate(candidate) {
@@ -231,9 +252,7 @@ function prepareAgentsCandidate(candidate) {
231
252
  name: docLabel,
232
253
  'when-and-why-to-read': `When working in ${docLabel}, this knowledge should be read because it is the project's operating guide.`,
233
254
  'short-form': synthesizeShortForm(body),
234
- 'system-prompt-visibility': 'none',
235
- 'file-read-visibility': 'content',
236
- 'applies-to': ['.'],
255
+ surfaces: frontDoorSurfaces(),
237
256
  };
238
257
  return {
239
258
  candidate,
@@ -244,9 +263,10 @@ function prepareAgentsCandidate(candidate) {
244
263
  };
245
264
  }
246
265
  /** `.claude/rules/<name>.md` → `<sourceDir>/.crouter/memory/<name>.md`. A
247
- * `paths:` rule keeps its `applies-to` file globs at none/content; a pathless
248
- * rule receives `applies-to: "."` and surfaces when the workspace opens or a
249
- * file beneath the store's owning dir is read. Like the INDEX docs, each rule
266
+ * `paths:` rule keeps its file globs as a `content` read entry (`./`-anchored
267
+ * when repo-relative); a pathless rule takes the front-door pair and surfaces
268
+ * when the workspace opens or a file beneath the store's owning dir is read.
269
+ * Like the INDEX docs, each rule
250
270
  * carries an explicit namespaced substrate name (`<root>/<subdir>/<leaf>`):
251
271
  * without it the resolver falls back to the store-relative leaf, and two
252
272
  * nested stores sharing a rule filename (e.g. apps/core and apps/gateway both
@@ -266,11 +286,11 @@ function prepareRuleCandidate(candidate) {
266
286
  ? routeFromDescription(description, 'knowledge')
267
287
  : `When working under ${dirLabel}, this knowledge should be read because it is migrated project rule guidance.`,
268
288
  'short-form': description !== '' ? description.replace(/\s+/g, ' ').trim() : synthesizeShortForm(parsed.body),
269
- 'system-prompt-visibility': 'none',
270
- 'file-read-visibility': 'content',
271
289
  };
272
- const globs = normalizeRulePaths(fm['paths']);
273
- frontmatter['applies-to'] = globs.length > 0 ? globs : ['.'];
290
+ const globs = normalizeRulePaths(fm['paths']).map(anchorReadGlob);
291
+ frontmatter['surfaces'] = globs.length > 0
292
+ ? [{ on: 'read', match: globs.length === 1 ? globs[0] : globs, at: 'content' }]
293
+ : frontDoorSurfaces();
274
294
  return {
275
295
  candidate,
276
296
  name,
@@ -321,7 +341,7 @@ function renderSummary(results) {
321
341
  export const sysSyncProjectGuidanceLeaf = defineLeaf({
322
342
  name: 'project-guidance',
323
343
  description: "migrate CLAUDE.md/AGENTS.md/.claude/rules into that directory's own crouter memory docs",
324
- whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its root INDEX front door; path-scoped rules keep their globs and pathless rules open with the workspace. Never exports memory docs back to these source files.",
344
+ whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its workspace front door; path-scoped rules keep their globs as read entries and pathless rules open with the workspace. Never exports memory docs back to these source files.",
325
345
  help: {
326
346
  name: 'sys sync project-guidance',
327
347
  summary: "one-way migration: CLAUDE.md/AGENTS.md/.claude/rules → that dir's own crouter memory docs",
@@ -338,7 +358,7 @@ export const sysSyncProjectGuidanceLeaf = defineLeaf({
338
358
  ],
339
359
  outputKind: 'object',
340
360
  effects: [
341
- "Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name at system-prompt-visibility none, file-read-visibility content, and applies-to `.`. Copies each .claude/rules/*.md into <dir>/.crouter/memory/<name>.md at none/content, using the source paths as applies-to globs or `.` (workspace mount + reads beneath the owning dir) when no paths are declared. Originals are left untouched.",
361
+ "Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name and the workspace front-door surfaces pair ({on: workspace-open, at: content} + {on: read, match: './**', at: content}). Copies each .claude/rules/*.md into <dir>/.crouter/memory/<name>.md with a content read entry over the source paths (‘./’-anchored when repo-relative), or the same front-door pair when no paths are declared. Originals are left untouched.",
342
362
  'Skips existing memory docs unless --overwrite is present.',
343
363
  'With --dry-run: read-only; writes nothing.',
344
364
  ],
@@ -167,6 +167,12 @@ function convertPrepared(prepared, opts) {
167
167
  delete frontmatter.description;
168
168
  delete frontmatter.type;
169
169
  delete frontmatter.keywords;
170
+ // Retired visibility-axis fields a stale source may still carry — they must
171
+ // never pass through into a substrate doc.
172
+ delete frontmatter['system-prompt-visibility'];
173
+ delete frontmatter['file-read-visibility'];
174
+ delete frontmatter['applies-to'];
175
+ delete frontmatter['read-when'];
170
176
  frontmatter.kind = prepared.kind;
171
177
  if (typeof frontmatter['when-and-why-to-read'] !== 'string' || frontmatter['when-and-why-to-read'].trim() === '') {
172
178
  frontmatter['when-and-why-to-read'] = routeFromDescription(description, prepared.kind);
@@ -174,10 +180,8 @@ function convertPrepared(prepared, opts) {
174
180
  if (typeof frontmatter['short-form'] !== 'string' || frontmatter['short-form'].trim() === '') {
175
181
  frontmatter['short-form'] = description.replace(/\s+/g, ' ').trim();
176
182
  }
177
- if (typeof frontmatter['system-prompt-visibility'] !== 'string')
178
- frontmatter['system-prompt-visibility'] = 'preview';
179
- if (typeof frontmatter['file-read-visibility'] !== 'string')
180
- frontmatter['file-read-visibility'] = 'none';
183
+ if (!Array.isArray(frontmatter['surfaces']))
184
+ frontmatter['surfaces'] = [{ on: 'boot', at: 'preview' }];
181
185
  if (!opts.dryRun)
182
186
  writeText(prepared.target, serializeMemoryDoc(frontmatter, prepared.parsed.body));
183
187
  return {
@@ -11,19 +11,20 @@ import { supportBranch } from './sys/support.js';
11
11
  import { sysSyspromptLeaf } from './sys/sysprompt.js';
12
12
  import { promptReviewLeaf } from './sys/prompt-review.js';
13
13
  import { sysSyncBranch } from './sys/sync.js';
14
+ import { sysMigrateLeaf } from './sys/migrate.js';
14
15
  import { sysUpdateLeaf, sysVersionLeaf } from './sys/update.js';
15
16
  export function registerSys() {
16
17
  return defineBranch({
17
18
  name: 'sys',
18
19
  rootEntry: {
19
20
  concept: 'crtr configuration, diagnostics, and self-management',
20
- desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, update, version, feedback, logs, support, daemon',
21
+ desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, migrate, update, version, feedback, logs, support, daemon',
21
22
  useWhen: 'managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process',
22
23
  },
23
24
  help: {
24
25
  name: 'sys',
25
26
  summary: 'crtr system configuration, settings, diagnostics, and self-management',
26
27
  },
27
- children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysSyncBranch, daemonBranch],
28
+ children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
28
29
  });
29
30
  }
@@ -3,7 +3,7 @@
3
3
  // Load-bearing corpus regression for `buildRefInventory` — the authoritative
4
4
  // inventory the broker and terminal attach viewer both resolve inline memory
5
5
  // references against. Covers scope precedence dedup, referenceability
6
- // (both kinds, gated and slash docs), INDEX folding, malformed-doc omission,
6
+ // (both kinds, gated and slash docs), directory browse names, malformed-doc omission,
7
7
  // metadata-only rows, and stable ordering, all over real on-disk fixtures.
8
8
  import { test, describe, before, beforeEach, after } from 'node:test';
9
9
  import assert from 'node:assert/strict';
@@ -17,7 +17,7 @@ import { buildRefInventory } from '../memory/inline-ref-inventory.js';
17
17
  // ---------------------------------------------------------------------------
18
18
  // Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
19
19
  // backed (node/project/user scope docs on disk), since precedence dedup,
20
- // referenceability-ignores-gate/slash, and INDEX folding are all corpus-shape
20
+ // referenceability-ignores-gate/slash, and directory browse names are all corpus-shape
21
21
  // facts that a pure in-memory test can't exercise. Fixture names are prefixed
22
22
  // to avoid any collision with the real shipped builtin/user corpus that
23
23
  // listAllMemoryDocs() also picks up in this environment.
@@ -128,12 +128,15 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
128
128
  assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-knowledge')?.kind, 'knowledge');
129
129
  assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-preference')?.kind, 'preference');
130
130
  });
131
- test('an INDEX doc folds to its bare directory name, not "<dir>/INDEX"', () => {
131
+ test('an INDEX doc keeps its canonical name; the bare dir name is a browse link, not a ref row', () => {
132
132
  writeDoc(join(projectMemoryDir(), 'crtr-inline-ref-fixture-taste'), 'INDEX.md', "kind: knowledge\nshort-form: 'taste index'");
133
133
  const { refs, names } = buildRefInventory();
134
+ assert.ok(names.has('crtr-inline-ref-fixture-taste/INDEX'));
135
+ // The directory name resolves as a listing link — in `names` for linkify,
136
+ // never a RefMeta row (no doc answers for a directory).
134
137
  assert.ok(names.has('crtr-inline-ref-fixture-taste'));
135
- assert.ok(!names.has('crtr-inline-ref-fixture-taste/INDEX'));
136
- const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste');
138
+ assert.ok(!refs.some((r) => r.name === 'crtr-inline-ref-fixture-taste'));
139
+ const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste/INDEX');
137
140
  assert.equal(winner?.shortForm, 'taste index');
138
141
  });
139
142
  test('a root INDEX doc is omitted, never exposed as an empty ref name', () => {
@@ -128,13 +128,14 @@ test('a plain path-named doc with no explicit name resolves to the nearest copy'
128
128
  process.chdir(prevCwd);
129
129
  }
130
130
  });
131
- test('a bare-directory query resolves to that dir\'s INDEX.md, nearest copy first', () => {
131
+ test('a bare-directory query does not resolve to a doc — directories answer with listings, not INDEX.md', () => {
132
132
  process.chdir(child);
133
133
  resetScopeCache();
134
134
  try {
135
- const doc = resolveMemoryDoc('taste');
136
- assert.equal(doc.path, join(child, '.crouter', 'memory', 'taste', 'INDEX.md'));
137
- assert.match(doc.body, /CHILD TASTE INDEX/);
135
+ assert.throws(() => resolveMemoryDoc('taste'), (err) => {
136
+ assert.equal(err.code, 'not_found');
137
+ return true;
138
+ });
138
139
  }
139
140
  finally {
140
141
  process.chdir(prevCwd);
@@ -76,9 +76,7 @@ test('resolveMemoryDocForTarget resolves a nested doc only with includeDescendan
76
76
  mkdirSync(nestedMemory, { recursive: true });
77
77
  writeFileSync(join(nestedMemory, 'guide.md'), '---\nkind: knowledge\n' +
78
78
  'name: nested-guide\n' +
79
- 'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n' +
80
- 'system-prompt-visibility: none\n' +
81
- 'file-read-visibility: none\n---\n' +
79
+ 'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n---\n' +
82
80
  'NESTED GUIDE BODY\n');
83
81
  const target = { cwd: proj, profileId: null, nodeId: null };
84
82
  assert.throws(() => resolveMemoryDocForTarget('nested-guide', target, {}), (e) => e instanceof CrtrError && e.code === 'not_found', 'without the opt-in the flat ancestor stack does not see the nested store');
@@ -50,15 +50,15 @@ test('a store nested in the crouter home does not leak into a canvas-artifact re
50
50
  mkdirSync(leakedStore, { recursive: true });
51
51
  writeFileSync(join(leakedStore, 'private-thing.md'), '---\nkind: knowledge\n' +
52
52
  "when-and-why-to-read: When X, read this because Y.\n" +
53
- 'file-read-visibility: content\n' +
54
- 'applies-to: "**/*.md"\n---\n' +
53
+ 'surfaces:\n' +
54
+ ' - {on: read, match: "**/*.md", at: content}\n---\n' +
55
55
  'PRIVATE ASSISTANT MEMORY MUST NOT LEAK\n');
56
56
  // A canvas runtime artifact under the crouter home — the shape of every node
57
57
  // report/context file the harness auto-reads.
58
58
  const reportFile = join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports', 'r.md');
59
59
  mkdirSync(join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports'), { recursive: true });
60
60
  writeFileSync(reportFile, '# a node report\n');
61
- const rendered = renderOnReadDocs(node, reportFile, new Set());
61
+ const rendered = renderOnReadDocs(node, reportFile, new Map());
62
62
  assert.ok(!rendered.includes('PRIVATE ASSISTANT MEMORY MUST NOT LEAK'), `crouter-home-nested store must not surface on a canvas read; got: ${rendered}`);
63
63
  assert.ok(!rendered.includes('private-thing'), `crouter-home-nested store name must not surface on a canvas read; got: ${rendered}`);
64
64
  });
@@ -45,7 +45,7 @@ after(() => {
45
45
  else
46
46
  process.env['HOME'] = prevHomeEnv;
47
47
  });
48
- test('project and user memory docs without applies-to do not fire on file reads', () => {
48
+ test('project and user memory docs without read surfaces do not fire on file reads', () => {
49
49
  const fakeHome = mkdtempSync(join(tmpdir(), 'crtr-onread-user-home-'));
50
50
  process.env['HOME'] = fakeHome;
51
51
  resetScopeCache();
@@ -53,18 +53,16 @@ test('project and user memory docs without applies-to do not fire on file reads'
53
53
  const userMemDir = join(fakeHome, '.crouter', 'memory');
54
54
  mkdirSync(userMemDir, { recursive: true });
55
55
  writeFileSync(join(userMemDir, 'noisy-global.md'), '---\nkind: preference\n' +
56
- 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n' +
57
- 'file-read-visibility: content\n---\n' +
56
+ 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n---\n' +
58
57
  'NOISY USER BODY\n');
59
58
  const projectMemDir = join(fakeHome, 'work', '.crouter', 'memory');
60
59
  mkdirSync(projectMemDir, { recursive: true });
61
60
  writeFileSync(join(projectMemDir, 'local.md'), '---\nkind: knowledge\n' +
62
- 'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n' +
63
- 'file-read-visibility: content\n---\n' +
61
+ 'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n---\n' +
64
62
  'LOCAL PROJECT BODY\n');
65
63
  const readFile = join(fakeHome, 'work', 'ideas.md');
66
64
  writeFileSync(readFile, 'turn off xml closer\n');
67
- const rendered = renderOnReadDocs(node, readFile, new Set());
65
+ const rendered = renderOnReadDocs(node, readFile, new Map());
68
66
  assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc has no explicit file route');
69
67
  assert.ok(!rendered.includes('LOCAL PROJECT BODY'), 'project doc also has no implicit file route');
70
68
  assert.equal(rendered, '', 'no file context renders without an explicit trigger');
@@ -72,13 +70,15 @@ test('project and user memory docs without applies-to do not fire on file reads'
72
70
  });
73
71
  test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
74
72
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
75
- // A file-routed substrate doc in a `.crouter/memory/` ancestor.
73
+ // A file-routed substrate doc in a `.crouter/memory/` ancestor. The
74
+ // `./`-anchored glob pins the repo-relative anchor: `src/file.ts` matches
75
+ // only relative to the doc's owning repo dir, never absolutely.
76
76
  const memDir = join(work, '.crouter', 'memory');
77
77
  mkdirSync(memDir, { recursive: true });
78
78
  writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
79
79
  'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
80
- 'file-read-visibility: content\n' +
81
- 'applies-to: "src/**"\n---\n' +
80
+ 'surfaces:\n' +
81
+ ' - {on: read, match: "./src/**", at: content}\n---\n' +
82
82
  `${FIXTURE_BODY}\n`);
83
83
  const readFile = join(work, 'src', 'file.ts');
84
84
  mkdirSync(join(work, 'src'), { recursive: true });
@@ -106,13 +106,13 @@ test('an explicitly routed doc injects on-read under its frontmatter name, not t
106
106
  writeFileSync(join(memDir, 'physical-guide.md'), '---\nkind: knowledge\n' +
107
107
  'name: acme-project\n' +
108
108
  'when-and-why-to-read: When reading acme source, this knowledge should be read because it carries the project-specific implementation constraints.\n' +
109
- 'file-read-visibility: content\n' +
110
- 'applies-to: "src/**"\n---\n' +
109
+ 'surfaces:\n' +
110
+ ' - {on: read, match: "./src/**", at: content}\n---\n' +
111
111
  'ACME PROJECT OPERATING GUIDE\n');
112
112
  const readFile = join(work, 'src', 'file.ts');
113
113
  mkdirSync(join(work, 'src'), { recursive: true });
114
114
  writeFileSync(readFile, 'export const x = 1;\n');
115
- const rendered = renderOnReadDocs(node, readFile, new Set());
115
+ const rendered = renderOnReadDocs(node, readFile, new Map());
116
116
  assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the matching explicit route fires');
117
117
  assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
118
118
  assert.ok(!rendered.includes('name="physical-guide"'), 'must not fall back to the path-derived name when frontmatter sets one');
@@ -1,9 +1,9 @@
1
1
  // A `.crouter/memory` store nested BELOW a mounted project root is delivered
2
- // by the read path, not by boot: its `.`-routed docs surface on the first read
3
- // beneath the store's owning dir, deduplicated against the workspace-open
4
- // render through the shared transcript dedup set. The `.` route stays fenced
5
- // to project-store docs — a hand-authored `.` in a user-scope doc must never
6
- // match a read.
2
+ // by the read path, not by boot: its `./**`-routed docs surface on the first
3
+ // read beneath the store's owning dir, deduplicated against the
4
+ // workspace-open render through the shared transcript dedup set. The `./`
5
+ // anchor stays fenced to project-store docs — a hand-authored `./` glob in a
6
+ // user-scope doc must never match a read.
7
7
  //
8
8
  // Run: node --import tsx/esm --test src/core/__tests__/on-read-nested-store.test.ts
9
9
  import { test, before, beforeEach, after } from 'node:test';
@@ -45,22 +45,25 @@ after(() => {
45
45
  process.env['HOME'] = prevHomeEnv;
46
46
  });
47
47
  /** A work tree with a root front door and a nested store front door, both
48
- * routed `.`. Returns the work dir. */
48
+ * carrying the explicit front-door pair (workspace-open + `./**` read).
49
+ * Returns the work dir. */
49
50
  function makeWorkTree() {
50
51
  const work = join(fakeHome, 'work');
51
52
  const rootStore = join(work, '.crouter', 'memory');
52
53
  mkdirSync(rootStore, { recursive: true });
53
54
  writeFileSync(join(rootStore, 'INDEX.md'), '---\nkind: knowledge\n' +
54
55
  'when-and-why-to-read: When working in this workspace, this knowledge should be read because it is the operating guide.\n' +
55
- 'file-read-visibility: content\n' +
56
- 'applies-to: "."\n---\n' +
56
+ 'surfaces:\n' +
57
+ ' - {on: workspace-open, at: content}\n' +
58
+ ' - {on: read, match: "./**", at: content}\n---\n' +
57
59
  `${ROOT_BODY}\n`);
58
60
  const nestedStore = join(work, 'pkg', '.crouter', 'memory');
59
61
  mkdirSync(nestedStore, { recursive: true });
60
62
  writeFileSync(join(nestedStore, 'INDEX.md'), '---\nkind: knowledge\n' +
61
63
  'when-and-why-to-read: When working in pkg, this knowledge should be read because it carries the package constraints.\n' +
62
- 'file-read-visibility: content\n' +
63
- 'applies-to: "."\n---\n' +
64
+ 'surfaces:\n' +
65
+ ' - {on: workspace-open, at: content}\n' +
66
+ ' - {on: read, match: "./**", at: content}\n---\n' +
64
67
  `${NESTED_BODY}\n`);
65
68
  return work;
66
69
  }
@@ -70,18 +73,18 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
70
73
  const inside = join(work, 'pkg', 'lib', 'file.ts');
71
74
  mkdirSync(join(work, 'pkg', 'lib'), { recursive: true });
72
75
  writeFileSync(inside, 'export const x = 1;\n');
73
- const rendered = renderOnReadDocs(node, inside, new Set());
76
+ const rendered = renderOnReadDocs(node, inside, new Map());
74
77
  assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
75
78
  const outside = join(work, 'docs', 'notes.ts');
76
79
  mkdirSync(join(work, 'docs'), { recursive: true });
77
80
  writeFileSync(outside, 'export const y = 2;\n');
78
- const renderedOutside = renderOnReadDocs(node, outside, new Set());
81
+ const renderedOutside = renderOnReadDocs(node, outside, new Map());
79
82
  assert.ok(!renderedOutside.includes(NESTED_BODY), `read outside pkg must not surface the nested front door; got: ${renderedOutside}`);
80
83
  });
81
84
  test('workspace-open and on-read share one dedup set: root front door once, nested store on first read beneath it', () => {
82
85
  const work = makeWorkTree();
83
86
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
84
- const seen = new Set();
87
+ const seen = new Map();
85
88
  const opened = renderWorkspaceOpenDocs(node, seen);
86
89
  assert.ok(opened.includes(ROOT_BODY), 'workspace open delivers the mounted root front door');
87
90
  assert.ok(!opened.includes(NESTED_BODY), 'boot stays flat: the nested store is not in the mounted stack');
@@ -92,18 +95,18 @@ test('workspace-open and on-read share one dedup set: root front door once, nest
92
95
  assert.ok(onRead.includes(NESTED_BODY), 'the nested front door delivers on the first read beneath it');
93
96
  assert.ok(!onRead.includes(ROOT_BODY), 'the root front door already delivered at workspace open — never twice');
94
97
  });
95
- test('a hand-authored `.` route in a user-scope doc never matches a read', () => {
98
+ test('a hand-authored `./`-anchored read glob in a user-scope doc never matches a read', () => {
96
99
  const userStore = join(fakeHome, '.crouter', 'memory');
97
100
  mkdirSync(userStore, { recursive: true });
98
101
  writeFileSync(join(userStore, 'control.md'), '---\nkind: knowledge\n' +
99
102
  'when-and-why-to-read: When reading markdown, this knowledge should be read because it is the corpus-loaded control fixture.\n' +
100
- 'file-read-visibility: content\n' +
101
- 'applies-to: "**/*.md"\n---\n' +
103
+ 'surfaces:\n' +
104
+ ' - {on: read, match: "**/*.md", at: content}\n---\n' +
102
105
  'USER CONTROL BODY\n');
103
106
  writeFileSync(join(userStore, 'dot-routed.md'), '---\nkind: knowledge\n' +
104
107
  'when-and-why-to-read: When anything, this knowledge should be read because it must never fire.\n' +
105
- 'file-read-visibility: content\n' +
106
- 'applies-to: "."\n---\n' +
108
+ 'surfaces:\n' +
109
+ ' - {on: read, match: "./**", at: content}\n---\n' +
107
110
  'USER DOT BODY MUST NOT FIRE\n');
108
111
  const work = join(fakeHome, 'work');
109
112
  mkdirSync(work, { recursive: true });
@@ -111,7 +114,7 @@ test('a hand-authored `.` route in a user-scope doc never matches a read', () =>
111
114
  const readFile = join(fakeHome, 'notes', 'thing.md');
112
115
  mkdirSync(join(fakeHome, 'notes'), { recursive: true });
113
116
  writeFileSync(readFile, '# a note\n');
114
- const rendered = renderOnReadDocs(node, readFile, new Set());
117
+ const rendered = renderOnReadDocs(node, readFile, new Map());
115
118
  assert.ok(rendered.includes('USER CONTROL BODY'), 'the glob-routed user doc proves the user store is in the corpus');
116
- assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`.\` route is project-store only — a user doc's owning root is ~; got: ${rendered}`);
119
+ assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`./\` anchor is project-store only — a user doc's owning root is ~; got: ${rendered}`);
117
120
  });
@@ -1,6 +1,8 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
3
- * `user_version` tracks how many have been applied. Append only. */
3
+ * `user_version` tracks how many have been applied. Append only. Journaled
4
+ * entries from `src/migrations/registry.ts` follow the local steps, so a new
5
+ * registry migration lands as the next version. */
4
6
  export declare const MIGRATIONS: ReadonlyArray<(db: DatabaseSync) => void>;
5
7
  /** Bring `db` up to the latest schema version. Reads `user_version`, runs each
6
8
  * pending migration in order, and bumps `user_version` after each so the work
@@ -7,7 +7,8 @@
7
7
  import { DatabaseSync } from 'node:sqlite';
8
8
  import { existsSync, readFileSync, readdirSync, appendFileSync } from 'node:fs';
9
9
  import { basename, join, resolve } from 'node:path';
10
- import { canvasDbPath, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
10
+ import { canvasDbPath, crtrHome, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
11
+ import { STATE_MIGRATIONS } from '../../migrations/registry.js';
11
12
  import { customExtensionPaths } from './extensions.js';
12
13
  import { updateJsonFileDurably } from './meta-file.js';
13
14
  // --- Schema as a forward-only migration list ------------------------------
@@ -1152,8 +1153,16 @@ ALTER TABLE crons ADD COLUMN held INTEGER NOT NULL DEFAULT 0;
1152
1153
  ALTER TABLE crons ADD COLUMN last_poke_at TEXT;
1153
1154
  `);
1154
1155
  }
1156
+ /** The journaled lane of the state-migration registry, adapted to this chain's
1157
+ * step shape. Registry order is preserved, so these versions stay stable as
1158
+ * long as the registry is append-only (same contract as the local list). */
1159
+ const JOURNALED_STATE_MIGRATIONS = STATE_MIGRATIONS
1160
+ .filter((m) => m.lane === 'journaled')
1161
+ .map((m) => (db) => m.apply(db, crtrHome()));
1155
1162
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
1156
- * `user_version` tracks how many have been applied. Append only. */
1163
+ * `user_version` tracks how many have been applied. Append only. Journaled
1164
+ * entries from `src/migrations/registry.ts` follow the local steps, so a new
1165
+ * registry migration lands as the next version. */
1157
1166
  export const MIGRATIONS = [
1158
1167
  /* v1 */ baselineSchema,
1159
1168
  /* v2 */ addRuntimeColumns,
@@ -1189,6 +1198,7 @@ export const MIGRATIONS = [
1189
1198
  /* v30 */ dropConsultOutbox,
1190
1199
  /* v31 */ addReviewSubmitRequested,
1191
1200
  /* v32 */ addCronHeldColumns,
1201
+ ...JOURNALED_STATE_MIGRATIONS,
1192
1202
  ];
1193
1203
  /** Migration indexes that manage their OWN transaction and therefore must not
1194
1204
  * be wrapped by `migrate()` — `node:sqlite` rejects a nested BEGIN. Index 3
@@ -4,13 +4,15 @@
4
4
  * rule of its own. */
5
5
  export declare const HISTORY_DIR = ".history";
6
6
  /** File operations only — deliberately not a review lifecycle. */
7
- export type HistoryOp = 'create' | 'edit' | 'delete';
7
+ export type HistoryOp = 'create' | 'edit' | 'delete' | 'move';
8
8
  export interface HistoryRecord {
9
9
  op: HistoryOp;
10
10
  at: string;
11
11
  /** Authoring node id; absent for a human at a bare CLI. */
12
12
  node?: string;
13
13
  cwd: string;
14
+ /** The canonical name this document answered to before a `move`; absent otherwise. */
15
+ from?: string;
14
16
  /** Why this revision happened. Required on `edit`, absent otherwise. */
15
17
  rationale?: string;
16
18
  /** True when the body was declared a verbatim payload from the principal. */
@@ -29,6 +31,7 @@ export declare function historyLogPathFor(memoryRoot: string, docPath: string):
29
31
  * serialized key order — the on-disk shape consumers read. */
30
32
  export declare function buildHistoryRecord(input: {
31
33
  op: HistoryOp;
34
+ from?: string;
32
35
  rationale?: string;
33
36
  verbatim?: boolean;
34
37
  before: string;
@@ -40,6 +40,7 @@ export function buildHistoryRecord(input) {
40
40
  at: new Date().toISOString(),
41
41
  ...(env['CRTR_NODE_ID'] ? { node: env['CRTR_NODE_ID'] } : {}),
42
42
  cwd: env['CRTR_NODE_CWD'] ?? process.cwd(),
43
+ ...(input.from !== undefined ? { from: input.from } : {}),
43
44
  ...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
44
45
  ...(input.verbatim === true ? { verbatim: true } : {}),
45
46
  before: input.before,
@@ -2,10 +2,11 @@ import type { RefMeta } from '../runtime/broker-protocol.js';
2
2
  /**
3
3
  * Build the inline memory-reference inventory: every referenceable document
4
4
  * in this node's corpus, precedence-deduped first-wins by canonical name,
5
- * both kinds, no gate/rung/`slash` filtering, INDEX docs folded to their bare
6
- * directory name. Returns display metadata (`refs`, stably sorted by winning
7
- * scope then name) plus the resolution set (`names`) that both the broker's
8
- * per-submission resolver and `buildGuidance` test tokens against.
5
+ * both kinds, no gate/rung/`slash` filtering. Returns display metadata
6
+ * (`refs`, stably sorted by winning scope then name) plus the resolution set
7
+ * (`names` — doc names plus every proper directory prefix, because a bare-dir
8
+ * ref is a legal listing link) that both the broker's per-submission resolver
9
+ * and `buildGuidance` test tokens against.
9
10
  */
10
11
  export declare function buildRefInventory(): {
11
12
  refs: RefMeta[];
@@ -18,8 +18,10 @@
18
18
  // automatic boot/on-read surfacing only (spec §7.1), and `slash` governs
19
19
  // command registration only (spec §4.3). An explicit human reference
20
20
  // outranks both.
21
- // • A directory's INDEX doc folds to its bare directory name (`taste`, not
22
- // `taste/INDEX`), matching what `crtr memory read taste` accepts.
21
+ // • Directory names are referenceable too: a bare-dir `[[ref]]` opens the
22
+ // directory's listing through `crtr memory read <dir>`, so every proper
23
+ // name-prefix of a winning doc joins the resolution set — resolution
24
+ // only, never a display row.
23
25
  // • A malformed doc (no valid `kind`) is already dropped by
24
26
  // `parseSubstrateDoc` returning null, so it never reaches the winners map.
25
27
  //
@@ -29,7 +31,6 @@
29
31
  // cache it reads through.
30
32
  import { listAllMemoryDocs } from '../memory-resolver.js';
31
33
  import { parseSubstrateDoc } from '../substrate/schema.js';
32
- import { displayName } from '../substrate/ceiling.js';
33
34
  import { cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
34
35
  // Display-sort weight matching resolution precedence (node > project >
35
36
  // profile > user > builtin) — display-only; resolution itself is
@@ -44,26 +45,21 @@ const SCOPE_SORT_RANK = {
44
45
  /**
45
46
  * Build the inline memory-reference inventory: every referenceable document
46
47
  * in this node's corpus, precedence-deduped first-wins by canonical name,
47
- * both kinds, no gate/rung/`slash` filtering, INDEX docs folded to their bare
48
- * directory name. Returns display metadata (`refs`, stably sorted by winning
49
- * scope then name) plus the resolution set (`names`) that both the broker's
50
- * per-submission resolver and `buildGuidance` test tokens against.
48
+ * both kinds, no gate/rung/`slash` filtering. Returns display metadata
49
+ * (`refs`, stably sorted by winning scope then name) plus the resolution set
50
+ * (`names` — doc names plus every proper directory prefix, because a bare-dir
51
+ * ref is a legal listing link) that both the broker's per-submission resolver
52
+ * and `buildGuidance` test tokens against.
51
53
  */
52
54
  export function buildRefInventory() {
53
55
  const docs = cachedSubstrateDocsInclusive(listAllMemoryDocs, parseSubstrateDoc);
54
- // First-wins by canonical (INDEX-folded) name: docs arrive already in
55
- // precedence order (listAllMemoryDocs' nearest-first source ordering), so
56
- // the first doc seen for a name is the winner.
56
+ // First-wins by canonical name: docs arrive already in precedence order
57
+ // (listAllMemoryDocs' nearest-first source ordering), so the first doc seen
58
+ // for a name is the winner.
57
59
  const winners = new Map();
58
60
  for (const doc of docs) {
59
- const name = displayName(doc.name);
60
- // A root INDEX folds to '' (no bare-directory token exists for it) and
61
- // has no other nonempty canonical identity in this corpus, so it has
62
- // no inline token to offer — omit it rather than expose `name: ''`.
63
- if (name === '')
64
- continue;
65
- if (!winners.has(name))
66
- winners.set(name, doc);
61
+ if (!winners.has(doc.name))
62
+ winners.set(doc.name, doc);
67
63
  }
68
64
  const refs = [];
69
65
  for (const [name, doc] of winners) {
@@ -75,5 +71,13 @@ export function buildRefInventory() {
75
71
  return scopeDelta;
76
72
  return a.name.localeCompare(b.name);
77
73
  });
78
- return { refs, names: new Set(refs.map((r) => r.name)) };
74
+ const names = new Set(refs.map((r) => r.name));
75
+ // Directory names resolve as listing links, so they join the set — but not
76
+ // the display rows: a dir has no kind/short-form to show.
77
+ for (const name of winners.keys()) {
78
+ const segs = name.split('/');
79
+ for (let i = 1; i < segs.length; i++)
80
+ names.add(segs.slice(0, i).join('/'));
81
+ }
82
+ return { refs, names };
79
83
  }
@@ -5,8 +5,8 @@ import { type InstalledPlugin, type Scope } from '../types.js';
5
5
  * stack nearest > ... > profile > user > builtin) for strong matches: (1) exact
6
6
  * substrate identity (`doc.name === query` — the explicit frontmatter `name`,
7
7
  * or its path-derived fallback), then (2) direct `memory/<name>.md` physical
8
- * path (plus the bare-dir/bare-plugin-name → `INDEX.md` convenience neither
9
- * identity nor a literal path expresses). Only when no source has a strong
8
+ * path. A directory name resolves to NO doc — the read leaf answers it with
9
+ * the directory's listing. Only when no source has a strong
10
10
  * match does a second nearest-first pass try bare leaf-name fallback (final
11
11
  * path segment only). Thus scope precedence resolves competing strong matches,
12
12
  * while a nearer nested doc's coincidental leaf can never shadow a farther
@@ -123,8 +123,8 @@ export declare function listAllMemoryDocs(scope?: MemoryScope, quiet?: boolean,
123
123
  * tree segment for segment with `.jsonl` in place of `.md`. Reuses the doc
124
124
  * resolution rules so a log outlives its doc under the same name the doc had:
125
125
  * numeric prefixes stay prefix-blind (`00-topic.md` logged at
126
- * `.history/00-topic.jsonl` still answers to `topic`) and a bare directory
127
- * name falls back to its INDEX log. Returns null when nothing resolves. */
126
+ * `.history/00-topic.jsonl` still answers to `topic`). Returns null when
127
+ * nothing resolves. */
128
128
  export declare function resolveHistoryLogPath(historyDir: string, segments: string[]): string | null;
129
129
  export interface MemoryDocSnapshot {
130
130
  /** Every document in default scope precedence order, loaded once. */