@north-light/crouter 0.3.208 → 0.3.209

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
@@ -0,0 +1,195 @@
1
+ import { appendFileSync, renameSync, rmSync } from 'node:fs';
2
+ import { dirname, resolve as resolvePath } from 'node:path';
3
+ import { defineLeaf } from '../../core/command.js';
4
+ import { CrtrError, notFound, usage } from '../../core/errors.js';
5
+ import { ensureDir, pathExists, readText, realpathOrSelf, writeText } from '../../core/fs-utils.js';
6
+ import { listAllMemoryDocs, resolveMemoryDoc } from '../../core/memory-resolver.js';
7
+ import { findDocLinks, isDocLinkName } from '../../core/memory/doc-link-grammar.js';
8
+ import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor } from '../../core/memory/history.js';
9
+ import { docsByName, isDirName } from '../../core/substrate/listings.js';
10
+ import { normalizeDocName } from '../../core/substrate/schema.js';
11
+ import { MEMORY_SCOPES, literalBodySegment, memoryFilePath, pruneEmptyParents } from './shared.js';
12
+ /** Replace every `[[oldName]]` doc link in a full document source with
13
+ * `[[newName]]`, splicing only within the literal body segment so the
14
+ * frontmatter block is never touched. Link spans come from the shared
15
+ * doc-link grammar (code blocks and inline code excluded), applied back to
16
+ * front so earlier offsets stay valid. */
17
+ function rewriteRefs(source, oldName, newName) {
18
+ const body = literalBodySegment(source);
19
+ const prefixLen = source.length - body.length;
20
+ const links = findDocLinks(body).filter((l) => l.name === oldName);
21
+ let text = source;
22
+ for (let i = links.length - 1; i >= 0; i -= 1) {
23
+ const l = links[i];
24
+ text = text.slice(0, prefixLen + l.start) + `[[${newName}]]` + text.slice(prefixLen + l.end);
25
+ }
26
+ return { text, count: links.length };
27
+ }
28
+ export const moveLeaf = defineLeaf({
29
+ name: 'move',
30
+ description: 'rename or relocate a memory document, rewriting inbound links',
31
+ whenToUse: 'a stored document should answer to a different name — renamed in place or relocated under another directory. The sanctioned way to move one, so you never mv the markdown off disk: this verb carries the document’s revision history along and rewrites every inbound `[[ref]]` across writable stores, so authored links break loudly or not at all. Moves stay within the document’s own store; one document per move — a directory never moves as a unit, so relocate its members individually.',
32
+ help: {
33
+ name: 'memory move',
34
+ summary: 'move memory/<name>.md to memory/<new-name>.md in its store, rewriting inbound [[refs]] corpus-wide',
35
+ params: [
36
+ { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`), resolved as `read` resolves it: exact identity/direct path by precedence node > project stack > profile > user, then bare leaf fallback. Builtin and installed-plugin docs are read-only and refuse to move. A directory name is rejected — move member docs individually.' },
37
+ { kind: 'flag', name: 'to', type: 'string', required: true, constraint: 'The new canonical name, in the same store: `/`-joined segments of [A-Za-z0-9_-], no `NN-` ordering prefix (identity is prefix-stripped, so a prefixed name would not answer to itself). Must not already answer to any document in the corpus.' },
38
+ { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Restrict resolution to this scope before moving. Use it to disambiguate a name present at multiple scopes. builtin is not a choice — builtin docs ship with the package and cannot move.' },
39
+ ],
40
+ output: [
41
+ { name: 'name', type: 'string', required: true, constraint: 'The document’s new canonical name.' },
42
+ { name: 'previous_name', type: 'string', required: true, constraint: 'The name it answered to before the move.' },
43
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope of the store the document moved within: node, user, project, or profile.' },
44
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path of the document at its new location.' },
45
+ { name: 'moved', type: 'boolean', required: true, constraint: 'Always true on success — the file and its revision log now live at the new name.' },
46
+ { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the revision log at its new location, ending in a `move` record that names the previous canonical name.' },
47
+ { name: 'refs_rewritten', type: 'object[]', required: true, constraint: 'Documents whose bodies were rewritten from `[[<previous_name>]]` to `[[<name>]]`. Each: {name, scope, path, refs}. Empty when nothing linked to the document — or when the moved document was shadowed by a same-named doc at a nearer scope, in which case inbound refs keep resolving to that survivor and are left alone.' },
48
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back at the new name, verify the corpus with lint.' },
49
+ ],
50
+ outputKind: 'object',
51
+ effects: [
52
+ 'Moves memory/<name>.md to memory/<new-name>.md within its store (creating parent directories, pruning any it leaves empty) and moves memory/.history/<name>.jsonl with it, appending a `move` record naming the previous canonical name.',
53
+ 'Rewrites every inbound `[[<name>]]` doc link across writable stores to `[[<new-name>]]`, appending an `edit` record to each rewritten document’s revision log. Aborts before any write when a read-only (builtin or installed-plugin) document carries such a link.',
54
+ ],
55
+ },
56
+ run: async (input) => {
57
+ const nameRaw = input['name'];
58
+ const newName = (input['to'] ?? '').trim();
59
+ const scopeArg = input['scope'];
60
+ let doc;
61
+ try {
62
+ doc = resolveMemoryDoc(nameRaw, { includeDescendants: true, ...(scopeArg !== undefined ? { scope: scopeArg } : {}) });
63
+ }
64
+ catch (e) {
65
+ if (e instanceof CrtrError && e.code === 'not_found') {
66
+ const dirName = normalizeDocName(nameRaw.replace(/\/+$/, ''));
67
+ if (dirName !== '' && isDirName(docsByName(listAllMemoryDocs(undefined, true, true)), dirName)) {
68
+ throw usage(`${dirName} is a directory, and a directory never moves as a unit — move its member docs individually.`, {
69
+ memory: dirName,
70
+ next: `Run \`crtr memory read ${dirName}\` to list the members, then \`crtr memory move <member> --to <new-name>\` per doc.`,
71
+ });
72
+ }
73
+ throw notFound(`memory document not found: ${nameRaw}`, {
74
+ memory: nameRaw,
75
+ next: 'Run `crtr memory find <query>` to locate it, or `crtr memory list` to browse the inventory. If it exists at another scope, pass --scope.',
76
+ });
77
+ }
78
+ throw e; // ambiguous / usage propagate with their own recovery guidance
79
+ }
80
+ if (doc.plugin !== undefined) {
81
+ throw usage(`${doc.name} belongs to the installed plugin "${doc.plugin}" — it is not a scope doc and cannot be moved here. Manage plugin docs with \`crtr pkg\`.`, { memory: doc.name, plugin: doc.plugin });
82
+ }
83
+ if (doc.scope === 'builtin') {
84
+ throw usage(`${doc.name} is a builtin document shipped with the package (read-only) — it cannot be moved. Override it with a same-named doc at a writable scope instead (\`crtr memory write ${doc.name} ...\`).`, { memory: doc.name, scope: 'builtin' });
85
+ }
86
+ // An explicit frontmatter `name` pins identity independent of the file's
87
+ // location, so a physical move would change nothing a reader can see.
88
+ const explicitName = doc.frontmatter?.['name'];
89
+ if (typeof explicitName === 'string' && explicitName.trim() !== '') {
90
+ throw usage(`${doc.name} pins its identity with an explicit \`name\` frontmatter field, so its name is not path-derived and a file move would not rename it. Edit the field with \`crtr memory edit ${doc.name}\` instead.`, { memory: doc.name, path: doc.path });
91
+ }
92
+ if (newName === '')
93
+ throw usage('--to requires the new canonical name');
94
+ if (!isDocLinkName(newName)) {
95
+ throw usage(`invalid new name: ${newName} — a canonical name is one or more [A-Za-z0-9_-] segments joined by \`/\``);
96
+ }
97
+ // Identity is derived from the path with `NN-` ordering prefixes stripped,
98
+ // so a prefixed target name would derive a different identity than asked.
99
+ if (normalizeDocName(newName) !== newName) {
100
+ throw usage(`invalid new name: ${newName} — a \`NN-\` ordering prefix is stripped from derived identity, so this doc would answer to ${normalizeDocName(newName)} instead. Name it that directly.`);
101
+ }
102
+ if (newName === doc.name)
103
+ throw usage(`${doc.name} already answers to that name — nothing to move`);
104
+ const newPath = memoryFilePath(doc.root, newName);
105
+ if (pathExists(newPath)) {
106
+ throw usage(`the target is occupied: ${newPath} already exists — pick a free name or delete the occupant first`, { memory: newName, path: newPath });
107
+ }
108
+ const corpus = listAllMemoryDocs(undefined, true, true);
109
+ const byName = docsByName(corpus);
110
+ const taken = byName.get(newName);
111
+ if (taken !== undefined) {
112
+ throw usage(`${newName} already answers to a document (${taken.scope} scope, ${taken.path}) — moving onto it would make every rewritten ref ambiguous. Pick a free name.`, { memory: newName, path: taken.path });
113
+ }
114
+ // Refs are rewritten only when this doc is what `[[<name>]]` actually
115
+ // resolves to. Moving a doc shadowed by a same-named nearer doc leaves
116
+ // every ref pointing at that surviving winner, so they are left alone.
117
+ const movedReal = realpathOrSelf(doc.path);
118
+ const winner = byName.get(doc.name);
119
+ const isWinner = winner !== undefined && realpathOrSelf(winner.path) === movedReal;
120
+ const referencing = [];
121
+ const blocked = [];
122
+ if (isWinner) {
123
+ const seenFiles = new Set([movedReal]);
124
+ for (const d of corpus) {
125
+ const real = realpathOrSelf(d.path);
126
+ if (seenFiles.has(real))
127
+ continue;
128
+ seenFiles.add(real);
129
+ if (!findDocLinks(d.body).some((l) => l.name === doc.name))
130
+ continue;
131
+ if (d.plugin !== undefined || d.scope === 'builtin')
132
+ blocked.push(d);
133
+ else
134
+ referencing.push(d);
135
+ }
136
+ if (blocked.length > 0) {
137
+ throw usage(`cannot move ${doc.name}: read-only docs link to it with [[${doc.name}]] and their refs cannot be rewritten — ` +
138
+ blocked.map((d) => `${d.plugin !== undefined ? `plugin ${d.plugin}` : d.scope}: ${d.name} (${d.path})`).join('; ') +
139
+ '. Nothing was changed.', { memory: doc.name });
140
+ }
141
+ }
142
+ // All checks passed — mutate. Inbound refs first, each an ordinary `edit`
143
+ // in that doc's own log; the mechanical splice touches only body bytes, so
144
+ // `last-updated` (recency of the knowledge itself) is deliberately not
145
+ // restamped.
146
+ const refsRewritten = [];
147
+ for (const d of referencing) {
148
+ const before = readText(d.path);
149
+ const { text: after, count } = rewriteRefs(before, doc.name, newName);
150
+ if (count === 0)
151
+ continue;
152
+ writeText(d.path, after);
153
+ appendHistoryRecord(historyLogPathFor(d.root, d.path), buildHistoryRecord({ op: 'edit', rationale: `\`crtr memory move\`: [[${doc.name}]] → [[${newName}]]`, before, after }));
154
+ refsRewritten.push({ name: d.name, scope: d.scope, path: d.path, refs: count });
155
+ }
156
+ // The doc itself: self-refs rewrite in the same motion as the move.
157
+ const before = readText(doc.path);
158
+ const after = isWinner ? rewriteRefs(before, doc.name, newName).text : before;
159
+ writeText(newPath, after);
160
+ rmSync(doc.path);
161
+ pruneEmptyParents(resolvePath(doc.path), doc.name.split('/').length);
162
+ // The revision log travels with the doc — history answers to the name the
163
+ // doc now answers to. A log already at the target (a prior, deleted life
164
+ // of that name) keeps its records; this doc's records append after them.
165
+ const oldLog = historyLogPathFor(doc.root, doc.path);
166
+ const newLog = historyLogPathFor(doc.root, newPath);
167
+ if (pathExists(oldLog)) {
168
+ ensureDir(dirname(newLog));
169
+ if (pathExists(newLog)) {
170
+ appendFileSync(newLog, readText(oldLog), 'utf8');
171
+ rmSync(oldLog);
172
+ }
173
+ else {
174
+ renameSync(oldLog, newLog);
175
+ }
176
+ pruneEmptyParents(resolvePath(oldLog), doc.name.split('/').length);
177
+ }
178
+ appendHistoryRecord(newLog, buildHistoryRecord({ op: 'move', from: doc.name, before, after }));
179
+ const refsNote = isWinner
180
+ ? refsRewritten.length > 0
181
+ ? ` Rewrote [[${doc.name}]] in ${refsRewritten.length} doc${refsRewritten.length === 1 ? '' : 's'}.`
182
+ : ' No inbound refs needed rewriting.'
183
+ : ` Inbound [[${doc.name}]] refs were left alone: a same-named doc at a nearer scope still answers to that name.`;
184
+ return {
185
+ name: newName,
186
+ previous_name: doc.name,
187
+ scope: doc.scope,
188
+ path: newPath,
189
+ moved: true,
190
+ log_path: newLog,
191
+ refs_rewritten: refsRewritten,
192
+ follow_up: `Moved.${refsNote} Read it back with \`crtr memory read ${newName}\`, and run \`crtr memory lint\` to verify no link dangles.`,
193
+ };
194
+ },
195
+ });
@@ -1,13 +1,16 @@
1
+ import { CrtrClient } from '../../api/index.js';
2
+ import { interpolateNodePaths } from '../../core/canvas/paths.js';
1
3
  import { defineLeaf } from '../../core/command.js';
