@crouton-kit/crouter 0.3.49 → 0.3.50
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.
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/clients/attach/attach-cmd.js +825 -863
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/memory/write.js +2 -0
- package/dist/commands/sys/__tests__/sync-import.test.js +156 -3
- package/dist/commands/sys/sync.js +82 -25
- package/dist/core/__tests__/broker-sdk-wiring.test.js +4 -7
- package/dist/core/__tests__/context-intro.test.js +15 -7
- package/dist/core/__tests__/memory-resolver-precedence.test.d.ts +1 -0
- package/dist/core/__tests__/memory-resolver-precedence.test.js +144 -0
- package/dist/core/__tests__/on-read-identity.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-identity.test.js +68 -0
- package/dist/core/fs-utils.d.ts +1 -1
- package/dist/core/fs-utils.js +5 -3
- package/dist/core/memory-resolver.d.ts +27 -11
- package/dist/core/memory-resolver.js +105 -109
- package/dist/core/runtime/bearings.d.ts +4 -9
- package/dist/core/runtime/bearings.js +10 -17
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +3 -1
- package/dist/core/substrate/on-read.js +14 -9
- package/dist/core/substrate/render.js +7 -3
- package/dist/core/substrate/schema.d.ts +8 -2
- package/dist/core/substrate/schema.js +19 -2
- package/package.json +3 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The on-read POSITIONAL loader resolves a doc's identity with the SAME
|
|
2
|
+
// explicit-name-then-path-fallback rule the boot resolver uses (schema.ts's
|
|
3
|
+
// resolveDocName), never deriving `name` from the physical path alone.
|
|
4
|
+
//
|
|
5
|
+
// A migrated AGENTS.md doc (`crtr sys sync`'s CLAUDE.md/AGENTS.md conversion)
|
|
6
|
+
// is written at `file-read-visibility: content` with an explicit frontmatter
|
|
7
|
+
// `name` (the project label, e.g. `crouter`) so sibling project roots do not
|
|
8
|
+
// collapse onto the shared `AGENTS` leaf. `loadPositionalDoc` must inject the
|
|
9
|
+
// doc under that explicit name, not the path-derived `AGENTS`.
|
|
10
|
+
//
|
|
11
|
+
// Run: node --import tsx/esm --test src/core/__tests__/on-read-identity.test.ts
|
|
12
|
+
import { test, before, beforeEach, after } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { closeDb } from '../canvas/db.js';
|
|
18
|
+
import { resetScopeCache } from '../scope.js';
|
|
19
|
+
import { spawnNode } from '../runtime/nodes.js';
|
|
20
|
+
import { renderOnReadDocs } from '../substrate/on-read.js';
|
|
21
|
+
let home;
|
|
22
|
+
let work;
|
|
23
|
+
let prevHomeEnv;
|
|
24
|
+
before(() => {
|
|
25
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-onread-identity-home-'));
|
|
26
|
+
process.env['CRTR_HOME'] = home;
|
|
27
|
+
prevHomeEnv = process.env['HOME'];
|
|
28
|
+
});
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
closeDb();
|
|
31
|
+
resetScopeCache();
|
|
32
|
+
rmSync(home, { recursive: true, force: true });
|
|
33
|
+
if (prevHomeEnv === undefined)
|
|
34
|
+
delete process.env['HOME'];
|
|
35
|
+
else
|
|
36
|
+
process.env['HOME'] = prevHomeEnv;
|
|
37
|
+
work = mkdtempSync(join(tmpdir(), 'crtr-onread-identity-work-'));
|
|
38
|
+
});
|
|
39
|
+
after(() => {
|
|
40
|
+
closeDb();
|
|
41
|
+
resetScopeCache();
|
|
42
|
+
rmSync(home, { recursive: true, force: true });
|
|
43
|
+
rmSync(work, { recursive: true, force: true });
|
|
44
|
+
delete process.env['CRTR_HOME'];
|
|
45
|
+
if (prevHomeEnv === undefined)
|
|
46
|
+
delete process.env['HOME'];
|
|
47
|
+
else
|
|
48
|
+
process.env['HOME'] = prevHomeEnv;
|
|
49
|
+
});
|
|
50
|
+
test('a migrated AGENTS doc injects on-read under its explicit frontmatter name, not the path-derived one', () => {
|
|
51
|
+
const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
|
|
52
|
+
// Shape of a `crtr sys sync` CLAUDE.md/AGENTS.md migration: physical filename
|
|
53
|
+
// stays AGENTS.md, but frontmatter carries the project's explicit identity.
|
|
54
|
+
const memDir = join(work, '.crouter', 'memory');
|
|
55
|
+
mkdirSync(memDir, { recursive: true });
|
|
56
|
+
writeFileSync(join(memDir, 'AGENTS.md'), '---\nkind: knowledge\n' +
|
|
57
|
+
'name: acme-project\n' +
|
|
58
|
+
'when-and-why-to-read: When working in acme-project, this knowledge should be read because it is the project\'s operating guide.\n' +
|
|
59
|
+
'file-read-visibility: content\n---\n' +
|
|
60
|
+
'ACME PROJECT OPERATING GUIDE\n');
|
|
61
|
+
const readFile = join(work, 'src', 'file.ts');
|
|
62
|
+
mkdirSync(join(work, 'src'), { recursive: true });
|
|
63
|
+
writeFileSync(readFile, 'export const x = 1;\n');
|
|
64
|
+
const rendered = renderOnReadDocs(node, readFile, new Set());
|
|
65
|
+
assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the migrated doc fires positionally');
|
|
66
|
+
assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
|
|
67
|
+
assert.ok(!rendered.includes('name="AGENTS"'), 'must not fall back to the path-derived name when frontmatter sets one');
|
|
68
|
+
});
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export declare function linkOrCopy(target: string, linkPath: string, opts?: {
|
|
|
15
15
|
noSymlink?: boolean;
|
|
16
16
|
}): 'symlink' | 'copy';
|
|
17
17
|
export declare function readSymlinkTarget(path: string): string | null;
|
|
18
|
-
export declare function walkFiles(root: string, predicate?: (name: string) => boolean): string[];
|
|
18
|
+
export declare function walkFiles(root: string, predicate?: (name: string) => boolean, skipDir?: (name: string) => boolean): string[];
|
|
19
19
|
export declare function nowIso(): string;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -90,7 +90,7 @@ export function readSymlinkTarget(path) {
|
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
export function walkFiles(root, predicate = () => true) {
|
|
93
|
+
export function walkFiles(root, predicate = () => true, skipDir = () => false) {
|
|
94
94
|
const out = [];
|
|
95
95
|
if (!existsSync(root))
|
|
96
96
|
return out;
|
|
@@ -106,8 +106,10 @@ export function walkFiles(root, predicate = () => true) {
|
|
|
106
106
|
}
|
|
107
107
|
for (const e of entries) {
|
|
108
108
|
const full = join(dir, e.name);
|
|
109
|
-
if (e.isDirectory())
|
|
110
|
-
|
|
109
|
+
if (e.isDirectory()) {
|
|
110
|
+
if (!skipDir(e.name))
|
|
111
|
+
stack.push(full);
|
|
112
|
+
}
|
|
111
113
|
else if (e.isFile() && predicate(e.name))
|
|
112
114
|
out.push(full);
|
|
113
115
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { InstalledPlugin, Scope } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Thin memory-document resolver for the document substrate.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Thin memory-document resolver for the document substrate. Precedence is the
|
|
4
|
+
* OUTER loop, not a global scan: scopes/sources are tried NEAREST-FIRST
|
|
5
|
+
* (project stack nearest > ... > profile > user > builtin), and WITHIN each
|
|
6
|
+
* source, in order: (1) exact substrate-identity match (`doc.name === query`
|
|
7
|
+
* — the explicit frontmatter `name`, or its path-derived fallback), (2) direct
|
|
8
|
+
* `memory/<name>.md` physical-path lookup (plus the bare-dir/bare-plugin-name
|
|
9
|
+
* → `INDEX.md` convenience neither identity nor a literal path expresses),
|
|
10
|
+
* (3) bare leaf-name fallback (final path segment only). The first hit wins,
|
|
11
|
+
* so a nearer source's match of ANY kind always beats a farther source's
|
|
12
|
+
* match of any kind — a farther-scope identity can never shadow a
|
|
13
|
+
* nearer-scope physical file. It returns the raw parsed frontmatter + body;
|
|
14
|
+
* it does NOT interpret the schema, kind, gate, or rungs — that is the
|
|
8
15
|
* schema/gate layer's job (callers filter by `frontmatter.kind`). Project
|
|
9
16
|
* resolution is a nearest-first stack of every ancestor `.crouter/` (widened by
|
|
10
17
|
* a selected profile's `projects` manifest entries — see `findProjectScopeRoots`
|
|
@@ -12,8 +19,9 @@ import type { InstalledPlugin, Scope } from '../types.js';
|
|
|
12
19
|
* (from `CRTR_PROFILE_ID`), and user/builtin remain singleton scopes.
|
|
13
20
|
*/
|
|
14
21
|
export interface MemoryDoc {
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
22
|
+
/** Resolver identity: the doc's explicit frontmatter `name` when present,
|
|
23
|
+
* otherwise its normalized path under the scope's memory/ root — e.g.
|
|
24
|
+
* memory/taste/foo.md → "taste/foo". */
|
|
17
25
|
name: string;
|
|
18
26
|
scope: MemoryScope;
|
|
19
27
|
/** Absolute path to the resolved .md file. */
|
|
@@ -59,15 +67,23 @@ export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: Mem
|
|
|
59
67
|
* caller's first-wins dedup. */
|
|
60
68
|
export declare function listAllMemoryDocs(scope?: MemoryScope, quiet?: boolean): MemoryDoc[];
|
|
61
69
|
/**
|
|
62
|
-
* Resolve a
|
|
70
|
+
* Resolve a memory document name to a single memory document.
|
|
63
71
|
*
|
|
64
72
|
* Accepted identifier forms:
|
|
65
73
|
* <name> — bare name; resolved by scope precedence project>user>builtin
|
|
66
74
|
* <scope>/<name> — pinned to one scope (user|project)
|
|
67
75
|
* `<name>` may carry topical subdirs (`taste/foo`); a bare leaf (`foo`) falls
|
|
68
|
-
* back to a last-segment match
|
|
76
|
+
* back to a last-segment match within the resolved scopes.
|
|
69
77
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
78
|
+
* Precedence is the outer loop: sources are visited nearest-first, and the
|
|
79
|
+
* FIRST source with any hit wins outright — a farther source is never even
|
|
80
|
+
* consulted once a nearer one matches. Within each source, in order: (1)
|
|
81
|
+
* exact substrate-identity match (`doc.name === query` — the explicit
|
|
82
|
+
* frontmatter `name`, else its normalized path-derived fallback, the SAME
|
|
83
|
+
* identity `listAllMemoryDocs` reports); (2) direct `memory/<name>.md`
|
|
84
|
+
* physical-path lookup (the bare-dir → INDEX.md and bare-plugin-name → plugin
|
|
85
|
+
* root INDEX.md conveniences neither identity nor a literal path expresses);
|
|
86
|
+
* (3) bare leaf-name fallback (final path segment), ambiguous only when two
|
|
87
|
+
* DIFFERENT identities in the SAME source share a leaf.
|
|
72
88
|
*/
|
|
73
89
|
export declare function resolveMemoryDoc(rawName: string, opts?: MemoryResolutionOpts): MemoryDoc;
|
|
@@ -6,7 +6,7 @@ import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier }
|
|
|
6
6
|
import { ambiguous, notFound, usage } from './errors.js';
|
|
7
7
|
import { warn } from './output.js';
|
|
8
8
|
import { pluginMemoryDir, projectScopeRoot, projectScopeRoots, scopeMemoryDir } from './scope.js';
|
|
9
|
-
import { normalizeDocName, normalizeNameSegment } from './substrate/schema.js';
|
|
9
|
+
import { normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
|
|
10
10
|
import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
|
|
11
11
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
12
12
|
export function memoryDocId(doc) {
|
|
@@ -75,14 +75,14 @@ function memorySourcesInPrecedence(scope) {
|
|
|
75
75
|
}
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
|
-
function loadMemoryDoc(
|
|
78
|
+
function loadMemoryDoc(scope, path, fallbackName) {
|
|
79
79
|
const { data, body } = parseFrontmatterGeneric(readText(path));
|
|
80
|
-
return { name, scope, path, frontmatter: data, body };
|
|
80
|
+
return { name: resolveDocName(data, fallbackName), scope, path, frontmatter: data, body };
|
|
81
81
|
}
|
|
82
82
|
/** All memory docs in one memory/ dir, scanned recursively for *.md (topical
|
|
83
|
-
* subdirs supported), sorted by
|
|
84
|
-
*
|
|
85
|
-
* memory/. */
|
|
83
|
+
* subdirs supported), sorted by resolver identity (explicit frontmatter name
|
|
84
|
+
* when present, else path-derived). SKILL.md bundles are legacy Agent Skills
|
|
85
|
+
* and are ignored; crouter memory docs are plain .md files under memory/. */
|
|
86
86
|
function listMemoryDocsInDir(scope, dir, quiet = false) {
|
|
87
87
|
if (!dir || !pathExists(dir))
|
|
88
88
|
return [];
|
|
@@ -117,7 +117,7 @@ function listMemoryDocsInDir(scope, dir, quiet = false) {
|
|
|
117
117
|
// `quiet` suppresses the notice for a targeted resolve (a leaf-name read),
|
|
118
118
|
// where another doc's health is irrelevant noise before the result.
|
|
119
119
|
try {
|
|
120
|
-
docs.push(loadMemoryDoc(
|
|
120
|
+
docs.push(loadMemoryDoc(scope, path, name));
|
|
121
121
|
}
|
|
122
122
|
catch (e) {
|
|
123
123
|
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
@@ -164,7 +164,7 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
|
|
|
164
164
|
continue;
|
|
165
165
|
const name = `${plugin.name}/${derived}`;
|
|
166
166
|
try {
|
|
167
|
-
docs.push({ ...loadMemoryDoc(
|
|
167
|
+
docs.push({ ...loadMemoryDoc(scope, file, name), plugin: plugin.name });
|
|
168
168
|
}
|
|
169
169
|
catch (e) {
|
|
170
170
|
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
@@ -174,16 +174,23 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
|
|
|
174
174
|
}
|
|
175
175
|
return docs.sort((a, b) => a.name.localeCompare(b.name));
|
|
176
176
|
}
|
|
177
|
+
/** All docs belonging to a single resolved source: native docs first, then
|
|
178
|
+
* enabled-plugin docs — native wins on a first-wins dedup within the source.
|
|
179
|
+
* Shared by `listAllMemoryDocs` (flattens every source) and `resolveMemoryDoc`
|
|
180
|
+
* (consults one source's docs at a time, nearest source first). */
|
|
181
|
+
function sourceMemoryDocs(source, quiet = false) {
|
|
182
|
+
return [
|
|
183
|
+
...listMemoryDocsInDir(source.scope, source.memoryDir, quiet),
|
|
184
|
+
...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
|
|
185
|
+
];
|
|
186
|
+
}
|
|
177
187
|
/** All memory docs across the resolved sources, in precedence order: each
|
|
178
188
|
* ancestor project `.crouter/` from nearest to farthest, then the selected
|
|
179
189
|
* profile's memory (if any), then user, then builtin. Within each source,
|
|
180
190
|
* native docs are emitted before enabled-plugin docs, so native wins on the
|
|
181
191
|
* caller's first-wins dedup. */
|
|
182
192
|
export function listAllMemoryDocs(scope, quiet = false) {
|
|
183
|
-
return memorySourcesInPrecedence(scope).flatMap((source) =>
|
|
184
|
-
...listMemoryDocsInDir(source.scope, source.memoryDir, quiet),
|
|
185
|
-
...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
|
|
186
|
-
]);
|
|
193
|
+
return memorySourcesInPrecedence(scope).flatMap((source) => sourceMemoryDocs(source, quiet));
|
|
187
194
|
}
|
|
188
195
|
/** Find the direct child of `dir` — a `.md` file (matched on name minus
|
|
189
196
|
* extension) or a directory — whose NORMALIZED display name equals
|
|
@@ -246,9 +253,10 @@ function resolveNormalizedPath(baseDir, segments) {
|
|
|
246
253
|
dirPath: matchNormalizedChild(curDir, last, 'dir'),
|
|
247
254
|
};
|
|
248
255
|
}
|
|
249
|
-
/** Direct full-path lookup of memory/<name>.md
|
|
250
|
-
*
|
|
251
|
-
* (
|
|
256
|
+
/** Direct full-path lookup of memory/<name>.md within ONE source. Returns that
|
|
257
|
+
* source's single hit, or undefined — a source can produce at most one direct
|
|
258
|
+
* match (native wins over plugin within the source, and at most one plugin's
|
|
259
|
+
* name can match).
|
|
252
260
|
*
|
|
253
261
|
* A directory INDEX is the cleaner contract: when `<name>.md` is absent but
|
|
254
262
|
* `<name>/INDEX.md` exists, the bare dir name (`taste`) resolves to the dir's
|
|
@@ -259,94 +267,72 @@ function resolveNormalizedPath(baseDir, segments) {
|
|
|
259
267
|
* never authored with a numeric prefix); resolution against the physical tree
|
|
260
268
|
* is prefix-blind via `resolveNormalizedPath` so a normalized name finds an
|
|
261
269
|
* `NN-`-pinned physical file/dir. */
|
|
262
|
-
function
|
|
263
|
-
const matches = [];
|
|
264
|
-
const segments = name.split('/');
|
|
270
|
+
function findMemoryMatchInSource(name, segments, source) {
|
|
265
271
|
const isLegacySkillDoc = segments.at(-1) === 'SKILL';
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (dirPath !== null) {
|
|
278
|
-
const indexPath = join(dirPath, 'INDEX.md');
|
|
279
|
-
if (pathExists(indexPath)) {
|
|
280
|
-
matches.push(loadMemoryDoc(name, source.scope, indexPath));
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
|
|
286
|
-
// plugin's memory/ tree (the `<pluginName>/` mount that listAllMemoryDocs
|
|
287
|
-
// enumerates — `read` must resolve what `list` shows). A BARE plugin name
|
|
288
|
-
// (no slash) resolves the plugin-root INDEX.md, mirroring the native
|
|
289
|
-
// bare-dir-name -> INDEX.md contract for the plugin mount root.
|
|
290
|
-
const slash = name.indexOf('/');
|
|
291
|
-
const pluginName = slash > 0 ? name.slice(0, slash) : name;
|
|
292
|
-
const rest = slash > 0 ? name.slice(slash + 1) : '';
|
|
293
|
-
for (const p of source.plugins) {
|
|
294
|
-
if (p.name !== pluginName)
|
|
295
|
-
continue;
|
|
296
|
-
const pdir = pluginMemoryDir(p);
|
|
297
|
-
const restSegments = rest ? rest.split('/') : [];
|
|
298
|
-
if (rest) {
|
|
299
|
-
const { filePath } = resolveNormalizedPath(pdir, restSegments);
|
|
300
|
-
if (restSegments.at(-1) !== 'SKILL' && filePath !== null) {
|
|
301
|
-
matches.push(loadMemoryDoc(name, source.scope, filePath));
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// Bare name -> <plugin>/memory/INDEX.md; slashed name -> dir INDEX.
|
|
306
|
-
const pIndexDir = rest ? resolveNormalizedPath(pdir, restSegments).dirPath : pdir;
|
|
307
|
-
if (pIndexDir !== null) {
|
|
308
|
-
const pindex = join(pIndexDir, 'INDEX.md');
|
|
309
|
-
if (pathExists(pindex)) {
|
|
310
|
-
matches.push(loadMemoryDoc(name, source.scope, pindex));
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
272
|
+
// Native memory dir first inside this source (native-before-plugin
|
|
273
|
+
// precedence), then its enabled plugins.
|
|
274
|
+
const dir = source.memoryDir;
|
|
275
|
+
if (dir) {
|
|
276
|
+
const { filePath, dirPath } = resolveNormalizedPath(dir, segments);
|
|
277
|
+
if (!isLegacySkillDoc && filePath !== null)
|
|
278
|
+
return loadMemoryDoc(source.scope, filePath, name);
|
|
279
|
+
if (dirPath !== null) {
|
|
280
|
+
const indexPath = join(dirPath, 'INDEX.md');
|
|
281
|
+
if (pathExists(indexPath))
|
|
282
|
+
return loadMemoryDoc(source.scope, indexPath, name);
|
|
314
283
|
}
|
|
315
284
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
285
|
+
// Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
|
|
286
|
+
// plugin's memory/ tree (the `<pluginName>/` mount that listAllMemoryDocs
|
|
287
|
+
// enumerates — `read` must resolve what `list` shows). A BARE plugin name
|
|
288
|
+
// (no slash) resolves the plugin-root INDEX.md, mirroring the native
|
|
289
|
+
// bare-dir-name -> INDEX.md contract for the plugin mount root.
|
|
290
|
+
const slash = name.indexOf('/');
|
|
291
|
+
const pluginName = slash > 0 ? name.slice(0, slash) : name;
|
|
292
|
+
const rest = slash > 0 ? name.slice(slash + 1) : '';
|
|
293
|
+
for (const p of source.plugins) {
|
|
294
|
+
if (p.name !== pluginName)
|
|
295
|
+
continue;
|
|
296
|
+
const pdir = pluginMemoryDir(p);
|
|
297
|
+
const restSegments = rest ? rest.split('/') : [];
|
|
298
|
+
if (rest) {
|
|
299
|
+
const { filePath } = resolveNormalizedPath(pdir, restSegments);
|
|
300
|
+
if (restSegments.at(-1) !== 'SKILL' && filePath !== null)
|
|
301
|
+
return loadMemoryDoc(source.scope, filePath, name);
|
|
302
|
+
}
|
|
303
|
+
// Bare name -> <plugin>/memory/INDEX.md; slashed name -> dir INDEX.
|
|
304
|
+
const pIndexDir = rest ? resolveNormalizedPath(pdir, restSegments).dirPath : pdir;
|
|
305
|
+
if (pIndexDir !== null) {
|
|
306
|
+
const pindex = join(pIndexDir, 'INDEX.md');
|
|
307
|
+
if (pathExists(pindex))
|
|
308
|
+
return loadMemoryDoc(source.scope, pindex, name);
|
|
309
|
+
}
|
|
332
310
|
}
|
|
333
|
-
return
|
|
311
|
+
return undefined;
|
|
334
312
|
}
|
|
335
313
|
function formatLeafAmbiguous(leaf, matches) {
|
|
336
314
|
const ids = matches.map(memoryDocId).join(', ');
|
|
337
315
|
return `ambiguous memory document: ${leaf} matches multiple documents: ${ids}`;
|
|
338
316
|
}
|
|
339
317
|
/**
|
|
340
|
-
* Resolve a
|
|
318
|
+
* Resolve a memory document name to a single memory document.
|
|
341
319
|
*
|
|
342
320
|
* Accepted identifier forms:
|
|
343
321
|
* <name> — bare name; resolved by scope precedence project>user>builtin
|
|
344
322
|
* <scope>/<name> — pinned to one scope (user|project)
|
|
345
323
|
* `<name>` may carry topical subdirs (`taste/foo`); a bare leaf (`foo`) falls
|
|
346
|
-
* back to a last-segment match
|
|
324
|
+
* back to a last-segment match within the resolved scopes.
|
|
347
325
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
326
|
+
* Precedence is the outer loop: sources are visited nearest-first, and the
|
|
327
|
+
* FIRST source with any hit wins outright — a farther source is never even
|
|
328
|
+
* consulted once a nearer one matches. Within each source, in order: (1)
|
|
329
|
+
* exact substrate-identity match (`doc.name === query` — the explicit
|
|
330
|
+
* frontmatter `name`, else its normalized path-derived fallback, the SAME
|
|
331
|
+
* identity `listAllMemoryDocs` reports); (2) direct `memory/<name>.md`
|
|
332
|
+
* physical-path lookup (the bare-dir → INDEX.md and bare-plugin-name → plugin
|
|
333
|
+
* root INDEX.md conveniences neither identity nor a literal path expresses);
|
|
334
|
+
* (3) bare leaf-name fallback (final path segment), ambiguous only when two
|
|
335
|
+
* DIFFERENT identities in the SAME source share a leaf.
|
|
350
336
|
*/
|
|
351
337
|
export function resolveMemoryDoc(rawName, opts = {}) {
|
|
352
338
|
const parsed = parseSkillQualifier(rawName);
|
|
@@ -357,28 +343,38 @@ export function resolveMemoryDoc(rawName, opts = {}) {
|
|
|
357
343
|
const name = parsed.segments.join('/');
|
|
358
344
|
if (name === '')
|
|
359
345
|
throw usage(`memory document name required`);
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
346
|
+
const segments = name.split('/');
|
|
347
|
+
const isLeafQuery = !name.includes('/');
|
|
348
|
+
for (const source of memorySourcesInPrecedence(effectiveScope)) {
|
|
349
|
+
// Quiet: a targeted read must not spew other docs' frontmatter warnings
|
|
350
|
+
// before its own result (esp. a not_found) — corpus health is `lint`'s job.
|
|
351
|
+
const docs = sourceMemoryDocs(source, true);
|
|
352
|
+
const identity = docs.find((d) => d.name === name);
|
|
353
|
+
if (identity)
|
|
354
|
+
return identity;
|
|
355
|
+
const direct = findMemoryMatchInSource(name, segments, source);
|
|
356
|
+
if (direct)
|
|
357
|
+
return direct;
|
|
358
|
+
if (isLeafQuery) {
|
|
359
|
+
const leafMatches = docs.filter((d) => (d.name.split('/').pop() ?? d.name) === name);
|
|
360
|
+
if (leafMatches.length > 0) {
|
|
361
|
+
// Same path-derived name within this source → precedence wins (return
|
|
362
|
+
// first); genuinely different docs sharing a leaf → ambiguous. Either
|
|
363
|
+
// way this source has an answer — a farther source is never checked.
|
|
364
|
+
const distinctNames = new Set(leafMatches.map((d) => d.name));
|
|
365
|
+
if (distinctNames.size === 1)
|
|
366
|
+
return leafMatches[0];
|
|
367
|
+
throw ambiguous(formatLeafAmbiguous(name, leafMatches), {
|
|
368
|
+
memory: name,
|
|
369
|
+
candidates: leafMatches.map((d) => ({
|
|
370
|
+
id: memoryDocId(d),
|
|
371
|
+
scope: d.scope,
|
|
372
|
+
path: d.path,
|
|
373
|
+
})),
|
|
374
|
+
next: 'Multiple documents share this leaf name. Re-run with one of the full names in candidates.',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
382
378
|
}
|
|
383
379
|
throw notFound(`memory document not found: ${rawName}`, {
|
|
384
380
|
memory: name,
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { type Trigger } from '../canvas/index.js';
|
|
2
|
-
/** The `<project_context>` block —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* in the document substrate (`crtr sys sync` migrates each CLAUDE.md/AGENTS.md
|
|
7
|
-
* into that dir's own `.crouter/memory/AGENTS.md` at content/content, so it
|
|
8
|
-
* surfaces via the `<memory kind="knowledge">` block instead). Always emits
|
|
9
|
-
* the wrapper because the environment snapshot is always present. Exported
|
|
10
|
-
* for testing. */
|
|
2
|
+
/** The `<project_context>` block — cwd/git `<environment>` snapshot only
|
|
3
|
+
* (dir listing + git status/worktrees). Project guidance lives in the document
|
|
4
|
+
* substrate, so this wrapper stays environment-only. It always emits because
|
|
5
|
+
* the environment snapshot is always present. Exported for testing. */
|
|
11
6
|
export declare function buildProjectContextBlock(cwd: string): string;
|
|
12
7
|
/** The context-directory note — names the path INLINE (no XML attribute, so it
|
|
13
8
|
* never scopes a sibling block) and frames the dir as durable, shared scratch.
|
|
@@ -13,15 +13,13 @@
|
|
|
13
13
|
// the source's whole conversation and then impersonates it. A non-fork
|
|
14
14
|
// node's bearings are its FIRST entry, so there is nothing earlier to
|
|
15
15
|
// disown; it gets only the declarative identity line. The message also
|
|
16
|
-
// carries the <project_context> block (buildProjectContextBlock)
|
|
17
|
-
// the cwd/git <environment> snapshot. Project
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// dir's own `.crouter/memory/AGENTS.md` at content/content, so it rides the
|
|
16
|
+
// carries the <project_context> block (buildProjectContextBlock), which is
|
|
17
|
+
// now only the cwd/git <environment> snapshot. Project guidance rides the
|
|
18
|
+
// document substrate instead: migrated CLAUDE.md/AGENTS.md docs live in the
|
|
19
|
+
// source dir's own `.crouter/memory/AGENTS.md` and surface through the
|
|
21
20
|
// <memory kind="knowledge"> block below (renderKnowledgeBlock) via the
|
|
22
|
-
// profile-widened boot render
|
|
23
|
-
//
|
|
24
|
-
// anymore (see `crtr memory read taste/document-substrate`);
|
|
21
|
+
// profile-widened boot render — no parallel project-context loader lives
|
|
22
|
+
// here anymore (see `crtr memory read taste/document-substrate`);
|
|
25
23
|
// • promote.ts folds orchestratorContextNote() into the promotion guidance
|
|
26
24
|
// dump, so a node that becomes an orchestrator MID-LIFE gets the
|
|
27
25
|
// orchestrator framing it never received at spawn — it spawned as a base
|
|
@@ -112,15 +110,10 @@ function parseWorktrees(lines) {
|
|
|
112
110
|
}
|
|
113
111
|
return worktrees;
|
|
114
112
|
}
|
|
115
|
-
/** The `<project_context>` block —
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* in the document substrate (`crtr sys sync` migrates each CLAUDE.md/AGENTS.md
|
|
120
|
-
* into that dir's own `.crouter/memory/AGENTS.md` at content/content, so it
|
|
121
|
-
* surfaces via the `<memory kind="knowledge">` block instead). Always emits
|
|
122
|
-
* the wrapper because the environment snapshot is always present. Exported
|
|
123
|
-
* for testing. */
|
|
113
|
+
/** The `<project_context>` block — cwd/git `<environment>` snapshot only
|
|
114
|
+
* (dir listing + git status/worktrees). Project guidance lives in the document
|
|
115
|
+
* substrate, so this wrapper stays environment-only. It always emits because
|
|
116
|
+
* the environment snapshot is always present. Exported for testing. */
|
|
124
117
|
export function buildProjectContextBlock(cwd) {
|
|
125
118
|
const envLines = directoryListingLines(cwd);
|
|
126
119
|
const git = gitSnapshot(cwd);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, normalizeNameSegment, normalizeDocName, } from './schema.js';
|
|
1
|
+
export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, normalizeNameSegment, normalizeDocName, resolveDocName, } from './schema.js';
|
|
2
2
|
export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from './schema.js';
|
|
3
3
|
export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
|
|
4
4
|
export type { NodeConfigSubject } from './subject.js';
|
|
@@ -13,7 +13,9 @@ FALLBACK_RUNG,
|
|
|
13
13
|
// parse + render-shared helpers
|
|
14
14
|
parseSubstrateFrontmatter, parseSubstrateDoc, previewLine,
|
|
15
15
|
// display-name normalization
|
|
16
|
-
normalizeNameSegment, normalizeDocName,
|
|
16
|
+
normalizeNameSegment, normalizeDocName,
|
|
17
|
+
// substrate identity (explicit-name-then-path-fallback)
|
|
18
|
+
resolveDocName, } from './schema.js';
|
|
17
19
|
export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
|
|
18
20
|
export { gatePasses } from './gate.js';
|
|
19
21
|
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// ancestor PROJECT `.crouter/memory/` surfaces (a doc surfaces when a file
|
|
11
11
|
// beside/under its own project scope dir is read), UNLESS it carries an
|
|
12
12
|
// explicit read-trigger (see D5 below). This is the substrate's own
|
|
13
|
-
// ancestor walk keyed on `.crouter/memory/` —
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// ancestor walk keyed on `.crouter/memory/` — project guidance that starts
|
|
14
|
+
// in CLAUDE.md/AGENTS.md or `.claude/rules` now lives here after
|
|
15
|
+
// `crtr sys sync` migrates it. The user-global
|
|
16
16
|
// `~/.crouter/memory/` store is NOT positional; user docs only fire on
|
|
17
17
|
// reads through explicit `applies-to` / `read-when` triggers.
|
|
18
18
|
// • applies-to GLOB — any RESOLVED substrate doc (user/project/builtin scope)
|
|
@@ -58,16 +58,16 @@ import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
|
58
58
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
59
59
|
import { evalCondition } from '../predicate.js';
|
|
60
60
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
61
|
-
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
61
|
+
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, } from './index.js';
|
|
62
62
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
63
63
|
// Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
|
|
64
64
|
// file may live under a build/dependency tree; `.crouter` is NOT junk here — it
|
|
65
65
|
// is the segment we explicitly join onto each surviving ancestor).
|
|
66
66
|
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
|
|
67
67
|
// ---------------------------------------------------------------------------
|
|
68
|
-
// Small path helpers (mirror the on-read precedent
|
|
69
|
-
//
|
|
70
|
-
//
|
|
68
|
+
// Small path helpers (mirror the on-read precedent established by
|
|
69
|
+
// frontmatter-rules so the injected envelope keeps the faithful substrate
|
|
70
|
+
// shape).
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
function realpathOrSelf(p) {
|
|
73
73
|
try {
|
|
@@ -120,13 +120,18 @@ function loadPositionalDoc(file, memDir, scope) {
|
|
|
120
120
|
.replace(/\.md$/i, '')
|
|
121
121
|
.split(sep)
|
|
122
122
|
.join('/');
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
123
|
+
const fallbackName = normalizeDocName(raw);
|
|
124
|
+
if (fallbackName === '')
|
|
125
125
|
return null;
|
|
126
126
|
const { data, body } = parseFrontmatterGeneric(readText(file));
|
|
127
127
|
const schema = parseSubstrateFrontmatter(data);
|
|
128
128
|
if (schema === null)
|
|
129
129
|
return null;
|
|
130
|
+
// ONE substrate-identity rule (schema.ts's resolveDocName), shared with the
|
|
131
|
+
// resolver's loadMemoryDoc: an explicit frontmatter `name` wins over the
|
|
132
|
+
// path-derived fallback, so a positionally-discovered doc surfaces on-read
|
|
133
|
+
// under the SAME identity the boot resolver would give it.
|
|
134
|
+
const name = resolveDocName(data, fallbackName);
|
|
130
135
|
return { ...schema, name, scope, path: file, body };
|
|
131
136
|
}
|
|
132
137
|
catch {
|