2
4
  import { CrtrError, notFound } from '../../core/errors.js';
3
- import { createMemoryDocSnapshot, listAllMemoryDocs, resolveMemoryDoc, resolveMemoryDocs } from '../../core/memory-resolver.js';
4
- import { effectiveDocKind, parseSubstrateDoc, previewLine } from '../../core/substrate/schema.js';
5
- import { displayName, indexDirOf, isIndexName } from '../../core/substrate/ceiling.js';
6
- import { interpolateNodePaths } from '../../core/canvas/paths.js';
7
- import { readText } from '../../core/fs-utils.js';
5
+ import { readText, realpathOrSelf } from '../../core/fs-utils.js';
8
6
  import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
9
7
  import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
8
+ import { createMemoryDocSnapshot, listAllMemoryDocs, resolveMemoryDoc, resolveMemoryDocs } from '../../core/memory-resolver.js';
10
9
  import { expandShellBlocks, hasShellBlocks, makeNodeShellRunner } from '../../core/runtime/shell-expansion.js';
10
+ import { deliveredAtOrAbove, loadInjectedDocs, recordDelivery, saveInjectedDocs } from '../../core/substrate/injected-store.js';
11
+ import { ancestorDirsOf, dirDedupKey, docsByName, isDirName, renderDirListing } from '../../core/substrate/listings.js';
12
+ import { memoryReadDocBlocks } from '../../core/substrate/on-read.js';
13
+ import { effectiveDocKind, normalizeDocName } from '../../core/substrate/schema.js';
11
14
  import { MEMORY_KINDS } from './shared.js';
12
15
  export { createMemoryDocSnapshot, resolveMemoryDocs };
13
16
  /** Load the body at an already-resolved memory path with the same frontmatter
@@ -20,117 +23,56 @@ export function readMemoryDocContent(path, includeFrontmatter = false) {
20
23
  const nodeId = process.env['CRTR_NODE_ID'];
21
24
  return nodeId ? interpolateNodePaths(content, nodeId) : content;
22
25
  }
23
- /** The routing list appended to a directory-INDEX read: one line per document
24
- * the INDEX gates, `[[name]]: <when-and-why-to-read>`. Membership is the
25
- * directory's docs unioned with the body's resolvable `[[links]]`, deduped —
26
- * a nested INDEX stands in for its whole subtree with a single line, while a
27
- * subdirectory without an INDEX is transparent and its leaves render directly
28
- * (mirroring how boot ceilings govern to the nearest INDEX). Every member
29
- * renders regardless of its own visibility rung: rungs price unsolicited
30
- * surfaces, and a read is a deliberate act. The root INDEX is excluded — the
31
- * top-level namespace merges across stores, so its "directory" would
32
- * enumerate the entire corpus. */
33
- function indexRoutes(doc) {
34
- // A dir INDEX resolves two ways: the explicit `dir/INDEX` name, or the bare
35
- // dir name (which carries the dir as its identity while the path is the
36
- // INDEX.md). Detect from the path so both shapes route identically.
37
- if (!doc.path.endsWith('/INDEX.md'))
38
- return [];
39
- const dir = isIndexName(doc.name) ? indexDirOf(doc.name) : doc.name;
40
- if (dir === '')
41
- return [];
42
- const canonicalName = `${dir}/INDEX`;
43
- let corpus;
26
+ /** The node-config subject for memory-read-event gate evaluation. A CLI
27
+ * process cannot take it from canvas-db (V-1); one `/v1` request supplies it.
28
+ * With no node identity or no reachable daemon the answer is null — gated
29
+ * docs then skip while ungated docs still deliver. Never autostarts a daemon:
30
+ * a memory read must not boot the runtime as a side effect. */
31
+ async function fetchSubject(nodeId) {
32
+ if (nodeId === undefined)
33
+ return null;
44
34
  try {
45
- corpus = listAllMemoryDocs(undefined, true, true);
35
+ return await CrtrClient.forLocalSocket({ autostart: false }).nodeSubject(nodeId);
46
36
  }
47
37
  catch {
48
- return [];
38
+ return null;
49
39
  }
50
- // First occurrence wins: listAllMemoryDocs emits in resolver-precedence
51
- // order, so the doc that renders a name's line is the doc a read of that
52
- // name would actually return.
53
- const byName = new Map();
54
- for (const d of corpus)
55
- if (!byName.has(d.name))
56
- byName.set(d.name, d);
57
- const prefix = dir + '/';
58
- // Keyed by RENDERED identity (`displayName`), not `.name`: one physical
59
- // directory INDEX answers to two names — `listAllMemoryDocs` emits the
60
- // path-derived `dir/INDEX`, while `resolveMemoryDoc('dir')` returns it
61
- // carrying the bare dir as its identity. Keying on `.name` let a body link
62
- // to a sub-INDEX (written the normal way, by bare dir name) miss the entry
63
- // the directory scan already added, and the doc rendered twice.
64
- const members = new Map();
65
- for (const [name, d] of byName) {
66
- if (!name.startsWith(prefix) || name === canonicalName)
67
- continue;
68
- // The shallowest INDEX between the gating dir and this doc stands in for
69
- // it (an INDEX finds itself here, keeping its own single line).
70
- const segs = name.slice(prefix.length).split('/');
71
- let member = d;
72
- for (let i = 1; i < segs.length; i++) {
73
- const gate = byName.get(`${prefix}${segs.slice(0, i).join('/')}/INDEX`);
74
- if (gate !== undefined) {
75
- member = gate;
76
- break;
77
- }
78
- }
79
- if (!members.has(displayName(member.name)))
80
- members.set(displayName(member.name), member);
40
+ }
41
+ function corpusDocs() {
42
+ try {
43
+ return listAllMemoryDocs(undefined, true, true);
81
44
  }
82
- // No members means this INDEX gates no cluster in the corpus namespace — a
83
- // store-root front door whose explicit frontmatter name dodges the root
84
- // check above lands here. Fall back to the plain links field.
85
- if (members.size === 0)
45
+ catch {
86
46
  return [];
87
- // Union the body's authored links so one list carries everything this INDEX
88
- // routes to — the routes list subsumes `links` on an INDEX read.
89
- for (const linkName of docLinkNames(doc.body)) {
90
- let linked;
91
- try {
92
- linked = resolveMemoryDoc(linkName, { includeDescendants: true });
93
- }
94
- catch {
95
- continue; // dangling link — lint's finding, not read's
96
- }
97
- if (displayName(linked.name) !== linkName)
98
- continue;
99
- if (linkName === displayName(canonicalName) || linkName === displayName(doc.name))
100
- continue;
101
- if (!members.has(linkName))
102
- members.set(linkName, linked);
103
47
  }
104
- return [...members.values()]
105
- .map((m) => {
106
- const label = displayName(m.name);
107
- const sub = parseSubstrateDoc(m);
108
- const line = sub === null ? '' : previewLine(sub);
109
- return { label, line: line === '' ? `[[${label}]]` : `[[${label}]]: ${line}` };
110
- })
111
- .sort((a, b) => a.label.localeCompare(b.label))
112
- .map((e) => e.line);
48
+ }
49
+ function attr(s) {
50
+ return s
51
+ .replace(/&/g, '&amp;')
52
+ .replace(/"/g, '&quot;')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;');
113
55
  }
114
56
  export const readLeaf = defineLeaf({
115
57
  name: 'read',
116
58
  description: 'load a memory document body by name',
117
- whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Exact identity and direct-path matches resolve by scope precedence (node > project stack > profile > user > builtin); bare leaf-name fallback is considered only when no such match exists. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
59
+ whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Exact identity and direct-path matches resolve by scope precedence (node > project stack > profile > user > builtin); bare leaf-name fallback is considered only when no such match exists. A directory name is also a valid target: it returns the neighborhood listing, and walking directories is the sanctioned browse move. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
118
60
  help: {
119
61
  name: 'memory read',
120
- summary: 'resolve a path-derived name to its document body, frontmatter stripped unless --frontmatter',
62
+ summary: 'resolve a path-derived name to its document body (frontmatter stripped unless --frontmatter), or a directory name to its listing',
121
63
  params: [
122
- { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Exact identity and direct-path matches resolve by scope precedence: node > project stack > profile > user > builtin. A bare leaf falls back only when no exact/direct match exists.' },
64
+ { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Exact identity and direct-path matches resolve by scope precedence: node > project stack > profile > user > builtin. A bare leaf falls back only when no exact/direct match exists. A directory name returns that directory\u2019s listing instead of a document.' },
123
65
  { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Narrows resolution when the name is ambiguous across kinds.' },
124
66
  { kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes the YAML frontmatter in the returned body. Off by default — only the body is returned.' },
125
67
  ],
126
68
  output: [
127
- { name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
128
- { name: 'kind', type: 'string', required: true, constraint: 'Resolved kind: knowledge or preference.' },
129
- { name: 'scope', type: 'string', required: true, constraint: 'Scope the document was resolved from: node, project, profile, user, or builtin.' },
130
- { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the document on disk. Revise it with `crtr memory edit`, never by editing this file — an edit records why the change happened and lands in the doc’s revision history.' },
131
- { name: 'content', type: 'string', required: true, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set. A document may embed shell as `!`cmd`` or a ```! fenced block; each runs once per read, in the current working directory, and is replaced by its output. Read `path` off disk when you need the literal unexecuted text.' },
132
- { name: 'links', type: 'string[]', required: false, constraint: 'Canonical names this document links to via `[[name]]` that resolve in the current corpus — further reading, loaded only on demand with `crtr memory read <name>`. Omitted when the body carries no resolvable links, and subsumed by `routes` on a directory-INDEX read.' },
133
- { name: 'routes', type: 'string[]', required: false, constraint: 'Present only when the document is a directory INDEX: one routing line per document the INDEX gates — the directory’s members plus the body’s resolvable links, a nested INDEX standing in for its subtree with a single line. Follow one with `crtr memory read <name>` when the task in front of you matches its line.' },
69
+ { name: 'name', type: 'string', required: true, constraint: 'Resolved document name, or the directory name on a directory read.' },
70
+ { name: 'kind', type: 'string', required: false, constraint: 'Resolved kind: knowledge or preference. Absent on a directory read.' },
71
+ { name: 'scope', type: 'string', required: false, constraint: 'Scope the document was resolved from: node, project, profile, user, or builtin. Absent on a directory read.' },
72
+ { name: 'path', type: 'string', required: false, constraint: 'Absolute path to the document on disk. Revise it with `crtr memory edit`, never by editing this file — an edit records why the change happened and lands in the doc’s revision history. Absent on a directory read.' },
73
+ { name: 'content', type: 'string', required: false, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set. A document may embed shell as `!`cmd`` or a ```! fenced block; each runs once per read, in the current working directory, and is replaced by its output. Read `path` off disk when you need the literal unexecuted text. May be preceded by one `<auto-loaded-context>` block carrying the doc\u2019s neighborhood listings and any docs routed to this read. Absent on a directory read.' },
74
+ { name: 'listing', type: 'string[]', required: false, constraint: 'Present only on a directory read: one `[[name]]: <when-and-why>` line per member doc, one bare `[[name]]` per subdirectory. Follow any line with `crtr memory read <name>`.' },
75
+ { name: 'links', type: 'string[]', required: false, constraint: 'Canonical names this document links to via `[[name]]` that resolve in the current corpus — further reading, loaded only on demand with `crtr memory read <name>`. A directory link returns that directory\u2019s listing. Omitted when the body carries no resolvable links.' },
134
76
  { name: 'follow_up', type: 'string', required: true, constraint: 'Hints at variant flags or next commands.' },
135
77
  ],
136
78
  outputKind: 'object',
@@ -156,52 +98,104 @@ export const readLeaf = defineLeaf({
156
98
  if (!(e instanceof CrtrError && e.code === 'not_found'))
157
99
  throw e;
158
100
  }
159
- if (doc !== undefined) {
160
- const kind = effectiveDocKind(doc);
161
- const raw = readMemoryDocContent(doc.path, includeFrontmatter);
162
- // A read is an explicit act by an agent that can already run shell, so a
163
- // document's embedded commands run here and their output lands inline —
164
- // the same contract as invoking the document as a slash command. Nothing
165
- // executes on the auto-load path, which never reaches this leaf.
166
- const content = hasShellBlocks(raw)
167
- ? await expandShellBlocks(raw, makeNodeShellRunner({ cwd: process.cwd() }))
168
- : raw;
169
- // `[[name]]` doc links are pointers, never transclusion: surface which
170
- // linked names actually resolve so the reader can follow one when the
171
- // task needs that depth, without ever auto-loading a linked body.
172
- // A directory INDEX is a gate: its read appends one routing line per doc
173
- // it gates, subsuming the links field. Non-INDEX reads keep plain links.
174
- const routes = indexRoutes(doc);
175
- const links = routes.length > 0 ? [] : docLinkNames(doc.body).filter((linkName) => {
176
- try {
177
- // `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
178
- // a stored graph edge does not. Compare the resolved canonical name
179
- // so an old shorthand never masquerades as a first-class doc link.
180
- return displayName(resolveMemoryDoc(linkName, { includeDescendants: true }).name) === linkName;
181
- }
182
- catch {
183
- return false;
101
+ const nodeId = process.env['CRTR_NODE_ID'] || undefined;
102
+ if (doc === undefined) {
103
+ // No doc answers for a directory — the bare listing does. An explicit
104
+ // dir read is a deliberate act, so every line renders (unlisted
105
+ // excluded), never dedup-filtered; rendered members still record at
106
+ // preview so unsolicited channels stay quiet about them later.
107
+ const dirName = normalizeDocName(nameRaw.replace(/\/+$/, ''));
108
+ const byName = docsByName(corpusDocs());
109
+ if (dirName !== '' && isDirName(byName, dirName)) {
110
+ const seen = nodeId !== undefined ? loadInjectedDocs(nodeId) : null;
111
+ const listing = renderDirListing(byName, dirName, seen, false);
112
+ if (nodeId !== undefined && seen !== null) {
113
+ recordDelivery(seen, dirDedupKey(dirName), 'preview');
114
+ saveInjectedDocs(nodeId, seen);
184
115
  }
116
+ return {
117
+ name: dirName,
118
+ listing,
119
+ follow_up: 'Each line is readable with `crtr memory read <name>` — a `[[name]]: <line>` entry is a doc, a bare `[[name]]` is a subdirectory whose read browses deeper. Browse the whole inventory with `crtr memory list`.',
120
+ };
121
+ }
122
+ throw notFound(`memory document not found: ${nameRaw}`, {
123
+ memory: nameRaw,
124
+ next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
185
125
  });
186
- return {
187
- name: doc.name,
188
- kind,
189
- scope: doc.scope,
190
- path: doc.path,
191
- content,
192
- ...(routes.length > 0 ? { routes } : {}),
193
- ...(links.length > 0 ? { links } : {}),
194
- follow_up: (routes.length > 0
195
- ? 'This INDEX gates the docs listed in `routes` — follow one with `crtr memory read <name>` when the task in front of you matches its line. '
196
- : links.length > 0
197
- ? 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth. '
198
- : '') +
199
- 'Use --frontmatter on this same command to inspect the YAML frontmatter, or revise the doc with `crtr memory edit` (recorded, with a rationale). Browse the inventory with `crtr memory list`.',
200
- };
201
126
  }
202
- throw notFound(`memory document not found: ${nameRaw}`, {
203
- memory: nameRaw,
204
- next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
127
+ const kind = effectiveDocKind(doc);
128
+ const raw = readMemoryDocContent(doc.path, includeFrontmatter);
129
+ // A read is an explicit act by an agent that can already run shell, so a
130
+ // document's embedded commands run here and their output lands inline —
131
+ // the same contract as invoking the document as a slash command. Nothing
132
+ // executes on the auto-load path, which never reaches this leaf.
133
+ const content = hasShellBlocks(raw)
134
+ ? await expandShellBlocks(raw, makeNodeShellRunner({ cwd: process.cwd() }))
135
+ : raw;
136
+ const byName = docsByName(corpusDocs());
137
+ // `[[name]]` doc links are pointers, never transclusion: surface which
138
+ // linked names actually resolve so the reader can follow one when the
139
+ // task needs that depth, without ever auto-loading a linked body. A link
140
+ // may also name a directory — a browse link to its listing.
141
+ const links = docLinkNames(doc.body).filter((linkName) => {
142
+ if (isDirName(byName, linkName))
143
+ return true;
144
+ try {
145
+ // `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
146
+ // a stored graph edge does not. Compare the resolved canonical name
147
+ // so an old shorthand never masquerades as a first-class doc link.
148
+ return resolveMemoryDoc(linkName, { includeDescendants: true }).name === linkName;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
205
153
  });
154
+ // Reading a doc discloses where it sits (its directory's listing and each
155
+ // ancestor's, once per transcript) and fires the memory-read event
156
+ // (corpus docs whose `memory-read` entries match the resolved name).
157
+ // Both prepend in one <auto-loaded-context> block. Without a node
158
+ // identity the call is stateless: listings render undeduped and nothing
159
+ // records.
160
+ const seen = nodeId !== undefined ? loadInjectedDocs(nodeId) : null;
161
+ const subject = await fetchSubject(nodeId);
162
+ const excludeReal = realpathOrSelf(doc.path);
163
+ // The explicit read IS a content delivery: record it first so unsolicited
164
+ // channels (including this doc's own line in its directory listing) stay
165
+ // quiet about a doc whose full body is already in the transcript.
166
+ if (seen !== null)
167
+ recordDelivery(seen, excludeReal, 'content');
168
+ const blocks = [];
169
+ for (const dir of ancestorDirsOf(doc.name)) {
170
+ if (seen !== null && deliveredAtOrAbove(seen, dirDedupKey(dir), 'preview'))
171
+ continue;
172
+ const lines = renderDirListing(byName, dir, seen, true);
173
+ if (seen !== null)
174
+ recordDelivery(seen, dirDedupKey(dir), 'preview');
175
+ if (lines.length > 0)
176
+ blocks.push(`<memory-listing dir="${attr(dir)}">\n${lines.join('\n')}\n</memory-listing>`);
177
+ }
178
+ const targetNames = [doc.name];
179
+ if (doc.plugin !== undefined) {
180
+ const slash = doc.name.indexOf('/');
181
+ if (slash > 0)
182
+ targetNames.push(doc.name.slice(slash + 1));
183
+ }
184
+ blocks.push(...memoryReadDocBlocks(subject, excludeReal, targetNames, seen ?? new Map()));
185
+ if (nodeId !== undefined && seen !== null)
186
+ saveInjectedDocs(nodeId, seen);
187
+ const finalContent = blocks.length === 0 ? content : `<auto-loaded-context>\n${blocks.join('\n')}\n</auto-loaded-context>\n\n${content}`;
188
+ return {
189
+ name: doc.name,
190
+ kind,
191
+ scope: doc.scope,
192
+ path: doc.path,
193
+ content: finalContent,
194
+ ...(links.length > 0 ? { links } : {}),
195
+ follow_up: (links.length > 0
196
+ ? 'The `[[name]]` links in the body are further reading — follow one with `crtr memory read <name>` only when the task needs that depth. '
197
+ : '') +
198
+ 'Use --frontmatter on this same command to inspect the YAML frontmatter, or revise the doc with `crtr memory edit` (recorded, with a rationale). Browse the inventory with `crtr memory list`.',
199
+ };
206
200
  },
207
201
  });
@@ -1,7 +1,6 @@
1
1
  import type { FlagParam } from '../../core/help.js';
2
2
  import type { MemoryScope } from '../../core/memory-resolver.js';
3
3
  export declare const MEMORY_KINDS: readonly ["knowledge", "preference"];
4
- export declare const VISIBILITY_RUNGS: readonly ["none", "name", "preview", "content"];
5
4
  export declare const MEMORY_SCOPES: readonly ["user", "project", "profile", "node"];
6
5
  /** Scope sort weight matching resolution precedence (node > project stack >
7
6
  * profile > user > builtin). Used by `list` for its "scope then kind then
@@ -25,21 +24,23 @@ export declare function resolveWriteTarget(scopeArg: string | undefined, profile
25
24
  scope: MemoryScope;
26
25
  memoryDir: string;
27
26
  };
27
+ /** Remove a just-removed file's now-empty parent directories up to (never
28
+ * including) its tree root, so removing the last entry under an `area/`
29
+ * prefix does not orphan an empty directory. The root is derived by walking
30
+ * up one dirname per name segment: a doc named `a/b` sits at `<root>/a/b.md`,
31
+ * so its root is two dirnames above — the same arithmetic holds for a
32
+ * `.history/<name>.jsonl` sidecar. Stops at the first non-empty ancestor. */
33
+ export declare function pruneEmptyParents(filePath: string, nameSegments: number): void;
28
34
  /** Map a path-derived name (`topic` or `area/topic`) to its file path under a
29
35
  * memory dir, guarding against traversal/absolute escapes. */
30
36
  export declare function memoryFilePath(memoryDir: string, name: string): string;
31
37
  export declare function coerceGate(raw: string): Record<string, unknown>;
32
- /** Coerce a `--read-when` string into a predicate tree. Like `--gate`, the
33
- * read-when field MUST be a YAML/JSON object (the field→matcher map the schema
34
- * expects, evaluated against a read file's own frontmatter). A scalar/array
35
- * read-when is inert (never matches), so passing one is always a mistake and is
36
- * caught at authoring time rather than stored. */
37
- export declare function coerceReadWhen(raw: string): Record<string, unknown>;
38
- /** Coerce repeatable `--applies-to` values (already an array from the argv
39
- * parser) to the schema's glob form: one value stays a bare string, several
40
- * become an array. Each value is trimmed; empty entries are rejected loudly
41
- * (a silently-dropped glob is a routing bug waiting to happen). */
42
- export declare function coerceAppliesTo(values: string[]): unknown;
38
+ /** Coerce one `--surface` value into a validated surfaces entry. Strict where
39
+ * the runtime parser is tolerant: a flag that would be silently dropped or
40
+ * trimmed at render time is an authoring mistake and fails HERE. The entry is
41
+ * returned in canonical key order with a single glob kept as a bare string,
42
+ * so the stored YAML stays as compact as the flag that authored it. */
43
+ export declare function coerceSurface(raw: string): Record<string, unknown>;
43
44
  /** Provenance for a doc at the moment it is created: when, where, and which
44
45
  * node's conversation authored it. Read from the env the runtime injects into
45
46
  * every node's pi process (CRTR_NODE_ID/KIND/MODE/NODE_CWD); when invoked
@@ -50,8 +51,8 @@ export declare function coerceAppliesTo(values: string[]): unknown;
50
51
  export declare function buildOrigin(): Record<string, unknown>;
51
52
  /** Serialize a substrate frontmatter record + body into a complete `.md`
52
53
  * document. Frontmatter is emitted as a `---` fenced YAML block (the `yaml`
53
- * package — the same one the parser uses — so nested gate maps and applies-to
54
- * arrays round-trip), in canonical field order with preserved extras last. */
54
+ * package — the same one the parser uses — so nested gate maps and surfaces
55
+ * entry lists round-trip), in canonical field order with preserved extras last. */
55
56
  export declare function serializeMemoryDoc(frontmatter: Record<string, unknown>, body: string): string;
56
57
  /** Everything after the closing frontmatter fence, byte for byte — leading blank
57
58
  * lines and indentation intact. Source without a fence is all body. */
@@ -75,7 +76,7 @@ export declare function overlayParam(name: string, overrides?: Partial<FlagParam
75
76
  * `--doc-rationale`, because `--rationale` there means why THIS REVISION is
76
77
  * happening. Same field, same prose, two flag names that cannot be confused. */
77
78
  export declare const DOC_RATIONALE_CONSTRAINT = "Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.";
78
- export declare const GUIDE_VISIBILITY_RUNGS = "The visibility rungs `none`, `name`, `preview`, and `content` move from least to most loaded: `none` keeps the doc out of auto-load and on-read surfaces, `name` is the bare doc tag only, `preview` is the name + envelope + routing line (`when-and-why-to-read`), and `content` inlines the whole body when the body is short enough to justify it. Each axis is independent; usually one carries a real rung and the other is `none`. When a doc fits in a single sentence \u2014 a one-line preference or a one-sentence knowledge fact \u2014 skip `preview` and use `name` or `content` directly: the routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.";
79
+ export declare const GUIDE_SURFACES = "Every doc appears in its directory\u2019s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing \u2014 entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc\u2019s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file\u2019s absolute path and basename, `./`-anchored globs vs its path relative to the store\u2019s owning repo dir, `match-frontmatter` predicates over the read file\u2019s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc\u2019s canonical name, `./` anchored to this doc\u2019s own name directory); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly \u2014 a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.";
79
80
  export declare const GUIDE_ROUTING_LINE = "The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: \"because only genuine first principles belong in taste memory.\" Bad: \"because keeping the test loop fast and free of speculative tests protects the development pace\" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: \"because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation.\" Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.";
80
- export declare const GUIDE_PREDICATE_VOCABULARY = "Gate and read-when share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.";
81
- export declare const GUIDE_DOC_LINKS = "Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes (a directory INDEX is linked by its bare directory name). Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.";
81
+ export declare const GUIDE_PREDICATE_VOCABULARY = "Gate and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.";
82
+ export declare const GUIDE_DOC_LINKS = "Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes. A bare directory name is a valid link too: following it returns that directory\u2019s listing, a browse entrance rather than a doc. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.";